teksilo_scene/item.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The [`SceneItem`] trait and its supporting context types.
5//!
6//! Lightweight items live in a [`Scene`](crate::Scene) without arena
7//! overhead. Each carries its own bounds (in **local item coordinates**,
8//! origin at the item's anchor) and paints itself via
9//! [`SceneView`](crate::SceneView)'s paint walk. Apps implement this
10//! trait directly for custom items; built-ins live in
11//! [`crate::items`].
12//!
13//! # Coordinate model
14//!
15//! An item is positioned in its parent's coordinate space by a
16//! `local_pos: Point` plus an optional `transform: Transform2D`
17//! (rotation/scale, applied around the local origin). The Scene
18//! composes those per-item transforms up the parent chain to produce
19//! a `scene_transform` (local→scene). Hit-test inverse-transforms a
20//! scene-coord point into local coords before testing the item's
21//! [`SceneItem::shape`]; paint pushes the scene transform
22//! onto the canvas before calling [`SceneItem::paint`].
23//!
24//! ## When to use
25//!
26//! Implement [`SceneItem`] when you need a lightweight, paint-only
27//! decoration or connector that isn't interactive enough to warrant a
28//! full widget (no keyboard focus, no complex event handling). For
29//! anything that needs focus, animations, drag-and-drop, or AT by
30//! default, prefer the heavyweight tier (`Scene::add_widget`).
31//!
32//! ## Custom item example
33//!
34//! ```ignore
35//! use teksilo_scene::{SceneItem, SceneItemPaintContext};
36//! use teksilo_canvas::{Canvas, Point, Rect};
37//! use teksilo_tokens::Color;
38//!
39//! #[derive(Debug)]
40//! struct DotItem { bounds: Rect }
41//!
42//! impl SceneItem for DotItem {
43//! fn local_bounds(&self) -> Rect { self.bounds }
44//! fn set_local_bounds(&mut self, b: Rect) { self.bounds = b; }
45//! fn paint(&self, canvas: &mut Canvas, _ctx: &SceneItemPaintContext<'_>) {
46//! canvas.fill_rect(self.bounds, Color::RED);
47//! }
48//! }
49//! ```
50
51use accesskit::Role;
52use teksilo_canvas::{Canvas, Rect, StrokeStyle, Transform2D};
53use teksilo_core::accessibility::AccessNodeBuilder;
54use teksilo_core::build_context::BuildContext;
55use teksilo_core::color_prop::ColorProp;
56use teksilo_core::styles::Theme;
57use teksilo_core::widget_id::WidgetId;
58
59use crate::flags::ItemFlags;
60
61/// Opaque identifier for a [`SceneItem`] inside a [`Scene`](crate::Scene).
62///
63/// Globally unique within a process, generated by `ItemId::next`.
64/// `ItemId`s are stable across the item's lifetime in a scene, and the scene
65/// never *reuses* one: `ItemId::next` only ever counts up, so a removal can
66/// never be confused with a later, unrelated insertion.
67///
68/// The one door that puts a retired id back is
69/// [`Scene::restore`](crate::Scene::restore), which re-inserts a
70/// [`RemovedItem`](crate::RemovedItem) salvaged by
71/// [`Scene::take`](crate::Scene::take) **at its original id**. That is not
72/// reuse — it is the same item coming back — and it is deliberate: selection,
73/// magnets, the logical AT tree and every app side-map key on the id, so an
74/// undone deletion that minted a fresh id would restore the pixels and lose
75/// all of it. See [`Scene::restore`](crate::Scene::restore).
76#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
77pub struct ItemId(pub(crate) u64);
78
79impl ItemId {
80 /// Mint a fresh globally-unique id. Used internally by Scene.
81 pub(crate) fn next() -> Self {
82 use std::sync::atomic::{AtomicU64, Ordering};
83 static COUNTER: AtomicU64 = AtomicU64::new(1);
84 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
85 }
86
87 /// Raw numeric value, used by AccessKit's synthetic-NodeId derivation.
88 pub fn as_u64(self) -> u64 {
89 self.0
90 }
91}
92
93/// Context handed to [`SceneItem::paint`].
94///
95/// `view_transform` is the composed pan/zoom/rotation of the SceneView
96/// that's painting this item; the canvas already has the item's
97/// `scene_transform` pushed, so paint methods work in local coords
98/// without further matrix math.
99///
100/// `theme`, `window_active`, and `enabled` mirror the widget-tier
101/// [`PaintContext`](teksilo_core::widget::PaintContext) so lightweight items
102/// can resolve theme-aware colours exactly like widgets do — call
103/// `some_color_prop.resolve(ctx.theme, ctx.enabled)` in `paint`. `theme` is
104/// already the fully-projected theme for this pass (the render walker swaps in
105/// the inactive-window / high-contrast variant *before* handing it here), so
106/// items never call `Theme::for_inactive_window` themselves; reading
107/// `ctx.theme` grants automatic window-blur desaturation of accent roles.
108///
109/// `#[non_exhaustive]`: the crate hands this *to* every `SceneItem`
110/// implementation, out-of-tree ones included, and it grew three fields the last
111/// time the paint pass learned something (`theme`, `window_active`, `enabled`).
112/// Build one with [`new`](Self::new) plus the `with_*` setters.
113#[derive(Clone, Copy)]
114#[non_exhaustive]
115pub struct SceneItemPaintContext<'a> {
116 /// View pan/zoom/rotation as a single affine. Items rarely need
117 /// this directly — the canvas's transform stack already accounts
118 /// for it — but custom items wanting to draw at a fixed pixel
119 /// size regardless of zoom can read the zoom factor from here.
120 pub view_transform: Transform2D,
121 /// Optional scene-coord region the renderer is currently
122 /// repainting. Items that internally subdivide their geometry
123 /// can use this to skip work outside the dirty region. `None`
124 /// means "full repaint".
125 pub dirty_scene_rect: Option<Rect>,
126 /// Global accessibility text-scale factor (`1.0` = 100 %), forwarded from
127 /// the widget `PaintContext`. Lightweight text items that opt in (see
128 /// `TextItem::follow_text_scale`) multiply their font size by this so they
129 /// grow with the app-wide "grow all text" setting. Off-by-default because
130 /// the scene already has its own pan/zoom.
131 pub text_scale: f32,
132 /// Fully-projected theme for this paint pass. Already swapped to the
133 /// inactive-window / high-contrast variant by the render walker — read it
134 /// directly and never call [`Theme::for_inactive_window`] yourself. Pass
135 /// to [`ColorProp::resolve`] to turn a role/reactive colour into a
136 /// concrete [`Color`](teksilo_tokens::Color).
137 pub theme: &'a Theme,
138 /// `true` iff the host window is focused AND not occluded (`true` in
139 /// headless tests). Read for behavioural blur cues the accent-desaturation
140 /// theme swap can't cover (e.g. hiding a caret / muting a selection).
141 pub window_active: bool,
142 /// Effective enabled state of the item being painted (derived from
143 /// [`ItemFlags::IS_ENABLED`]). Forwarded to [`ColorProp::resolve`] so a
144 /// role-based colour picks its disabled variant on a disabled item.
145 pub enabled: bool,
146}
147
148impl<'a> SceneItemPaintContext<'a> {
149 /// Construct a paint context with the given view transform, optional dirty
150 /// region, and the active theme. `text_scale` defaults to `1.0`,
151 /// `window_active` and `enabled` to `true`; use the `with_*` builders to
152 /// carry the accessibility scale, window-active state, and per-item enabled
153 /// state from the widget paint pass.
154 pub fn new(
155 view_transform: Transform2D,
156 dirty_scene_rect: Option<Rect>,
157 theme: &'a Theme,
158 ) -> Self {
159 Self {
160 view_transform,
161 dirty_scene_rect,
162 text_scale: 1.0,
163 theme,
164 window_active: true,
165 enabled: true,
166 }
167 }
168
169 /// Set the global accessibility text-scale factor carried to opted-in items.
170 pub fn with_text_scale(mut self, text_scale: f32) -> Self {
171 self.text_scale = text_scale;
172 self
173 }
174
175 /// Set whether the host window is currently active (focused and unoccluded).
176 pub fn with_window_active(mut self, window_active: bool) -> Self {
177 self.window_active = window_active;
178 self
179 }
180
181 /// Set the effective enabled state of the item being painted.
182 pub fn with_enabled(mut self, enabled: bool) -> Self {
183 self.enabled = enabled;
184 self
185 }
186}
187
188/// Context handed to [`SceneItem::accessibility`].
189///
190/// # The space every rectangle here is in
191///
192/// **Scene coordinates, and so is everything the item emits.** An item's
193/// AccessKit node carries its scene-space rectangle, and the view transform is
194/// declared once, on the node at the top of the scene subtree, via
195/// [`to_accesskit_affine`](teksilo_core::accessibility::to_accesskit_affine).
196/// The consumer composes that chain for `bounding_box()` and inverts it per
197/// step when hit-testing, so the projection happens where AccessKit expects it
198/// — which is what makes an item's rectangle, its per-character text geometry
199/// and an explore-by-touch probe all agree at any pan, zoom or rotation.
200///
201/// It is also what keeps the two tiers in one space: a heavyweight card's
202/// arena bounds are scene coordinates too, and the framework walker declares
203/// the same transform on it. An item that projected its own geometry into
204/// window space instead would have it transformed a second time.
205///
206/// So: **never multiply an emitted rectangle by the view transform.** The same
207/// rule the framework states for the device scale factor, one level down.
208///
209/// # A box, not a silhouette
210///
211/// The box reported for an item is its `local_bounds` **rectangle**, not its
212/// [`ItemShape`](crate::ItemShape). AccessKit has no other shape to offer, so
213/// an item whose silhouette is narrower than its box — a rounded
214/// [`RectItem`](crate::RectItem), a stroke-only [`PathItem`](crate::PathItem) —
215/// advertises ground a pointer aimed there would miss. That is deliberate and
216/// it does not cost an AT client the element: a client identifies an element by
217/// node id, announces and spatially navigates by this rectangle, and an
218/// AccessKit action arrives addressed to the node rather than as a synthetic
219/// press at a coordinate. Advertising the narrower rectangle instead would be
220/// strictly worse — it would shrink what the element is *said* to cover without
221/// making any more of it reachable. The per-item magnitude is on
222/// [`RectItem::shape`](crate::RectItem).
223///
224/// `#[non_exhaustive]`: the crate hands this *to* every `SceneItem`
225/// implementation, out-of-tree ones included, and an item's accessibility call
226/// may learn to be told more. Build one with [`new`](Self::new).
227#[non_exhaustive]
228pub struct SceneItemA11yContext {
229 /// View transform (pan/zoom/rotation) at AT-build time.
230 ///
231 /// Not for projecting geometry — see the type's own documentation. It is
232 /// here for an item whose *content* depends on the camera: how much detail
233 /// to describe at the current zoom, say.
234 pub view_transform: Transform2D,
235 /// The item's bounds in scene coordinates — the box written onto its
236 /// AccessKit node.
237 pub scene_bounds: Rect,
238 /// The id under which this item is being emitted.
239 pub item_id: ItemId,
240 /// The item's own local coordinates mapped into scene coordinates — its
241 /// `scene_transform`.
242 ///
243 /// An item emitting sub-element geometry of its own (a
244 /// [`TextItem`](crate::TextItem)'s AccessKit text runs, whose per-character
245 /// positions and widths live in the same space as the node's box) needs
246 /// this rather than [`scene_bounds`](Self::scene_bounds), which is an
247 /// axis-aligned box and says nothing about where a point *inside* the item
248 /// lands.
249 pub local_to_scene: Transform2D,
250}
251
252impl SceneItemA11yContext {
253 /// An accessibility context stated field by field — the constructor
254 /// [`#[non_exhaustive]`](Self) takes the place of a struct literal for.
255 ///
256 /// The view builds its own; this exists so an item author can call their
257 /// own `accessibility` from a test.
258 pub fn new(
259 view_transform: Transform2D,
260 scene_bounds: Rect,
261 item_id: ItemId,
262 local_to_scene: Transform2D,
263 ) -> Self {
264 Self {
265 view_transform,
266 scene_bounds,
267 item_id,
268 local_to_scene,
269 }
270 }
271}
272
273/// What a [`SceneItem`] appearance setter did — and, when it did anything,
274/// the value it overwrote.
275///
276/// # Why the old value comes *out of the setter*
277///
278/// [`ItemChange::AppearanceChanged`](crate::ItemChange) carries an `old`, so a
279/// data layer can reverse a colour edit. That `old` has to come from
280/// somewhere, and the item is the only thing that has it: a fill lives inside
281/// the item, not on the scene entry.
282///
283/// The obvious shape — a `fn fill(&self) -> Option<ColorProp>` getter beside
284/// the setter — cannot be required, because most items have no fill slot at
285/// all, so it would have to be **defaulted**. A defaulted getter returning
286/// `None` would then report "there was no fill" for every third-party item
287/// that implements `set_fill` and forgets the getter, and an undone fill
288/// change would *clear* the fill instead of restoring it. Silent-wrong is the
289/// worst failure a journal can have.
290///
291/// Returning the old value from the setter has no default to get wrong: the
292/// code that overwrites the slot is the code that hands back what it
293/// overwrote, and the compiler will not let an implementor write one without
294/// the other. The default impl is [`Refused`](Self::Refused), which is exactly
295/// true of an item with no such slot and emits no change at all.
296#[derive(Debug, Clone)]
297#[non_exhaustive]
298pub enum AppearanceWrite<T> {
299 /// This item kind has no such slot, or rejected this particular value
300 /// ([`TextItem`](crate::TextItem) always has a foreground colour, so it
301 /// refuses a `None` fill). **Nothing was written**, and the scene emits no
302 /// change.
303 Refused,
304 /// The slot took the value. `was` is what it held immediately before —
305 /// `None` when the slot was empty.
306 Accepted {
307 /// The overwritten value, or `None` if the slot was empty.
308 was: Option<T>,
309 },
310}
311
312impl<T> AppearanceWrite<T> {
313 /// Sugar for `Accepted { was: Some(previous) }`.
314 pub fn replaced(previous: T) -> Self {
315 Self::Accepted {
316 was: Some(previous),
317 }
318 }
319
320 /// Whether the item took the write.
321 pub fn accepted(&self) -> bool {
322 matches!(self, Self::Accepted { .. })
323 }
324
325 /// The overwritten slot contents, or `None` when the write was refused.
326 ///
327 /// The outer `Option` is "did anything happen"; the inner one is "was the
328 /// slot occupied".
329 pub fn into_previous(self) -> Option<Option<T>> {
330 match self {
331 Self::Refused => None,
332 Self::Accepted { was } => Some(was),
333 }
334 }
335}
336
337/// A lightweight, paint-only scene-graph item.
338///
339/// Implementations carry their own bounds (in local coords, anchored
340/// at the origin) and provide a `paint` method that draws into a
341/// [`Canvas`]. The Scene takes care of positioning, transform-chain
342/// composition, hit-test, accessibility, and repaint scheduling.
343///
344/// # Required methods
345///
346/// * [`SceneItem::local_bounds`] — AABB in local item coords.
347/// * [`SceneItem::set_local_bounds`] — write back the bounds field
348/// when the Scene mutates it.
349/// * [`SceneItem::paint`] — draw the item into the canvas. The canvas
350/// already has the item's `scene_transform` pushed, so coordinates
351/// are in local item space.
352///
353/// # Optional methods
354///
355/// * [`SceneItem::shape`] — the item's geometry in local coords, used
356/// for hit-test, marquee, collision and path queries alike. Default
357/// is the local AABB.
358/// * [`SceneItem::label`] — human-readable label for AT and debug.
359/// * [`SceneItem::register_bindings`] — bind reactive signals to the
360/// SceneView's repaint machinery.
361/// * [`SceneItem::accessibility`] — populate the AccessKit node.
362pub trait SceneItem: std::fmt::Debug + 'static {
363 /// AABB in **local item coordinates** (origin at the item's
364 /// anchor). The Scene composes this with the item's
365 /// `scene_transform` to compute its scene-space AABB for the
366 /// spatial index.
367 fn local_bounds(&self) -> Rect;
368
369 /// Write back new bounds. Called by `Scene::set_local_bounds`
370 /// when the bounds change. Implementations update their stored
371 /// bounds field; geometry-bearing items (e.g. [`crate::PathItem`])
372 /// must keep their geometry consistent with the new bounds.
373 ///
374 /// Two obligations, because the Scene reads
375 /// [`local_bounds`](SceneItem::local_bounds) back afterwards and stores
376 /// *that* as the rectangle the spatial index buckets on:
377 ///
378 /// * **Be idempotent.** Calling twice with the same rectangle must leave
379 /// the item where the first call left it. An item that only ever
380 /// converges towards the request turns a setter driven per frame into
381 /// an endless stream of `ItemChange::LocalBoundsChanged`.
382 /// * **Be honest about what you cannot do.** An item that fits geometry
383 /// to the box rather than adopting the box may be unable to honour an
384 /// axis (a perfectly horizontal stroke has no height to stretch). Leave
385 /// `local_bounds` reporting what you settled on rather than what was
386 /// asked for, and say so in your own documentation — the Scene stores
387 /// the answer, so a lie here is a lie in the index.
388 fn set_local_bounds(&mut self, bounds: Rect);
389
390 /// Paint the item into the canvas. The canvas already has this
391 /// item's `scene_transform` (parent chain × view) pushed, so
392 /// coordinates are in **local** item space — `(0, 0)` is the
393 /// item's anchor.
394 ///
395 /// `ctx` carries the active [`Theme`],
396 /// `window_active`, and per-item `enabled` state, so colour-bearing items
397 /// resolve their [`ColorProp`] fills/strokes with
398 /// `prop.resolve(ctx.theme, ctx.enabled)`.
399 fn paint(&self, canvas: &mut Canvas, ctx: &SceneItemPaintContext<'_>);
400
401 /// Replace the primary fill colour, returning what was there before. Backs
402 /// [`SceneModel::set_item_fill`](crate::SceneModel::set_item_fill) /
403 /// [`clear_item_fill`](crate::SceneModel::clear_item_fill). Rectangles,
404 /// paths, and groups set their fill; text items map it onto their
405 /// foreground colour (so a `None` is rejected — text always has a colour);
406 /// image items have no fill. Default:
407 /// [`AppearanceWrite::Refused`].
408 ///
409 /// **An implementation that writes the slot must return the value it
410 /// overwrote.** That value is what
411 /// [`ItemChange::AppearanceChanged`](crate::ItemChange)'s `old` carries, and
412 /// therefore what a data layer reverses the edit from; see
413 /// [`AppearanceWrite`] for why it rides out of the setter instead of coming
414 /// from a getter.
415 fn set_fill(&mut self, fill: Option<ColorProp>) -> AppearanceWrite<ColorProp> {
416 let _ = fill;
417 AppearanceWrite::Refused
418 }
419
420 /// Replace the stroke (colour + [`StrokeStyle`]), returning what was there
421 /// before. Backs
422 /// [`SceneModel::set_item_stroke`](crate::SceneModel::set_item_stroke) /
423 /// [`clear_item_stroke`](crate::SceneModel::clear_item_stroke). Rectangles,
424 /// paths, and groups accept it; text and image items refuse. Default:
425 /// [`AppearanceWrite::Refused`].
426 ///
427 /// Same obligation as [`set_fill`](SceneItem::set_fill): an implementation
428 /// that writes the slot returns what it overwrote.
429 fn set_stroke(
430 &mut self,
431 stroke: Option<(ColorProp, StrokeStyle)>,
432 ) -> AppearanceWrite<(ColorProp, StrokeStyle)> {
433 let _ = stroke;
434 AppearanceWrite::Refused
435 }
436
437 /// This item's geometry in **local** coordinates — the single source of
438 /// truth for hit-test, marquee selection, collision and path queries.
439 ///
440 /// Default: [`ItemShape::bounds`](crate::ItemShape::bounds) of
441 /// [`SceneItem::local_bounds`], which
442 /// allocates nothing and is exact for a rectangle. Override it when the
443 /// item's silhouette is not its box: a stroke-only connector whose
444 /// bounding box is mostly empty ([`PathItem`](crate::PathItem)), a rounded
445 /// card whose corners are transparent ([`RectItem`](crate::RectItem)), a
446 /// logical-only container that must let clicks fall through
447 /// ([`GroupItem`](crate::GroupItem) returns
448 /// [`ItemShape::none`](crate::ItemShape::none)).
449 ///
450 /// # Stay inside your own bounds
451 ///
452 /// The shape must lie within [`local_bounds`](SceneItem::local_bounds) —
453 /// that rectangle is what the spatial index buckets on and what every
454 /// query broad-phases against, so a silhouette poking out of it is a
455 /// silhouette that is sometimes never asked. It is also what makes the
456 /// four [`ItemSelectionMode`](crate::ItemSelectionMode)s consistent:
457 /// because the shape is inside the box, anything an `…ItemShape` query
458 /// picks an `…ItemBoundingRect` query picks too. Both built-ins that
459 /// narrow their shape stay inside by construction (a rounded rect is
460 /// inside its rect; a `PathItem` *derives* its bounds from its geometry),
461 /// and an item that widens its clickable area should widen its bounds to
462 /// match — as `PathItem::hit_stroke_width` does.
463 ///
464 /// # Called once per layout pass
465 ///
466 /// The [`SceneView`](crate::SceneView) refreshes a dispatch snapshot from
467 /// this on every layout pass, so **return a cheap clone of state you
468 /// already own** — an `Rc<ShapeGeometry>` built in your constructor —
469 /// rather than building geometry here. Every `ItemShape` is O(1) to clone
470 /// by construction; the only way to make this expensive is to flatten a
471 /// path inside it.
472 ///
473 /// # One value, not two predicates
474 ///
475 /// This returns a *value*, not a closure, and that is the point. Because
476 /// the value is `Clone + 'static` and carries everything a test needs —
477 /// outline, fill rule, stroke band and its
478 /// [`StrokeSpace`](teksilo_canvas::StrokeSpace) — the view's
479 /// snapshot stores the shape itself and every query reads the same one.
480 /// The pair this replaced (a `&self` predicate for the eager path, a
481 /// boxed closure for the snapshot) had to be kept in agreement by hand.
482 fn shape(&self) -> crate::shape::ItemShape {
483 crate::shape::ItemShape::bounds(self.local_bounds())
484 }
485
486 /// Dominant color to draw as the item's representation in
487 /// minimap-style thumbnails. Default: an opaque mid-grey,
488 /// which gives a recognisable but neutral marker for any item.
489 /// Built-in items override: [`RectItem`](crate::items::RectItem)
490 /// returns its fill, [`PathItem`](crate::items::PathItem)
491 /// returns stroke or fill, [`GroupItem`](crate::items::GroupItem)
492 /// returns fill or stroke and paints a logical group fully
493 /// transparent. [`ImageItem`](crate::items::ImageItem) does not
494 /// override — it keeps the neutral grey.
495 ///
496 /// Consumed by [`Scene::item_thumbnails`](crate::Scene::item_thumbnails)
497 /// — the typical minimap input. Apps with non-standard items
498 /// can override on their own `SceneItem` impls.
499 fn thumbnail_color(&self) -> teksilo_tokens::Color {
500 teksilo_tokens::Color::new(0.6, 0.6, 0.6, 1.0)
501 }
502
503 /// The flags this item should carry into the Scene at insert
504 /// time. Default: `ItemFlags::default()` — visible, enabled,
505 /// selectable. Built-ins read their accumulated builder state
506 /// (e.g. `.draggable(true)` flips `IS_DRAGGABLE`); custom items
507 /// override this to opt into hover acceptance, focus, clipping,
508 /// or `IGNORES_TRANSFORMATIONS`.
509 ///
510 /// Read once by [`Scene::add_item`](crate::Scene::add_item) and
511 /// stored on the entry. Subsequent flag changes go through
512 /// [`Scene::set_flag`](crate::Scene::set_flag) /
513 /// [`Scene::set_flags`](crate::Scene::set_flags).
514 fn initial_flags(&self) -> ItemFlags {
515 ItemFlags::default()
516 }
517
518 /// Optional human-readable label, surfaced in debug introspection
519 /// and used by the default [`SceneItem::accessibility`] impl as
520 /// the AT name when an item author hasn't overridden it via the
521 /// per-item `.access_label(...)` chain.
522 fn label(&self) -> Option<String> {
523 None
524 }
525
526 /// AT subtree treatment for descendants.
527 ///
528 /// `Inherit` (default) — descendants emit AT nodes normally.
529 /// `Exclude` — descendants are pruned from the AT tree.
530 /// `Merge` — descendants' labels concatenate into this item's
531 /// AT name and they're pruned from individual emission, so the
532 /// subtree reads as a single AT element. Built-ins read this
533 /// from their per-item `.access_subtree(...)` chain.
534 fn access_subtree_mode(&self) -> crate::items::AccessSubtreeMode {
535 crate::items::AccessSubtreeMode::Inherit
536 }
537
538 /// Per-item paint caching strategy. Default
539 /// [`CacheMode::None`](crate::cache::CacheMode::None): the
540 /// item's `paint` runs every frame.
541 ///
542 /// Returning [`CacheMode::ItemCoordinate`](crate::cache::CacheMode::ItemCoordinate)
543 /// asks the [`SceneView`](crate::SceneView) to record the
544 /// item's paint output in **local item coordinates** as a
545 /// [`RenderFrame`](teksilo_canvas::RenderFrame) and replay it on
546 /// subsequent frames instead of re-running `paint`. Only
547 /// suitable for items whose visual depends solely on data the
548 /// Scene knows about (geometry, flags, opacity) — not on
549 /// arbitrary signal state outside `local_bounds`. The cache
550 /// for an id is evicted on
551 /// [`ItemChange::LocalBoundsChanged`](crate::ItemChange) for
552 /// that id.
553 fn cache_mode(&self) -> crate::cache::CacheMode {
554 crate::cache::CacheMode::None
555 }
556
557 /// Register reactive bindings the item depends on. Called once
558 /// per [`SceneView::build`](crate::SceneView) for every item in
559 /// the scene, with the SceneView's `WidgetId` as `view_id`. Items
560 /// with signal-bound state bind their signals here at the
561 /// appropriate [`BindingLevel`](teksilo_core::binding::BindingLevel).
562 fn register_bindings(&self, _ctx: &mut BuildContext, _view_id: WidgetId) {}
563
564 /// Populate the AccessKit node for this item. Default: role
565 /// [`Role::GraphicsObject`] plus the [`SceneItem::label`] as the
566 /// AT name if set. The per-item `.access_*` builder chain layers
567 /// overrides on top of whatever this method writes.
568 fn accessibility(&self, builder: &mut AccessNodeBuilder, _ctx: &SceneItemA11yContext) {
569 builder.set_role(Role::GraphicsObject);
570 if let Some(label) = self.label() {
571 builder.set_name(label);
572 }
573 }
574}