ItemId
The SceneItem trait and its supporting context types.
Public types
| Kind | Name |
|---|---|
struct | ItemId — Opaque identifier for a SceneItem inside a Scene |
struct | SceneItemPaintContext — Context handed to SceneItem::paint |
struct | SceneItemA11yContext — Context handed to SceneItem::accessibility |
enum | AppearanceWrite — What a SceneItem appearance setter did — and, when it did anything, the value it overwrote |
trait | SceneItem — A lightweight, paint-only scene-graph item |
Public functions
ItemId
| Returns | Function |
|---|---|
| Methods | |
u64 | as_u64() |
SceneItemPaintContext
| Returns | Function |
|---|---|
| Constructors | |
Self | new(view_transform: Transform2D, dirty_scene_rect: Option<Rect>, theme: &'a Theme) |
| Builder methods | |
Self | with_text_scale(text_scale: f32) |
Self | with_window_active(window_active: bool) |
Self | with_enabled(enabled: bool) |
SceneItemA11yContext
| Returns | Function |
|---|---|
| Constructors | |
Self | new(view_transform: Transform2D, scene_bounds: Rect, item_id: ItemId, local_to_scene: Transform2D) |
AppearanceWrite
| Returns | Function |
|---|---|
| Constructors | |
Self | replaced(previous: T) |
| Methods | |
bool | accepted() |
Option<Option<T>> | into_previous() |
SceneItem
| Returns | Function |
|---|---|
| Methods | |
Rect | local_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.Nonemeans "full repaint".text_scale:f32— Global accessibility text-scale factor (1.0= 100 %), forwarded from the widgetPaintContext. Lightweight text items that opt in (seeTextItem::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 callTheme::for_inactive_windowyourself. Pass toColorProp::resolveto turn a role/reactive colour into a concreteColor.window_active:bool—trueiff the host window is focused AND not occluded (truein 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 fromItemFlags::IS_ENABLED). Forwarded toColorProp::resolveso 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 — itsscene_transform. An item emitting sub-element geometry of its own (aTextItem's AccessKit text runs, whose per-character positions and widths live in the same space as the node's box) needs this rather thanscene_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 (TextItemalways has a foreground colour, so it refuses aNonefill). Nothing was written, and the scene emits no change.Accepted{ was: Option<T> }— The slot took the value.wasis what it held immediately before —Nonewhen 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'sscene_transformpushed, 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_boundsreporting 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.