Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ItemId

The SceneItem trait and its supporting context types.

Public types

KindName
structItemId — Opaque identifier for a SceneItem inside a Scene
structSceneItemPaintContext — Context handed to SceneItem::paint
structSceneItemA11yContext — Context handed to SceneItem::accessibility
enumAppearanceWrite — What a SceneItem appearance setter did — and, when it did anything, the value it overwrote
traitSceneItem — A lightweight, paint-only scene-graph item

Public functions

ItemId

ReturnsFunction
Methods
u64as_u64()

SceneItemPaintContext

SceneItemA11yContext

AppearanceWrite

ReturnsFunction
Constructors
Selfreplaced(previous: T)
Methods
boolaccepted()
Option<Option<T>>into_previous()

SceneItem

ReturnsFunction
Methods
Rectlocal_bounds()
set_local_bounds(bounds: Rect)
paint(canvas: &mut Canvas, ctx: &SceneItemPaintContext<'_>)
AppearanceWrite<ColorProp> { /* default implementation */ }set_fill(fill: Option<ColorProp>)
AppearanceWrite<(ColorProp, StrokeStyle)> { /* default implementation */ }set_stroke(stroke: Option<(ColorProp, StrokeStyle)>)
crate::shape::ItemShape { /* default implementation */ }shape()
teksilo_tokens::Color { /* default implementation */ }thumbnail_color()
ItemFlags { /* default implementation */ }initial_flags()
Option<String> { /* default implementation */ }label()
crate::items::AccessSubtreeMode { /* default implementation */ }access_subtree_mode()
crate::cache::CacheMode { /* default implementation */ }cache_mode()
register_bindings(_ctx: &mut BuildContext, _view_id: WidgetId)
accessibility(builder: &mut AccessNodeBuilder, _ctx: &SceneItemA11yContext)

Detailed description

Lightweight items live in a Scene without arena overhead. Each carries its own bounds (in local item coordinates, origin at the item's anchor) and paints itself via SceneView's paint walk. Apps implement this trait directly for custom items; built-ins live in crate::items.

Coordinate model

An item is positioned in its parent's coordinate space by a local_pos: Point plus an optional transform: Transform2D (rotation/scale, applied around the local origin). The Scene composes those per-item transforms up the parent chain to produce a scene_transform (local→scene). Hit-test inverse-transforms a scene-coord point into local coords before testing the item's SceneItem::shape; paint pushes the scene transform onto the canvas before calling SceneItem::paint.

When to use

Implement SceneItem when you need a lightweight, paint-only decoration or connector that isn't interactive enough to warrant a full widget (no keyboard focus, no complex event handling). For anything that needs focus, animations, drag-and-drop, or AT by default, prefer the heavyweight tier (Scene::add_widget).

Custom item example

use teksilo_scene::{SceneItem, SceneItemPaintContext};
use teksilo_canvas::{Canvas, Point, Rect};
use teksilo_tokens::Color;

#[derive(Debug)]
struct DotItem { bounds: Rect }

impl SceneItem for DotItem {
    fn local_bounds(&self) -> Rect { self.bounds }
    fn set_local_bounds(&mut self, b: Rect) { self.bounds = b; }
    fn paint(&self, canvas: &mut Canvas, _ctx: &SceneItemPaintContext<'_>) {
        canvas.fill_rect(self.bounds, Color::RED);
    }
}

API reference

📖 Full rustdoc API for this module

pub struct ItemId

Import: use teksilo_scene::ItemId; — add teksilo-scene to [dependencies]: teksilo does not re-export it.

Opaque identifier for a SceneItem inside a Scene.

Globally unique within a process, generated by ItemId::next. ItemIds are stable across the item's lifetime in a scene, and the scene never reuses one: ItemId::next only ever counts up, so a removal can never be confused with a later, unrelated insertion.

The one door that puts a retired id back is Scene::restore, which re-inserts a RemovedItem salvaged by Scene::take at its original id. That is not reuse — it is the same item coming back — and it is deliberate: selection, magnets, the logical AT tree and every app side-map key on the id, so an undone deletion that minted a fresh id would restore the pixels and lose all of it. See Scene::restore.

#![allow(unused)]
fn main() {
pub struct ItemId(pub(crate) u64);
}

Methods

pub fn as_u64(self) -> u64

Raw numeric value, used by AccessKit's synthetic-NodeId derivation.

pub struct SceneItemPaintContext

Import: use teksilo_scene::SceneItemPaintContext; — add teksilo-scene to [dependencies]: teksilo does not re-export it.

Context handed to SceneItem::paint.

view_transform is the composed pan/zoom/rotation of the SceneView that's painting this item; the canvas already has the item's scene_transform pushed, so paint methods work in local coords without further matrix math.

theme, window_active, and enabled mirror the widget-tier PaintContext so lightweight items can resolve theme-aware colours exactly like widgets do — call some_color_prop.resolve(ctx.theme, ctx.enabled) in paint. theme is already the fully-projected theme for this pass (the render walker swaps in the inactive-window / high-contrast variant before handing it here), so items never call Theme::for_inactive_window themselves; reading ctx.theme grants automatic window-blur desaturation of accent roles.

#[non_exhaustive]: the crate hands this to every SceneItem implementation, out-of-tree ones included, and it grew three fields the last time the paint pass learned something (theme, window_active, enabled). Build one with new plus the with_* setters.

#![allow(unused)]
fn main() {
pub struct SceneItemPaintContext<'a> { /* fields */ }
}

Fields

  • view_transform: Transform2D — View pan/zoom/rotation as a single affine. Items rarely need this directly — the canvas's transform stack already accounts for it — but custom items wanting to draw at a fixed pixel size regardless of zoom can read the zoom factor from here.
  • dirty_scene_rect: Option<Rect> — Optional scene-coord region the renderer is currently repainting. Items that internally subdivide their geometry can use this to skip work outside the dirty region. None means "full repaint".
  • text_scale: f32 — Global accessibility text-scale factor (1.0 = 100 %), forwarded from the widget PaintContext. Lightweight text items that opt in (see TextItem::follow_text_scale) multiply their font size by this so they grow with the app-wide "grow all text" setting. Off-by-default because the scene already has its own pan/zoom.
  • theme: &'a Theme — Fully-projected theme for this paint pass. Already swapped to the inactive-window / high-contrast variant by the render walker — read it directly and never call Theme::for_inactive_window yourself. Pass to ColorProp::resolve to turn a role/reactive colour into a concrete Color.
  • window_active: bool — true iff the host window is focused AND not occluded (true in headless tests). Read for behavioural blur cues the accent-desaturation theme swap can't cover (e.g. hiding a caret / muting a selection).
  • enabled: bool — Effective enabled state of the item being painted (derived from ItemFlags::IS_ENABLED). Forwarded to ColorProp::resolve so a role-based colour picks its disabled variant on a disabled item.

Methods

pub fn new( view_transform: Transform2D, dirty_scene_rect: Option<Rect>, theme: &'a Theme, ) -> Self

Construct a paint context with the given view transform, optional dirty region, and the active theme. text_scale defaults to 1.0, window_active and enabled to true; use the with_* builders to carry the accessibility scale, window-active state, and per-item enabled state from the widget paint pass.

pub fn with_text_scale(mut self, text_scale: f32) -> Self (default: 1.0)

Set the global accessibility text-scale factor carried to opted-in items.

pub fn with_window_active(mut self, window_active: bool) -> Self (default: true)

Set whether the host window is currently active (focused and unoccluded).

pub fn with_enabled(mut self, enabled: bool) -> Self (default: true)

Set the effective enabled state of the item being painted.

pub struct SceneItemA11yContext

Import: use teksilo_scene::SceneItemA11yContext; — add teksilo-scene to [dependencies]: teksilo does not re-export it.

Context handed to SceneItem::accessibility.

The space every rectangle here is in

Scene coordinates, and so is everything the item emits. An item's AccessKit node carries its scene-space rectangle, and the view transform is declared once, on the node at the top of the scene subtree, via to_accesskit_affine. The consumer composes that chain for bounding_box() and inverts it per step when hit-testing, so the projection happens where AccessKit expects it — which is what makes an item's rectangle, its per-character text geometry and an explore-by-touch probe all agree at any pan, zoom or rotation.

It is also what keeps the two tiers in one space: a heavyweight card's arena bounds are scene coordinates too, and the framework walker declares the same transform on it. An item that projected its own geometry into window space instead would have it transformed a second time.

So: never multiply an emitted rectangle by the view transform. The same rule the framework states for the device scale factor, one level down.

A box, not a silhouette

The box reported for an item is its local_bounds rectangle, not its ItemShape. AccessKit has no other shape to offer, so an item whose silhouette is narrower than its box — a rounded RectItem, a stroke-only PathItem — advertises ground a pointer aimed there would miss. That is deliberate and it does not cost an AT client the element: a client identifies an element by node id, announces and spatially navigates by this rectangle, and an AccessKit action arrives addressed to the node rather than as a synthetic press at a coordinate. Advertising the narrower rectangle instead would be strictly worse — it would shrink what the element is said to cover without making any more of it reachable. The per-item magnitude is on RectItem::shape.

#[non_exhaustive]: the crate hands this to every SceneItem implementation, out-of-tree ones included, and an item's accessibility call may learn to be told more. Build one with new.

#![allow(unused)]
fn main() {
pub struct SceneItemA11yContext { /* fields */ }
}

Fields

  • view_transform: Transform2D — View transform (pan/zoom/rotation) at AT-build time. Not for projecting geometry — see the type's own documentation. It is here for an item whose content depends on the camera: how much detail to describe at the current zoom, say.
  • scene_bounds: Rect — The item's bounds in scene coordinates — the box written onto its AccessKit node.
  • item_id: ItemId — The id under which this item is being emitted.
  • local_to_scene: Transform2D — The item's own local coordinates mapped into scene coordinates — its scene_transform. An item emitting sub-element geometry of its own (a TextItem's AccessKit text runs, whose per-character positions and widths live in the same space as the node's box) needs this rather than scene_bounds, which is an axis-aligned box and says nothing about where a point inside the item lands.

Methods

pub fn new( view_transform: Transform2D, scene_bounds: Rect, item_id: ItemId, local_to_scene: Transform2D, ) -> Self

An accessibility context stated field by field — the constructor #[non_exhaustive] takes the place of a struct literal for.

The view builds its own; this exists so an item author can call their own accessibility from a test.

pub enum AppearanceWrite

Import: use teksilo_scene::AppearanceWrite; — add teksilo-scene to [dependencies]: teksilo does not re-export it.

What a SceneItem appearance setter did — and, when it did anything, the value it overwrote.

Why the old value comes out of the setter

ItemChange::AppearanceChanged carries an old, so a data layer can reverse a colour edit. That old has to come from somewhere, and the item is the only thing that has it: a fill lives inside the item, not on the scene entry.

The obvious shape — a fn fill(&self) -> Option<ColorProp> getter beside the setter — cannot be required, because most items have no fill slot at all, so it would have to be defaulted. A defaulted getter returning None would then report "there was no fill" for every third-party item that implements set_fill and forgets the getter, and an undone fill change would clear the fill instead of restoring it. Silent-wrong is the worst failure a journal can have.

Returning the old value from the setter has no default to get wrong: the code that overwrites the slot is the code that hands back what it overwrote, and the compiler will not let an implementor write one without the other. The default impl is Refused, which is exactly true of an item with no such slot and emits no change at all.

#![allow(unused)]
fn main() {
pub enum AppearanceWrite<T> { /* variants */ }
}

Variants

  • Refused — This item kind has no such slot, or rejected this particular value (TextItem always has a foreground colour, so it refuses a None fill). Nothing was written, and the scene emits no change.
  • Accepted { was: Option<T> } — The slot took the value. was is what it held immediately before — None when the slot was empty.

Methods

pub fn replaced(previous: T) -> Self

Sugar for Accepted { was: Some(previous) }.

pub fn accepted(&self) -> bool

Whether the item took the write.

pub fn into_previous(self) -> Option<Option<T>>

The overwritten slot contents, or None when the write was refused.

The outer Option is "did anything happen"; the inner one is "was the slot occupied".

pub trait SceneItem

Import: use teksilo_scene::SceneItem; — add teksilo-scene to [dependencies]: teksilo does not re-export it.

A lightweight, paint-only scene-graph item.

Implementations carry their own bounds (in local coords, anchored at the origin) and provide a paint method that draws into a Canvas. The Scene takes care of positioning, transform-chain composition, hit-test, accessibility, and repaint scheduling.

Required methods

  • SceneItem::local_bounds — AABB in local item coords.
  • SceneItem::set_local_bounds — write back the bounds field when the Scene mutates it.
  • SceneItem::paint — draw the item into the canvas. The canvas already has the item's scene_transform pushed, so coordinates are in local item space.

Optional methods

  • SceneItem::shape — the item's geometry in local coords, used for hit-test, marquee, collision and path queries alike. Default is the local AABB.
  • SceneItem::label — human-readable label for AT and debug.
  • SceneItem::register_bindings — bind reactive signals to the SceneView's repaint machinery.
  • SceneItem::accessibility — populate the AccessKit node.
#![allow(unused)]
fn main() {
pub trait SceneItem: std::fmt::Debug + 'static { /* associated items below */ }
}

Associated items

fn local_bounds(&self) -> Rect;

AABB in local item coordinates (origin at the item's anchor). The Scene composes this with the item's scene_transform to compute its scene-space AABB for the spatial index.

fn set_local_bounds(&mut self, bounds: Rect);

Write back new bounds. Called by Scene::set_local_bounds when the bounds change. Implementations update their stored bounds field; geometry-bearing items (e.g. crate::PathItem) must keep their geometry consistent with the new bounds.

Two obligations, because the Scene reads local_bounds back afterwards and stores that as the rectangle the spatial index buckets on:

  • Be idempotent. Calling twice with the same rectangle must leave the item where the first call left it. An item that only ever converges towards the request turns a setter driven per frame into an endless stream of ItemChange::LocalBoundsChanged.
  • Be honest about what you cannot do. An item that fits geometry to the box rather than adopting the box may be unable to honour an axis (a perfectly horizontal stroke has no height to stretch). Leave local_bounds reporting what you settled on rather than what was asked for, and say so in your own documentation — the Scene stores the answer, so a lie here is a lie in the index.

fn paint(&self, canvas: &mut Canvas, ctx: &SceneItemPaintContext<'_>);

Paint the item into the canvas. The canvas already has this item's scene_transform (parent chain × view) pushed, so coordinates are in local item space — (0, 0) is the item's anchor.

ctx carries the active Theme, window_active, and per-item enabled state, so colour-bearing items resolve their ColorProp fills/strokes with prop.resolve(ctx.theme, ctx.enabled).

fn set_fill(&mut self, fill: Option<ColorProp>) -> AppearanceWrite<ColorProp> { /* default implementation */ }

Replace the primary fill colour, returning what was there before. Backs SceneModel::set_item_fill / clear_item_fill. Rectangles, paths, and groups set their fill; text items map it onto their foreground colour (so a None is rejected — text always has a colour); image items have no fill. Default: AppearanceWrite::Refused.

An implementation that writes the slot must return the value it overwrote. That value is what ItemChange::AppearanceChanged's old carries, and therefore what a data layer reverses the edit from; see AppearanceWrite for why it rides out of the setter instead of coming from a getter.

fn set_stroke( &mut self, stroke: Option<(ColorProp, StrokeStyle)>, ) -> AppearanceWrite<(ColorProp, StrokeStyle)> { /* default implementation */ }

Replace the stroke (colour + StrokeStyle), returning what was there before. Backs SceneModel::set_item_stroke / clear_item_stroke. Rectangles, paths, and groups accept it; text and image items refuse. Default: AppearanceWrite::Refused.

Same obligation as set_fill: an implementation that writes the slot returns what it overwrote.

fn shape(&self) -> crate::shape::ItemShape { /* default implementation */ }

This item's geometry in local coordinates — the single source of truth for hit-test, marquee selection, collision and path queries.

Default: ItemShape::bounds of SceneItem::local_bounds, which allocates nothing and is exact for a rectangle. Override it when the item's silhouette is not its box: a stroke-only connector whose bounding box is mostly empty (PathItem), a rounded card whose corners are transparent (RectItem), a logical-only container that must let clicks fall through (GroupItem returns ItemShape::none).

Stay inside your own bounds

The shape must lie within local_bounds — that rectangle is what the spatial index buckets on and what every query broad-phases against, so a silhouette poking out of it is a silhouette that is sometimes never asked. It is also what makes the four ItemSelectionModes consistent: because the shape is inside the box, anything an …ItemShape query picks an …ItemBoundingRect query picks too. Both built-ins that narrow their shape stay inside by construction (a rounded rect is inside its rect; a PathItem derives its bounds from its geometry), and an item that widens its clickable area should widen its bounds to match — as PathItem::hit_stroke_width does.

Called once per layout pass

The SceneView refreshes a dispatch snapshot from this on every layout pass, so return a cheap clone of state you already own — an Rc<ShapeGeometry> built in your constructor — rather than building geometry here. Every ItemShape is O(1) to clone by construction; the only way to make this expensive is to flatten a path inside it.

One value, not two predicates

This returns a value, not a closure, and that is the point. Because the value is Clone + 'static and carries everything a test needs — outline, fill rule, stroke band and its StrokeSpace — the view's snapshot stores the shape itself and every query reads the same one. The pair this replaced (a &self predicate for the eager path, a boxed closure for the snapshot) had to be kept in agreement by hand.

fn thumbnail_color(&self) -> teksilo_tokens::Color { /* default implementation */ }

Dominant color to draw as the item's representation in minimap-style thumbnails. Default: an opaque mid-grey, which gives a recognisable but neutral marker for any item. Built-in items override: RectItem returns its fill, PathItem returns stroke or fill, GroupItem returns fill or stroke and paints a logical group fully transparent. ImageItem does not override — it keeps the neutral grey.

Consumed by Scene::item_thumbnails — the typical minimap input. Apps with non-standard items can override on their own SceneItem impls.

fn initial_flags(&self) -> ItemFlags { /* default implementation */ }

The flags this item should carry into the Scene at insert time. Default: ItemFlags::default() — visible, enabled, selectable. Built-ins read their accumulated builder state (e.g. .draggable(true) flips IS_DRAGGABLE); custom items override this to opt into hover acceptance, focus, clipping, or IGNORES_TRANSFORMATIONS.

Read once by Scene::add_item and stored on the entry. Subsequent flag changes go through Scene::set_flag / Scene::set_flags.

fn label(&self) -> Option<String> { /* default implementation */ }

Optional human-readable label, surfaced in debug introspection and used by the default SceneItem::accessibility impl as the AT name when an item author hasn't overridden it via the per-item .access_label(...) chain.

fn access_subtree_mode(&self) -> crate::items::AccessSubtreeMode { /* default implementation */ }

AT subtree treatment for descendants.

Inherit (default) — descendants emit AT nodes normally. Exclude — descendants are pruned from the AT tree. Merge — descendants' labels concatenate into this item's AT name and they're pruned from individual emission, so the subtree reads as a single AT element. Built-ins read this from their per-item .access_subtree(...) chain.

fn cache_mode(&self) -> crate::cache::CacheMode { /* default implementation */ }

Per-item paint caching strategy. Default CacheMode::None: the item's paint runs every frame.

Returning CacheMode::ItemCoordinate asks the SceneView to record the item's paint output in local item coordinates as a RenderFrame and replay it on subsequent frames instead of re-running paint. Only suitable for items whose visual depends solely on data the Scene knows about (geometry, flags, opacity) — not on arbitrary signal state outside local_bounds. The cache for an id is evicted on ItemChange::LocalBoundsChanged for that id.

fn register_bindings(&self, _ctx: &mut BuildContext, _view_id: WidgetId) { /* default implementation */ }

Register reactive bindings the item depends on. Called once per SceneView::build for every item in the scene, with the SceneView's WidgetId as view_id. Items with signal-bound state bind their signals here at the appropriate BindingLevel.

fn accessibility(&self, builder: &mut AccessNodeBuilder, _ctx: &SceneItemA11yContext) { /* default implementation */ }

Populate the AccessKit node for this item. Default: role Role::GraphicsObject plus the SceneItem::label as the AT name if set. The per-item .access_* builder chain layers overrides on top of whatever this method writes.