teksilo_scene/scene.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! The [`Scene`] data model — the owner of all items in a pannable/zoomable
5//! scene.
6//!
7//! `Scene` holds a flat list of entries in a parent-relative scene-graph, plus
8//! a pluggable [`SpatialIndex`] for rectangular queries. Items are positioned
9//! by `local_pos` (in their parent's coordinate frame, or scene-root if they
10//! have none) and an optional `transform` (rotation/scale around the local
11//! origin); the Scene composes those up the parent chain to derive each item's
12//! `scene_transform` and axis-aligned bounding box for hit-test, paint, and
13//! culling. Two content tiers coexist in one `Scene`: heavyweight `Widget`s
14//! (full focus/animation/DnD/AT — placed at scene coordinates) and lightweight
15//! [`SceneItem`]s (paint-only, no arena overhead, thousands
16//! cheap). All mutations update the [`SpatialIndex`] in lockstep, so
17//! [`Scene::items_in_rect`] and [`Scene::item_at`] stay `O(visible)`.
18//!
19//! `Scene` is rarely used directly. The normal entry point is
20//! [`SceneModel`](crate::SceneModel), a cloneable `Rc<RefCell<Scene>>` handle
21//! with `&self` mutators (the `ListModel` pattern) that lets multiple handlers
22//! and multiple [`SceneView`](crate::SceneView)s share one model.
23//!
24//! ## When to use
25//!
26//! Use `Scene` (via `SceneModel`) when you need a pannable/zoomable canvas —
27//! story corkboards, node-graph editors, mind maps, timeline views, CAD
28//! canvases, or simple spatial maps. Prefer a plain `ListView` or `TreeView`
29//! when the content is linear or tree-shaped without spatial relationships.
30//!
31//! ## Example
32//!
33//! ```rust
34//! use teksilo_scene::{Scene, ItemChange, SceneLayer};
35//! use teksilo_scene::{RectItem, ItemId};
36//! use teksilo_canvas::{Point, Rect};
37//! use teksilo_tokens::Color;
38//!
39//! let mut scene = Scene::new();
40//!
41//! // Add a lightweight rectangle item at scene coordinates (50, 50).
42//! let id: ItemId = scene.add_item(
43//! RectItem::new(Rect::new(0.0, 0.0, 80.0, 40.0)).fill(Color::BLUE),
44//! Point::new(50.0, 50.0),
45//! );
46//!
47//! // Observe every mutation — fires after the change is already applied.
48//! let _guard = scene.item_change_signal().observe(|change| {
49//! if let ItemChange::LocalPosChanged { id: _, old: _, new } = change {
50//! let _ = new; // react to the new position
51//! }
52//! });
53//!
54//! // Move the item; the observer fires and the spatial index updates.
55//! scene.set_local_pos(id, Point::new(100.0, 100.0));
56//! assert_eq!(scene.scene_pos(id), Some(Point::new(100.0, 100.0)));
57//! ```
58
59use std::cell::Cell;
60use std::collections::HashMap;
61use std::collections::HashSet;
62use std::rc::Rc;
63
64use crate::a11y::{A11yCategory, A11yGroup, A11yGroupBuilder, A11yGroupId, A11yNode, A11yRelation};
65use crate::flags::ItemFlags;
66use crate::index::{GridHashIndex, SpatialIndex};
67use crate::item::{ItemId, SceneItem};
68use crate::item_handlers::SceneItemHandlerSet;
69use crate::magnet::{Magnet, MagnetId, MagnetRef, MagnetSnap, MagnetVerdict};
70use crate::transform::local_to_parent;
71use teksilo_canvas::{Path, Point, Rect, StrokeStyle, Transform2D, Vec2};
72use teksilo_core::color_prop::ColorProp;
73use teksilo_core::signal::Signal;
74use teksilo_core::widget::Widget;
75
76/// A change to an item's state, fired through
77/// [`Scene::item_change_signal`] for every mutation. Apps observe
78/// to wire snap-to-grid, validation, side effects, etc. The model
79/// is "fire after the change has been applied" — by the time the
80/// observer sees the event, the Scene already reflects it.
81#[derive(Debug, Clone, Copy, PartialEq)]
82pub enum ItemChange {
83 /// `set_local_pos`: position in parent coords moved.
84 LocalPosChanged { id: ItemId, old: Point, new: Point },
85 /// `set_local_bounds`: AABB in local coords changed.
86 LocalBoundsChanged { id: ItemId, old: Rect, new: Rect },
87 /// `set_transform`: local→parent transform changed.
88 TransformChanged { id: ItemId },
89 /// `set_visible` flipped IS_VISIBLE.
90 VisibilityChanged { id: ItemId, visible: bool },
91 /// `set_flags` / `set_flag` changed the bitset.
92 FlagsChanged {
93 id: ItemId,
94 old: ItemFlags,
95 new: ItemFlags,
96 },
97 /// `set_opacity`: local opacity multiplier changed.
98 OpacityChanged { id: ItemId, old: f32, new: f32 },
99 /// `set_z`: paint z-order changed.
100 ZChanged { id: ItemId, old: f32, new: f32 },
101 /// `set_layer`: the Under/Over paint band changed.
102 LayerChanged {
103 id: ItemId,
104 old: SceneLayer,
105 new: SceneLayer,
106 },
107 /// `set_item_parent`: logical parent changed.
108 ParentChanged {
109 id: ItemId,
110 old: Option<ItemId>,
111 new: Option<ItemId>,
112 },
113 /// `remove`: item is gone.
114 Removed { id: ItemId },
115 /// `add_item` / `add_widget`: item was inserted.
116 Added { id: ItemId },
117 /// `set_payload`: the type-erased payload of a `Delegated` heavyweight
118 /// entry was replaced. A `SceneView` rebuilds that entry's widget
119 /// (re-invokes its delegate) on the next build. Routed through
120 /// `emit_item_change`, so `mutation_seq` advances and the AT-walk gate
121 /// notices.
122 PayloadChanged { id: ItemId },
123 /// `set_item_fill` / `set_item_stroke` / `clear_item_*`: a lightweight
124 /// item's paint-only appearance (fill / stroke colour or style) changed.
125 /// Never moves geometry, so the observing `SceneView` evicts the item's
126 /// cached frame and repaints **without** relayout or rebuild.
127 AppearanceChanged { id: ItemId },
128}
129
130/// Which paint band a lightweight [`SceneItem`] sits in, relative to
131/// the heavyweight widget tier.
132///
133/// A `SceneView` paints in three passes: lightweight `Under` items
134/// (its `paint`, a backdrop), then the heavyweight widget children
135/// (the arena child-walk), then lightweight `Over` items (its
136/// `post_paint`, a foreground). Within each band, `z` still orders
137/// items among themselves.
138///
139/// This is a binary band, not a continuous z across the tiers, because
140/// the render walker offers exactly two lightweight paint positions
141/// (before and after the child subtree). The heavyweight tier is one
142/// contiguous block in between — to interleave a lightweight item
143/// *between* two specific heavyweight nodes you must promote it to a
144/// heavyweight widget. `Under` is the default (background furniture:
145/// connectors, grids, decorations); `Over` is for foreground overlays
146/// that must sit above the cards (selection halos, highlighted edges).
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
148pub enum SceneLayer {
149 /// Painted under the heavyweight widget children (the default).
150 #[default]
151 Under,
152 /// Painted over the heavyweight widget children.
153 Over,
154}
155
156/// Which axes a [`SceneView`](crate::SceneView) is allowed to pan
157/// along. Set on the [`Scene`] (not the View) because a given scene
158/// model often makes sense at one orientation only — a horizontal
159/// timeline, a vertical timeline, a fixed-extent diagram. All views
160/// of the same scene inherit the constraint.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
162pub enum PanAxes {
163 /// No user-driven pan in either axis. Programmatic
164 /// [`SceneView::set_pan`](crate::SceneView::set_pan) /
165 /// [`pan_to`](crate::SceneView::pan_to) become no-ops too.
166 None,
167 /// Pan only along X. Vertical scroll deltas pass through to
168 /// ancestor scrollables.
169 Horizontal,
170 /// Pan only along Y. Horizontal scroll deltas pass through to
171 /// ancestor scrollables.
172 Vertical,
173 /// Default: pan freely in both axes.
174 #[default]
175 Both,
176}
177
178/// Reactive interaction-policy bundle owned by [`Scene`]. Apps
179/// configure pan/zoom behaviour by writing to these signals; gesture
180/// closures in [`SceneView`](crate::SceneView) read them live, so
181/// runtime mode switches (e.g. a toolbar toggling pan locks) take
182/// effect on the next event without rebuilding the view.
183///
184/// All four signals are exposed individually via [`Scene`] accessors
185/// (`pan_axes_signal`, `pan_bounds_signal`, `zoom_range_signal`,
186/// `zoomable_signal`). Per-(sub-)scene independence falls out of the
187/// model: each nested `SceneView` carries its own `Scene` with its
188/// own `SceneConstraints`.
189///
190/// View-level *tightening* overrides (`pan_bounds_override`,
191/// `zoom_range_override`) layer on top per-`SceneView` — the
192/// effective constraint is the intersection. Two views over the
193/// same `Scene` can lock down independently; neither can loosen
194/// what the `Scene` declares.
195pub struct SceneConstraints {
196 pan_axes: Signal<PanAxes>,
197 /// Scene-coord rectangle that the visible viewport must stay
198 /// inside. `None` (default) = unconstrained. When `Some(r)`,
199 /// pan is clamped so the visible scene region overlaps the
200 /// rect; when the viewport is bigger than the rect, the rect
201 /// is centered.
202 pan_bounds: Signal<Option<Rect>>,
203 /// Inclusive `[min, max]` clamp on zoom factor. `None` =
204 /// unconstrained from the `Scene` side (the `SceneView` may
205 /// still impose its own range override).
206 zoom_range: Signal<Option<std::ops::RangeInclusive<f32>>>,
207 zoomable: Signal<bool>,
208}
209
210impl SceneConstraints {
211 fn new() -> Self {
212 Self {
213 pan_axes: Signal::new(PanAxes::Both),
214 pan_bounds: Signal::new(None),
215 zoom_range: Signal::new(None),
216 zoomable: Signal::new(true),
217 }
218 }
219
220 /// Reactive pan-axes signal. Gesture handlers read live.
221 pub fn pan_axes_signal(&self) -> Signal<PanAxes> {
222 self.pan_axes.clone()
223 }
224 /// Reactive pan-bounds signal. `None` = unconstrained.
225 pub fn pan_bounds_signal(&self) -> Signal<Option<Rect>> {
226 self.pan_bounds.clone()
227 }
228 /// Reactive zoom-range signal. `None` = unconstrained from
229 /// the Scene side.
230 pub fn zoom_range_signal(&self) -> Signal<Option<std::ops::RangeInclusive<f32>>> {
231 self.zoom_range.clone()
232 }
233 /// Reactive zoomable-on/off signal. Equivalent to a zero-width
234 /// zoom_range — kept as a separate boolean for clarity and
235 /// efficient short-circuit at gesture time.
236 pub fn zoomable_signal(&self) -> Signal<bool> {
237 self.zoomable.clone()
238 }
239}
240
241impl Default for SceneConstraints {
242 fn default() -> Self {
243 Self::new()
244 }
245}
246
247impl std::fmt::Debug for SceneConstraints {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("SceneConstraints")
250 .field("pan_axes", &self.pan_axes.get())
251 .field("pan_bounds", &self.pan_bounds.get())
252 .field("zoom_range", &self.zoom_range.get())
253 .field("zoomable", &self.zoomable.get())
254 .finish()
255 }
256}
257
258/// A single entry in a [`Scene`]. The two variants mirror the two
259/// content tiers: heavyweight `Widget`s consumed into the arena at
260/// build time, and lightweight `SceneItem`s painted directly from the
261/// SceneView's paint walk.
262pub(crate) struct SceneEntry {
263 pub(crate) id: ItemId,
264 /// Origin of the item's local coordinate frame, in **parent**
265 /// coordinates (or scene coords if `parent == None`).
266 pub(crate) local_pos: Point,
267 /// Item's AABB in **local** coordinates. For lightweight items
268 /// this is read once at insert time from `SceneItem::local_bounds`
269 /// and tracked through `Scene::set_local_bounds`. For widgets it
270 /// records the size requested at `add_widget` time, anchored at
271 /// the origin: `Rect::new(0, 0, w, h)`.
272 pub(crate) local_bounds: Rect,
273 /// Optional rotation/scale applied around the local origin
274 /// before translating by `local_pos`. Default identity.
275 pub(crate) transform: Transform2D,
276 pub(crate) kind: SceneEntryKind,
277 /// Z-order for paint — higher values paint *later* (on top).
278 /// Equal-z entries fall back to insertion order. Applies to **both**
279 /// tiers: lightweight items sort within their band each paint, and
280 /// heavyweight widget entries restack the SceneView's arena children
281 /// by z on the next rebuild (see [`Scene::set_z`]).
282 pub(crate) z: f32,
283 /// Which lightweight paint band this item sits in relative to the
284 /// heavyweight tier — [`SceneLayer::Under`] (default, backdrop) or
285 /// [`SceneLayer::Over`] (foreground). Lightweight tier only; ignored
286 /// for heavyweight widget entries (they paint via the arena).
287 pub(crate) layer: SceneLayer,
288 /// Logical parent. `None` means the item is rooted directly in
289 /// the Scene. Composes coordinate frames: a child's `local_pos`
290 /// is in the parent's local frame, and the child's
291 /// `scene_transform` is the parent's `scene_transform` composed
292 /// with the child's `local_to_parent`.
293 pub(crate) parent: Option<ItemId>,
294 /// Per-item behavior flags. Read once from
295 /// [`SceneItem::initial_flags`] at insert time, mutable through
296 /// [`Scene::set_flags`] / [`Scene::set_flag`].
297 pub(crate) flags: ItemFlags,
298 /// Multiplicative opacity in `[0.0, 1.0]`. Composes through the
299 /// parent chain: an item's `effective_opacity` is the product
300 /// of every ancestor's opacity and its own.
301 pub(crate) opacity: f32,
302 /// Per-item event handlers, cursor and tooltip overrides.
303 /// `None` until the app calls `Scene::set_item_handlers` /
304 /// `Scene::handlers_mut`.
305 pub(crate) handlers: Option<Box<SceneItemHandlerSet>>,
306 /// Whether the item's `local_bounds` may change between
307 /// build/layout passes (a signal-driven AABB). Static items
308 /// (default) snapshot bounds at insert and only update through
309 /// explicit [`Scene::set_local_bounds`]. Dynamic items added via
310 /// [`Scene::add_item_dynamic`] have their `local_bounds` re-read
311 /// each rebuild via [`Scene::refresh_dynamic_bounds`], with the
312 /// spatial index re-bucketed when the value changes.
313 pub(crate) dynamic_bounds: bool,
314}
315
316/// How a heavyweight `Widget` entry makes its instance available to a
317/// `SceneView`. The two variants are the single-view and multi-view
318/// production paths.
319pub(crate) enum WidgetSource {
320 /// Single-view sugar ([`Scene::add_widget`]). The first `SceneView` to
321 /// build drains the `Option` via `take()`; subsequent views (sharing the
322 /// same [`SceneModel`](crate::SceneModel)) find `None` and produce no
323 /// arena child for this entry. Use [`Scene::add_widget_delegated`] +
324 /// a view delegate for multi-view content.
325 Once(Option<Box<dyn Widget>>),
326 /// Multi-view path ([`Scene::add_widget_delegated`], surfaced as
327 /// [`SceneModel::add_widget_item`](crate::SceneModel::add_widget_item)).
328 /// Each view calls its own delegate with this type-erased `payload`
329 /// to build a fresh `Widget` instance. The payload is `Rc` so a view
330 /// can clone it out of a model borrow before invoking the delegate.
331 Delegated { payload: Rc<dyn std::any::Any> },
332}
333
334pub(crate) enum SceneEntryKind {
335 /// A heavyweight `Widget` materialised into the arena, via either the
336 /// single-view `Once` slot or the multi-view `Delegated` payload.
337 Widget(WidgetSource),
338 /// A lightweight `SceneItem` that lives in the scene
339 /// permanently; painted by the SceneView's paint walk.
340 Item(Box<dyn SceneItem>),
341}
342
343/// The data model behind a `SceneView`: a flat list of entries in a
344/// parent-relative scene-graph plus a [`SpatialIndex`] for rectangular
345/// queries.
346///
347/// The Scene itself does no rendering — it's a passive container the view
348/// reads from at build / place / paint time. Mutations (`add_widget`,
349/// `add_item`, `set_local_pos`, `set_transform`, `set_local_bounds`, `remove`)
350/// update the spatial index in lockstep, so `items_in_rect`, `item_at`, and
351/// SceneView's viewport-cull path are all `O(visible)` instead of `O(N)`. When
352/// a parent's `local_pos` or `transform` changes, every descendant's
353/// scene-AABB shifts; the Scene re-buckets the entire subtree.
354///
355/// In practice most callers operate on a [`SceneModel`](crate::SceneModel)
356/// handle (`Rc<RefCell<Scene>>` with `&self` mutators) rather than a bare
357/// `Scene`. Prefer `SceneModel` for any widget or handler that needs to share
358/// the scene across multiple owners.
359pub struct Scene {
360 pub(crate) entries: Vec<SceneEntry>,
361 /// `ItemId` → index into `entries` for O(1) lookup.
362 entry_index: HashMap<ItemId, usize>,
363 index: Box<dyn SpatialIndex>,
364
365 /// User-declared scene extent. `None` means "auto-compute from
366 /// items each query". Set via [`Scene::set_scene_rect`]. Used
367 /// by [`SceneView::adopt_scene_size`](crate::SceneView::adopt_scene_size).
368 /// (Distinct from `constraints.pan_bounds` which clamps the
369 /// visible viewport.)
370 user_scene_rect: Option<Rect>,
371 /// Reactive interaction policy: pan axes, pan bounds, zoom
372 /// range, zoomable on/off. Apps mutate via the dedicated
373 /// `Scene::pan_axes` / `set_pan_bounds` / `set_zoom_range` /
374 /// `zoomable` methods (still classic mutator shape) or read
375 /// the underlying signals via the `*_signal` accessors for
376 /// live observation.
377 constraints: SceneConstraints,
378 /// Reactive change signal. Every mutation fires an
379 /// [`ItemChange`] through this signal so apps can observe
380 /// geometry / visibility / parent / z / opacity changes.
381 item_change_signal: Signal<ItemChange>,
382 /// Reactive change counter for the *logical AT structure* (groups,
383 /// parents, relations, live, landmarks, categories). These mutations are
384 /// not item geometry, so they do not flow through `item_change_signal`;
385 /// `SceneView` observes this separately to re-walk the AccessKit tree. The
386 /// AT tree is fully separate from the visual scene, so it needs its own
387 /// notification channel.
388 a11y_change_signal: Signal<u64>,
389 /// Monotonic counter of *every* model mutation — item geometry / visibility
390 /// / structure (each [`ItemChange`] fire) **and** logical-AT structure (each
391 /// `bump_a11y_change`). Read via [`Scene::mutation_version`]. `SceneView`
392 /// gates its (expensive) AccessKit re-walk on this advancing, so a `build()`
393 /// triggered purely by dynamic-bounds churn it already accounted for doesn't
394 /// re-walk the AT tree every frame. A plain `Cell` because the bump path
395 /// (`bump_mutation`) is `&self` (shared with `bump_a11y_change`).
396 mutation_seq: Cell<u64>,
397
398 // --- logical AT structure ----------------------------------------
399 pub(crate) a11y_groups: Vec<A11yGroup>,
400 pub(crate) a11y_group_index: HashMap<A11yGroupId, usize>,
401 pub(crate) a11y_parents: HashMap<A11yNode, A11yNode>,
402 pub(crate) a11y_relations: Vec<(A11yNode, A11yRelation, A11yNode)>,
403 pub(crate) a11y_live: HashMap<A11yNode, accesskit::Live>,
404 pub(crate) a11y_landmarks: HashMap<A11yNode, accesskit::Role>,
405 pub(crate) a11y_categories: HashMap<A11yNode, Vec<A11yCategory>>,
406
407 // --- magnetism ---------------------------------------------------
408 /// Magnets attached to each item, in insertion order. Kept in a
409 /// side map (not on `SceneEntry`) so the magnet subsystem is
410 /// modular — the same shape as the logical-AT maps above.
411 magnets: HashMap<ItemId, Vec<(MagnetId, Magnet)>>,
412 /// Reverse lookup `MagnetId -> owning ItemId` for O(1) resolution
413 /// of a magnet's owner (and cleanup on `remove_magnet`).
414 magnet_owner: HashMap<MagnetId, ItemId>,
415}
416
417impl Scene {
418 /// An empty scene with the default [`GridHashIndex`].
419 pub fn new() -> Self {
420 Self::with_index(Box::new(GridHashIndex::default()))
421 }
422
423 /// An empty scene with a custom [`SpatialIndex`].
424 pub fn with_index(index: Box<dyn SpatialIndex>) -> Self {
425 Self {
426 entries: Vec::new(),
427 entry_index: HashMap::new(),
428 index,
429 user_scene_rect: None,
430 constraints: SceneConstraints::new(),
431 item_change_signal: Signal::new(ItemChange::Added { id: ItemId(0) }),
432 a11y_change_signal: Signal::new(0),
433 mutation_seq: Cell::new(0),
434 a11y_groups: Vec::new(),
435 a11y_group_index: HashMap::new(),
436 a11y_parents: HashMap::new(),
437 a11y_relations: Vec::new(),
438 a11y_live: HashMap::new(),
439 a11y_landmarks: HashMap::new(),
440 a11y_categories: HashMap::new(),
441 magnets: HashMap::new(),
442 magnet_owner: HashMap::new(),
443 }
444 }
445
446 // -----------------------------------------------------------------
447 // Insertion
448 // -----------------------------------------------------------------
449
450 /// Place a heavyweight `Widget` at `local_rect`'s origin, sized
451 /// `local_rect.size`. The rect is interpreted as
452 /// `(local_pos = local_rect.origin, local_bounds = (0, 0, w, h))`.
453 /// Returns the [`ItemId`] for later mutation. The widget is
454 /// consumed at SceneView build time and added to the arena.
455 pub fn add_widget<W: Widget + 'static>(&mut self, widget: W, local_rect: Rect) -> ItemId {
456 let id = ItemId::next();
457 let local_pos = Point::new(local_rect.x, local_rect.y);
458 let local_bounds = Rect::new(0.0, 0.0, local_rect.width, local_rect.height);
459 let entry = SceneEntry {
460 id,
461 local_pos,
462 local_bounds,
463 transform: Transform2D::identity(),
464 kind: SceneEntryKind::Widget(WidgetSource::Once(Some(Box::new(widget)))),
465 z: 0.0,
466 layer: SceneLayer::Under,
467 parent: None,
468 flags: ItemFlags::default(),
469 opacity: 1.0,
470 handlers: None,
471 dynamic_bounds: false,
472 };
473 self.push_entry(entry)
474 }
475
476 /// Multi-view heavyweight insertion: store a type-erased `payload`; each
477 /// [`SceneView`](crate::SceneView) builds its own instance via its
478 /// delegate. Surfaced publicly as
479 /// [`SceneModel::add_widget_item`](crate::SceneModel::add_widget_item).
480 pub(crate) fn add_widget_delegated(
481 &mut self,
482 payload: Rc<dyn std::any::Any>,
483 local_rect: Rect,
484 ) -> ItemId {
485 let id = ItemId::next();
486 let local_pos = Point::new(local_rect.x, local_rect.y);
487 let local_bounds = Rect::new(0.0, 0.0, local_rect.width, local_rect.height);
488 let entry = SceneEntry {
489 id,
490 local_pos,
491 local_bounds,
492 transform: Transform2D::identity(),
493 kind: SceneEntryKind::Widget(WidgetSource::Delegated { payload }),
494 z: 0.0,
495 layer: SceneLayer::Under,
496 parent: None,
497 flags: ItemFlags::default(),
498 opacity: 1.0,
499 handlers: None,
500 dynamic_bounds: false,
501 };
502 self.push_entry(entry)
503 }
504
505 /// Replace the type-erased payload of a `Delegated` heavyweight entry and
506 /// fire [`ItemChange::PayloadChanged`].
507 ///
508 /// # Panics
509 ///
510 /// Panics if `id` is unknown, refers to a `Once` widget entry, or refers to
511 /// a lightweight item. These are all caller-side precondition violations:
512 /// the caller obtained `id` from `add_widget_item` and is responsible for
513 /// only passing it back to `set_payload` while the entry is alive.
514 pub(crate) fn set_payload(&mut self, id: ItemId, payload: Rc<dyn std::any::Any>) {
515 let Some(&pos) = self.entry_index.get(&id) else {
516 panic!("set_payload: unknown ItemId {id:?}");
517 };
518 match &mut self.entries[pos].kind {
519 SceneEntryKind::Widget(WidgetSource::Delegated { payload: slot }) => *slot = payload,
520 _ => panic!("set_payload: {id:?} is not a Delegated widget entry"),
521 }
522 // Entry borrow dropped above; `emit_item_change` is `&self`.
523 self.emit_item_change(ItemChange::PayloadChanged { id });
524 }
525
526 /// The current type-erased payload of a `Delegated` heavyweight entry.
527 /// `None` for unknown ids, `Once` widget entries, and lightweight items.
528 pub(crate) fn payload(&self, id: ItemId) -> Option<Rc<dyn std::any::Any>> {
529 let pos = *self.entry_index.get(&id)?;
530 match &self.entries[pos].kind {
531 SceneEntryKind::Widget(WidgetSource::Delegated { payload }) => Some(payload.clone()),
532 _ => None,
533 }
534 }
535
536 /// Drain every still-pending `Once` heavyweight widget, in entry order.
537 /// Each is `take()`n from its slot, so a second `SceneView` over the same
538 /// model returns nothing for it — `Once` widgets are single-view. Called
539 /// by `SceneView::build`.
540 pub(crate) fn drain_all_once(&mut self) -> Vec<(ItemId, Box<dyn Widget>)> {
541 let mut out = Vec::new();
542 for entry in self.entries.iter_mut() {
543 if let SceneEntryKind::Widget(WidgetSource::Once(pending)) = &mut entry.kind
544 && let Some(w) = pending.take()
545 {
546 out.push((entry.id, w));
547 }
548 }
549 out
550 }
551
552 /// `(id, payload)` for every `Delegated` heavyweight entry, in entry order.
553 /// The payload `Rc` is cloned so the caller can drop the model borrow before
554 /// invoking its delegate (the reentrancy contract). Called by `SceneView::build`.
555 pub(crate) fn delegated_payloads(&self) -> Vec<(ItemId, Rc<dyn std::any::Any>)> {
556 self.entries
557 .iter()
558 .filter_map(|e| match &e.kind {
559 SceneEntryKind::Widget(WidgetSource::Delegated { payload }) => {
560 Some((e.id, payload.clone()))
561 }
562 _ => None,
563 })
564 .collect()
565 }
566
567 /// Ids of every heavyweight `Widget` entry (`Once` and `Delegated`), in
568 /// entry order. Used by `SceneView::build` for child ordering and the
569 /// orphan-reap live-set.
570 pub(crate) fn heavyweight_ids(&self) -> Vec<ItemId> {
571 self.entries
572 .iter()
573 .filter_map(|e| match &e.kind {
574 SceneEntryKind::Widget(_) => Some(e.id),
575 SceneEntryKind::Item(_) => None,
576 })
577 .collect()
578 }
579
580 /// Place a lightweight [`SceneItem`] at `local_pos`. The item's
581 /// `local_bounds` and `initial_flags` are read once at insert
582 /// time. The item is **not** added to the arena — it's painted
583 /// directly from `SceneView::paint`.
584 pub fn add_item<I: SceneItem + 'static>(&mut self, item: I, local_pos: Point) -> ItemId {
585 self.add_item_inner(item, local_pos, false)
586 }
587
588 /// Like [`add_item`](Self::add_item) but flags the entry as
589 /// having signal-driven `local_bounds`. The Scene re-reads
590 /// `item.local_bounds()` each rebuild via
591 /// [`refresh_dynamic_bounds`](Self::refresh_dynamic_bounds) — the
592 /// SceneView calls that at the start of every build pass. The
593 /// spatial index gets re-bucketed when the read-back differs
594 /// from the cached value, so `items_in_rect` / hit-test stay
595 /// correct without app-side `set_local_bounds` plumbing.
596 ///
597 /// Use only when the bounds genuinely depend on a `Signal<T>`
598 /// the item reads in `local_bounds`. Static items pay an
599 /// unnecessary per-rebuild bounds read otherwise; prefer
600 /// [`add_item`](Self::add_item) for the common case.
601 pub fn add_item_dynamic<I: SceneItem + 'static>(
602 &mut self,
603 item: I,
604 local_pos: Point,
605 ) -> ItemId {
606 self.add_item_inner(item, local_pos, true)
607 }
608
609 fn add_item_inner<I: SceneItem + 'static>(
610 &mut self,
611 item: I,
612 local_pos: Point,
613 dynamic_bounds: bool,
614 ) -> ItemId {
615 self.insert_boxed(Box::new(item), local_pos, dynamic_bounds)
616 }
617
618 /// The single lightweight-entry construction site, shared by the generic
619 /// [`add_item`](Self::add_item) / [`add_item_dynamic`](Self::add_item_dynamic)
620 /// path (via `add_item_inner`) and the boxed-`dyn`
621 /// [`add_boxed_item`](Self::add_boxed_item) path, so a future `SceneEntry`
622 /// field can't be added to one and silently missed by the other.
623 fn insert_boxed(
624 &mut self,
625 item: Box<dyn SceneItem>,
626 local_pos: Point,
627 dynamic_bounds: bool,
628 ) -> ItemId {
629 let id = ItemId::next();
630 let local_bounds = item.local_bounds();
631 let flags = item.initial_flags();
632 let entry = SceneEntry {
633 id,
634 local_pos,
635 local_bounds,
636 transform: Transform2D::identity(),
637 kind: SceneEntryKind::Item(item),
638 z: 0.0,
639 layer: SceneLayer::Under,
640 parent: None,
641 flags,
642 opacity: 1.0,
643 handlers: None,
644 dynamic_bounds,
645 };
646 self.push_entry(entry)
647 }
648
649 /// Re-read every dynamic item's current `local_bounds`, applying
650 /// `set_local_bounds` (and re-bucketing the spatial index) for
651 /// any entry whose value has changed. No-op for static entries.
652 /// Called by [`SceneView`](crate::SceneView) at the start of each
653 /// `build()` so signal-driven bounds propagate to bucketing
654 /// without explicit app-side calls.
655 ///
656 /// Returns `true` if at least one dynamic entry's bounds changed this call.
657 /// `SceneView` uses the `true → false` transition (an animation settling) as
658 /// the one moment to walk the final animated bounds into the AccessKit tree,
659 /// since it otherwise suppresses per-frame AT re-walks during the animation.
660 pub fn refresh_dynamic_bounds(&mut self) -> bool {
661 // Snapshot ids first to avoid borrow conflicts.
662 let dynamic_ids: Vec<ItemId> = self
663 .entries
664 .iter()
665 .filter(|e| e.dynamic_bounds)
666 .map(|e| e.id)
667 .collect();
668 let mut changed = false;
669 for id in dynamic_ids {
670 let Some(&pos) = self.entry_index.get(&id) else {
671 continue;
672 };
673 let SceneEntryKind::Item(item) = &self.entries[pos].kind else {
674 continue;
675 };
676 let new = item.local_bounds();
677 if new != self.entries[pos].local_bounds {
678 self.set_local_bounds(id, new);
679 changed = true;
680 }
681 }
682 changed
683 }
684
685 fn push_entry(&mut self, entry: SceneEntry) -> ItemId {
686 let id = entry.id;
687 let pos = self.entries.len();
688 self.entries.push(entry);
689 self.entry_index.insert(id, pos);
690 let aabb = self.compute_scene_aabb(id).unwrap_or(Rect::ZERO);
691 self.index.insert(id, aabb);
692 self.emit_item_change(ItemChange::Added { id });
693 id
694 }
695
696 /// Reactive notification stream for every Scene mutation. Apps
697 /// observe via `signal.observe(|change| …)` to wire snap-to-grid,
698 /// clamping, validation, and side effects without having to
699 /// poll the Scene each frame. The signal fires *after* the
700 /// mutation has been applied — by the time the observer runs
701 /// the Scene already reflects the new state.
702 pub fn item_change_signal(&self) -> Signal<ItemChange> {
703 self.item_change_signal.clone()
704 }
705
706 /// Reactive notification for logical-AT-structure mutations
707 /// (`add_a11y_group` / `remove_a11y_group` / `set_a11y_parent` /
708 /// `add_a11y_relation` / `set_a11y_live` / `set_a11y_landmark` /
709 /// `set_a11y_categories`). A monotonic counter bumped after each such
710 /// mutation. `SceneView` observes this to re-walk the AccessKit tree —
711 /// these changes don't flow through [`item_change_signal`](Self::item_change_signal)
712 /// because they aren't item geometry, and the AT tree is separate from the
713 /// visual scene.
714 pub fn a11y_change_signal(&self) -> Signal<u64> {
715 self.a11y_change_signal.clone()
716 }
717
718 /// Bump the logical-AT-structure change counter. Called at the end of every
719 /// a11y-structure mutator so observers re-walk AccessKit. Also advances the
720 /// unified [`mutation_version`](Self::mutation_version) so a logical-AT
721 /// mutation (which never fires `item_change_signal`) still un-gates the
722 /// SceneView's AT re-walk.
723 fn bump_a11y_change(&self) {
724 self.a11y_change_signal
725 .set(self.a11y_change_signal.get().wrapping_add(1));
726 self.bump_mutation();
727 }
728
729 /// Fire an [`ItemChange`] through `item_change_signal` and advance the
730 /// unified [`mutation_version`](Self::mutation_version). The single choke
731 /// point every geometry / visibility / structure mutation routes through, so
732 /// the version counts each one without per-site bookkeeping.
733 fn emit_item_change(&self, change: ItemChange) {
734 self.bump_mutation();
735 self.item_change_signal.set(change);
736 }
737
738 /// Advance the unified model-mutation counter (wrapping). Shared by
739 /// `emit_item_change` and `bump_a11y_change`; `&self` because both notify
740 /// paths are `&self`.
741 fn bump_mutation(&self) {
742 self.mutation_seq
743 .set(self.mutation_seq.get().wrapping_add(1));
744 }
745
746 /// Monotonic counter of every model mutation applied so far — item geometry
747 /// / visibility / structure (each [`ItemChange`]) **and** logical-AT
748 /// structure (groups, parents, relations, live, landmarks, categories).
749 ///
750 /// [`SceneView`](crate::SceneView) snapshots this each `build()` and only
751 /// re-walks the (separate, expensive) AccessKit tree when it has advanced
752 /// since the previous walk — so an actively-animating
753 /// [`add_item_dynamic`](Self::add_item_dynamic) item, which rebuilds every
754 /// frame, does not issue an AT re-walk per frame. The counter wraps; compare
755 /// for equality, not ordering.
756 pub fn mutation_version(&self) -> u64 {
757 self.mutation_seq.get()
758 }
759
760 // -----------------------------------------------------------------
761 // Geometry — local
762 // -----------------------------------------------------------------
763
764 /// Read an item's `local_pos` (its anchor in parent coords).
765 pub fn local_pos(&self, id: ItemId) -> Option<Point> {
766 let pos = *self.entry_index.get(&id)?;
767 Some(self.entries[pos].local_pos)
768 }
769
770 /// Move an item to a new `local_pos` in its parent's coordinate
771 /// frame. Re-buckets the item *and* every descendant in the
772 /// spatial index since the descendants' scene-AABBs shift along.
773 /// No-op if the id is unknown.
774 pub fn set_local_pos(&mut self, id: ItemId, local_pos: Point) {
775 if let Some(&pos) = self.entry_index.get(&id) {
776 let old = self.entries[pos].local_pos;
777 if old == local_pos {
778 return;
779 }
780 self.entries[pos].local_pos = local_pos;
781 self.rebucket_subtree(id);
782 self.emit_item_change(ItemChange::LocalPosChanged {
783 id,
784 old,
785 new: local_pos,
786 });
787 }
788 }
789
790 /// Read an item's `local_bounds` (its AABB in local coords).
791 pub fn local_bounds(&self, id: ItemId) -> Option<Rect> {
792 let pos = *self.entry_index.get(&id)?;
793 Some(self.entries[pos].local_bounds)
794 }
795
796 /// Update an item's `local_bounds`. For lightweight items this
797 /// also calls [`SceneItem::set_local_bounds`] on the item so its
798 /// next `paint` reflects the new geometry. The spatial index is
799 /// re-bucketed; only this item moves (descendants' local frames
800 /// are unchanged). No-op if the id is unknown.
801 pub fn set_local_bounds(&mut self, id: ItemId, local_bounds: Rect) {
802 if let Some(&pos) = self.entry_index.get(&id) {
803 let old = self.entries[pos].local_bounds;
804 if old == local_bounds {
805 return;
806 }
807 self.entries[pos].local_bounds = local_bounds;
808 if let SceneEntryKind::Item(item) = &mut self.entries[pos].kind {
809 item.set_local_bounds(local_bounds);
810 }
811 // Bounds are local — only this entry's scene-AABB shifts;
812 // descendants' local frames are unchanged.
813 let aabb = self.compute_scene_aabb(id).unwrap_or(Rect::ZERO);
814 self.index.insert(id, aabb);
815 self.emit_item_change(ItemChange::LocalBoundsChanged {
816 id,
817 old,
818 new: local_bounds,
819 });
820 }
821 }
822
823 /// Read an item's local→parent transform (rotation/scale around
824 /// the local origin). Identity by default.
825 pub fn transform(&self, id: ItemId) -> Option<Transform2D> {
826 let pos = *self.entry_index.get(&id)?;
827 Some(self.entries[pos].transform)
828 }
829
830 /// Set an item's local→parent transform. Re-buckets the item's
831 /// subtree in the spatial index. No-op if the id is unknown.
832 pub fn set_transform(&mut self, id: ItemId, transform: Transform2D) {
833 if let Some(&pos) = self.entry_index.get(&id) {
834 self.entries[pos].transform = transform;
835 self.rebucket_subtree(id);
836 self.emit_item_change(ItemChange::TransformChanged { id });
837 }
838 }
839
840 // -----------------------------------------------------------------
841 // Geometry — scene (computed via parent chain)
842 // -----------------------------------------------------------------
843
844 /// The composed local→scene transform for this item, walking up
845 /// the parent chain. Identity for an item that doesn't exist.
846 pub fn scene_transform(&self, id: ItemId) -> Transform2D {
847 let mut acc = Transform2D::identity();
848 let mut cur = Some(id);
849 let cap = self.entries.len();
850 let mut hops = 0;
851 while let Some(cid) = cur {
852 let Some(&pos) = self.entry_index.get(&cid) else {
853 break;
854 };
855 let entry = &self.entries[pos];
856 let l2p = local_to_parent(entry.local_pos, &entry.transform);
857 acc = acc.then(&l2p);
858 cur = entry.parent;
859 hops += 1;
860 if hops > cap {
861 break;
862 }
863 }
864 acc
865 }
866
867 /// The item's anchor in scene coords (its local origin
868 /// transformed through the parent chain).
869 pub fn scene_pos(&self, id: ItemId) -> Option<Point> {
870 if !self.entry_index.contains_key(&id) {
871 return None;
872 }
873 Some(self.scene_transform(id).apply_point(Point::ZERO))
874 }
875
876 /// The AABB enclosing the item's `local_bounds` after composing
877 /// through the parent chain — i.e. the rectangle the spatial
878 /// index buckets on. `None` if the id is unknown.
879 pub fn scene_rect(&self, id: ItemId) -> Option<Rect> {
880 let local_bounds = self.local_bounds(id)?;
881 Some(self.scene_transform(id).apply_rect(local_bounds))
882 }
883
884 /// Map a point in the item's local frame to scene coords.
885 pub fn map_to_scene(&self, id: ItemId, local_pt: Point) -> Option<Point> {
886 if !self.entry_index.contains_key(&id) {
887 return None;
888 }
889 Some(self.scene_transform(id).apply_point(local_pt))
890 }
891
892 /// Map a point in scene coords to the item's local frame.
893 /// Returns `None` if the item is unknown or its scene transform
894 /// is degenerate (zero scale).
895 pub fn map_from_scene(&self, id: ItemId, scene_pt: Point) -> Option<Point> {
896 if !self.entry_index.contains_key(&id) {
897 return None;
898 }
899 self.scene_transform(id)
900 .inverse()
901 .map(|inv| inv.apply_point(scene_pt))
902 }
903
904 fn compute_scene_aabb(&self, id: ItemId) -> Option<Rect> {
905 let pos = *self.entry_index.get(&id)?;
906 let local_bounds = self.entries[pos].local_bounds;
907 Some(self.scene_transform(id).apply_rect(local_bounds))
908 }
909
910 fn rebucket_subtree(&mut self, root: ItemId) {
911 // Re-bucket `root` and every descendant whose scene-AABB
912 // depends on the root's frame.
913 //
914 // Build a parent→children adjacency map once (O(N)) so the walk is
915 // O(N) instead of O(N²) (the previous code rescanned every entry per
916 // node).
917 let mut children: HashMap<ItemId, Vec<ItemId>> = HashMap::new();
918 for entry in &self.entries {
919 if let Some(parent) = entry.parent {
920 children.entry(parent).or_default().push(entry.id);
921 }
922 }
923
924 // Cycle guard: the parent-pointer walkers (`scene_transform` etc.)
925 // bound their *upward* walk with a hop cap; this *downward* walk can
926 // loop forever if the parent graph ever contains a cycle (e.g. from a
927 // future de-serialization bug), so we track visited nodes. A
928 // well-formed tree never revisits a node, so this is also a redundant-
929 // work guard.
930 let mut visited: HashSet<ItemId> = HashSet::new();
931 let mut stack: Vec<ItemId> = vec![root];
932 while let Some(id) = stack.pop() {
933 if !visited.insert(id) {
934 continue;
935 }
936 if let Some(aabb) = self.compute_scene_aabb(id) {
937 self.index.insert(id, aabb);
938 }
939 if let Some(kids) = children.get(&id) {
940 stack.extend(kids.iter().copied());
941 }
942 }
943 }
944
945 // -----------------------------------------------------------------
946 // Flags, visibility, opacity (per item)
947 // -----------------------------------------------------------------
948
949 /// Read an item's [`ItemFlags`] bitset.
950 pub fn flags(&self, id: ItemId) -> Option<ItemFlags> {
951 let pos = *self.entry_index.get(&id)?;
952 Some(self.entries[pos].flags)
953 }
954
955 /// Replace an item's flags wholesale. No-op if unknown.
956 pub fn set_flags(&mut self, id: ItemId, flags: ItemFlags) {
957 if let Some(&pos) = self.entry_index.get(&id) {
958 let old = self.entries[pos].flags;
959 if old == flags {
960 return;
961 }
962 self.entries[pos].flags = flags;
963 self.emit_item_change(ItemChange::FlagsChanged {
964 id,
965 old,
966 new: flags,
967 });
968 }
969 }
970
971 /// Set or clear a single flag on an item. No-op if unknown.
972 pub fn set_flag(&mut self, id: ItemId, flag: ItemFlags, on: bool) {
973 if let Some(&pos) = self.entry_index.get(&id) {
974 let old = self.entries[pos].flags;
975 self.entries[pos].flags.set(flag, on);
976 let new = self.entries[pos].flags;
977 if old != new {
978 if flag == ItemFlags::IS_VISIBLE {
979 self.emit_item_change(ItemChange::VisibilityChanged { id, visible: on });
980 }
981 self.emit_item_change(ItemChange::FlagsChanged { id, old, new });
982 }
983 }
984 }
985
986 /// Toggle the [`ItemFlags::IS_VISIBLE`] bit. Convenience for
987 /// the common "hide this item" operation.
988 pub fn set_visible(&mut self, id: ItemId, visible: bool) {
989 self.set_flag(id, ItemFlags::IS_VISIBLE, visible);
990 }
991
992 /// Whether the item is visible AND every ancestor in its chain
993 /// is visible. Returns `true` when nothing in the chain has
994 /// `IS_VISIBLE` cleared. `false` for unknown ids.
995 pub fn is_effectively_visible(&self, id: ItemId) -> bool {
996 let cap = self.entries.len();
997 let mut hops = 0;
998 let mut cur = Some(id);
999 while let Some(cid) = cur {
1000 let Some(&pos) = self.entry_index.get(&cid) else {
1001 return false;
1002 };
1003 let entry = &self.entries[pos];
1004 if !entry.flags.contains(ItemFlags::IS_VISIBLE) {
1005 return false;
1006 }
1007 cur = entry.parent;
1008 hops += 1;
1009 if hops > cap {
1010 break;
1011 }
1012 }
1013 true
1014 }
1015
1016 /// Read an item's local opacity multiplier (`1.0` by default).
1017 pub fn opacity(&self, id: ItemId) -> Option<f32> {
1018 let pos = *self.entry_index.get(&id)?;
1019 Some(self.entries[pos].opacity)
1020 }
1021
1022 /// Set an item's local opacity, clamped to `[0.0, 1.0]`.
1023 pub fn set_opacity(&mut self, id: ItemId, opacity: f32) {
1024 if let Some(&pos) = self.entry_index.get(&id) {
1025 let new = opacity.clamp(0.0, 1.0);
1026 let old = self.entries[pos].opacity;
1027 if (old - new).abs() < f32::EPSILON {
1028 return;
1029 }
1030 self.entries[pos].opacity = new;
1031 self.emit_item_change(ItemChange::OpacityChanged { id, old, new });
1032 }
1033 }
1034
1035 /// Replace a lightweight item's fill colour live, emitting
1036 /// [`ItemChange::AppearanceChanged`] — **always repaint-only**, never a
1037 /// relayout, rebuild, or AccessKit re-walk. The colour is a [`ColorProp`],
1038 /// so it accepts a plain [`Color`](teksilo_tokens::Color), a theme role, a
1039 /// `Signal<Color>`, or a `Signal<Role>`. No-op for item kinds without a fill
1040 /// (e.g. `ImageItem`).
1041 ///
1042 /// # Reactivity contract
1043 ///
1044 /// A colour becomes **continuously** reactive by being registered at build
1045 /// time (`SceneItem::register_bindings`). So:
1046 ///
1047 /// - **Construct** the item with a `Signal`/role colour (`.fill(my_signal)`)
1048 /// for a colour that tracks its signal forever. This is the recommended
1049 /// path and needs no mutator at all.
1050 /// - **This mutator** installs a *snapshot*: it repaints immediately, which
1051 /// is all a static colour ever needs. If you pass a `Signal`/dynamic role
1052 /// here, it paints the signal's current value now and starts tracking it
1053 /// continuously from the owning view's next rebuild (whenever some other
1054 /// structural change re-runs `register_bindings`). Deliberately *not*
1055 /// forced: a colour change must never cost a rebuild + AT re-walk.
1056 pub fn set_item_fill(&mut self, id: ItemId, fill: impl Into<ColorProp>) {
1057 let prop = fill.into();
1058 let Some(&pos) = self.entry_index.get(&id) else {
1059 return;
1060 };
1061 let applied = match &mut self.entries[pos].kind {
1062 SceneEntryKind::Item(item) => item.set_fill(Some(prop)),
1063 _ => false,
1064 };
1065 if applied {
1066 self.emit_item_change(ItemChange::AppearanceChanged { id });
1067 }
1068 }
1069
1070 /// Clear a lightweight item's fill (Rect/Path/Group become fill-less),
1071 /// emitting [`ItemChange::AppearanceChanged`] (repaint-only). No-op for items
1072 /// whose fill can't be cleared (e.g. `TextItem`, which always has a
1073 /// foreground colour).
1074 pub fn clear_item_fill(&mut self, id: ItemId) {
1075 let Some(&pos) = self.entry_index.get(&id) else {
1076 return;
1077 };
1078 let applied = match &mut self.entries[pos].kind {
1079 SceneEntryKind::Item(item) => item.set_fill(None),
1080 _ => false,
1081 };
1082 if applied {
1083 self.emit_item_change(ItemChange::AppearanceChanged { id });
1084 }
1085 }
1086
1087 /// Replace a lightweight item's stroke (colour + [`StrokeStyle`]) live,
1088 /// emitting [`ItemChange::AppearanceChanged`] (repaint-only). No-op for item
1089 /// kinds without a stroke slot (`TextItem` / `ImageItem`). See
1090 /// [`set_item_fill`](Self::set_item_fill) for the reactivity contract.
1091 pub fn set_item_stroke(&mut self, id: ItemId, color: impl Into<ColorProp>, style: StrokeStyle) {
1092 let prop = color.into();
1093 let Some(&pos) = self.entry_index.get(&id) else {
1094 return;
1095 };
1096 let applied = match &mut self.entries[pos].kind {
1097 SceneEntryKind::Item(item) => item.set_stroke(Some((prop, style))),
1098 _ => false,
1099 };
1100 if applied {
1101 self.emit_item_change(ItemChange::AppearanceChanged { id });
1102 }
1103 }
1104
1105 /// Clear a lightweight item's stroke, emitting
1106 /// [`ItemChange::AppearanceChanged`] (repaint-only). No-op for item kinds
1107 /// without a stroke.
1108 pub fn clear_item_stroke(&mut self, id: ItemId) {
1109 let Some(&pos) = self.entry_index.get(&id) else {
1110 return;
1111 };
1112 let applied = match &mut self.entries[pos].kind {
1113 SceneEntryKind::Item(item) => item.set_stroke(None),
1114 _ => false,
1115 };
1116 if applied {
1117 self.emit_item_change(ItemChange::AppearanceChanged { id });
1118 }
1119 }
1120
1121 /// Insert an already-boxed lightweight item at `local_pos`, returning its
1122 /// id. The boxed-`dyn` counterpart of [`add_item`](Self::add_item) — used by
1123 /// [`SceneListAdapter`](crate::SceneListAdapter) whose delegate yields
1124 /// `Box<dyn SceneItem>`.
1125 pub fn add_boxed_item(&mut self, item: Box<dyn SceneItem>, local_pos: Point) -> ItemId {
1126 self.insert_boxed(item, local_pos, false)
1127 }
1128
1129 /// Replace an item's handler set. Pass `None` to clear.
1130 pub fn set_item_handlers(&mut self, id: ItemId, handlers: Option<SceneItemHandlerSet>) {
1131 if let Some(&pos) = self.entry_index.get(&id) {
1132 self.entries[pos].handlers = handlers.map(Box::new);
1133 }
1134 }
1135
1136 /// Mutably borrow an item's handler set, lazily creating an
1137 /// empty one if none exists. Returns `None` for unknown ids.
1138 /// Allows fluent chains: `scene.handlers_mut(id).unwrap().on_tap(…).cursor(…);`.
1139 pub fn handlers_mut(&mut self, id: ItemId) -> Option<&mut SceneItemHandlerSet> {
1140 let pos = *self.entry_index.get(&id)?;
1141 let entry = self.entries.get_mut(pos)?;
1142 if entry.handlers.is_none() {
1143 entry.handlers = Some(Box::new(SceneItemHandlerSet::new()));
1144 }
1145 entry.handlers.as_deref_mut()
1146 }
1147
1148 /// Read-only access to an item's handler set, if one is set.
1149 pub fn handlers(&self, id: ItemId) -> Option<&SceneItemHandlerSet> {
1150 let pos = *self.entry_index.get(&id)?;
1151 self.entries[pos].handlers.as_deref()
1152 }
1153
1154 /// Effective opacity composed up the parent chain — the product
1155 /// of every ancestor's opacity and this item's. `1.0` for an
1156 /// unknown id (so callers don't end up multiplying by a stale
1157 /// value).
1158 pub fn effective_opacity(&self, id: ItemId) -> f32 {
1159 let cap = self.entries.len();
1160 let mut hops = 0;
1161 let mut cur = Some(id);
1162 let mut acc = 1.0_f32;
1163 while let Some(cid) = cur {
1164 let Some(&pos) = self.entry_index.get(&cid) else {
1165 return acc;
1166 };
1167 let entry = &self.entries[pos];
1168 acc *= entry.opacity;
1169 cur = entry.parent;
1170 hops += 1;
1171 if hops > cap {
1172 break;
1173 }
1174 }
1175 acc
1176 }
1177
1178 // -----------------------------------------------------------------
1179 // Scene rect (Qt setSceneRect) + pan/zoom policy
1180 // -----------------------------------------------------------------
1181
1182 /// Declare the scene's logical extent. `None` (the default)
1183 /// means "auto-compute from items each query"; `Some(rect)`
1184 /// fixes the extent regardless of item placement. Used by
1185 /// `SceneView` for pan clamping and `fit_to_content`.
1186 pub fn set_scene_rect(&mut self, rect: Option<Rect>) {
1187 self.user_scene_rect = rect;
1188 }
1189
1190 /// The resolved scene extent — user-declared via
1191 /// [`Scene::set_scene_rect`] if set, otherwise the AABB
1192 /// enclosing every item's scene rect. `None` when neither is
1193 /// available (the user didn't declare and the scene is empty).
1194 pub fn scene_rect_extent(&self) -> Option<Rect> {
1195 if let Some(r) = self.user_scene_rect {
1196 return Some(r);
1197 }
1198 let ids = self.ids();
1199 let mut acc: Option<Rect> = None;
1200 for id in ids {
1201 let Some(r) = self.scene_rect(id) else {
1202 continue;
1203 };
1204 acc = Some(match acc {
1205 None => r,
1206 Some(a) => union_two_rects(a, r),
1207 });
1208 }
1209 acc
1210 }
1211
1212 /// Set the axes the view may pan along. Default
1213 /// [`PanAxes::Both`]. Writes to the reactive signal; gesture
1214 /// closures pick the change up on the next event.
1215 pub fn pan_axes(&mut self, axes: PanAxes) {
1216 self.constraints.pan_axes.set(axes);
1217 }
1218
1219 /// The currently-declared pan axes. Live read of the signal.
1220 pub fn current_pan_axes(&self) -> PanAxes {
1221 self.constraints.pan_axes.get()
1222 }
1223
1224 /// Set whether the view honors zoom gestures. Default `true`.
1225 /// Writes to the reactive signal.
1226 pub fn zoomable(&mut self, on: bool) {
1227 self.constraints.zoomable.set(on);
1228 }
1229
1230 /// Whether the scene currently allows zoom. Live read.
1231 pub fn is_zoomable(&self) -> bool {
1232 self.constraints.zoomable.get()
1233 }
1234
1235 /// Clamp the visible viewport to this scene-coord rect. `None`
1236 /// (default) leaves pan unconstrained. When `Some(r)`, the
1237 /// [`SceneView`](crate::SceneView)'s pan is clamped so the
1238 /// visible scene region overlaps `r`. When `r` is smaller than
1239 /// the visible viewport, the rect is centered.
1240 ///
1241 /// Distinct from [`set_scene_rect`](Self::set_scene_rect):
1242 /// `scene_rect` declares the scene's logical extent (used by
1243 /// `adopt_scene_size`); `pan_bounds` controls what region the
1244 /// user can scroll to. A doc-style app typically sets both to
1245 /// the same rect.
1246 pub fn set_pan_bounds(&mut self, bounds: Option<Rect>) {
1247 self.constraints.pan_bounds.set(bounds);
1248 }
1249
1250 /// The currently-declared pan-bounds rect. Live read.
1251 pub fn current_pan_bounds(&self) -> Option<Rect> {
1252 self.constraints.pan_bounds.get()
1253 }
1254
1255 /// Inclusive `[min, max]` zoom-factor clamp. `None` (default)
1256 /// is unconstrained from the `Scene` side — the `SceneView`
1257 /// may still impose its own override.
1258 ///
1259 /// The effective range applied by the `SceneView` is the
1260 /// intersection of `Scene` + view-level override, so apps
1261 /// cannot loosen a `Scene`-declared range by setting a wider
1262 /// override on the view.
1263 pub fn set_zoom_range(&mut self, range: Option<std::ops::RangeInclusive<f32>>) {
1264 self.constraints.zoom_range.set(range);
1265 }
1266
1267 /// The currently-declared zoom range. Live read.
1268 pub fn current_zoom_range(&self) -> Option<std::ops::RangeInclusive<f32>> {
1269 self.constraints.zoom_range.get()
1270 }
1271
1272 /// Reactive accessors for live observation.
1273 pub fn pan_axes_signal(&self) -> Signal<PanAxes> {
1274 self.constraints.pan_axes_signal()
1275 }
1276 /// Reactive pan-bounds signal.
1277 pub fn pan_bounds_signal(&self) -> Signal<Option<Rect>> {
1278 self.constraints.pan_bounds_signal()
1279 }
1280 /// Reactive zoom-range signal.
1281 pub fn zoom_range_signal(&self) -> Signal<Option<std::ops::RangeInclusive<f32>>> {
1282 self.constraints.zoom_range_signal()
1283 }
1284 /// Reactive zoomable on/off signal.
1285 pub fn zoomable_signal(&self) -> Signal<bool> {
1286 self.constraints.zoomable_signal()
1287 }
1288
1289 /// Read-only view of the full constraint bundle. Useful when
1290 /// passing all four signals to a custom view implementation.
1291 pub fn constraints(&self) -> &SceneConstraints {
1292 &self.constraints
1293 }
1294
1295 // -----------------------------------------------------------------
1296 // Z-order and parenting
1297 // -----------------------------------------------------------------
1298
1299 /// Set paint z-order for an entry. Higher z paints later (on top);
1300 /// equal-z falls back to insertion order. Default 0.0.
1301 ///
1302 /// Works for **both** tiers: lightweight items re-sort within their
1303 /// band on the next paint, and heavyweight widget entries restack the
1304 /// arena children on the next rebuild (the SceneView reorders
1305 /// `node.children` by z without recreating the widgets, so focus /
1306 /// text-edit / animation state survives the restack). No-op for
1307 /// unknown ids.
1308 pub fn set_z(&mut self, id: ItemId, z: f32) {
1309 if let Some(&pos) = self.entry_index.get(&id) {
1310 let old = self.entries[pos].z;
1311 if (old - z).abs() < f32::EPSILON {
1312 return;
1313 }
1314 self.entries[pos].z = z;
1315 self.emit_item_change(ItemChange::ZChanged { id, old, new: z });
1316 }
1317 }
1318
1319 /// Raise an entry above all current entries by giving it a z one
1320 /// greater than the current maximum. The drag-to-front primitive —
1321 /// call it on drag-start so the grabbed card (and its text) renders
1322 /// over the others. Works for both tiers (see [`set_z`](Self::set_z)).
1323 pub fn bring_to_front(&mut self, id: ItemId) {
1324 if !self.entry_index.contains_key(&id) {
1325 return;
1326 }
1327 let max_z = self
1328 .entries
1329 .iter()
1330 .map(|e| e.z)
1331 .fold(f32::NEG_INFINITY, f32::max);
1332 let target = if max_z.is_finite() { max_z + 1.0 } else { 1.0 };
1333 self.set_z(id, target);
1334 }
1335
1336 /// Lower an entry below all current entries by giving it a z one less
1337 /// than the current minimum. Works for both tiers (see
1338 /// [`set_z`](Self::set_z)).
1339 pub fn send_to_back(&mut self, id: ItemId) {
1340 if !self.entry_index.contains_key(&id) {
1341 return;
1342 }
1343 let min_z = self
1344 .entries
1345 .iter()
1346 .map(|e| e.z)
1347 .fold(f32::INFINITY, f32::min);
1348 let target = if min_z.is_finite() { min_z - 1.0 } else { -1.0 };
1349 self.set_z(id, target);
1350 }
1351
1352 /// Read an entry's z-order.
1353 pub fn z(&self, id: ItemId) -> Option<f32> {
1354 let pos = *self.entry_index.get(&id)?;
1355 Some(self.entries[pos].z)
1356 }
1357
1358 /// Set the Under/Over paint band for a lightweight entry. `Over`
1359 /// items paint *after* the heavyweight widget children (in the
1360 /// SceneView's `post_paint`), so they sit on top of the cards;
1361 /// `Under` items (the default) paint before them. Within a band,
1362 /// [`set_z`](Self::set_z) still orders items among themselves.
1363 /// No-op for unknown ids.
1364 pub fn set_layer(&mut self, id: ItemId, layer: SceneLayer) {
1365 if let Some(&pos) = self.entry_index.get(&id) {
1366 let old = self.entries[pos].layer;
1367 if old == layer {
1368 return;
1369 }
1370 self.entries[pos].layer = layer;
1371 self.emit_item_change(ItemChange::LayerChanged {
1372 id,
1373 old,
1374 new: layer,
1375 });
1376 }
1377 }
1378
1379 /// Read an entry's Under/Over paint band. `None` for unknown ids.
1380 pub fn layer(&self, id: ItemId) -> Option<SceneLayer> {
1381 let pos = *self.entry_index.get(&id)?;
1382 Some(self.entries[pos].layer)
1383 }
1384
1385 /// Whether any entry is in the [`SceneLayer::Over`] band. The
1386 /// SceneView consults this in `wants_post_paint` to skip the
1387 /// foreground pass entirely when nothing is raised above the cards.
1388 /// Linear in entry count, called once per frame.
1389 pub(crate) fn has_over_layer_items(&self) -> bool {
1390 self.entries.iter().any(|e| e.layer == SceneLayer::Over)
1391 }
1392
1393 /// Declare a parent/child relationship. `child`'s `local_pos`
1394 /// and `transform` are reinterpreted as relative to the new
1395 /// parent's local frame — the visual position changes unless
1396 /// the caller compensates. Re-buckets `child`'s subtree.
1397 ///
1398 /// Pass `parent = None` to detach (child's local frame becomes
1399 /// scene-rooted again).
1400 ///
1401 /// **Cycle guard:** if the proposed parent is `child` itself
1402 /// or a descendant of `child`, the call is a no-op (no parent
1403 /// change, no rebucket, no signal fire). Without this guard
1404 /// the downstream `rebucket_subtree` walk loops indefinitely.
1405 pub fn set_item_parent(&mut self, child: ItemId, parent: Option<ItemId>) {
1406 if let Some(&pos) = self.entry_index.get(&child) {
1407 let old = self.entries[pos].parent;
1408 if old == parent {
1409 return;
1410 }
1411 // Reject self-parent and any parent in the child's
1412 // subtree (would create a cycle).
1413 if let Some(p) = parent
1414 && (p == child || self.is_descendant_of(p, child))
1415 {
1416 return;
1417 }
1418 self.entries[pos].parent = parent;
1419 self.rebucket_subtree(child);
1420 self.emit_item_change(ItemChange::ParentChanged {
1421 id: child,
1422 old,
1423 new: parent,
1424 });
1425 }
1426 }
1427
1428 /// Parent of `id`, if any.
1429 pub fn parent_of(&self, id: ItemId) -> Option<ItemId> {
1430 let pos = *self.entry_index.get(&id)?;
1431 self.entries[pos].parent
1432 }
1433
1434 /// Whether `id`'s ancestor chain contains `ancestor`.
1435 pub fn is_descendant_of(&self, id: ItemId, ancestor: ItemId) -> bool {
1436 let mut cur = self.parent_of(id);
1437 let cap = self.entries.len();
1438 let mut hops = 0;
1439 while let Some(p) = cur {
1440 if p == ancestor {
1441 return true;
1442 }
1443 cur = self.parent_of(p);
1444 hops += 1;
1445 if hops > cap {
1446 break;
1447 }
1448 }
1449 false
1450 }
1451
1452 /// Append every direct + transitive descendant of `id` into
1453 /// `out`, breadth-first across declaration order. The id
1454 /// itself is **not** included.
1455 pub fn collect_descendants(&self, id: ItemId, out: &mut Vec<ItemId>) {
1456 let mut frontier: Vec<ItemId> = vec![id];
1457 while let Some(parent) = frontier.pop() {
1458 for entry in &self.entries {
1459 if entry.parent == Some(parent) {
1460 out.push(entry.id);
1461 frontier.push(entry.id);
1462 }
1463 }
1464 }
1465 }
1466
1467 // -----------------------------------------------------------------
1468 // Lookup
1469 // -----------------------------------------------------------------
1470
1471 /// Borrow a lightweight [`SceneItem`] by id. `None` for unknown
1472 /// ids and for heavyweight widget entries.
1473 pub fn item(&self, id: ItemId) -> Option<&dyn SceneItem> {
1474 let pos = *self.entry_index.get(&id)?;
1475 match &self.entries[pos].kind {
1476 SceneEntryKind::Item(item) => Some(item.as_ref()),
1477 SceneEntryKind::Widget(_) => None,
1478 }
1479 }
1480
1481 /// Sort `ids` by z-order ascending, stable for equal values.
1482 /// Crate-private helper for `SceneView::paint`.
1483 pub(crate) fn sort_by_z(&self, ids: &mut [ItemId]) {
1484 ids.sort_by(|a, b| {
1485 let za = self.z(*a).unwrap_or(0.0);
1486 let zb = self.z(*b).unwrap_or(0.0);
1487 za.partial_cmp(&zb).unwrap_or(std::cmp::Ordering::Equal)
1488 });
1489 }
1490
1491 // -----------------------------------------------------------------
1492 // Removal
1493 // -----------------------------------------------------------------
1494
1495 /// Remove an item by id, recursively dropping every descendant.
1496 ///
1497 /// Mirrors Qt's `QGraphicsScene::removeItem` semantics: deleting
1498 /// a parent deletes its children too. No-op if `id` is unknown.
1499 /// Fires one [`ItemChange::Removed`] per id, descendants first
1500 /// then the named parent — observers see a consistent
1501 /// "leaves-then-root" order.
1502 ///
1503 /// To remove `id` without deleting its children, call
1504 /// [`Scene::orphan`] first to promote them to root-level, then
1505 /// `remove(id)`.
1506 pub fn remove(&mut self, id: ItemId) {
1507 use std::collections::HashSet;
1508 if !self.entry_index.contains_key(&id) {
1509 return;
1510 }
1511 // Descendants, deepest-first via collect_descendants's BFS
1512 // (the order is leaf-to-root because we push children as we
1513 // visit each parent). Append the named id last.
1514 let mut to_remove: Vec<ItemId> = Vec::new();
1515 self.collect_descendants(id, &mut to_remove);
1516 to_remove.reverse();
1517 to_remove.push(id);
1518 let removal_set: HashSet<ItemId> = to_remove.iter().copied().collect();
1519 self.entries.retain(|e| !removal_set.contains(&e.id));
1520 self.entry_index.clear();
1521 for (pos, entry) in self.entries.iter().enumerate() {
1522 self.entry_index.insert(entry.id, pos);
1523 }
1524 // The AT tree is separate from the visual tree, but a visually-removed
1525 // item must also vanish from AccessKit. Drop every logical-structure
1526 // entry that targets a removed item. For `a11y_parents` this also
1527 // re-roots any *still-alive* node that was AT-parented under a removed
1528 // item — dropping the `(child → removed)` mapping makes the child fall
1529 // back to the SceneView root (mirrors `remove_a11y_group`). Removal
1530 // itself fires `ItemChange::Removed`, so `SceneView` already re-walks
1531 // AT through the item-change observer; no `a11y_change_signal` bump
1532 // is needed here.
1533 let is_removed = |n: &A11yNode| matches!(n, A11yNode::Item(i) if removal_set.contains(i));
1534 self.a11y_parents
1535 .retain(|child, parent| !is_removed(child) && !is_removed(parent));
1536 self.a11y_relations
1537 .retain(|(from, _, to)| !is_removed(from) && !is_removed(to));
1538 for removed_id in &removal_set {
1539 let node = A11yNode::Item(*removed_id);
1540 self.a11y_live.remove(&node);
1541 self.a11y_landmarks.remove(&node);
1542 self.a11y_categories.remove(&node);
1543 // Drop any magnets attached to the removed item, retiring
1544 // their ids from the reverse-lookup map. Magnets are local
1545 // to the item, so a removed item takes its magnets with it.
1546 if let Some(magnets) = self.magnets.remove(removed_id) {
1547 for (mid, _) in magnets {
1548 self.magnet_owner.remove(&mid);
1549 }
1550 }
1551 }
1552
1553 for removed_id in to_remove {
1554 self.index.remove(removed_id);
1555 self.emit_item_change(ItemChange::Removed { id: removed_id });
1556 }
1557 }
1558
1559 /// Promote `id`'s direct children to root-level (clear their
1560 /// `parent` field). Used when an app wants to remove `id` without
1561 /// dropping its children — call `orphan(id)` then `remove(id)`.
1562 /// No-op when `id` is unknown or has no children.
1563 ///
1564 /// Fires one [`ItemChange::ParentChanged`] per detached child and
1565 /// re-buckets every detached subtree in the spatial index — the
1566 /// children's `scene_transform` shifts (no longer composes
1567 /// `id`'s) so their scene-space AABBs change. Without re-bucketing
1568 /// the index, [`items_in_rect`](Self::items_in_rect) and
1569 /// [`item_at`](Self::item_at) would return stale results.
1570 ///
1571 /// Apps wanting *visual* stability across the orphan call should
1572 /// first bake `id`'s `scene_transform` into each child's
1573 /// `local_pos` + `transform`; otherwise children visibly jump.
1574 pub fn orphan(&mut self, id: ItemId) {
1575 if !self.entry_index.contains_key(&id) {
1576 return;
1577 }
1578 let children: Vec<ItemId> = self
1579 .entries
1580 .iter()
1581 .filter(|e| e.parent == Some(id))
1582 .map(|e| e.id)
1583 .collect();
1584 for child in children {
1585 if let Some(&pos) = self.entry_index.get(&child) {
1586 self.entries[pos].parent = None;
1587 // Re-bucket the entire detached subtree: each child's
1588 // scene_transform changed (no longer composes `id`'s),
1589 // so spatial-index AABBs are stale. Subtree-walk
1590 // because grandchildren depend on the chain too.
1591 self.rebucket_subtree(child);
1592 self.emit_item_change(ItemChange::ParentChanged {
1593 id: child,
1594 old: Some(id),
1595 new: None,
1596 });
1597 }
1598 }
1599 }
1600
1601 // -----------------------------------------------------------------
1602 // Queries
1603 // -----------------------------------------------------------------
1604
1605 /// All items whose scene-AABB intersects `scene_rect`.
1606 ///
1607 /// Broad phase: the spatial index returns every id bucketed in
1608 /// any cell touched by `scene_rect`. Narrow phase: each candidate
1609 /// goes through [`scene_rect`](Self::scene_rect), which itself
1610 /// dispatches via `entry_index` (an `HashMap<ItemId, usize>`),
1611 /// so the per-candidate cost is O(parent-chain-depth) — not
1612 /// O(N). Total query is O(visible × chain) instead of O(N).
1613 pub fn items_in_rect(&self, scene_rect: Rect) -> Vec<ItemId> {
1614 self.index
1615 .query(scene_rect)
1616 .into_iter()
1617 .filter(|id| {
1618 self.scene_rect(*id)
1619 .map(|r| rects_intersect(r, scene_rect))
1620 .unwrap_or(false)
1621 })
1622 .collect()
1623 }
1624
1625 /// Snapshot every visible item — **both tiers** — as a `(scene_rect,
1626 /// color)` pair suitable for a minimap thumbnail. Filters out items with
1627 /// `HAS_NO_CONTENTS` (logical-only) and items hidden by `IS_VISIBLE` / a
1628 /// hidden ancestor — the visible-effective set matches what the SceneView's
1629 /// paint walk renders.
1630 ///
1631 /// Ordered by insertion (low z first). A lightweight item's color comes
1632 /// from [`SceneItem::thumbnail_color`] (its fill / stroke / a neutral grey);
1633 /// a heavyweight widget entry has no `SceneItem`, so it's shown in a neutral
1634 /// tint — a minimap that omitted the heavyweight tier would misrepresent a
1635 /// widget-heavy scene (cards, nodes), so both tiers are included.
1636 pub fn item_thumbnails(&self) -> Vec<(Rect, teksilo_tokens::Color)> {
1637 let mut out = Vec::new();
1638 for entry in &self.entries {
1639 // Skip invisible / logical-only items (either tier).
1640 if !self.is_effectively_visible(entry.id) {
1641 continue;
1642 }
1643 if entry.flags.contains(ItemFlags::HAS_NO_CONTENTS) {
1644 continue;
1645 }
1646 let Some(rect) = self.scene_rect(entry.id) else {
1647 continue;
1648 };
1649 let color = match &entry.kind {
1650 SceneEntryKind::Item(item) => item.thumbnail_color(),
1651 // Heavyweight widget: no `thumbnail_color`, so use a neutral tint.
1652 SceneEntryKind::Widget(_) => teksilo_tokens::Color::new(0.45, 0.52, 0.65, 0.85),
1653 };
1654 out.push((rect, color));
1655 }
1656 out
1657 }
1658
1659 /// Topmost lightweight item whose `shape_contains` fires for
1660 /// `scene_pt`. Iterates `items_in_rect` for a tiny rect around
1661 /// the point, sorts by z descending, and returns the first hit.
1662 /// Heavyweight widget entries are skipped (their hit-testing is
1663 /// handled by the arena event dispatch).
1664 ///
1665 /// **Limitation:** items flagged
1666 /// [`IGNORES_TRANSFORMATIONS`](crate::flags::ItemFlags::IGNORES_TRANSFORMATIONS)
1667 /// hit-test in screen space, not scene space — so this scene-only
1668 /// query may incorrectly hit them or miss them depending on the
1669 /// current view transform. Apps that route pointer events through
1670 /// `SceneView`'s dispatch get screen-space hit-test for IGNORES
1671 /// items automatically; only use `item_at` directly for normal
1672 /// items, or pair with the view transform to filter.
1673 pub fn item_at(&self, scene_pt: Point) -> Option<ItemId> {
1674 let probe = Rect::new(scene_pt.x, scene_pt.y, 0.0, 0.0);
1675 let mut candidates = self.items_in_rect(probe);
1676 candidates.sort_by(|a, b| {
1677 let za = self.z(*a).unwrap_or(0.0);
1678 let zb = self.z(*b).unwrap_or(0.0);
1679 zb.partial_cmp(&za).unwrap_or(std::cmp::Ordering::Equal)
1680 });
1681 for id in candidates {
1682 let Some(item) = self.item(id) else {
1683 continue;
1684 };
1685 let Some(local_pt) = self.map_from_scene(id, scene_pt) else {
1686 continue;
1687 };
1688 if item.shape_contains(local_pt) {
1689 return Some(id);
1690 }
1691 }
1692 None
1693 }
1694
1695 /// Items whose scene-AABB intersects the AABB of `id`. Excludes
1696 /// `id` itself. Apps use this for "which other items overlap
1697 /// this card?" queries — graph editors checking node-on-node
1698 /// overlap, CAD canvases finding adjacent geometry. Backed by
1699 /// the spatial index, so the cost is `O(visible)` not `O(N)`.
1700 pub fn colliding_items(&self, id: ItemId) -> Vec<ItemId> {
1701 let Some(rect) = self.scene_rect(id) else {
1702 return Vec::new();
1703 };
1704 self.items_in_rect(rect)
1705 .into_iter()
1706 .filter(|other| *other != id)
1707 .collect()
1708 }
1709
1710 /// Items whose scene-AABB intersects `path`'s bounding rect.
1711 /// Apps use this for "which items lie under this connector?"
1712 /// queries — graph editors highlighting hovered connectors,
1713 /// CAD canvases doing point-in-polygon style picking. The
1714 /// narrow phase is AABB-vs-AABB; per-segment-distance precision
1715 /// is left to the app.
1716 pub fn items_along_path(&self, path: &Path) -> Vec<ItemId> {
1717 let Some(rect) = path_aabb(path) else {
1718 return Vec::new();
1719 };
1720 self.items_in_rect(rect)
1721 }
1722
1723 /// All lightweight items whose `shape_contains` fires for
1724 /// `scene_pt`, sorted topmost-first by z.
1725 pub fn items_at(&self, scene_pt: Point) -> Vec<ItemId> {
1726 let probe = Rect::new(scene_pt.x, scene_pt.y, 0.0, 0.0);
1727 let mut candidates = self.items_in_rect(probe);
1728 candidates.sort_by(|a, b| {
1729 let za = self.z(*a).unwrap_or(0.0);
1730 let zb = self.z(*b).unwrap_or(0.0);
1731 zb.partial_cmp(&za).unwrap_or(std::cmp::Ordering::Equal)
1732 });
1733 candidates
1734 .into_iter()
1735 .filter(|id| {
1736 let Some(item) = self.item(*id) else {
1737 return false;
1738 };
1739 let Some(local_pt) = self.map_from_scene(*id, scene_pt) else {
1740 return false;
1741 };
1742 item.shape_contains(local_pt)
1743 })
1744 .collect()
1745 }
1746
1747 // -----------------------------------------------------------------
1748 // Metadata
1749 // -----------------------------------------------------------------
1750
1751 /// Number of entries in the scene.
1752 pub fn len(&self) -> usize {
1753 self.entries.len()
1754 }
1755
1756 /// Whether the scene is empty.
1757 pub fn is_empty(&self) -> bool {
1758 self.entries.is_empty()
1759 }
1760
1761 /// All ids in insertion order.
1762 pub fn ids(&self) -> Vec<ItemId> {
1763 self.entries.iter().map(|e| e.id).collect()
1764 }
1765
1766 /// Borrow the spatial index (diagnostics / tests).
1767 pub fn index(&self) -> &dyn SpatialIndex {
1768 &*self.index
1769 }
1770
1771 // -----------------------------------------------------------------
1772 // Magnetism
1773 // -----------------------------------------------------------------
1774
1775 /// Attach a [`Magnet`] to `item` and return its [`MagnetId`].
1776 ///
1777 /// Magnets are local to their item (their `local_pos` is in the
1778 /// item's frame), so they follow the item under any move / rotate /
1779 /// scale via the same `scene_transform` the item uses. No-op
1780 /// returning a fresh-but-unowned id if `item` is unknown — callers
1781 /// add magnets to items they just created.
1782 ///
1783 /// Bumps the AT-structure change counter (magnets are AT structure)
1784 /// so a `SceneView` with magnetism enabled re-walks its synthetic
1785 /// magnet nodes.
1786 pub fn add_magnet(&mut self, item: ItemId, magnet: Magnet) -> MagnetId {
1787 let id = MagnetId::next();
1788 if !self.entry_index.contains_key(&item) {
1789 return id;
1790 }
1791 self.magnets.entry(item).or_default().push((id, magnet));
1792 self.magnet_owner.insert(id, item);
1793 self.bump_a11y_change();
1794 id
1795 }
1796
1797 /// Remove a magnet by id. No-op if the id is unknown.
1798 pub fn remove_magnet(&mut self, magnet: MagnetId) {
1799 let Some(owner) = self.magnet_owner.remove(&magnet) else {
1800 return;
1801 };
1802 if let Some(list) = self.magnets.get_mut(&owner) {
1803 list.retain(|(mid, _)| *mid != magnet);
1804 if list.is_empty() {
1805 self.magnets.remove(&owner);
1806 }
1807 }
1808 self.bump_a11y_change();
1809 }
1810
1811 /// Remove every magnet attached to `item`. No-op if none.
1812 pub fn clear_magnets(&mut self, item: ItemId) {
1813 if let Some(list) = self.magnets.remove(&item) {
1814 for (mid, _) in list {
1815 self.magnet_owner.remove(&mid);
1816 }
1817 self.bump_a11y_change();
1818 }
1819 }
1820
1821 /// Move a magnet to a new position in its owning item's local
1822 /// frame. No-op if the id is unknown.
1823 pub fn set_magnet_local_pos(&mut self, magnet: MagnetId, local_pos: Point) {
1824 let Some(&owner) = self.magnet_owner.get(&magnet) else {
1825 return;
1826 };
1827 if let Some(list) = self.magnets.get_mut(&owner)
1828 && let Some((_, m)) = list.iter_mut().find(|(mid, _)| *mid == magnet)
1829 {
1830 m.local_pos = local_pos;
1831 self.bump_a11y_change();
1832 }
1833 }
1834
1835 /// Enable or disable a magnet. Disabled magnets are skipped by
1836 /// broad-phase, feedback, the keyboard cycle, and AT emission.
1837 /// No-op if the id is unknown.
1838 pub fn set_magnet_enabled(&mut self, magnet: MagnetId, enabled: bool) {
1839 let Some(&owner) = self.magnet_owner.get(&magnet) else {
1840 return;
1841 };
1842 if let Some(list) = self.magnets.get_mut(&owner)
1843 && let Some((_, m)) = list.iter_mut().find(|(mid, _)| *mid == magnet)
1844 && m.enabled != enabled
1845 {
1846 m.enabled = enabled;
1847 self.bump_a11y_change();
1848 }
1849 }
1850
1851 /// The ids of every magnet attached to `item`, in insertion order
1852 /// (enabled and disabled alike). Empty if `item` is unknown or has
1853 /// no magnets.
1854 pub fn magnet_ids_of(&self, item: ItemId) -> Vec<MagnetId> {
1855 self.magnets
1856 .get(&item)
1857 .map(|list| list.iter().map(|(mid, _)| *mid).collect())
1858 .unwrap_or_default()
1859 }
1860
1861 /// The owning item of a magnet, or `None` if the id is unknown.
1862 pub fn magnet_owner(&self, magnet: MagnetId) -> Option<ItemId> {
1863 self.magnet_owner.get(&magnet).copied()
1864 }
1865
1866 /// The label set on a magnet (for the AT walker). `None` if unset
1867 /// or the id is unknown.
1868 pub(crate) fn magnet_label(&self, magnet: MagnetId) -> Option<teksilo_i18n::LocalizedString> {
1869 let owner = self.magnet_owner.get(&magnet)?;
1870 let list = self.magnets.get(owner)?;
1871 list.iter()
1872 .find(|(mid, _)| *mid == magnet)
1873 .and_then(|(_, m)| m.label.clone())
1874 }
1875
1876 /// Whether a magnet is enabled. `false` for an unknown id.
1877 pub fn magnet_enabled(&self, magnet: MagnetId) -> bool {
1878 let Some(owner) = self.magnet_owner.get(&magnet) else {
1879 return false;
1880 };
1881 self.magnets
1882 .get(owner)
1883 .and_then(|list| list.iter().find(|(mid, _)| *mid == magnet))
1884 .map(|(_, m)| m.enabled)
1885 .unwrap_or(false)
1886 }
1887
1888 /// A magnet's position in scene coordinates (its local position
1889 /// projected through its owning item's `scene_transform`). `None`
1890 /// for an unknown id or a degenerate item transform.
1891 pub fn magnet_scene_pos(&self, magnet: MagnetId) -> Option<Point> {
1892 let &owner = self.magnet_owner.get(&magnet)?;
1893 let list = self.magnets.get(&owner)?;
1894 let (_, m) = list.iter().find(|(mid, _)| *mid == magnet)?;
1895 self.map_to_scene(owner, m.local_pos)
1896 }
1897
1898 /// Resolve a magnet to a borrow-free [`MagnetRef`] snapshot (id,
1899 /// owning item, role, payload clone, current scene position).
1900 /// `None` for an unknown id or a degenerate item transform.
1901 pub fn magnet(&self, magnet: MagnetId) -> Option<MagnetRef> {
1902 let &owner = self.magnet_owner.get(&magnet)?;
1903 let list = self.magnets.get(&owner)?;
1904 let (_, m) = list.iter().find(|(mid, _)| *mid == magnet)?;
1905 let scene_pos = self.map_to_scene(owner, m.local_pos)?;
1906 Some(MagnetRef {
1907 id: magnet,
1908 item: owner,
1909 role: m.role,
1910 payload: m.payload.clone(),
1911 scene_pos,
1912 })
1913 }
1914
1915 /// Collect every enabled magnet whose scene position lies inside
1916 /// `scene_rect`, as borrow-free [`MagnetRef`] snapshots, excluding
1917 /// any magnet on `exclude_item`. Broad-phase over the spatial index
1918 /// (`items_in_rect`) so the cost is `O(visible × magnets/item)`.
1919 ///
1920 /// This is the shared narrow-phase input for both snap helpers: the
1921 /// candidates are materialised as owned snapshots, so the predicate
1922 /// that runs over them touches no scene state. The predicate may read
1923 /// the model (a shared borrow is re-entrant) but must not mutate it;
1924 /// mutation belongs in the consumer's `on_connect`, which fires after
1925 /// the snap call returns and every borrow is dropped.
1926 fn collect_candidate_magnets(
1927 &self,
1928 scene_rect: Rect,
1929 exclude_item: Option<ItemId>,
1930 ) -> Vec<MagnetRef> {
1931 let mut out = Vec::new();
1932 for item in self.items_in_rect(scene_rect) {
1933 if Some(item) == exclude_item {
1934 continue;
1935 }
1936 let Some(list) = self.magnets.get(&item) else {
1937 continue;
1938 };
1939 let xform = self.scene_transform(item);
1940 for (mid, m) in list {
1941 if !m.enabled {
1942 continue;
1943 }
1944 let scene_pos = xform.apply_point(m.local_pos);
1945 if !scene_rect.contains(scene_pos) {
1946 continue;
1947 }
1948 out.push(MagnetRef {
1949 id: *mid,
1950 item,
1951 role: m.role,
1952 payload: m.payload.clone(),
1953 scene_pos,
1954 });
1955 }
1956 }
1957 out
1958 }
1959
1960 /// Square-rect of half-extent `radius` centred on `center`.
1961 fn capture_rect(center: Point, radius: f32) -> Rect {
1962 Rect::new(
1963 center.x - radius,
1964 center.y - radius,
1965 radius * 2.0,
1966 radius * 2.0,
1967 )
1968 }
1969
1970 /// Compute the best item-drag snap: the dragged item is visually
1971 /// offset by `drag_delta`, and each of its enabled magnets seeks the
1972 /// nearest *accepting* magnet on another item within `capture_radius`
1973 /// (in scene units). Returns the globally closest accepting pair, or
1974 /// `None` if nothing accepts within range.
1975 ///
1976 /// Pure mechanism: it collects candidates under a brief read, then
1977 /// runs the consumer `predicate` with no scene borrow held, so the
1978 /// predicate may inspect payloads freely. `snap_vector` added to
1979 /// `drag_delta` aligns the dragged magnet onto its target.
1980 pub fn compute_item_snap(
1981 &self,
1982 dragged: ItemId,
1983 drag_delta: Vec2,
1984 capture_radius: f32,
1985 predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
1986 ) -> Option<MagnetSnap> {
1987 if capture_radius <= 0.0 {
1988 return None;
1989 }
1990 let dragged_list = self.magnets.get(&dragged)?;
1991 if dragged_list.is_empty() {
1992 return None;
1993 }
1994 // Visual scene positions of the dragged item's enabled magnets:
1995 // committed scene pos + the live drag delta.
1996 let xform = self.scene_transform(dragged);
1997 let dragged_magnets: Vec<MagnetRef> = dragged_list
1998 .iter()
1999 .filter(|(_, m)| m.enabled)
2000 .map(|(mid, m)| {
2001 let committed = xform.apply_point(m.local_pos);
2002 MagnetRef {
2003 id: *mid,
2004 item: dragged,
2005 role: m.role,
2006 payload: m.payload.clone(),
2007 scene_pos: Point::new(committed.x + drag_delta.x, committed.y + drag_delta.y),
2008 }
2009 })
2010 .collect();
2011 if dragged_magnets.is_empty() {
2012 return None;
2013 }
2014
2015 let mut best: Option<MagnetSnap> = None;
2016 for from in &dragged_magnets {
2017 let rect = Self::capture_rect(from.scene_pos, capture_radius);
2018 let candidates = self.collect_candidate_magnets(rect, Some(dragged));
2019 for to in &candidates {
2020 let dx = to.scene_pos.x - from.scene_pos.x;
2021 let dy = to.scene_pos.y - from.scene_pos.y;
2022 let dist = (dx * dx + dy * dy).sqrt();
2023 if dist > capture_radius {
2024 continue;
2025 }
2026 let MagnetVerdict::Accept(payload) = predicate(from, to) else {
2027 continue;
2028 };
2029 let better = best.as_ref().map(|b| dist < b.distance).unwrap_or(true);
2030 if better {
2031 best = Some(MagnetSnap {
2032 from: from.id,
2033 to: to.id,
2034 snap_vector: Vec2::new(dx, dy),
2035 payload,
2036 distance: dist,
2037 });
2038 }
2039 }
2040 }
2041 best
2042 }
2043
2044 /// Compute the best port-drag snap: a single `source` magnet is
2045 /// dragging a transient wire whose free end is at `cursor_scene`.
2046 /// Finds the nearest *accepting* target magnet within
2047 /// `capture_radius` (scene units), excluding the source's own
2048 /// magnet. Returns the target [`MagnetRef`] and the accepting
2049 /// verdict's payload, or `None`.
2050 pub fn compute_port_snap(
2051 &self,
2052 source: MagnetId,
2053 cursor_scene: Point,
2054 capture_radius: f32,
2055 predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
2056 ) -> Option<(MagnetRef, Option<Rc<dyn std::any::Any>>)> {
2057 if capture_radius <= 0.0 {
2058 return None;
2059 }
2060 let from = self.magnet(source)?;
2061 let rect = Self::capture_rect(cursor_scene, capture_radius);
2062 // Don't exclude the source's whole item — a node may legitimately
2063 // connect to another of its own ports in some graphs; only the
2064 // source magnet itself is excluded (below).
2065 let candidates = self.collect_candidate_magnets(rect, None);
2066 let mut best: Option<(MagnetRef, Option<Rc<dyn std::any::Any>>, f32)> = None;
2067 for to in candidates {
2068 if to.id == source {
2069 continue;
2070 }
2071 let dx = to.scene_pos.x - cursor_scene.x;
2072 let dy = to.scene_pos.y - cursor_scene.y;
2073 let dist = (dx * dx + dy * dy).sqrt();
2074 if dist > capture_radius {
2075 continue;
2076 }
2077 let MagnetVerdict::Accept(payload) = predicate(&from, &to) else {
2078 continue;
2079 };
2080 let better = best.as_ref().map(|b| dist < b.2).unwrap_or(true);
2081 if better {
2082 best = Some((to, payload, dist));
2083 }
2084 }
2085 best.map(|(to, payload, _)| (to, payload))
2086 }
2087
2088 /// The nearest enabled magnet to `scene_pt` within `radius` (scene
2089 /// units), or `None`. Used by the view to start a port-drag from a
2090 /// grabbed magnet handle (the handle's grab area is a screen-pixel
2091 /// disc, converted to scene units by the caller).
2092 pub fn nearest_magnet(&self, scene_pt: Point, radius: f32) -> Option<MagnetId> {
2093 if radius <= 0.0 {
2094 return None;
2095 }
2096 let rect = Self::capture_rect(scene_pt, radius);
2097 let mut best: Option<(MagnetId, f32)> = None;
2098 for c in self.collect_candidate_magnets(rect, None) {
2099 let dx = c.scene_pos.x - scene_pt.x;
2100 let dy = c.scene_pos.y - scene_pt.y;
2101 let dist = (dx * dx + dy * dy).sqrt();
2102 if dist > radius {
2103 continue;
2104 }
2105 let better = best.map(|b| dist < b.1).unwrap_or(true);
2106 if better {
2107 best = Some((c.id, dist));
2108 }
2109 }
2110 best.map(|(id, _)| id)
2111 }
2112
2113 // -----------------------------------------------------------------
2114 // Logical AT structure (kept verbatim from R0)
2115 // -----------------------------------------------------------------
2116
2117 /// Declare a virtual AT group. The group has no visual
2118 /// counterpart — it exists so the AT walker can emit an AT node
2119 /// under which items / other groups / widgets can be reparented.
2120 pub fn add_a11y_group(&mut self, builder: A11yGroupBuilder) -> A11yGroupId {
2121 let id = A11yGroupId::next();
2122 let group = A11yGroup {
2123 id,
2124 label: builder.label,
2125 role: builder.role,
2126 };
2127 let pos = self.a11y_groups.len();
2128 self.a11y_groups.push(group);
2129 self.a11y_group_index.insert(id, pos);
2130 self.bump_a11y_change();
2131 id
2132 }
2133
2134 /// Remove a logical group; orphaned references fall back to
2135 /// SceneView root. Relations / live / landmarks / categories
2136 /// targeting this group are cleaned up too.
2137 pub fn remove_a11y_group(&mut self, id: A11yGroupId) {
2138 let prev = self.a11y_groups.len();
2139 self.a11y_groups.retain(|g| g.id != id);
2140 if self.a11y_groups.len() != prev {
2141 self.a11y_group_index.clear();
2142 for (pos, group) in self.a11y_groups.iter().enumerate() {
2143 self.a11y_group_index.insert(group.id, pos);
2144 }
2145 }
2146 let target = A11yNode::Group(id);
2147 self.a11y_parents
2148 .retain(|child, parent| *child != target && *parent != target);
2149 self.a11y_relations
2150 .retain(|(from, _, to)| *from != target && *to != target);
2151 self.a11y_live.remove(&target);
2152 self.a11y_landmarks.remove(&target);
2153 self.a11y_categories.remove(&target);
2154 self.bump_a11y_change();
2155 }
2156
2157 /// Borrow a logical group by id.
2158 pub fn a11y_group(&self, id: A11yGroupId) -> Option<&A11yGroup> {
2159 let pos = *self.a11y_group_index.get(&id)?;
2160 self.a11y_groups.get(pos)
2161 }
2162
2163 /// Declare a logical-parent relationship for AT (independent of
2164 /// visual placement).
2165 pub fn set_a11y_parent(&mut self, child: A11yNode, parent: Option<A11yNode>) {
2166 match parent {
2167 Some(p) => {
2168 self.a11y_parents.insert(child, p);
2169 }
2170 None => {
2171 self.a11y_parents.remove(&child);
2172 }
2173 }
2174 self.bump_a11y_change();
2175 }
2176
2177 /// The currently-declared logical parent of a node.
2178 pub fn a11y_parent_of(&self, child: A11yNode) -> Option<A11yNode> {
2179 self.a11y_parents.get(&child).copied()
2180 }
2181
2182 /// Declare an AT relationship between two nodes.
2183 pub fn add_a11y_relation(&mut self, from: A11yNode, kind: A11yRelation, to: A11yNode) {
2184 self.a11y_relations.push((from, kind, to));
2185 self.bump_a11y_change();
2186 }
2187
2188 /// All declared AT relations.
2189 pub fn a11y_relations(&self) -> &[(A11yNode, A11yRelation, A11yNode)] {
2190 &self.a11y_relations
2191 }
2192
2193 /// Mark a node as a live region. Pass `Live::Off` to clear.
2194 pub fn set_a11y_live(&mut self, node: A11yNode, live: accesskit::Live) {
2195 if matches!(live, accesskit::Live::Off) {
2196 self.a11y_live.remove(&node);
2197 } else {
2198 self.a11y_live.insert(node, live);
2199 }
2200 self.bump_a11y_change();
2201 }
2202
2203 /// Mark a node as a landmark by overriding its role. Pass
2204 /// `Role::Unknown` to clear.
2205 pub fn set_a11y_landmark(&mut self, node: A11yNode, role: accesskit::Role) {
2206 if matches!(role, accesskit::Role::Unknown) {
2207 self.a11y_landmarks.remove(&node);
2208 } else {
2209 self.a11y_landmarks.insert(node, role);
2210 }
2211 self.bump_a11y_change();
2212 }
2213
2214 /// Tag a node with rotor / quick-nav categories.
2215 pub fn set_a11y_categories(&mut self, node: A11yNode, categories: &[A11yCategory]) {
2216 if categories.is_empty() {
2217 self.a11y_categories.remove(&node);
2218 } else {
2219 self.a11y_categories.insert(node, categories.to_vec());
2220 }
2221 self.bump_a11y_change();
2222 }
2223
2224 /// Read declared categories for a node.
2225 pub fn a11y_categories_of(&self, node: A11yNode) -> Option<&[A11yCategory]> {
2226 self.a11y_categories.get(&node).map(|v| v.as_slice())
2227 }
2228}
2229
2230impl Default for Scene {
2231 fn default() -> Self {
2232 Self::new()
2233 }
2234}
2235
2236impl std::fmt::Debug for Scene {
2237 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2238 f.debug_struct("Scene")
2239 .field("len", &self.entries.len())
2240 .field("index", &self.index)
2241 .finish_non_exhaustive()
2242 }
2243}
2244
2245/// Half-open AABB intersection: two rects intersect iff their
2246/// projections overlap on both axes.
2247pub(crate) fn rects_intersect(a: Rect, b: Rect) -> bool {
2248 a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height
2249}
2250
2251/// AABB of the union of two rectangles.
2252fn union_two_rects(a: Rect, b: Rect) -> Rect {
2253 let x = a.x.min(b.x);
2254 let y = a.y.min(b.y);
2255 let r = a.right().max(b.right());
2256 let bot = a.bottom().max(b.bottom());
2257 Rect::new(x, y, r - x, bot - y)
2258}
2259
2260/// AABB enclosing every point in a path. Returns `None` for an
2261/// empty path. Curves contribute their control / end points only —
2262/// callers needing tight bounds for cubics should pre-compute and
2263/// pass the AABB directly via `Scene::items_in_rect`.
2264fn path_aabb(path: &Path) -> Option<Rect> {
2265 let mut min_x = f32::INFINITY;
2266 let mut min_y = f32::INFINITY;
2267 let mut max_x = f32::NEG_INFINITY;
2268 let mut max_y = f32::NEG_INFINITY;
2269 let mut include = |p: Point| {
2270 min_x = min_x.min(p.x);
2271 min_y = min_y.min(p.y);
2272 max_x = max_x.max(p.x);
2273 max_y = max_y.max(p.y);
2274 };
2275 for cmd in &path.commands {
2276 match cmd {
2277 teksilo_canvas::PathCommand::MoveTo(p) | teksilo_canvas::PathCommand::LineTo(p) => {
2278 include(*p)
2279 }
2280 teksilo_canvas::PathCommand::QuadTo { control, to } => {
2281 include(*control);
2282 include(*to);
2283 }
2284 teksilo_canvas::PathCommand::CubicTo {
2285 control1,
2286 control2,
2287 to,
2288 } => {
2289 include(*control1);
2290 include(*control2);
2291 include(*to);
2292 }
2293 teksilo_canvas::PathCommand::ArcTo { rect, .. } => {
2294 include(Point::new(rect.x, rect.y));
2295 include(Point::new(rect.right(), rect.bottom()));
2296 }
2297 teksilo_canvas::PathCommand::Close => {}
2298 }
2299 }
2300 if !min_x.is_finite() {
2301 return None;
2302 }
2303 Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
2304}
2305
2306#[cfg(test)]
2307mod tests {
2308 use super::*;
2309 use crate::items::RectItem;
2310 use teksilo_canvas::{Size, SizeProposal};
2311 use teksilo_core::widget::{LayoutContext, LayoutResponse, Widget};
2312 use teksilo_tokens::Color;
2313
2314 #[derive(Debug)]
2315 struct FillWidget;
2316
2317 impl FillWidget {
2318 fn new() -> Self {
2319 Self
2320 }
2321 }
2322
2323 impl Widget for FillWidget {
2324 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
2325 Size::new(0.0, 0.0).into()
2326 }
2327 }
2328
2329 #[test]
2330 fn add_widget_round_trip() {
2331 let mut scene = Scene::new();
2332 let r = Rect::new(10.0, 20.0, 100.0, 50.0);
2333 let id = scene.add_widget(FillWidget::new(), r);
2334 assert_eq!(scene.len(), 1);
2335 // scene_rect is computed from local_pos + local_bounds.
2336 assert_eq!(scene.scene_rect(id), Some(r));
2337 assert_eq!(scene.local_pos(id), Some(Point::new(10.0, 20.0)));
2338 assert_eq!(
2339 scene.local_bounds(id),
2340 Some(Rect::new(0.0, 0.0, 100.0, 50.0))
2341 );
2342 assert_eq!(scene.ids(), vec![id]);
2343 }
2344
2345 #[test]
2346 fn add_item_at_local_pos() {
2347 let mut scene = Scene::new();
2348 let id = scene.add_item(
2349 RectItem::new(Rect::new(0.0, 0.0, 30.0, 40.0)).fill(Color::RED),
2350 Point::new(10.0, 20.0),
2351 );
2352 assert_eq!(
2353 scene.scene_rect(id),
2354 Some(Rect::new(10.0, 20.0, 30.0, 40.0))
2355 );
2356 assert_eq!(scene.scene_pos(id), Some(Point::new(10.0, 20.0)));
2357 }
2358
2359 #[test]
2360 fn set_local_pos_updates_scene_rect_and_index() {
2361 let mut scene = Scene::new();
2362 let id = scene.add_item(
2363 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2364 Point::new(0.0, 0.0),
2365 );
2366 scene.set_local_pos(id, Point::new(500.0, 500.0));
2367 assert_eq!(
2368 scene.scene_rect(id),
2369 Some(Rect::new(500.0, 500.0, 10.0, 10.0))
2370 );
2371 let near_origin = scene.items_in_rect(Rect::new(0.0, 0.0, 50.0, 50.0));
2372 assert!(!near_origin.contains(&id));
2373 let near_far = scene.items_in_rect(Rect::new(490.0, 490.0, 30.0, 30.0));
2374 assert!(near_far.contains(&id));
2375 }
2376
2377 #[test]
2378 fn parent_relative_position_composes_through_chain() {
2379 let mut scene = Scene::new();
2380 let parent = scene.add_item(
2381 RectItem::new(Rect::new(0.0, 0.0, 100.0, 100.0)),
2382 Point::new(50.0, 50.0),
2383 );
2384 let child = scene.add_item(
2385 RectItem::new(Rect::new(0.0, 0.0, 20.0, 20.0)),
2386 Point::new(10.0, 10.0),
2387 );
2388 scene.set_item_parent(child, Some(parent));
2389
2390 // Child's scene_pos = parent local_pos + child local_pos.
2391 assert_eq!(scene.scene_pos(child), Some(Point::new(60.0, 60.0)));
2392 // Move parent — child's scene_pos shifts in lockstep.
2393 scene.set_local_pos(parent, Point::new(150.0, 150.0));
2394 assert_eq!(scene.scene_pos(child), Some(Point::new(160.0, 160.0)));
2395 }
2396
2397 #[test]
2398 fn set_local_pos_propagates_to_descendants_scene_pos() {
2399 // Three-deep chain: grandparent → parent → child.
2400 let mut scene = Scene::new();
2401 let gp = scene.add_item(
2402 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2403 Point::new(0.0, 0.0),
2404 );
2405 let p = scene.add_item(
2406 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2407 Point::new(20.0, 0.0),
2408 );
2409 let c = scene.add_item(
2410 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2411 Point::new(5.0, 0.0),
2412 );
2413 scene.set_item_parent(p, Some(gp));
2414 scene.set_item_parent(c, Some(p));
2415
2416 assert_eq!(scene.scene_pos(c), Some(Point::new(25.0, 0.0)));
2417 scene.set_local_pos(gp, Point::new(100.0, 100.0));
2418 assert_eq!(scene.scene_pos(c), Some(Point::new(125.0, 100.0)));
2419 }
2420
2421 #[test]
2422 fn remove_drops_the_entry() {
2423 let mut scene = Scene::new();
2424 let a = scene.add_widget(FillWidget::new(), Rect::ZERO);
2425 let b = scene.add_widget(FillWidget::new(), Rect::ZERO);
2426 scene.remove(a);
2427 assert_eq!(scene.len(), 1);
2428 assert_eq!(scene.scene_rect(a), None);
2429 assert!(scene.scene_rect(b).is_some());
2430 }
2431
2432 #[test]
2433 fn items_in_rect_brute_force() {
2434 let mut scene = Scene::new();
2435 let a = scene.add_widget(FillWidget::new(), Rect::new(0.0, 0.0, 10.0, 10.0));
2436 let b = scene.add_widget(FillWidget::new(), Rect::new(100.0, 100.0, 10.0, 10.0));
2437 let c = scene.add_widget(FillWidget::new(), Rect::new(5.0, 5.0, 10.0, 10.0));
2438
2439 let near_origin = scene.items_in_rect(Rect::new(0.0, 0.0, 20.0, 20.0));
2440 assert!(near_origin.contains(&a));
2441 assert!(near_origin.contains(&c));
2442 assert!(!near_origin.contains(&b));
2443
2444 let far = scene.items_in_rect(Rect::new(95.0, 95.0, 20.0, 20.0));
2445 assert_eq!(far, vec![b]);
2446
2447 let empty = scene.items_in_rect(Rect::new(500.0, 500.0, 1.0, 1.0));
2448 assert!(empty.is_empty());
2449 }
2450
2451 #[test]
2452 fn item_at_picks_topmost() {
2453 let mut scene = Scene::new();
2454 let bottom = scene.add_item(
2455 RectItem::new(Rect::new(0.0, 0.0, 100.0, 100.0)),
2456 Point::new(0.0, 0.0),
2457 );
2458 let top = scene.add_item(
2459 RectItem::new(Rect::new(0.0, 0.0, 50.0, 50.0)),
2460 Point::new(25.0, 25.0),
2461 );
2462 scene.set_z(top, 1.0);
2463 scene.set_z(bottom, 0.0);
2464 // Click in the overlap region.
2465 assert_eq!(scene.item_at(Point::new(50.0, 50.0)), Some(top));
2466 // Click outside the top, inside the bottom.
2467 assert_eq!(scene.item_at(Point::new(10.0, 10.0)), Some(bottom));
2468 // Click outside everything.
2469 assert_eq!(scene.item_at(Point::new(500.0, 500.0)), None);
2470 }
2471
2472 #[test]
2473 fn item_accessor_returns_lightweight_only() {
2474 let mut scene = Scene::new();
2475 let widget_id = scene.add_widget(FillWidget::new(), Rect::ZERO);
2476 let item_id = scene.add_item(
2477 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2478 Point::new(0.0, 0.0),
2479 );
2480 assert!(scene.item(item_id).is_some());
2481 assert!(scene.item(widget_id).is_none());
2482 }
2483
2484 #[test]
2485 fn map_to_scene_round_trips() {
2486 let mut scene = Scene::new();
2487 let id = scene.add_item(
2488 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2489 Point::new(50.0, 50.0),
2490 );
2491 let local = Point::new(3.0, 4.0);
2492 let scene_pt = scene.map_to_scene(id, local).unwrap();
2493 let back = scene.map_from_scene(id, scene_pt).unwrap();
2494 assert!((back.x - local.x).abs() < 1e-5);
2495 assert!((back.y - local.y).abs() < 1e-5);
2496 }
2497
2498 #[test]
2499 fn rects_intersect_edge_touching_excluded() {
2500 let a = Rect::new(0.0, 0.0, 10.0, 10.0);
2501 let b = Rect::new(10.0, 0.0, 10.0, 10.0);
2502 assert!(!rects_intersect(a, b));
2503 assert!(!rects_intersect(b, a));
2504 }
2505
2506 #[test]
2507 fn flags_default_carries_visible_enabled_selectable() {
2508 let mut scene = Scene::new();
2509 let id = scene.add_item(
2510 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2511 Point::new(0.0, 0.0),
2512 );
2513 let f = scene.flags(id).unwrap();
2514 assert!(f.contains(ItemFlags::IS_VISIBLE));
2515 assert!(f.contains(ItemFlags::IS_ENABLED));
2516 assert!(f.contains(ItemFlags::IS_SELECTABLE));
2517 assert!(!f.contains(ItemFlags::IS_DRAGGABLE));
2518 }
2519
2520 #[test]
2521 fn draggable_builder_sets_is_draggable_flag() {
2522 let mut scene = Scene::new();
2523 let id = scene.add_item(
2524 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)).draggable(true),
2525 Point::ZERO,
2526 );
2527 assert!(scene.flags(id).unwrap().contains(ItemFlags::IS_DRAGGABLE));
2528 }
2529
2530 #[test]
2531 fn set_visible_flag_chains_through_parent() {
2532 let mut scene = Scene::new();
2533 let parent = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
2534 let child = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 5.0, 5.0)), Point::ZERO);
2535 scene.set_item_parent(child, Some(parent));
2536 assert!(scene.is_effectively_visible(child));
2537 scene.set_visible(parent, false);
2538 assert!(!scene.is_effectively_visible(child));
2539 assert!(!scene.is_effectively_visible(parent));
2540 }
2541
2542 #[test]
2543 fn effective_opacity_composes_through_chain() {
2544 let mut scene = Scene::new();
2545 let p = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
2546 let c = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 5.0, 5.0)), Point::ZERO);
2547 scene.set_item_parent(c, Some(p));
2548 scene.set_opacity(p, 0.5);
2549 scene.set_opacity(c, 0.5);
2550 assert!((scene.effective_opacity(c) - 0.25).abs() < 1e-5);
2551 }
2552
2553 #[test]
2554 fn opacity_clamps_to_unit_range() {
2555 let mut scene = Scene::new();
2556 let id = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
2557 scene.set_opacity(id, 1.5);
2558 assert_eq!(scene.opacity(id), Some(1.0));
2559 scene.set_opacity(id, -0.3);
2560 assert_eq!(scene.opacity(id), Some(0.0));
2561 }
2562
2563 #[test]
2564 fn scene_rect_extent_uses_user_set_when_present() {
2565 let mut scene = Scene::new();
2566 let user = Rect::new(0.0, 0.0, 1000.0, 1000.0);
2567 scene.set_scene_rect(Some(user));
2568 assert_eq!(scene.scene_rect_extent(), Some(user));
2569 scene.set_scene_rect(None);
2570 // No items, no auto-extent.
2571 assert_eq!(scene.scene_rect_extent(), None);
2572 }
2573
2574 #[test]
2575 fn scene_rect_extent_auto_unions_items_when_unset() {
2576 let mut scene = Scene::new();
2577 scene.add_item(
2578 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2579 Point::new(5.0, 5.0),
2580 );
2581 scene.add_item(
2582 RectItem::new(Rect::new(0.0, 0.0, 20.0, 20.0)),
2583 Point::new(100.0, 100.0),
2584 );
2585 let extent = scene.scene_rect_extent().unwrap();
2586 // (5,5)-(15,15) ∪ (100,100)-(120,120) = (5,5)-(120,120).
2587 assert!((extent.x - 5.0).abs() < 1e-3);
2588 assert!((extent.y - 5.0).abs() < 1e-3);
2589 assert!((extent.width - 115.0).abs() < 1e-3);
2590 assert!((extent.height - 115.0).abs() < 1e-3);
2591 }
2592
2593 #[test]
2594 fn pan_axes_default_is_both() {
2595 let scene = Scene::new();
2596 assert_eq!(scene.current_pan_axes(), PanAxes::Both);
2597 assert!(scene.is_zoomable());
2598 }
2599
2600 #[test]
2601 fn pan_axes_set_round_trip() {
2602 let mut scene = Scene::new();
2603 scene.pan_axes(PanAxes::Horizontal);
2604 assert_eq!(scene.current_pan_axes(), PanAxes::Horizontal);
2605 scene.zoomable(false);
2606 assert!(!scene.is_zoomable());
2607 }
2608
2609 #[test]
2610 fn item_change_signal_fires_on_set_local_pos() {
2611 use std::cell::Cell;
2612 use std::rc::Rc;
2613 let mut scene = Scene::new();
2614 let id = scene.add_item(
2615 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2616 Point::new(0.0, 0.0),
2617 );
2618 let last = Rc::new(Cell::new(None::<ItemChange>));
2619 let last_clone = last.clone();
2620 let _h = scene.item_change_signal().observe(move |c| {
2621 last_clone.set(Some(*c));
2622 });
2623 scene.set_local_pos(id, Point::new(50.0, 60.0));
2624 match last.get() {
2625 Some(ItemChange::LocalPosChanged { new, .. }) => {
2626 assert_eq!(new, Point::new(50.0, 60.0));
2627 }
2628 other => panic!("expected LocalPosChanged, got {:?}", other),
2629 }
2630 }
2631
2632 #[test]
2633 fn item_change_signal_fires_on_set_visible() {
2634 use std::cell::Cell;
2635 use std::rc::Rc;
2636 let mut scene = Scene::new();
2637 let id = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
2638 let count = Rc::new(Cell::new(0_u32));
2639 let count_clone = count.clone();
2640 let _h = scene.item_change_signal().observe(move |c| {
2641 if matches!(c, ItemChange::VisibilityChanged { .. }) {
2642 count_clone.set(count_clone.get() + 1);
2643 }
2644 });
2645 scene.set_visible(id, false);
2646 scene.set_visible(id, true);
2647 // Same value twice: only one fire.
2648 scene.set_visible(id, true);
2649 assert_eq!(count.get(), 2);
2650 }
2651
2652 #[test]
2653 fn colliding_items_returns_overlapping_set_excluding_self() {
2654 let mut scene = Scene::new();
2655 let a = scene.add_item(
2656 RectItem::new(Rect::new(0.0, 0.0, 50.0, 50.0)),
2657 Point::new(10.0, 10.0),
2658 );
2659 let b = scene.add_item(
2660 RectItem::new(Rect::new(0.0, 0.0, 50.0, 50.0)),
2661 Point::new(40.0, 10.0),
2662 );
2663 let c = scene.add_item(
2664 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2665 Point::new(500.0, 500.0),
2666 );
2667 let collisions = scene.colliding_items(a);
2668 assert!(collisions.contains(&b));
2669 assert!(!collisions.contains(&a));
2670 assert!(!collisions.contains(&c));
2671 }
2672
2673 #[test]
2674 fn items_along_path_finds_items_under_path_aabb() {
2675 let mut scene = Scene::new();
2676 let a = scene.add_item(
2677 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2678 Point::new(20.0, 20.0),
2679 );
2680 let b = scene.add_item(
2681 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2682 Point::new(200.0, 200.0),
2683 );
2684 let mut path = Path::new();
2685 path.move_to(Point::new(15.0, 15.0));
2686 path.line_to(Point::new(40.0, 40.0));
2687 let hits = scene.items_along_path(&path);
2688 assert!(hits.contains(&a));
2689 assert!(!hits.contains(&b));
2690 }
2691
2692 // -----------------------------------------------------------------
2693 // R6 — code-level fixes
2694 // -----------------------------------------------------------------
2695
2696 #[test]
2697 fn scene_remove_recursively_removes_descendants() {
2698 let mut scene = Scene::new();
2699 let parent = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 50.0, 50.0)), Point::ZERO);
2700 let child = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 20.0, 20.0)), Point::ZERO);
2701 let grandchild = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 5.0, 5.0)), Point::ZERO);
2702 scene.set_item_parent(child, Some(parent));
2703 scene.set_item_parent(grandchild, Some(child));
2704 assert_eq!(scene.entries.len(), 3);
2705 scene.remove(parent);
2706 // Parent + child + grandchild all gone.
2707 assert!(scene.scene_rect(parent).is_none());
2708 assert!(scene.scene_rect(child).is_none());
2709 assert!(scene.scene_rect(grandchild).is_none());
2710 assert_eq!(scene.entries.len(), 0);
2711 }
2712
2713 #[test]
2714 fn scene_orphan_promotes_children_to_root() {
2715 let mut scene = Scene::new();
2716 let parent = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 50.0, 50.0)), Point::ZERO);
2717 let child = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 20.0, 20.0)), Point::ZERO);
2718 scene.set_item_parent(child, Some(parent));
2719 scene.orphan(parent);
2720 // Child's parent is now None.
2721 assert_eq!(scene.parent_of(child), None);
2722 // Both still present.
2723 scene.remove(parent);
2724 assert!(scene.scene_rect(child).is_some());
2725 }
2726
2727 #[test]
2728 fn scene_orphan_rebuckets_detached_children() {
2729 // After orphaning, the spatial index must reflect children's
2730 // new scene-AABBs. Move a parent off-origin, attach a child,
2731 // then orphan — items_in_rect at the child's *child-local*
2732 // origin must now return it (because its scene_transform no
2733 // longer composes the parent's offset).
2734 let mut scene = Scene::new();
2735 let parent = scene.add_item(
2736 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2737 Point::new(500.0, 500.0),
2738 );
2739 let child = scene.add_item(
2740 RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)),
2741 Point::new(0.0, 0.0),
2742 );
2743 scene.set_item_parent(child, Some(parent));
2744 // Pre-orphan: child sits at scene (500, 500).
2745 assert!(
2746 scene
2747 .items_in_rect(Rect::new(495.0, 495.0, 20.0, 20.0))
2748 .contains(&child)
2749 );
2750 scene.orphan(parent);
2751 // Post-orphan: child sits at scene (0, 0); the index must
2752 // reflect that — query at the new origin must hit, query at
2753 // the old origin must miss.
2754 assert!(
2755 scene
2756 .items_in_rect(Rect::new(-5.0, -5.0, 20.0, 20.0))
2757 .contains(&child)
2758 );
2759 assert!(
2760 !scene
2761 .items_in_rect(Rect::new(495.0, 495.0, 20.0, 20.0))
2762 .contains(&child)
2763 );
2764 }
2765
2766 #[test]
2767 fn add_item_dynamic_re_reads_bounds_on_refresh() {
2768 // An item whose `local_bounds` reads from a Cell. Mutating
2769 // the cell + calling refresh_dynamic_bounds must update the
2770 // entry and re-bucket the spatial index.
2771 use crate::item::{SceneItem, SceneItemPaintContext};
2772 use std::cell::Cell;
2773 use std::rc::Rc;
2774
2775 #[derive(Debug)]
2776 struct DynRect {
2777 bounds: Rc<Cell<Rect>>,
2778 }
2779 impl SceneItem for DynRect {
2780 fn local_bounds(&self) -> Rect {
2781 self.bounds.get()
2782 }
2783 fn set_local_bounds(&mut self, b: Rect) {
2784 self.bounds.set(b);
2785 }
2786 fn paint(&self, _: &mut teksilo_canvas::Canvas, _: &SceneItemPaintContext<'_>) {}
2787 }
2788
2789 let bounds = Rc::new(Cell::new(Rect::new(0.0, 0.0, 10.0, 10.0)));
2790 let mut scene = Scene::new();
2791 let id = scene.add_item_dynamic(
2792 DynRect {
2793 bounds: bounds.clone(),
2794 },
2795 Point::ZERO,
2796 );
2797 // Initially items_in_rect over the small AABB hits.
2798 assert!(
2799 scene
2800 .items_in_rect(Rect::new(0.0, 0.0, 50.0, 50.0))
2801 .contains(&id)
2802 );
2803 // Grow the bounds via the Cell — Scene's cached entry/index
2804 // is stale until refresh_dynamic_bounds runs.
2805 bounds.set(Rect::new(0.0, 0.0, 500.0, 500.0));
2806 scene.refresh_dynamic_bounds();
2807 // After refresh, the spatial index sees the larger AABB.
2808 assert!(
2809 scene
2810 .items_in_rect(Rect::new(400.0, 400.0, 10.0, 10.0))
2811 .contains(&id)
2812 );
2813 }
2814
2815 #[test]
2816 fn add_item_static_does_not_track_signal_changes() {
2817 // Counterpart to the dynamic test: a static item's bounds
2818 // are snapshotted at insert time; refresh_dynamic_bounds
2819 // does not re-read them.
2820 use crate::item::{SceneItem, SceneItemPaintContext};
2821 use std::cell::Cell;
2822 use std::rc::Rc;
2823
2824 #[derive(Debug)]
2825 struct DynRect {
2826 bounds: Rc<Cell<Rect>>,
2827 }
2828 impl SceneItem for DynRect {
2829 fn local_bounds(&self) -> Rect {
2830 self.bounds.get()
2831 }
2832 fn set_local_bounds(&mut self, b: Rect) {
2833 self.bounds.set(b);
2834 }
2835 fn paint(&self, _: &mut teksilo_canvas::Canvas, _: &SceneItemPaintContext<'_>) {}
2836 }
2837
2838 let bounds = Rc::new(Cell::new(Rect::new(0.0, 0.0, 10.0, 10.0)));
2839 let mut scene = Scene::new();
2840 let id = scene.add_item(
2841 DynRect {
2842 bounds: bounds.clone(),
2843 },
2844 Point::ZERO,
2845 );
2846 bounds.set(Rect::new(0.0, 0.0, 500.0, 500.0));
2847 scene.refresh_dynamic_bounds();
2848 // Static entry's spatial index unchanged.
2849 assert!(
2850 !scene
2851 .items_in_rect(Rect::new(400.0, 400.0, 10.0, 10.0))
2852 .contains(&id)
2853 );
2854 }
2855}