Skip to main content

teksilo_scene/
scene_model.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! [`SceneModel`] — a shared, cloneable handle to a [`Scene`].
5//!
6//! Mirrors the `ListModel = Rc<RefCell<ListModelInner>>` pattern from
7//! `teksilo-data`: cloning a `SceneModel` produces a **second handle to the
8//! same scene**, so multiple [`SceneView`](crate::SceneView)s can render one
9//! scene (overview + detail panes, same-document multi-window, headless model
10//! reuse). Mutate the model once and every attached view reconciles.
11//!
12//! ## Heavyweight content across views
13//!
14//! A heavyweight `Widget` instance can live in only one arena, so a shared
15//! model cannot hand the *same* `Box<dyn Widget>` to two views. Two paths:
16//!
17//! - **Single-view** — [`add_widget`](SceneModel::add_widget) stores the
18//!   widget in a one-shot slot drained by the first view that builds. A
19//!   second view sharing the model produces no child for it.
20//! - **Multi-view** — [`add_widget_item`](SceneModel::add_widget_item) stores
21//!   a type-erased `payload`; each view's delegate
22//!   ([`SceneView::delegate_typed`](crate::SceneView::delegate_typed)) builds
23//!   its **own** instance from the payload. [`set_payload`](SceneModel::set_payload)
24//!   replaces the data and every view rebuilds that item.
25//!
26//! ## Borrow / observer contract
27//!
28//! Every mutator takes `&self`, borrows the inner `RefCell<Scene>` mutably,
29//! mutates, and the borrow drops at the end of the statement. The change
30//! signal fires *inside* that borrow (via `Scene::emit_item_change`), but
31//! `Signal::try_set` snapshots its observers and releases the signal's own
32//! cell before invoking them — so the only rule is: **an observer registered
33//! on [`item_change_signal`](SceneModel::item_change_signal) /
34//! [`a11y_change_signal`](SceneModel::a11y_change_signal) must not re-borrow
35//! the `SceneModel` in its callback.** A `SceneView` observer only bumps its
36//! own per-view signals, so it is safe. Likewise a view **delegate** must not
37//! synchronously mutate the model during a build-time call (the view drops all
38//! model borrows before invoking it; the delegate's *handlers* may mutate
39//! later).
40
41use std::cell::RefCell;
42use std::rc::Rc;
43
44use teksilo_canvas::{Point, Rect, StrokeStyle, Transform2D};
45use teksilo_core::color_prop::ColorProp;
46use teksilo_core::signal::Signal;
47use teksilo_core::widget::Widget;
48
49use crate::a11y::{A11yCategory, A11yGroupBuilder, A11yGroupId, A11yNode, A11yRelation};
50use crate::flags::ItemFlags;
51use crate::index::SpatialIndex;
52use crate::item::{ItemId, SceneItem};
53use crate::item_handlers::SceneItemHandlerSet;
54use crate::magnet::{Magnet, MagnetId, MagnetRef, MagnetSnap, MagnetVerdict};
55use crate::scene::{ItemChange, PanAxes, Scene, SceneLayer};
56use teksilo_canvas::Vec2;
57
58/// A shared, cloneable handle to a [`Scene`].
59pub struct SceneModel(pub(crate) Rc<RefCell<Scene>>);
60
61impl Clone for SceneModel {
62    /// Produce a second handle to the **same** scene (cheap `Rc` clone).
63    fn clone(&self) -> Self {
64        Self(self.0.clone())
65    }
66}
67
68impl Default for SceneModel {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl std::fmt::Debug for SceneModel {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        match self.0.try_borrow() {
77            Ok(scene) => f
78                .debug_struct("SceneModel")
79                .field("handles", &Rc::strong_count(&self.0))
80                .field("len", &scene.len())
81                .finish(),
82            Err(_) => f
83                .debug_struct("SceneModel")
84                .field("handles", &Rc::strong_count(&self.0))
85                .field("len", &"<borrowed>")
86                .finish(),
87        }
88    }
89}
90
91impl SceneModel {
92    // -----------------------------------------------------------------
93    // Construction
94    // -----------------------------------------------------------------
95
96    /// A handle to a fresh empty scene with the default spatial index.
97    pub fn new() -> Self {
98        Self(Rc::new(RefCell::new(Scene::new())))
99    }
100
101    /// A handle to a fresh scene with a custom [`SpatialIndex`].
102    pub fn with_index(index: Box<dyn SpatialIndex>) -> Self {
103        Self(Rc::new(RefCell::new(Scene::with_index(index))))
104    }
105
106    /// Wrap an existing [`Scene`] in a handle. Used by
107    /// [`SceneView::new`](crate::SceneView::new) for the single-view path.
108    pub fn from_scene(scene: Scene) -> Self {
109        Self(Rc::new(RefCell::new(scene)))
110    }
111
112    /// Number of distinct handles to this scene (1 = unshared).
113    pub fn handle_count(&self) -> usize {
114        Rc::strong_count(&self.0)
115    }
116
117    // -----------------------------------------------------------------
118    // Heavyweight insertion
119    // -----------------------------------------------------------------
120
121    /// Single-view heavyweight widget (the one-shot `Once` path). The first
122    /// view to build drains it; a second view sharing this model produces no
123    /// child for it. For multi-view, use [`add_widget_item`](Self::add_widget_item).
124    pub fn add_widget<W: Widget + 'static>(&self, widget: W, rect: Rect) -> ItemId {
125        self.0.borrow_mut().add_widget(widget, rect)
126    }
127
128    /// Multi-view heavyweight item: store a typed `payload`; each view builds
129    /// its own widget instance from it via its delegate. Returns the [`ItemId`].
130    pub fn add_widget_item<P: 'static>(&self, payload: P, rect: Rect) -> ItemId {
131        self.0
132            .borrow_mut()
133            .add_widget_delegated(Rc::new(payload), rect)
134    }
135
136    /// Replace the payload of a `Delegated` heavyweight item; every view
137    /// rebuilds that item's widget on the next pass.
138    ///
139    /// # Panics
140    ///
141    /// Panics if `id` is unknown, refers to a single-view `add_widget` (Once)
142    /// entry, or refers to a lightweight item.
143    pub fn set_payload<P: 'static>(&self, id: ItemId, payload: P) {
144        self.0.borrow_mut().set_payload(id, Rc::new(payload));
145    }
146
147    /// The current type-erased payload of a `Delegated` item, if any.
148    pub fn payload(&self, id: ItemId) -> Option<Rc<dyn std::any::Any>> {
149        self.0.borrow().payload(id)
150    }
151
152    // -----------------------------------------------------------------
153    // Lightweight insertion
154    // -----------------------------------------------------------------
155
156    /// Add a lightweight [`SceneItem`] at `local_pos`.
157    pub fn add_item<I: SceneItem + 'static>(&self, item: I, local_pos: Point) -> ItemId {
158        self.0.borrow_mut().add_item(item, local_pos)
159    }
160
161    /// Add a lightweight item with signal-driven (dynamic) bounds.
162    pub fn add_item_dynamic<I: SceneItem + 'static>(&self, item: I, local_pos: Point) -> ItemId {
163        self.0.borrow_mut().add_item_dynamic(item, local_pos)
164    }
165
166    /// Add an already-boxed lightweight item at `local_pos`. The boxed-`dyn`
167    /// counterpart of [`add_item`](Self::add_item), used by
168    /// [`SceneListAdapter`](crate::SceneListAdapter).
169    pub fn add_boxed_item(&self, item: Box<dyn SceneItem>, local_pos: Point) -> ItemId {
170        self.0.borrow_mut().add_boxed_item(item, local_pos)
171    }
172
173    // -----------------------------------------------------------------
174    // Geometry mutation
175    // -----------------------------------------------------------------
176
177    /// Move `id` to `local_pos` in its parent's coordinate space; notifies all views.
178    pub fn set_local_pos(&self, id: ItemId, local_pos: Point) {
179        self.0.borrow_mut().set_local_pos(id, local_pos);
180    }
181    /// Replace the local bounding rect of `id`; notifies all views.
182    pub fn set_local_bounds(&self, id: ItemId, local_bounds: Rect) {
183        self.0.borrow_mut().set_local_bounds(id, local_bounds);
184    }
185    /// Set an additional local-to-parent transform (rotation, scale) on `id`; notifies all views.
186    pub fn set_transform(&self, id: ItemId, transform: Transform2D) {
187        self.0.borrow_mut().set_transform(id, transform);
188    }
189
190    // -----------------------------------------------------------------
191    // Flags / visibility / opacity mutation
192    // -----------------------------------------------------------------
193
194    /// Replace the complete [`ItemFlags`] bitset for `id`; notifies all views.
195    pub fn set_flags(&self, id: ItemId, flags: ItemFlags) {
196        self.0.borrow_mut().set_flags(id, flags);
197    }
198    /// Set or clear a single [`ItemFlags`] bit on `id`; notifies all views.
199    pub fn set_flag(&self, id: ItemId, flag: ItemFlags, on: bool) {
200        self.0.borrow_mut().set_flag(id, flag, on);
201    }
202    /// Show or hide `id` (also hides its descendants); notifies all views.
203    pub fn set_visible(&self, id: ItemId, visible: bool) {
204        self.0.borrow_mut().set_visible(id, visible);
205    }
206    /// Set the paint opacity of `id` (0.0 = transparent, 1.0 = opaque); notifies all views.
207    pub fn set_opacity(&self, id: ItemId, opacity: f32) {
208        self.0.borrow_mut().set_opacity(id, opacity);
209    }
210
211    // -----------------------------------------------------------------
212    // Appearance mutation (paint-only, repaint without relayout)
213    // -----------------------------------------------------------------
214
215    /// Replace a lightweight item's fill colour live; every view repaints
216    /// (no relayout/rebuild). Accepts a plain [`Color`](teksilo_tokens::Color),
217    /// a theme role, a `Signal<Color>`, or a `Signal<Role>`. See
218    /// [`Scene::set_item_fill`] for the reactive-colour contract.
219    pub fn set_item_fill(&self, id: ItemId, fill: impl Into<ColorProp>) {
220        self.0.borrow_mut().set_item_fill(id, fill);
221    }
222    /// Clear a lightweight item's fill; every view repaints.
223    pub fn clear_item_fill(&self, id: ItemId) {
224        self.0.borrow_mut().clear_item_fill(id);
225    }
226    /// Replace a lightweight item's stroke (colour + [`StrokeStyle`]) live;
227    /// every view repaints (no relayout/rebuild).
228    pub fn set_item_stroke(&self, id: ItemId, color: impl Into<ColorProp>, style: StrokeStyle) {
229        self.0.borrow_mut().set_item_stroke(id, color, style);
230    }
231    /// Clear a lightweight item's stroke; every view repaints.
232    pub fn clear_item_stroke(&self, id: ItemId) {
233        self.0.borrow_mut().clear_item_stroke(id);
234    }
235
236    // -----------------------------------------------------------------
237    // Z-order / layer / parenting mutation
238    // -----------------------------------------------------------------
239
240    /// Set the z-order of `id` within its layer; higher values paint on top.
241    pub fn set_z(&self, id: ItemId, z: f32) {
242        self.0.borrow_mut().set_z(id, z);
243    }
244    /// Give `id` the highest z-value in its layer so it paints on top of all siblings.
245    pub fn bring_to_front(&self, id: ItemId) {
246        self.0.borrow_mut().bring_to_front(id);
247    }
248    /// Give `id` the lowest z-value in its layer so it paints beneath all siblings.
249    pub fn send_to_back(&self, id: ItemId) {
250        self.0.borrow_mut().send_to_back(id);
251    }
252    /// Move `id` to a different [`SceneLayer`] (background, default, foreground); notifies all views.
253    pub fn set_layer(&self, id: ItemId, layer: SceneLayer) {
254        self.0.borrow_mut().set_layer(id, layer);
255    }
256    /// Re-parent `child` under `parent` (or under the scene root when `None`); notifies all views.
257    pub fn set_item_parent(&self, child: ItemId, parent: Option<ItemId>) {
258        self.0.borrow_mut().set_item_parent(child, parent);
259    }
260
261    // -----------------------------------------------------------------
262    // Removal
263    // -----------------------------------------------------------------
264
265    /// Remove an item and its descendants. Drops any `Delegated` payload `Rc`
266    /// and cleans the item's a11y mappings; alive logical children re-root.
267    pub fn remove(&self, id: ItemId) {
268        self.0.borrow_mut().remove(id);
269    }
270    /// Promote an item's children to the scene root.
271    pub fn orphan(&self, id: ItemId) {
272        self.0.borrow_mut().orphan(id);
273    }
274
275    // -----------------------------------------------------------------
276    // Handlers
277    // -----------------------------------------------------------------
278
279    /// Replace the [`SceneItemHandlerSet`] of `id`, or clear it with `None`.
280    pub fn set_item_handlers(&self, id: ItemId, handlers: Option<SceneItemHandlerSet>) {
281        self.0.borrow_mut().set_item_handlers(id, handlers);
282    }
283    /// Mutate an item's handler set through a closure (avoids returning a
284    /// borrow guard tied to the `RefMut`).
285    pub fn with_handlers_mut<R>(
286        &self,
287        id: ItemId,
288        f: impl FnOnce(&mut SceneItemHandlerSet) -> R,
289    ) -> Option<R> {
290        self.0.borrow_mut().handlers_mut(id).map(f)
291    }
292
293    // -----------------------------------------------------------------
294    // Magnetism
295    // -----------------------------------------------------------------
296
297    /// Attach a [`Magnet`] to `item`; see [`Scene::add_magnet`].
298    pub fn add_magnet(&self, item: ItemId, magnet: Magnet) -> MagnetId {
299        self.0.borrow_mut().add_magnet(item, magnet)
300    }
301    /// Remove a magnet by id; see [`Scene::remove_magnet`].
302    pub fn remove_magnet(&self, magnet: MagnetId) {
303        self.0.borrow_mut().remove_magnet(magnet);
304    }
305    /// Remove every magnet on `item`; see [`Scene::clear_magnets`].
306    pub fn clear_magnets(&self, item: ItemId) {
307        self.0.borrow_mut().clear_magnets(item);
308    }
309    /// Move a magnet in its item's local frame; see [`Scene::set_magnet_local_pos`].
310    pub fn set_magnet_local_pos(&self, magnet: MagnetId, local_pos: Point) {
311        self.0.borrow_mut().set_magnet_local_pos(magnet, local_pos);
312    }
313    /// Enable or disable a magnet; see [`Scene::set_magnet_enabled`].
314    pub fn set_magnet_enabled(&self, magnet: MagnetId, enabled: bool) {
315        self.0.borrow_mut().set_magnet_enabled(magnet, enabled);
316    }
317    /// Ids of every magnet on `item`; see [`Scene::magnet_ids_of`].
318    pub fn magnet_ids_of(&self, item: ItemId) -> Vec<MagnetId> {
319        self.0.borrow().magnet_ids_of(item)
320    }
321    /// The owning item of a magnet; see [`Scene::magnet_owner`].
322    pub fn magnet_owner(&self, magnet: MagnetId) -> Option<ItemId> {
323        self.0.borrow().magnet_owner(magnet)
324    }
325    /// A magnet's scene position; see [`Scene::magnet_scene_pos`].
326    pub fn magnet_scene_pos(&self, magnet: MagnetId) -> Option<Point> {
327        self.0.borrow().magnet_scene_pos(magnet)
328    }
329    /// Resolve a magnet to a [`MagnetRef`] snapshot; see [`Scene::magnet`].
330    pub fn magnet(&self, magnet: MagnetId) -> Option<MagnetRef> {
331        self.0.borrow().magnet(magnet)
332    }
333    /// Best item-drag snap; see [`Scene::compute_item_snap`]. A shared
334    /// (read-only) borrow is held while the `predicate` runs over owned
335    /// candidate snapshots, so the predicate may read but must not mutate
336    /// the model.
337    pub fn compute_item_snap(
338        &self,
339        dragged: ItemId,
340        drag_delta: Vec2,
341        capture_radius: f32,
342        predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
343    ) -> Option<MagnetSnap> {
344        self.0
345            .borrow()
346            .compute_item_snap(dragged, drag_delta, capture_radius, predicate)
347    }
348    /// Best port-drag snap; see [`Scene::compute_port_snap`].
349    pub fn compute_port_snap(
350        &self,
351        source: MagnetId,
352        cursor_scene: Point,
353        capture_radius: f32,
354        predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
355    ) -> Option<(MagnetRef, Option<std::rc::Rc<dyn std::any::Any>>)> {
356        self.0
357            .borrow()
358            .compute_port_snap(source, cursor_scene, capture_radius, predicate)
359    }
360    /// Nearest enabled magnet within `radius`; see [`Scene::nearest_magnet`].
361    pub fn nearest_magnet(&self, scene_pt: Point, radius: f32) -> Option<MagnetId> {
362        self.0.borrow().nearest_magnet(scene_pt, radius)
363    }
364
365    // -----------------------------------------------------------------
366    // Scene extent / interaction constraints
367    // -----------------------------------------------------------------
368
369    /// Set the logical extent of the scene (used for scroll-bar sizing); `None` = unbounded.
370    pub fn set_scene_rect(&self, rect: Option<Rect>) {
371        self.0.borrow_mut().set_scene_rect(rect);
372    }
373    /// Restrict panning to horizontal, vertical, or both axes; updates [`pan_axes_signal`](Self::pan_axes_signal).
374    pub fn pan_axes(&self, axes: PanAxes) {
375        self.0.borrow_mut().pan_axes(axes);
376    }
377    /// Enable or disable pinch/scroll zoom; updates [`zoomable_signal`](Self::zoomable_signal).
378    pub fn zoomable(&self, on: bool) {
379        self.0.borrow_mut().zoomable(on);
380    }
381    /// Clamp the camera pan to `bounds` (scene coordinates); `None` = no limit; updates [`pan_bounds_signal`](Self::pan_bounds_signal).
382    pub fn set_pan_bounds(&self, bounds: Option<Rect>) {
383        self.0.borrow_mut().set_pan_bounds(bounds);
384    }
385    /// Restrict the zoom factor to `range`; `None` = no limit; updates [`zoom_range_signal`](Self::zoom_range_signal).
386    pub fn set_zoom_range(&self, range: Option<std::ops::RangeInclusive<f32>>) {
387        self.0.borrow_mut().set_zoom_range(range);
388    }
389
390    // -----------------------------------------------------------------
391    // Accessibility structure mutation
392    // -----------------------------------------------------------------
393
394    /// Register a logical AT group (landmark / rotor category container); returns its stable [`A11yGroupId`].
395    pub fn add_a11y_group(&self, builder: A11yGroupBuilder) -> A11yGroupId {
396        self.0.borrow_mut().add_a11y_group(builder)
397    }
398    /// Remove a previously registered AT group; triggers an `a11y_change_signal` bump.
399    pub fn remove_a11y_group(&self, id: A11yGroupId) {
400        self.0.borrow_mut().remove_a11y_group(id);
401    }
402    /// Re-parent `child` in the AT tree, overriding the default visual parent; `None` re-attaches under the scene root.
403    pub fn set_a11y_parent(&self, child: A11yNode, parent: Option<A11yNode>) {
404        self.0.borrow_mut().set_a11y_parent(child, parent);
405    }
406    /// Declare a cross-node AT relationship (controls, describes, labels) from `from` to `to`.
407    pub fn add_a11y_relation(&self, from: A11yNode, kind: A11yRelation, to: A11yNode) {
408        self.0.borrow_mut().add_a11y_relation(from, kind, to);
409    }
410    /// Mark `node` as a live region (`Polite` or `Assertive`) so assistive tech announces changes to it.
411    pub fn set_a11y_live(&self, node: A11yNode, live: accesskit::Live) {
412        self.0.borrow_mut().set_a11y_live(node, live);
413    }
414    /// Assign a landmark `role` to `node` (e.g. `Role::Region`, `Role::Main`) for rotor navigation.
415    pub fn set_a11y_landmark(&self, node: A11yNode, role: accesskit::Role) {
416        self.0.borrow_mut().set_a11y_landmark(node, role);
417    }
418    /// Register `node` under the given rotor [`A11yCategory`] slices so it appears in category-filtered navigation.
419    pub fn set_a11y_categories(&self, node: A11yNode, categories: &[A11yCategory]) {
420        self.0.borrow_mut().set_a11y_categories(node, categories);
421    }
422
423    // -----------------------------------------------------------------
424    // Dynamic-bounds refresh (called by SceneView::build)
425    // -----------------------------------------------------------------
426
427    /// Re-read signal-driven bounds for `add_item_dynamic` entries; returns
428    /// `true` if any changed.
429    pub fn refresh_dynamic_bounds(&self) -> bool {
430        self.0.borrow_mut().refresh_dynamic_bounds()
431    }
432
433    // -----------------------------------------------------------------
434    // Reactive signals + version
435    // -----------------------------------------------------------------
436
437    /// Reactive signal fired on every structural scene change; all views observe this to reconcile.
438    pub fn item_change_signal(&self) -> Signal<ItemChange> {
439        self.0.borrow().item_change_signal()
440    }
441    /// Reactive monotonic counter bumped on every AT-structure change; views re-walk accessibility on any increment.
442    pub fn a11y_change_signal(&self) -> Signal<u64> {
443        self.0.borrow().a11y_change_signal()
444    }
445    /// Monotonic counter incremented on every mutation; useful for cache invalidation without observing a signal.
446    pub fn mutation_version(&self) -> u64 {
447        self.0.borrow().mutation_version()
448    }
449    /// Reactive current [`PanAxes`] restriction; updated by [`pan_axes`](Self::pan_axes).
450    pub fn pan_axes_signal(&self) -> Signal<PanAxes> {
451        self.0.borrow().pan_axes_signal()
452    }
453    /// Reactive camera-pan clamp bounds; updated by [`set_pan_bounds`](Self::set_pan_bounds).
454    pub fn pan_bounds_signal(&self) -> Signal<Option<Rect>> {
455        self.0.borrow().pan_bounds_signal()
456    }
457    /// Reactive zoom-factor clamp range; updated by [`set_zoom_range`](Self::set_zoom_range).
458    pub fn zoom_range_signal(&self) -> Signal<Option<std::ops::RangeInclusive<f32>>> {
459        self.0.borrow().zoom_range_signal()
460    }
461    /// Reactive zoom-enabled flag; updated by [`zoomable`](Self::zoomable).
462    pub fn zoomable_signal(&self) -> Signal<bool> {
463        self.0.borrow().zoomable_signal()
464    }
465
466    // -----------------------------------------------------------------
467    // Value queries
468    // -----------------------------------------------------------------
469
470    /// Total number of items in the scene (lightweight + heavyweight).
471    pub fn len(&self) -> usize {
472        self.0.borrow().len()
473    }
474    /// Returns `true` when the scene contains no items.
475    pub fn is_empty(&self) -> bool {
476        self.0.borrow().is_empty()
477    }
478    /// All [`ItemId`]s currently in the scene, in insertion order.
479    pub fn ids(&self) -> Vec<ItemId> {
480        self.0.borrow().ids()
481    }
482    /// The local position of `id` in its parent's coordinate space; `None` if `id` is unknown.
483    pub fn local_pos(&self, id: ItemId) -> Option<Point> {
484        self.0.borrow().local_pos(id)
485    }
486    /// The local bounding rect of `id`; `None` if `id` is unknown.
487    pub fn local_bounds(&self, id: ItemId) -> Option<Rect> {
488        self.0.borrow().local_bounds(id)
489    }
490    /// The additional local-to-parent transform of `id` (beyond position); `None` if none is set.
491    pub fn transform(&self, id: ItemId) -> Option<Transform2D> {
492        self.0.borrow().transform(id)
493    }
494    /// The full local-to-scene transform for `id` (parent chain composed); identity if `id` is unknown.
495    pub fn scene_transform(&self, id: ItemId) -> Transform2D {
496        self.0.borrow().scene_transform(id)
497    }
498    /// The origin of `id` mapped into scene coordinates; `None` if `id` is unknown.
499    pub fn scene_pos(&self, id: ItemId) -> Option<Point> {
500        self.0.borrow().scene_pos(id)
501    }
502    /// The bounding rect of `id` in scene coordinates (local bounds transformed by the parent chain); `None` if unknown.
503    pub fn scene_rect(&self, id: ItemId) -> Option<Rect> {
504        self.0.borrow().scene_rect(id)
505    }
506    /// The [`ItemFlags`] bitset of `id`; `None` if `id` is unknown.
507    pub fn flags(&self, id: ItemId) -> Option<ItemFlags> {
508        self.0.borrow().flags(id)
509    }
510    /// Returns `true` if `id` and all of its ancestors are visible.
511    pub fn is_effectively_visible(&self, id: ItemId) -> bool {
512        self.0.borrow().is_effectively_visible(id)
513    }
514    /// The own opacity of `id` (ignoring ancestors); `None` if `id` is unknown.
515    pub fn opacity(&self, id: ItemId) -> Option<f32> {
516        self.0.borrow().opacity(id)
517    }
518    /// Accumulated opacity for `id` (own × each ancestor's opacity).
519    pub fn effective_opacity(&self, id: ItemId) -> f32 {
520        self.0.borrow().effective_opacity(id)
521    }
522    /// The z-order value of `id` within its layer; `None` if `id` is unknown.
523    pub fn z(&self, id: ItemId) -> Option<f32> {
524        self.0.borrow().z(id)
525    }
526    /// The [`SceneLayer`] of `id`; `None` if `id` is unknown.
527    pub fn layer(&self, id: ItemId) -> Option<SceneLayer> {
528        self.0.borrow().layer(id)
529    }
530    /// The direct parent of `id`, or `None` if it is a root item (or unknown).
531    pub fn parent_of(&self, id: ItemId) -> Option<ItemId> {
532        self.0.borrow().parent_of(id)
533    }
534    /// Returns `true` if `id` is anywhere in `ancestor`'s subtree.
535    pub fn is_descendant_of(&self, id: ItemId, ancestor: ItemId) -> bool {
536        self.0.borrow().is_descendant_of(id, ancestor)
537    }
538    /// The logical extent set via [`set_scene_rect`](Self::set_scene_rect); `None` = unbounded.
539    pub fn scene_rect_extent(&self) -> Option<Rect> {
540        self.0.borrow().scene_rect_extent()
541    }
542    /// The current pan-axis restriction without subscribing to its signal.
543    pub fn current_pan_axes(&self) -> PanAxes {
544        self.0.borrow().current_pan_axes()
545    }
546    /// Returns `true` if zoom is currently enabled (snapshot; use [`zoomable_signal`](Self::zoomable_signal) for reactivity).
547    pub fn is_zoomable(&self) -> bool {
548        self.0.borrow().is_zoomable()
549    }
550    /// Current pan-clamp bounds without subscribing to its signal.
551    pub fn current_pan_bounds(&self) -> Option<Rect> {
552        self.0.borrow().current_pan_bounds()
553    }
554    /// Current zoom-factor clamp range without subscribing to its signal.
555    pub fn current_zoom_range(&self) -> Option<std::ops::RangeInclusive<f32>> {
556        self.0.borrow().current_zoom_range()
557    }
558    /// All items whose bounding rects overlap `scene_rect` (spatial-index query).
559    pub fn items_in_rect(&self, scene_rect: Rect) -> Vec<ItemId> {
560        self.0.borrow().items_in_rect(scene_rect)
561    }
562    /// The topmost item under `scene_pt` using exact-shape hit-testing; `None` if no item is hit.
563    pub fn item_at(&self, scene_pt: Point) -> Option<ItemId> {
564        self.0.borrow().item_at(scene_pt)
565    }
566    /// All items under `scene_pt` (exact-shape hit-test), ordered front-to-back.
567    pub fn items_at(&self, scene_pt: Point) -> Vec<ItemId> {
568        self.0.borrow().items_at(scene_pt)
569    }
570    /// All items whose bounding rects intersect `id`'s bounding rect.
571    pub fn colliding_items(&self, id: ItemId) -> Vec<ItemId> {
572        self.0.borrow().colliding_items(id)
573    }
574    /// The AT-tree parent of `child` as set by [`set_a11y_parent`](Self::set_a11y_parent); `None` = visual default.
575    pub fn a11y_parent_of(&self, child: A11yNode) -> Option<A11yNode> {
576        self.0.borrow().a11y_parent_of(child)
577    }
578
579    // -----------------------------------------------------------------
580    // Build-support (consumed by SceneView::build)
581    // -----------------------------------------------------------------
582
583    /// Drain every still-pending single-view (`Once`) widget, in entry order.
584    pub(crate) fn drain_all_once(&self) -> Vec<(ItemId, Box<dyn Widget>)> {
585        self.0.borrow_mut().drain_all_once()
586    }
587    /// `(id, payload)` for every multi-view (`Delegated`) item, in entry order.
588    pub(crate) fn delegated_payloads(&self) -> Vec<(ItemId, Rc<dyn std::any::Any>)> {
589        self.0.borrow().delegated_payloads()
590    }
591    /// Ids of every heavyweight widget entry, in entry order.
592    pub(crate) fn heavyweight_ids(&self) -> Vec<ItemId> {
593        self.0.borrow().heavyweight_ids()
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use crate::items::RectItem;
601    use teksilo_canvas::Point;
602
603    fn rect() -> Rect {
604        Rect::new(0.0, 0.0, 10.0, 10.0)
605    }
606
607    #[test]
608    fn clone_shares_data() {
609        let m1 = SceneModel::new();
610        let m2 = m1.clone();
611        let id = m1.add_item(RectItem::new(rect()), Point::ZERO);
612        assert_eq!(m2.len(), 1);
613        assert_eq!(m2.local_pos(id), Some(Point::ZERO));
614        m1.set_local_pos(id, Point::new(10.0, 0.0));
615        assert_eq!(m2.local_pos(id), Some(Point::new(10.0, 0.0)));
616        assert_eq!(m1.handle_count(), 2);
617    }
618
619    #[test]
620    fn payload_round_trip_and_signal_fires() {
621        let m1 = SceneModel::new();
622        let m2 = m1.clone();
623        let fired = Rc::new(std::cell::Cell::new(false));
624        let f = fired.clone();
625        let _h = m1.item_change_signal().observe(move |c| {
626            if matches!(c, ItemChange::PayloadChanged { .. }) {
627                f.set(true);
628            }
629        });
630        let id = m1.add_widget_item(42u32, rect());
631        assert_eq!(
632            m1.payload(id)
633                .and_then(|p| p.downcast_ref::<u32>().copied()),
634            Some(42)
635        );
636        assert_eq!(
637            m2.payload(id)
638                .and_then(|p| p.downcast_ref::<u32>().copied()),
639            Some(42)
640        );
641        m1.set_payload(id, 99u32);
642        assert!(fired.get());
643        assert_eq!(
644            m2.payload(id)
645                .and_then(|p| p.downcast_ref::<u32>().copied()),
646            Some(99)
647        );
648    }
649
650    #[test]
651    fn remove_drops_payload_rc() {
652        let m = SceneModel::new();
653        let id = m.add_widget_item(42u32, rect());
654        let weak = Rc::downgrade(&m.payload(id).unwrap());
655        assert!(weak.upgrade().is_some());
656        m.remove(id);
657        assert!(weak.upgrade().is_none(), "payload Rc leaked after remove");
658    }
659
660    #[test]
661    fn delegated_storage_and_build_helpers() {
662        // The `Once` drain-with-a-real-widget path is covered by the view
663        // multi-view tests (which have an arena); here we exercise the model
664        // bookkeeping for `Delegated` entries without needing a `Widget`.
665        let m = SceneModel::new();
666        let a = m.add_widget_item(7u8, rect());
667        let b = m.add_widget_item(8u8, rect());
668        assert!(m.payload(a).is_some());
669        assert!(m.payload(b).is_some());
670        assert_eq!(m.heavyweight_ids(), vec![a, b]);
671        assert_eq!(m.delegated_payloads().len(), 2);
672        assert!(m.drain_all_once().is_empty(), "no Once entries to drain");
673    }
674
675    #[test]
676    fn mutation_version_advances() {
677        let m = SceneModel::new();
678        let v0 = m.mutation_version();
679        let id = m.add_widget_item(0u32, rect());
680        let v1 = m.mutation_version();
681        assert_ne!(v1, v0);
682        m.set_payload(id, 1u32);
683        let v2 = m.mutation_version();
684        assert_ne!(v2, v1);
685        m.remove(id);
686        assert_ne!(m.mutation_version(), v2);
687    }
688
689    #[test]
690    fn item_change_signal_shared_across_handles() {
691        let m1 = SceneModel::new();
692        let m2 = m1.clone();
693        assert!(Signal::same(
694            &m1.item_change_signal(),
695            &m2.item_change_signal()
696        ));
697    }
698}