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}