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` and runs through [`SceneModel::write`], which
29//! is the `teksilo-data` **mutate-then-notify** discipline adapted to a borrow
30//! that spans a whole `&mut Scene` method. `ListModel` can scope its
31//! `borrow_mut()` and call `notify` after it (`list_model.rs`); `Scene` cannot,
32//! because the notification is emitted from *inside* a `&mut self` method whose
33//! borrow Rust only releases on return. So the notification is **queued in the
34//! `Scene`** and drained by the owner of the borrow — `write`, or
35//! [`SceneWriteGuard`] — once that borrow has dropped.
36//!
37//! The consequence is the contract app authors actually want: **an observer
38//! registered on [`item_change_signal`](SceneModel::item_change_signal) or
39//! [`a11y_change_signal`](SceneModel::a11y_change_signal) may read the scene
40//! and write it back.** A write made from an observer queues in turn and is
41//! delivered by the same drain, after whatever was already queued ahead of it —
42//! see [`SceneModel::flush_changes`] for ordering, re-entrancy and termination.
43//!
44//! Three doors are **not** covered, each for a stated reason:
45//!
46//! - A bare `&mut Scene` fans out synchronously, because nothing is holding a
47//!   `RefCell` open for it to escape from. For a `Scene` you own outright that
48//!   is safe — no second handle to it can exist. For a `&mut Scene` *reborrowed
49//!   out of a `SceneModel`* it is not: a raw `RefMut` from the shared cell
50//!   leaves the write scope closed, so observers run under the live exclusive
51//!   borrow and one that re-enters the model panics, exactly as every mutator
52//!   used to. [`SceneModel::write_guard`] is the fix and the substitute — it
53//!   derefs to `&mut Scene`, so the call sites are unchanged.
54//! - The four constraint signals (`pan_axes` / `zoomable` / `pan_bounds` /
55//!   `zoom_range`) are scene *state*, read back by `current_pan_axes` and
56//!   friends, so their writes stay synchronous — deferring them would make the
57//!   scene contradict itself inside a write scope. An observer on one of those
58//!   four must not re-enter the `SceneModel`.
59//! - [`with_handlers_mut`](SceneModel::with_handlers_mut) runs the caller's
60//!   closure *inside* the borrow (it hands out a `&mut` into the scene, which
61//!   cannot outlive it), so that closure must not re-enter the `SceneModel`
62//!   either. It is unwind-safe — a panic in the closure cannot strand the write
63//!   scope — but it is not re-entrant.
64//!
65//! A view **delegate** must also not synchronously mutate the model during a
66//! build-time call (the view drops all model borrows before invoking it; the
67//! delegate's *handlers* may mutate later).
68
69use std::cell::{RefCell, RefMut};
70use std::rc::{Rc, Weak};
71
72use teksilo_canvas::{Point, Rect, Size, StrokeStyle, Transform2D};
73use teksilo_core::color_prop::ColorProp;
74use teksilo_core::signal::Signal;
75use teksilo_core::widget::Widget;
76
77use crate::a11y::{A11yCategory, A11yGroupBuilder, A11yGroupId, A11yNode, A11yRelation};
78use crate::flags::ItemFlags;
79use crate::index::SpatialIndex;
80use crate::item::{ItemId, SceneItem};
81use crate::item_handlers::SceneItemHandlerSet;
82use crate::journal::{
83    ChangeSource, EditJournal, EphemeralScope, HistoryMode, SceneChange, SceneTransactionRecord,
84    TxnId, TxnOutcome,
85};
86use crate::magnet::{Magnet, MagnetId, MagnetRef, MagnetSnap, MagnetVerdict};
87use crate::salvage::{RemovedItem, ReplaceRejected, RestoreError};
88use crate::scene::{CascadeBudget, PanAxes, Placement, Scene, SceneLayer, SizePolicy};
89use crate::shape::{ItemSelectionMode, ItemShape, SceneRegion};
90use teksilo_canvas::Vec2;
91
92/// A shared, cloneable handle to a [`Scene`].
93pub struct SceneModel(pub(crate) Rc<RefCell<Scene>>);
94
95impl Clone for SceneModel {
96    /// Produce a second handle to the **same** scene (cheap `Rc` clone).
97    fn clone(&self) -> Self {
98        Self(self.0.clone())
99    }
100}
101
102impl Default for SceneModel {
103    fn default() -> Self {
104        Self::new()
105    }
106}
107
108impl std::fmt::Debug for SceneModel {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self.0.try_borrow() {
111            Ok(scene) => f
112                .debug_struct("SceneModel")
113                .field("handles", &Rc::strong_count(&self.0))
114                .field("len", &scene.len())
115                .finish(),
116            Err(_) => f
117                .debug_struct("SceneModel")
118                .field("handles", &Rc::strong_count(&self.0))
119                .field("len", &"<borrowed>")
120                .finish(),
121        }
122    }
123}
124
125/// A non-owning handle to a [`Scene`] — [`SceneModel`] without the ownership.
126///
127/// Produced by [`SceneModel::downgrade`] and turned back into a `SceneModel`,
128/// for as long as one still exists, by [`upgrade`](Self::upgrade). Holding one
129/// keeps nothing alive.
130///
131/// Its reason to exist is the closure the scene owns: a geometry constraint
132/// that captures a `SceneModel` closes the ring
133/// `SceneModel → Scene → constraint → SceneModel`, and nothing in it is ever
134/// dropped. Capturing this instead breaks the ring. See
135/// [`SceneModel::set_geometry_constraint`] and
136/// [`ProposedChange`](crate::ProposedChange) for the rule and the safe forms.
137pub struct WeakSceneModel(Weak<RefCell<Scene>>);
138
139impl Clone for WeakSceneModel {
140    fn clone(&self) -> Self {
141        Self(self.0.clone())
142    }
143}
144
145impl std::fmt::Debug for WeakSceneModel {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("WeakSceneModel")
148            .field("alive", &self.is_alive())
149            .finish()
150    }
151}
152
153impl WeakSceneModel {
154    /// An owning handle to the scene, if any still exists.
155    ///
156    /// `None` once the last [`SceneModel`] has been dropped — which is the
157    /// point: a constraint outliving its scene does nothing instead of keeping
158    /// it alive.
159    pub fn upgrade(&self) -> Option<SceneModel> {
160        self.0.upgrade().map(SceneModel)
161    }
162
163    /// Whether the scene is still alive, without building a handle.
164    pub fn is_alive(&self) -> bool {
165        self.0.strong_count() > 0
166    }
167}
168
169/// Write guard over a [`SceneModel`]'s [`Scene`]: `Deref`/`DerefMut` to the
170/// scene, with the deferred-notification contract of a `SceneModel` mutator.
171///
172/// Holding one keeps a *write scope* open, so every notification the edits
173/// produce queues in emission order. On drop the guard closes the scope,
174/// releases the `RefCell` borrow, and **then** fans the whole batch out — so a
175/// block of edits is one notification round, and an observer may read the
176/// scene or write it back.
177///
178/// ```
179/// # use teksilo_scene::{RectItem, SceneModel};
180/// # use teksilo_canvas::{Point, Rect};
181/// let model = SceneModel::new();
182/// let a = model.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
183/// {
184///     let mut scene = model.write_guard();
185///     scene.set_local_pos(a, Point::new(10.0, 10.0));
186///     scene.set_visible(a, false);
187/// } // both changes fan out here, in this order
188/// ```
189///
190/// If the thread is unwinding when the guard drops, the scope is closed and
191/// the borrow released but the fan-out is **skipped**: running observers from
192/// a `Drop` during an unwind would abort the process the moment one of them
193/// panicked. The queued changes survive and are delivered by the next
194/// [`flush_changes`](SceneModel::flush_changes) — which a `catch_unwind`
195/// recovery path can call explicitly.
196pub struct SceneWriteGuard<'a> {
197    /// `Some` for the guard's whole life; `None` only inside `Drop`, which
198    /// takes the `RefMut` out in order to release the borrow *before* the
199    /// fan-out.
200    scene: Option<RefMut<'a, Scene>>,
201    model: &'a SceneModel,
202}
203
204impl std::fmt::Debug for SceneWriteGuard<'_> {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        f.debug_struct("SceneWriteGuard")
207            .field("len", &self.scene.as_ref().map(|s| s.len()))
208            .finish_non_exhaustive()
209    }
210}
211
212impl std::ops::Deref for SceneWriteGuard<'_> {
213    type Target = Scene;
214    fn deref(&self) -> &Scene {
215        self.scene.as_ref().expect("guard alive outside Drop")
216    }
217}
218
219impl std::ops::DerefMut for SceneWriteGuard<'_> {
220    fn deref_mut(&mut self) -> &mut Scene {
221        self.scene.as_mut().expect("guard alive outside Drop")
222    }
223}
224
225impl Drop for SceneWriteGuard<'_> {
226    fn drop(&mut self) {
227        if let Some(scene) = self.scene.take() {
228            scene.exit_write_scope();
229            drop(scene); // borrow released here, before any observer runs
230        }
231        if std::thread::panicking() {
232            return;
233        }
234        self.model.flush_changes();
235    }
236}
237
238/// An open transaction over a [`SceneModel`]: every edit until it drops is one
239/// logical change.
240///
241/// Opened by [`SceneModel::transaction`] / [`SceneModel::user_edit`]. On drop it
242/// closes the scope and — with an edit sink installed — delivers one
243/// [`SceneTransactionRecord`] with the scene unborrowed.
244///
245/// Nesting **joins**: a guard opened while another is live adds no boundary, and
246/// the outer stamp wins. Everything a guard says (`squash`, `ephemeral`,
247/// `abandon`) therefore applies to the transaction it is *part of*, not to a
248/// private one of its own.
249#[must_use = "a SceneTransaction commits when it drops; binding it to `_` commits \
250              immediately and groups nothing — bind it to a named `_txn`"]
251pub struct SceneTransaction<'a> {
252    model: &'a SceneModel,
253    journal: Rc<EditJournal>,
254    /// `Some` while [`ephemeral`](SceneTransaction::ephemeral) is in effect;
255    /// dropped before the scope closes so the depth cannot outlive the guard.
256    ephemeral: Option<EphemeralScope>,
257}
258
259impl std::fmt::Debug for SceneTransaction<'_> {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        f.debug_struct("SceneTransaction")
262            .field("journal", &self.journal)
263            .finish_non_exhaustive()
264    }
265}
266
267impl SceneTransaction<'_> {
268    /// Coalesce this transaction's repeated writes to one continuous quantity
269    /// — position, bounds, transform, opacity, z, placement — into a single
270    /// edit keeping the **first** `old` and the **last** `new`.
271    ///
272    /// **Off by default**, and deliberately: squashing throws the intermediate
273    /// path away, which a replay, a presence indicator or a collaboration relay
274    /// needs, and losing it silently by default is exactly the quiet data loss
275    /// this seam exists to prevent. The built-in item drag already commits one
276    /// `set_local_pos` per gesture, so the default costs nothing in the common
277    /// case; a caller that writes per sample and wants endpoints asks for it.
278    ///
279    /// Discrete edits — adds, removals, replacements, flags, parent changes —
280    /// are never coalesced and keep their order.
281    pub fn squash(self) -> Self {
282        self.journal.request_squash();
283        self
284    }
285
286    /// Mark this transaction's changes as per-frame artefacts: they must be
287    /// **rendered** and must not be **recorded**.
288    ///
289    /// The app-side half of the knob the scene's own dynamic-bounds refresh
290    /// uses. An item whose `transform` follows a rotation `Signal` writes the
291    /// model every frame; without this, an app watching the change stream
292    /// cannot tell that churn from an edit. `ephemeral` rides every
293    /// [`SceneChange`] and the record, and the framework does nothing else with
294    /// it — what counts as history is the consumer's call.
295    pub fn ephemeral(mut self) -> Self {
296        if self.ephemeral.is_none() {
297            self.ephemeral = Some(EphemeralScope::new(&self.journal));
298        }
299        self
300    }
301
302    /// Mark the interaction cancelled, then commit.
303    ///
304    /// The scene is **not** rolled back: applying an inverse is the first 80 %
305    /// of an undo stack, and undo lives on the other side of this seam. The
306    /// record arrives with [`TxnOutcome::Abandoned`] so the consumer can revert
307    /// from the `old` values it was handed without pushing anything a redo
308    /// could replay — tldraw's `bail`, with the stack where it belongs.
309    ///
310    /// On a nested guard this abandons the transaction it joined, because that
311    /// is the transaction it is part of.
312    pub fn abandon(self) {
313        self.journal.set_outcome(TxnOutcome::Abandoned);
314    }
315}
316
317impl Drop for SceneTransaction<'_> {
318    fn drop(&mut self) {
319        // Ephemeral depth first: it must not outlive the scope it was raised
320        // for, and `exit_transaction` is what reads the accumulated flag.
321        self.ephemeral = None;
322        // Closed through the journal handle the guard already holds, **not**
323        // through the scene: a `Drop` that needed the `RefCell` would leave the
324        // scope permanently open whenever it could not get it — silently, in
325        // release — and a scene whose transaction never closes never delivers
326        // another record.
327        self.journal.exit_scope();
328        if std::thread::panicking() {
329            // Same rule as SceneWriteGuard: running an app callback from a Drop
330            // during an unwind aborts the process the moment it panics. The
331            // record survives in the queue for the next delivery.
332            return;
333        }
334        self.model.deliver_records();
335    }
336}
337
338impl SceneModel {
339    // -----------------------------------------------------------------
340    // Write scope / deferred fan-out
341    // -----------------------------------------------------------------
342
343    /// Open a write scope over the scene; see [`SceneWriteGuard`].
344    ///
345    /// The one door for a *block* of edits that must fan out together, and the
346    /// door a `&mut Scene` escape hatch should be built on.
347    pub fn write_guard(&self) -> SceneWriteGuard<'_> {
348        let scene = match self.0.try_borrow_mut() {
349            Ok(scene) => scene,
350            // The happy path never reaches here, so the two diagnostics below
351            // cost nothing. They exist because `RefCell already borrowed` names
352            // neither the rule that was broken nor the thing to do instead, and
353            // both of these are rules this crate advertises.
354            Err(_) => {
355                if self
356                    .0
357                    .try_borrow()
358                    .is_ok_and(|scene| scene.in_geometry_constraint())
359                {
360                    panic!(
361                        "a geometry constraint tried to write the scene. A constraint is \
362                         handed the scene read-only and decides by *returning* a \
363                         ChangeVerdict — return ChangeVerdict::Adjust(frame) (or \
364                         ChangeVerdict::Reject) instead of mutating. Reads are fine."
365                    );
366                }
367                panic!(
368                    "SceneModel::write_guard while the scene is already borrowed. A \
369                     read-only policy closure (SceneView::focus_order, a magnetism \
370                     predicate, a geometry constraint) may read the scene but not write \
371                     it; a write scope already open must be closed first."
372                );
373            }
374        };
375        scene.enter_write_scope();
376        SceneWriteGuard {
377            scene: Some(scene),
378            model: self,
379        }
380    }
381
382    /// Mutate the scene under a write scope, then fan out.
383    ///
384    /// The body every `&self` mutator on this type is. Unwind-safe by
385    /// construction: the scope is closed by [`SceneWriteGuard`]'s `Drop`, so a
386    /// panic inside `f` cannot strand the scene in "deferring forever, never
387    /// flushing" — the one way this mechanism could fail silently.
388    fn write<R>(&self, f: impl FnOnce(&mut Scene) -> R) -> R {
389        let mut guard = self.write_guard();
390        let out = f(&mut guard);
391        drop(guard); // release the borrow, then fan out
392        out
393    }
394
395    /// Deliver every queued notification.
396    ///
397    /// Called automatically when a write scope closes, so apps rarely need it.
398    /// The exception is recovering from a caught panic: an unwinding
399    /// [`SceneWriteGuard`] deliberately skips its fan-out, and a panicking
400    /// observer stops the drain where it stood, so a `catch_unwind` boundary
401    /// that intends to keep using the scene calls this once.
402    ///
403    /// # When it does nothing
404    ///
405    /// *Where* it is called from never makes it panic — this is the door a
406    /// recovery path is told to knock on, so it cannot be one that only works
407    /// from some of them. (An observer can still panic once the drain reaches
408    /// it, and so can the runaway budgets below; both are the observer's
409    /// doing, not the call site's.) It is a no-op, and says so rather than
410    /// delivering half a batch, when:
411    ///
412    /// - nothing is queued (overwhelmingly the common case — every mutator that
413    ///   early-returned on an unchanged value, and every per-frame
414    ///   [`refresh_dynamic_bounds`](Self::refresh_dynamic_bounds));
415    /// - **any** borrow on the scene is outstanding. A write scope is the
416    ///   obvious one — it owns its batch and fans out when it closes, so
417    ///   flushing from inside it would deliver a half-finished mutation, and
418    ///   the old implementation's `borrow()` simply panicked there. But a
419    ///   *shared* borrow disqualifies a flush just as firmly, because an
420    ///   observer is allowed to write and a write needs the cell exclusively:
421    ///   draining under a read-only policy closure (`SceneView::focus_order`,
422    ///   `MagnetismConfig`'s predicate) would panic the first observer that
423    ///   took the invitation. So the probe is `try_borrow_mut`, the only
424    ///   question whose answer is "the cell is completely free";
425    /// - a drain is already running, i.e. this call came from inside an
426    ///   observer. The outermost drain owns the queue and picks that observer's
427    ///   changes up on its next round, which is what keeps delivery in emission
428    ///   order rather than depth-first.
429    ///
430    /// # Ordering
431    ///
432    /// FIFO. Observers see changes in the order they were emitted, including
433    /// across a batch — so `remove`'s documented leaves-then-root order, and
434    /// the interleaving of the item and logical-AT channels, both survive.
435    ///
436    /// # Termination
437    ///
438    /// The drain keeps going until the queue is empty, so an observer's own
439    /// writes are always delivered rather than silently dropped. Most cycles
440    /// terminate on their own — `Scene::set_local_pos` and friends early-return
441    /// when the value is unchanged, so a clamp-it-back-into-bounds observer
442    /// settles in two rounds. (Snapping a *gesture* is not this channel's job at
443    /// all; see [`set_geometry_constraint`](Self::set_geometry_constraint).)
444    ///
445    /// An observer that writes on **every** change with no equality guard never
446    /// settles, and is stopped by a budget that panics with a diagnostic naming
447    /// the item at fault.
448    ///
449    /// **What the budget counts is total observer-generated deliveries in one
450    /// drain, and nothing else.** The caller's own batch — everything queued
451    /// before the first observer ran — is free at any size, because it is finite
452    /// by construction; so are cascade depth and the number of distinct subjects
453    /// a cascade touches. A cap on **rounds** would cap how long a legitimate
454    /// chain may be (a settling 300-link chain is 300 rounds); a cap **per
455    /// subject** would cap how wide a graph may be, and scaling that cap with
456    /// the scene makes the work a runaway gets to do scale with the scene too. A
457    /// flat total is the one bound under which a runaway costs the same number
458    /// of deliveries and the same peak memory whatever the model's size, and the
459    /// shapes this tier runs clear it by more than an order of magnitude. See
460    /// [`CascadeBudget`] for the measurements.
461    ///
462    /// Per-subject counts are still kept — they are what lets the panic name the
463    /// culprit, which a round counter never could — but they are diagnostics,
464    /// not a second trip condition.
465    ///
466    /// The budget is **not** debug-only, and this is the one place the
467    /// mechanism deliberately does not copy `Signal::try_set`: `try_set`
468    /// *recurses*, so an unchecked feedback loop there exhausts the stack and
469    /// aborts loudly by itself. This drain is an iterative loop, so an unchecked
470    /// loop here is a frozen UI thread with no diagnostic and no core dump — the
471    /// one failure mode a release build must not have. Trading a hang for a
472    /// panic is not a close call.
473    ///
474    /// The queue is left intact when a budget trips (dropping it would be the
475    /// silent notification loss the whole mechanism exists to prevent), so a
476    /// later flush with the same observer still installed panics again. That is
477    /// the intended reading: the observer, not the flush, is what has to change.
478    ///
479    /// # Unwind
480    ///
481    /// An observer that panics costs exactly its own delivery. Each
482    /// notification leaves the queue immediately before it is handed to the
483    /// signal and is never parked in a local batch, so everything not yet
484    /// delivered is still queued, in order, when the unwind passes through —
485    /// and the next call here delivers it. (A local batch would have its
486    /// untouched tail dropped by the unwind with nothing able to redeliver it:
487    /// the scene would have advanced with no notification, permanently.)
488    ///
489    /// # Coalescing
490    ///
491    /// None. Repeated `LocalPosChanged` for one item are delivered once each.
492    /// Two reasons: the advertised uses of this channel — persistence,
493    /// validation, audit, mirroring to a data layer — are the ones that need
494    /// every event; and collapsing two changes would require merging one's
495    /// `old` with the other's `new`, a transaction semantic this crate does not
496    /// define. A drag is not a reason to reconsider: each pointer sample is its
497    /// own `SceneModel` call, hence its own one-element batch, exactly as
498    /// before. If a transaction layer ever wants coalescing, [`SceneWriteGuard`]
499    /// is already its boundary.
500    ///
501    /// # Lifetime
502    ///
503    /// Queued notifications live in the `Scene`. Dropping the last
504    /// `SceneModel` handle drops the scene and discards anything still queued —
505    /// which can only be a batch abandoned by an unwind, and by then there is
506    /// no scene left to observe.
507    pub fn flush_changes(&self) {
508        let (queue, budget) = {
509            // `try_borrow_mut` is the probe, not `try_borrow`: it is the only
510            // one that answers "is this cell completely free?", and anything
511            // less means an observer taking up the invitation to write would
512            // panic on the borrow. Failure is a no-op by contract — see the
513            // "When it does nothing" section above.
514            let Ok(scene) = self.0.try_borrow_mut() else {
515                return;
516            };
517            if !scene.has_pending_notifications() {
518                return;
519            }
520            // Read here, under the borrow, because the drain below has none
521            // — and read once, so an observer cannot raise its own ceiling
522            // mid-drain.
523            (scene.change_queue(), scene.cascade_budget())
524        };
525        // The borrow above is gone: the observers below run with the scene
526        // free, and may read it or write it back.
527        queue.drain(budget);
528        // Then the owning channel, in the same free window. Records after
529        // changes, deliberately: by the time the edit sink sees a transaction,
530        // every view has already reconciled from it, so a sink that reads the
531        // scene sees a settled one.
532        self.deliver_records();
533    }
534
535    /// Hand every committed transaction to the edit sink, with the scene
536    /// unborrowed.
537    ///
538    /// Called automatically wherever a transaction closes — the end of a write
539    /// scope, the drop of a [`SceneTransaction`]. Apps rarely need it; the
540    /// exception is the same as [`flush_changes`](Self::flush_changes)', a
541    /// `catch_unwind` recovery path.
542    ///
543    /// A no-op when nothing is queued, when the scene is borrowed at all (the
544    /// sink is allowed to write, and a write needs the cell exclusively), when
545    /// a delivery is already running, and while the **change** fan-out is
546    /// mid-drain.
547    ///
548    /// That last one is the ordering this seam promises: an observer that
549    /// writes the scene commits its own transaction from inside the drain, and
550    /// delivering from there would hand the edit sink a transaction some views
551    /// had not reconciled from yet. The records wait, and the drain's own
552    /// caller delivers them when it settles.
553    pub fn deliver_records(&self) {
554        let (journal, budget) = {
555            let Ok(scene) = self.0.try_borrow_mut() else {
556                return;
557            };
558            if !scene.has_pending_records() || scene.changes_are_draining() {
559                return;
560            }
561            (scene.journal(), scene.cascade_budget())
562        };
563        journal.deliver(budget);
564    }
565
566    // -----------------------------------------------------------------
567    // Transactions and the edit sink
568    // -----------------------------------------------------------------
569
570    /// Group every edit until the returned guard drops into **one**
571    /// transaction, stamped with a [`ChangeSource`] and a [`HistoryMode`].
572    ///
573    /// Each [`SceneChange`] emitted inside it carries the same [`TxnId`] and
574    /// the same stamp, and — when an edit sink is installed — the whole group
575    /// arrives as one [`SceneTransactionRecord`] once the guard drops.
576    ///
577    /// # Nesting joins
578    ///
579    /// An inner `transaction` adds no boundary: it joins the open one and its
580    /// stamp is ignored, so an observer opening its own transaction inside a
581    /// framework-opened gesture cannot split that gesture into two undo steps.
582    ///
583    /// # Why the guard is safe to hold
584    ///
585    /// Every mutator on this type borrows the scene for the length of one call
586    /// and releases it at the semicolon. The guard is held by the *caller*,
587    /// outside any borrow, so its `Drop` runs with the `RefCell` free — which is
588    /// what lets the edit sink read the scene and write it back.
589    ///
590    /// A transaction is a **synchronous scope**. Holding one across a frame
591    /// means its edits are never delivered and its salvage accumulates; a debug
592    /// assertion in [`SceneView`](crate::SceneView)'s build catches the common
593    /// case.
594    ///
595    /// ```
596    /// # use teksilo_canvas::{Point, Rect};
597    /// # use teksilo_scene::{ChangeSource, HistoryMode, RectItem, SceneModel};
598    /// let model = SceneModel::new();
599    /// let a = model.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
600    /// let b = model.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
601    /// {
602    ///     let _txn = model.transaction(ChangeSource::User, HistoryMode::Record);
603    ///     model.set_local_pos(a, Point::new(10.0, 0.0));
604    ///     model.set_local_pos(b, Point::new(20.0, 0.0));
605    /// } // one transaction, two edits
606    /// ```
607    pub fn transaction(&self, source: ChangeSource, history: HistoryMode) -> SceneTransaction<'_> {
608        let journal = {
609            // A shared borrow only, and only to reach the journal handle beside
610            // the scene. It fails when a write scope is already open, and
611            // `RefCell already mutably borrowed` would name neither the rule
612            // nor the fix.
613            let Ok(scene) = self.0.try_borrow() else {
614                panic!(
615                    "SceneModel::transaction while the scene is already borrowed. A \
616                     SceneWriteGuard is already a transaction boundary — open the \
617                     transaction *around* the guard, not inside it — and a read-only \
618                     policy closure (a geometry constraint, a magnetism predicate) may \
619                     not open one at all."
620                );
621            };
622            scene.journal()
623        };
624        journal.enter_scope(source, history);
625        SceneTransaction {
626            model: self,
627            journal,
628            ephemeral: None,
629        }
630    }
631
632    /// [`transaction`](Self::transaction)`(ChangeSource::User,
633    /// HistoryMode::Record)` — one user-visible edit.
634    pub fn user_edit(&self) -> SceneTransaction<'_> {
635        self.transaction(ChangeSource::User, HistoryMode::Record)
636    }
637
638    /// Install the edit sink, returning whatever was there.
639    ///
640    /// Invoked once per committed transaction that changed anything, with the
641    /// scene **unborrowed** — so the sink may read the scene and write it back.
642    /// Its own writes are ordinary transactions: they produce their own records
643    /// and are delivered to it on a later round of the same loop, which is what
644    /// keeps the history it is feeding in agreement with the scene.
645    ///
646    /// **Exactly one.** A scene's edit history has one owner; a second consumer
647    /// composes inside the first. Owning is why: the sink is handed the removed
648    /// items themselves, and two owners of one `Box` is not a thing.
649    ///
650    /// With no sink installed, nothing is recorded and a removal drops its
651    /// salvage — today's behaviour at today's cost, for an app that wants no
652    /// history.
653    ///
654    /// # This is not where snap-to-grid goes
655    ///
656    /// The sink runs *after* the write. Snapping a gesture belongs in
657    /// [`set_geometry_constraint`](Self::set_geometry_constraint), which runs
658    /// *before* it and is consulted by every gesture route — so the frame the
659    /// last preview drew is the frame the commit writes. Use the sink for what
660    /// happens once an edit is real: recording it, persisting it, marking the
661    /// document dirty, mirroring it to a peer.
662    ///
663    /// # Panics
664    ///
665    /// If called from inside the sink itself.
666    pub fn set_edit_sink(
667        &self,
668        sink: impl FnMut(SceneTransactionRecord) + 'static,
669    ) -> Option<Box<dyn FnMut(SceneTransactionRecord)>> {
670        let journal = self.0.borrow().journal();
671        journal.set_sink(Some(Box::new(sink)))
672    }
673
674    /// Remove the edit sink, returning it. The scene stops recording.
675    pub fn clear_edit_sink(&self) -> Option<Box<dyn FnMut(SceneTransactionRecord)>> {
676        let journal = self.0.borrow().journal();
677        journal.set_sink(None)
678    }
679
680    /// Fires once per committed transaction, **after** the edit sink, with the
681    /// scene still unborrowed.
682    ///
683    /// The per-gesture counterpart of
684    /// [`item_change_signal`](Self::item_change_signal): one notification for a
685    /// whole logical edit rather than one per intermediate write. This is where
686    /// a document-dirty flag, a debounced save or a validation pass belongs —
687    /// they want to run once when the user lets go, not six times while the
688    /// item is moving.
689    pub fn transaction_signal(&self) -> Signal<TxnId> {
690        self.0.borrow().transaction_signal()
691    }
692
693    /// How many transaction scopes are currently open on this scene — zero
694    /// between mutations.
695    ///
696    /// A transaction is a **synchronous** scope: holding one across a frame
697    /// means its edits never reach the sink and its salvage accumulates. There
698    /// is no way to enforce that at the type level (nothing stops a guard being
699    /// stashed in a struct field), so this is the door an assertion knocks on.
700    /// [`SceneView`](crate::SceneView) asserts it is zero at the top of every
701    /// build in debug, which catches the common case.
702    pub fn open_transaction_depth(&self) -> u32 {
703        self.0.borrow().open_transaction_depth()
704    }
705
706    /// The runaway-detection budget for this scene's change fan-out — how much
707    /// work **observers** may generate from one batch before the drain declares
708    /// the cascade non-terminating and panics.
709    ///
710    /// See [`CascadeBudget`] for the rule and its default.
711    pub fn cascade_budget(&self) -> CascadeBudget {
712        self.0.borrow().cascade_budget()
713    }
714
715    /// Replace the runaway-detection budget for this scene's change fan-out.
716    ///
717    /// The mechanism is in the framework; this is where the policy lives. The
718    /// default — 100 000 observer-generated deliveries per drain — is more than
719    /// an order of magnitude above the legitimate shapes this tier runs, but it
720    /// cannot tell a guarded cascade that is genuinely enormous from an
721    /// unguarded write-back: the two look identical from the queue's side. An
722    /// app whose graph genuinely settles past the default therefore has a
723    /// supported answer here rather than a crash:
724    ///
725    /// ```
726    /// use teksilo_scene::{CascadeBudget, SceneModel};
727    ///
728    /// let model = SceneModel::new();
729    /// model.set_cascade_budget(CascadeBudget::new(1_000_000));
730    /// assert_eq!(model.cascade_budget().total, 1_000_000);
731    /// ```
732    ///
733    /// Raising it to silence a cycle that does **not** settle only postpones
734    /// the freeze it is there to prevent — the drain is still unbounded in
735    /// time, just later. Check the write is guarded first.
736    ///
737    /// A budget of zero is clamped to the smallest usable one rather than
738    /// stored: it would forbid the reactive write-back this channel exists for.
739    ///
740    /// Takes effect on the next drain: a drain already running read its budget
741    /// when it began, so an observer cannot enlarge its own.
742    pub fn set_cascade_budget(&self, budget: CascadeBudget) {
743        self.0.borrow().set_cascade_budget(budget);
744    }
745
746    // -----------------------------------------------------------------
747    // Construction
748    // -----------------------------------------------------------------
749
750    /// A handle to a fresh empty scene with the default spatial index.
751    pub fn new() -> Self {
752        Self(Rc::new(RefCell::new(Scene::new())))
753    }
754
755    /// A handle to a fresh scene with a custom [`SpatialIndex`].
756    pub fn with_index(index: Box<dyn SpatialIndex>) -> Self {
757        Self(Rc::new(RefCell::new(Scene::with_index(index))))
758    }
759
760    /// Wrap an existing [`Scene`] in a handle. Used by
761    /// [`SceneView::new`](crate::SceneView::new) for the single-view path.
762    pub fn from_scene(scene: Scene) -> Self {
763        Self(Rc::new(RefCell::new(scene)))
764    }
765
766    /// Number of distinct handles to this scene (1 = unshared).
767    pub fn handle_count(&self) -> usize {
768        Rc::strong_count(&self.0)
769    }
770
771    /// A **non-owning** handle to the same scene.
772    ///
773    /// For a closure the scene itself owns. The geometry constraint is the one
774    /// in this crate: the scene holds it, so a closure holding a `SceneModel`
775    /// back closes a reference cycle and the whole scene — items, heavyweight
776    /// payloads, journal, spatial index — is never dropped. A constraint
777    /// normally needs no handle at all (it is handed
778    /// [`ProposedChange::scene`](crate::ProposedChange::scene), which is the
779    /// whole read surface); this is for the case where a policy object holds a
780    /// model for its *other* work and is also installed as the closure.
781    ///
782    /// ```
783    /// # use teksilo_scene::SceneModel;
784    /// let model = SceneModel::new();
785    /// let weak = model.downgrade();
786    /// assert!(weak.upgrade().is_some());
787    /// drop(model);
788    /// assert!(weak.upgrade().is_none());
789    /// ```
790    pub fn downgrade(&self) -> WeakSceneModel {
791        WeakSceneModel(Rc::downgrade(&self.0))
792    }
793
794    // -----------------------------------------------------------------
795    // Heavyweight insertion
796    // -----------------------------------------------------------------
797
798    /// Single-view heavyweight widget (the one-shot `Once` path). The first
799    /// view to build drains it; a second view sharing this model produces no
800    /// child for it. For multi-view, use [`add_widget_item`](Self::add_widget_item).
801    pub fn add_widget<W: Widget + 'static>(&self, widget: W, rect: Rect) -> ItemId {
802        self.write(|s| s.add_widget(widget, rect))
803    }
804
805    /// Multi-view heavyweight item: store a typed `payload`; each view builds
806    /// its own widget instance from it via its delegate. Returns the [`ItemId`].
807    pub fn add_widget_item<P: 'static>(&self, payload: P, rect: Rect) -> ItemId {
808        self.write(|s| s.add_widget_delegated(Rc::new(payload), rect))
809    }
810
811    /// Replace the payload of a `Delegated` heavyweight item; every view
812    /// rebuilds that item's widget on the next pass.
813    ///
814    /// # Panics
815    ///
816    /// Panics if `id` is unknown, refers to a single-view `add_widget` (Once)
817    /// entry, or refers to a lightweight item.
818    pub fn set_payload<P: 'static>(&self, id: ItemId, payload: P) {
819        self.write(|s| s.set_payload(id, Rc::new(payload)));
820    }
821
822    /// The current type-erased payload of a `Delegated` item, if any.
823    pub fn payload(&self, id: ItemId) -> Option<Rc<dyn std::any::Any>> {
824        self.0.borrow().payload(id)
825    }
826
827    // -----------------------------------------------------------------
828    // Lightweight insertion
829    // -----------------------------------------------------------------
830
831    /// Add a lightweight [`SceneItem`] at `local_pos`.
832    pub fn add_item<I: SceneItem + 'static>(&self, item: I, local_pos: Point) -> ItemId {
833        self.write(|s| s.add_item(item, local_pos))
834    }
835
836    /// Add a lightweight item with signal-driven (dynamic) bounds.
837    pub fn add_item_dynamic<I: SceneItem + 'static>(&self, item: I, local_pos: Point) -> ItemId {
838        self.write(|s| s.add_item_dynamic(item, local_pos))
839    }
840
841    /// Add an already-boxed lightweight item at `local_pos`. The boxed-`dyn`
842    /// counterpart of [`add_item`](Self::add_item), used by
843    /// [`SceneListAdapter`](crate::SceneListAdapter).
844    pub fn add_boxed_item(&self, item: Box<dyn SceneItem>, local_pos: Point) -> ItemId {
845        self.write(|s| s.add_boxed_item(item, local_pos))
846    }
847
848    // -----------------------------------------------------------------
849    // Geometry mutation
850    // -----------------------------------------------------------------
851
852    /// Move `id` to `local_pos` in its parent's coordinate space; notifies all views.
853    pub fn set_local_pos(&self, id: ItemId, local_pos: Point) {
854        self.write(|s| s.set_local_pos(id, local_pos));
855    }
856    /// Replace the local bounding rect of `id`; notifies all views.
857    ///
858    /// Idempotent, and an item whose box is derived from its geometry fits
859    /// itself to the rectangle rather than adopting it — see
860    /// [`Scene::set_local_bounds`] for both contracts, which matter here
861    /// because this is the form an app drives per frame.
862    pub fn set_local_bounds(&self, id: ItemId, local_bounds: Rect) {
863        self.write(|s| s.set_local_bounds(id, local_bounds));
864    }
865    /// Set an additional local-to-parent transform (rotation, scale) on `id`; notifies all views.
866    pub fn set_transform(&self, id: ItemId, transform: Transform2D) {
867        self.write(|s| s.set_transform(id, transform));
868    }
869    /// Which axes of `id`'s box its widget decides. See [`SizePolicy`].
870    pub fn size_policy(&self, id: ItemId) -> SizePolicy {
871        self.0.borrow().size_policy(id)
872    }
873    /// Hand one or both axes of `id`'s box to its widget, on **every** view of
874    /// this model. See [`SizePolicy`] and [`Scene::set_size_policy`] — including
875    /// for the note that it is refused for a lightweight entry.
876    pub fn set_size_policy(&self, id: ItemId, policy: SizePolicy) -> bool {
877        self.write(|s| s.set_size_policy(id, policy))
878    }
879    /// Write a size **derived** from content rather than authored by the user;
880    /// notifies all views. See [`Scene::set_measured_size`] for which of the two
881    /// geometry doors this is and when to reach for the other one.
882    pub fn set_measured_size(&self, id: ItemId, size: Size) -> bool {
883        self.write(|s| s.set_measured_size(id, size))
884    }
885
886    // -----------------------------------------------------------------
887    // Geometry constraint
888    // -----------------------------------------------------------------
889
890    /// Install the document's standing geometry rule — snap-to-grid, axis lock,
891    /// page-bounds clamp — consulted before every **user-driven** gesture
892    /// applies anything, on every view attached to this model.
893    ///
894    /// One per scene. Programmatic mutators
895    /// ([`set_local_pos`](Self::set_local_pos) and friends) are **never**
896    /// constrained: a document load must land where it says.
897    ///
898    /// The closure is handed a [`ProposedChange`](crate::ProposedChange) —
899    /// the scene read-only, the roots the gesture moves, and the gesture's
900    /// start and proposed *frames* in scene coordinates — and returns a
901    /// [`ChangeVerdict`](crate::ChangeVerdict). It may read the scene and may
902    /// not write it; a write panics naming this hook.
903    ///
904    /// ```
905    /// use teksilo_canvas::{Point, Rect};
906    /// use teksilo_scene::{ChangeVerdict, RectItem, SceneModel};
907    ///
908    /// let model = SceneModel::new();
909    /// let card = model.add_item(
910    ///     RectItem::new(Rect::new(0.0, 0.0, 80.0, 50.0)),
911    ///     Point::new(4.0, 4.0),
912    /// );
913    ///
914    /// let grid = 25.0;
915    /// let page = Rect::new(0.0, 0.0, 1000.0, 700.0);
916    /// model.set_geometry_constraint(move |c| {
917    ///     // An explicit magnet outranks the standing rule.
918    ///     if c.magnet_snapped {
919    ///         return ChangeVerdict::Accept;
920    ///     }
921    ///     let mut f = c.proposed;
922    ///     // The frame is the item's own box in scene coordinates, so this
923    ///     // snaps what the user can see — no grab offset to carry.
924    ///     f.rect.x = (f.rect.x / grid).round() * grid;
925    ///     f.rect.y = (f.rect.y / grid).round() * grid;
926    ///     f.rect.x = f.rect.x.clamp(page.x, page.right() - f.rect.width);
927    ///     f.rect.y = f.rect.y.clamp(page.y, page.bottom() - f.rect.height);
928    ///     ChangeVerdict::Adjust(f)
929    /// });
930    ///
931    /// // Programmatic writes are exact — the constraint does not see them.
932    /// model.set_local_pos(card, Point::new(7.0, 9.0));
933    /// assert_eq!(model.local_pos(card), Some(Point::new(7.0, 9.0)));
934    /// ```
935    pub fn set_geometry_constraint(
936        &self,
937        f: impl Fn(&crate::constrain::ProposedChange<'_>) -> crate::constrain::ChangeVerdict + 'static,
938    ) {
939        self.write(|s| s.set_geometry_constraint(f));
940    }
941
942    /// Remove the geometry constraint. See [`Scene::clear_geometry_constraint`].
943    pub fn clear_geometry_constraint(&self) {
944        self.write(|s| s.clear_geometry_constraint());
945    }
946
947    /// Whether a geometry constraint is installed.
948    pub fn has_geometry_constraint(&self) -> bool {
949        self.0.borrow().has_geometry_constraint()
950    }
951
952    /// Run the installed constraint over a proposed **frame** and return the
953    /// frame to apply. Returns `proposed` unchanged when none is installed.
954    ///
955    /// The general door for an app that drives its own gesture — a heavyweight
956    /// card's `on_drag`, a custom resize grip, a layout command. The
957    /// [`SceneView`](crate::SceneView)'s own routes call exactly this, so an
958    /// app-driven gesture and a framework-driven one obey the same rule.
959    ///
960    /// `start` is the frame the gesture began from
961    /// ([`transform_frame`](Self::transform_frame) at press time), and it must
962    /// stay fixed for the gesture's life — that is what lets a constraint
963    /// measure total travel, and what keeps the preview and the commit in
964    /// agreement.
965    ///
966    /// # Panics
967    ///
968    /// Twice, each naming what to do instead:
969    ///
970    /// * From inside a constraint that is already running on this scene — the
971    ///   constraint *is* the answer, and asking for another one would recurse
972    ///   without end.
973    /// * While a **write scope** is open on this model — a
974    ///   [`SceneWriteGuard`], any mutator, or an observer running inside one.
975    ///   Asking the constraint reads the scene, and an open write scope holds
976    ///   it exclusively. Another *shared* borrow is fine: two reads coexist,
977    ///   which is what lets a constraint read the scene it is deciding about.
978    pub fn constrain_frame(
979        &self,
980        items: &[ItemId],
981        op: crate::transform_session::TransformOp,
982        start: crate::transform_session::TransformFrame,
983        proposed: crate::transform_session::TransformFrame,
984        source: crate::transform_session::TransformSource,
985    ) -> crate::transform_session::TransformFrame {
986        let scene = self.read_for_constraint("constrain_frame");
987        match crate::constrain::ConstraintCall::new(&scene) {
988            Some(call) => call.frame(op, items, &start, proposed, source, false),
989            None => proposed,
990        }
991    }
992
993    /// The shared borrow the two `constrain_*` doors read the scene through.
994    ///
995    /// Separate from a bare `borrow()` for one reason: these two are the doors
996    /// an app-driven drag calls, and the natural place to call them from is a
997    /// batched commit — inside an open [`SceneWriteGuard`], which is exactly
998    /// where a bare `borrow()` reports `RefCell already mutably borrowed` and
999    /// names neither the rule nor the fix. Every other named panic on this type
1000    /// exists for the same reason.
1001    fn read_for_constraint(&self, door: &str) -> std::cell::Ref<'_, Scene> {
1002        match self.0.try_borrow() {
1003            Ok(scene) => scene,
1004            Err(_) => panic!(
1005                "SceneModel::{door} while a write scope is open on this scene. \
1006                 Asking the geometry constraint reads the scene, and a \
1007                 SceneWriteGuard (or any mutator, or an observer running inside \
1008                 one) holds it exclusively. Constrain FIRST, then open the write \
1009                 scope and apply the answer: the constraint decides what to \
1010                 write, so it cannot run in the middle of writing it."
1011            ),
1012        }
1013    }
1014
1015    /// Run the installed constraint over a proposed **move** and return the
1016    /// scene-space translation to apply. Returns `translation` unchanged when
1017    /// none is installed.
1018    ///
1019    /// The door a heavyweight card's own drag handler uses, and the reason the
1020    /// constraint lives on the model rather than on a view — a card built by a
1021    /// per-view delegate holds a `SceneModel` clone and never a `&SceneView`:
1022    ///
1023    /// ```
1024    /// # use teksilo_canvas::{Point, Rect, Vec2};
1025    /// # use teksilo_scene::{ChangeVerdict, SceneModel, TransformSource};
1026    /// # let model = SceneModel::new();
1027    /// # let card = model.add_widget_item((), Rect::new(0.0, 0.0, 80.0, 50.0));
1028    /// # model.set_geometry_constraint(|c| {
1029    /// #     let mut f = c.proposed;
1030    /// #     f.rect.x = (f.rect.x / 25.0).round() * 25.0;
1031    /// #     f.rect.y = (f.rect.y / 25.0).round() * 25.0;
1032    /// #     ChangeVerdict::Adjust(f)
1033    /// # });
1034    /// // At the press: the frame the gesture starts from.
1035    /// let start = model.transform_frame(&[card]).expect("card resolves");
1036    /// // On each sample: the raw travel, constrained.
1037    /// let travel = Vec2::new(31.0, 12.0);
1038    /// let applied = model.constrain_move(&[card], start, travel, TransformSource::Pointer);
1039    /// assert_eq!(applied, Vec2::new(25.0, 0.0));
1040    /// ```
1041    ///
1042    /// Only the frame's **origin** is read back, because a translation is all a
1043    /// move can express: a constraint that also resizes or rotates the frame
1044    /// here has that part of its answer ignored. Use
1045    /// [`constrain_frame`](Self::constrain_frame) plus
1046    /// [`apply_transform_delta`](Self::apply_transform_delta) for a gesture
1047    /// that changes extent or orientation.
1048    ///
1049    /// # Panics
1050    ///
1051    /// The same two as [`constrain_frame`](Self::constrain_frame): from inside
1052    /// a running constraint, and while a write scope is open on this model.
1053    pub fn constrain_move(
1054        &self,
1055        items: &[ItemId],
1056        start: crate::transform_session::TransformFrame,
1057        translation: Vec2,
1058        source: crate::transform_session::TransformSource,
1059    ) -> Vec2 {
1060        let scene = self.read_for_constraint("constrain_move");
1061        let Some(call) = crate::constrain::ConstraintCall::new(&scene) else {
1062            return translation;
1063        };
1064        crate::constrain::constrained_translation(&call, items, &start, translation, source, false)
1065    }
1066
1067    // -----------------------------------------------------------------
1068    // Selection transforms
1069    // -----------------------------------------------------------------
1070
1071    /// Apply one transform delta to `roots` as a single operation; notifies all
1072    /// views. See [`Scene::apply_transform_delta`].
1073    pub fn apply_transform_delta(
1074        &self,
1075        roots: &[ItemId],
1076        delta: &crate::transform_session::TransformDelta,
1077    ) -> usize {
1078        self.write(|s| s.apply_transform_delta(roots, delta))
1079    }
1080
1081    /// `ids` pruned to its roots. See [`Scene::selection_roots`].
1082    pub fn selection_roots(&self, ids: &[ItemId]) -> Vec<ItemId> {
1083        self.0.borrow().selection_roots(ids)
1084    }
1085
1086    /// The roots of `ids` eligible for `op`. See [`Scene::transformable_roots`].
1087    pub fn transformable_roots(
1088        &self,
1089        ids: &[ItemId],
1090        op: crate::transform_session::TransformOp,
1091    ) -> Vec<ItemId> {
1092        self.0.borrow().transformable_roots(ids, op)
1093    }
1094
1095    /// The selection frame enclosing `roots`. See [`Scene::transform_frame`].
1096    pub fn transform_frame(
1097        &self,
1098        roots: &[ItemId],
1099    ) -> Option<crate::transform_session::TransformFrame> {
1100        self.0.borrow().transform_frame(roots)
1101    }
1102
1103    /// The item's own rotation in scene space, in radians. See
1104    /// [`Scene::scene_rotation`].
1105    pub fn scene_rotation(&self, id: ItemId) -> Option<f32> {
1106        self.0.borrow().scene_rotation(id)
1107    }
1108
1109    // -----------------------------------------------------------------
1110    // Flags / visibility / opacity mutation
1111    // -----------------------------------------------------------------
1112
1113    /// Replace the complete [`ItemFlags`] bitset for `id`; notifies all views.
1114    pub fn set_flags(&self, id: ItemId, flags: ItemFlags) {
1115        self.write(|s| s.set_flags(id, flags));
1116    }
1117    /// Set or clear a single [`ItemFlags`] bit on `id`; notifies all views.
1118    pub fn set_flag(&self, id: ItemId, flag: ItemFlags, on: bool) {
1119        self.write(|s| s.set_flag(id, flag, on));
1120    }
1121    /// Show or hide `id` (also hides its descendants); notifies all views.
1122    pub fn set_visible(&self, id: ItemId, visible: bool) {
1123        self.write(|s| s.set_visible(id, visible));
1124    }
1125    /// Set the paint opacity of `id` (0.0 = transparent, 1.0 = opaque); notifies all views.
1126    pub fn set_opacity(&self, id: ItemId, opacity: f32) {
1127        self.write(|s| s.set_opacity(id, opacity));
1128    }
1129
1130    // -----------------------------------------------------------------
1131    // Appearance mutation (paint-only, repaint without relayout)
1132    // -----------------------------------------------------------------
1133
1134    /// Replace a lightweight item's fill colour live; every view repaints
1135    /// (no relayout/rebuild). Accepts a plain [`Color`](teksilo_tokens::Color),
1136    /// a theme role, a `Signal<Color>`, or a `Signal<Role>`. See
1137    /// [`Scene::set_item_fill`] for the reactive-colour contract.
1138    pub fn set_item_fill(&self, id: ItemId, fill: impl Into<ColorProp>) {
1139        self.write(|s| s.set_item_fill(id, fill));
1140    }
1141    /// Clear a lightweight item's fill; every view repaints.
1142    pub fn clear_item_fill(&self, id: ItemId) {
1143        self.write(|s| s.clear_item_fill(id));
1144    }
1145    /// Replace a lightweight item's stroke (colour + [`StrokeStyle`]) live;
1146    /// every view repaints (no relayout/rebuild).
1147    pub fn set_item_stroke(&self, id: ItemId, color: impl Into<ColorProp>, style: StrokeStyle) {
1148        self.write(|s| s.set_item_stroke(id, color, style));
1149    }
1150    /// Clear a lightweight item's stroke; every view repaints.
1151    pub fn clear_item_stroke(&self, id: ItemId) {
1152        self.write(|s| s.clear_item_stroke(id));
1153    }
1154
1155    // -----------------------------------------------------------------
1156    // Z-order / layer / parenting mutation
1157    // -----------------------------------------------------------------
1158
1159    /// Set the z-order of `id` within its layer; higher values paint on top.
1160    pub fn set_z(&self, id: ItemId, z: f32) {
1161        self.write(|s| s.set_z(id, z));
1162    }
1163    /// Give `id` the highest z-value in its layer so it paints on top of all siblings.
1164    pub fn bring_to_front(&self, id: ItemId) {
1165        self.write(|s| s.bring_to_front(id));
1166    }
1167    /// Give `id` the lowest z-value in its layer so it paints beneath all siblings.
1168    pub fn send_to_back(&self, id: ItemId) {
1169        self.write(|s| s.send_to_back(id));
1170    }
1171    /// Move `id` to a different [`SceneLayer`] (`Under`, `Interleaved`, `Over`); notifies all views.
1172    pub fn set_layer(&self, id: ItemId, layer: SceneLayer) {
1173        self.write(|s| s.set_layer(id, layer));
1174    }
1175    /// Re-parent `child` under `parent` (or under the scene root when `None`); notifies all views.
1176    pub fn set_item_parent(&self, child: ItemId, parent: Option<ItemId>) {
1177        self.write(|s| s.set_item_parent(child, parent));
1178    }
1179
1180    // -----------------------------------------------------------------
1181    // Removal
1182    // -----------------------------------------------------------------
1183
1184    /// Remove an item and its descendants. Drops any `Delegated` payload `Rc`
1185    /// and cleans the item's a11y mappings; alive logical children re-root.
1186    pub fn remove(&self, id: ItemId) {
1187        self.write(|s| s.remove(id));
1188    }
1189    /// Remove `id` and its descendants, **handing the salvage back** — item
1190    /// box, handlers, magnets, logical-AT decorations, each at its original
1191    /// [`ItemId`]. See [`Scene::take`].
1192    #[must_use = "the salvage is the only copy of the removed items; dropping it \
1193                  makes the removal irreversible — call SceneModel::remove if \
1194                  that is what you meant"]
1195    pub fn take(&self, id: ItemId) -> Vec<RemovedItem> {
1196        self.write(|s| s.take(id))
1197    }
1198    /// Put one salvaged item back at its original id. See [`Scene::restore`].
1199    pub fn restore(&self, salvage: RemovedItem) -> Result<ItemId, RestoreError> {
1200        self.write(|s| s.restore(salvage))
1201    }
1202    /// Put a whole [`take`](Self::take) result back, roots first. See
1203    /// [`Scene::restore_all`].
1204    pub fn restore_all(&self, salvage: Vec<RemovedItem>) -> Result<Vec<ItemId>, RestoreError> {
1205        self.write(|s| s.restore_all(salvage))
1206    }
1207    /// Swap the lightweight item box at `id`, keeping the entry and the id.
1208    /// Returns the box that was there. See [`Scene::replace_item`].
1209    pub fn replace_item(
1210        &self,
1211        id: ItemId,
1212        item: Box<dyn SceneItem>,
1213    ) -> Result<Box<dyn SceneItem>, ReplaceRejected> {
1214        self.write(|s| s.replace_item(id, item))
1215    }
1216    /// Where `id` sits, as one value: parent, z, position, transform.
1217    pub fn placement(&self, id: ItemId) -> Option<Placement> {
1218        self.0.borrow().placement(id)
1219    }
1220    /// Write parent, z, position and transform together, as one change. See
1221    /// [`Scene::set_placement`].
1222    pub fn set_placement(&self, id: ItemId, placement: Placement) {
1223        self.write(|s| s.set_placement(id, placement));
1224    }
1225    /// Reparent `id` while holding it visually still. See
1226    /// [`Scene::reparent_keeping_scene_pos`].
1227    pub fn reparent_keeping_scene_pos(&self, id: ItemId, parent: Option<ItemId>) {
1228        self.write(|s| s.reparent_keeping_scene_pos(id, parent));
1229    }
1230    /// A `z` strictly between two items', or `None` when there is no room left
1231    /// at that locus. See [`Scene::z_between`].
1232    pub fn z_between(&self, below: ItemId, above: ItemId) -> Option<f32> {
1233        self.0.borrow().z_between(below, above)
1234    }
1235    /// Promote an item's children to the scene root.
1236    pub fn orphan(&self, id: ItemId) {
1237        self.write(|s| s.orphan(id));
1238    }
1239
1240    // -----------------------------------------------------------------
1241    // Handlers
1242    // -----------------------------------------------------------------
1243
1244    /// Replace the [`SceneItemHandlerSet`] of `id`, or clear it with `None`.
1245    pub fn set_item_handlers(&self, id: ItemId, handlers: Option<SceneItemHandlerSet>) {
1246        self.write(|s| s.set_item_handlers(id, handlers));
1247    }
1248    /// Mutate an item's handler set through a closure (avoids returning a
1249    /// borrow guard tied to the `RefMut`).
1250    pub fn with_handlers_mut<R>(
1251        &self,
1252        id: ItemId,
1253        f: impl FnOnce(&mut SceneItemHandlerSet) -> R,
1254    ) -> Option<R> {
1255        self.write(|s| s.handlers_mut(id).map(f))
1256    }
1257
1258    // -----------------------------------------------------------------
1259    // Magnetism
1260    // -----------------------------------------------------------------
1261
1262    /// Attach a [`Magnet`] to `item`; see [`Scene::add_magnet`].
1263    pub fn add_magnet(&self, item: ItemId, magnet: Magnet) -> MagnetId {
1264        self.write(|s| s.add_magnet(item, magnet))
1265    }
1266    /// Remove a magnet by id; see [`Scene::remove_magnet`].
1267    pub fn remove_magnet(&self, magnet: MagnetId) {
1268        self.write(|s| s.remove_magnet(magnet));
1269    }
1270    /// Remove every magnet on `item`; see [`Scene::clear_magnets`].
1271    pub fn clear_magnets(&self, item: ItemId) {
1272        self.write(|s| s.clear_magnets(item));
1273    }
1274    /// Move a magnet in its item's local frame; see [`Scene::set_magnet_local_pos`].
1275    pub fn set_magnet_local_pos(&self, magnet: MagnetId, local_pos: Point) {
1276        self.write(|s| s.set_magnet_local_pos(magnet, local_pos));
1277    }
1278    /// Enable or disable a magnet; see [`Scene::set_magnet_enabled`].
1279    pub fn set_magnet_enabled(&self, magnet: MagnetId, enabled: bool) {
1280        self.write(|s| s.set_magnet_enabled(magnet, enabled));
1281    }
1282    /// Ids of every magnet on `item`; see [`Scene::magnet_ids_of`].
1283    pub fn magnet_ids_of(&self, item: ItemId) -> Vec<MagnetId> {
1284        self.0.borrow().magnet_ids_of(item)
1285    }
1286    /// The owning item of a magnet; see [`Scene::magnet_owner`].
1287    pub fn magnet_owner(&self, magnet: MagnetId) -> Option<ItemId> {
1288        self.0.borrow().magnet_owner(magnet)
1289    }
1290    /// A magnet's scene position; see [`Scene::magnet_scene_pos`].
1291    pub fn magnet_scene_pos(&self, magnet: MagnetId) -> Option<Point> {
1292        self.0.borrow().magnet_scene_pos(magnet)
1293    }
1294    /// Resolve a magnet to a [`MagnetRef`] snapshot; see [`Scene::magnet`].
1295    pub fn magnet(&self, magnet: MagnetId) -> Option<MagnetRef> {
1296        self.0.borrow().magnet(magnet)
1297    }
1298    /// Best item-drag snap; see [`Scene::compute_item_snap`]. A shared
1299    /// (read-only) borrow is held while the `predicate` runs over owned
1300    /// candidate snapshots, so the predicate may read but must not mutate
1301    /// the model.
1302    pub fn compute_item_snap(
1303        &self,
1304        dragged: ItemId,
1305        drag_delta: Vec2,
1306        capture_radius: f32,
1307        predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
1308    ) -> Option<MagnetSnap> {
1309        self.0
1310            .borrow()
1311            .compute_item_snap(dragged, drag_delta, capture_radius, predicate)
1312    }
1313    /// Best port-drag snap; see [`Scene::compute_port_snap`].
1314    pub fn compute_port_snap(
1315        &self,
1316        source: MagnetId,
1317        cursor_scene: Point,
1318        capture_radius: f32,
1319        predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
1320    ) -> Option<(MagnetRef, Option<std::rc::Rc<dyn std::any::Any>>)> {
1321        self.0
1322            .borrow()
1323            .compute_port_snap(source, cursor_scene, capture_radius, predicate)
1324    }
1325    /// Nearest enabled magnet within `radius`; see [`Scene::nearest_magnet`].
1326    pub fn nearest_magnet(&self, scene_pt: Point, radius: f32) -> Option<MagnetId> {
1327        self.0.borrow().nearest_magnet(scene_pt, radius)
1328    }
1329
1330    // -----------------------------------------------------------------
1331    // Scene extent / interaction constraints
1332    // -----------------------------------------------------------------
1333
1334    /// Set the logical extent of the scene (used for scroll-bar sizing); `None` = unbounded.
1335    pub fn set_scene_rect(&self, rect: Option<Rect>) {
1336        self.write(|s| s.set_scene_rect(rect));
1337    }
1338    /// Restrict panning to horizontal, vertical, or both axes; updates [`pan_axes_signal`](Self::pan_axes_signal).
1339    pub fn pan_axes(&self, axes: PanAxes) {
1340        self.write(|s| s.pan_axes(axes));
1341    }
1342    /// Enable or disable pinch/scroll zoom; updates [`zoomable_signal`](Self::zoomable_signal).
1343    pub fn zoomable(&self, on: bool) {
1344        self.write(|s| s.zoomable(on));
1345    }
1346    /// Clamp the camera pan to `bounds` (scene coordinates); `None` = no limit; updates [`pan_bounds_signal`](Self::pan_bounds_signal).
1347    pub fn set_pan_bounds(&self, bounds: Option<Rect>) {
1348        self.write(|s| s.set_pan_bounds(bounds));
1349    }
1350    /// Restrict the zoom factor to `range`; `None` = no limit; updates [`zoom_range_signal`](Self::zoom_range_signal).
1351    pub fn set_zoom_range(&self, range: Option<std::ops::RangeInclusive<f32>>) {
1352        self.write(|s| s.set_zoom_range(range));
1353    }
1354
1355    // -----------------------------------------------------------------
1356    // Accessibility structure mutation
1357    // -----------------------------------------------------------------
1358
1359    /// Register a logical AT group (landmark / rotor category container); returns its stable [`A11yGroupId`].
1360    pub fn add_a11y_group(&self, builder: A11yGroupBuilder) -> A11yGroupId {
1361        self.write(|s| s.add_a11y_group(builder))
1362    }
1363    /// Remove a previously registered AT group; triggers an `a11y_change_signal` bump.
1364    pub fn remove_a11y_group(&self, id: A11yGroupId) {
1365        self.write(|s| s.remove_a11y_group(id));
1366    }
1367    /// Re-parent `child` in the AT tree, overriding the default visual parent; `None` re-attaches under the scene root.
1368    pub fn set_a11y_parent(&self, child: A11yNode, parent: Option<A11yNode>) {
1369        self.write(|s| s.set_a11y_parent(child, parent));
1370    }
1371    /// Declare a cross-node AT relationship (controls, describes, labels) from `from` to `to`.
1372    pub fn add_a11y_relation(&self, from: A11yNode, kind: A11yRelation, to: A11yNode) {
1373        self.write(|s| s.add_a11y_relation(from, kind, to));
1374    }
1375    /// Mark `node` as a live region (`Polite` or `Assertive`) so assistive tech announces changes to it.
1376    pub fn set_a11y_live(&self, node: A11yNode, live: accesskit::Live) {
1377        self.write(|s| s.set_a11y_live(node, live));
1378    }
1379    /// Assign a landmark `role` to `node` (e.g. `Role::Region`, `Role::Main`) for rotor navigation.
1380    pub fn set_a11y_landmark(&self, node: A11yNode, role: accesskit::Role) {
1381        self.write(|s| s.set_a11y_landmark(node, role));
1382    }
1383    /// Register `node` under the given rotor [`A11yCategory`] slices so it appears in category-filtered navigation.
1384    pub fn set_a11y_categories(&self, node: A11yNode, categories: &[A11yCategory]) {
1385        self.write(|s| s.set_a11y_categories(node, categories));
1386    }
1387
1388    // -----------------------------------------------------------------
1389    // Dynamic-bounds refresh (called by SceneView::build)
1390    // -----------------------------------------------------------------
1391
1392    /// Re-read signal-driven bounds for `add_item_dynamic` entries; returns
1393    /// `true` if any changed.
1394    pub fn refresh_dynamic_bounds(&self) -> bool {
1395        self.write(|s| s.refresh_dynamic_bounds())
1396    }
1397
1398    // -----------------------------------------------------------------
1399    // Reactive signals + version
1400    // -----------------------------------------------------------------
1401
1402    /// Reactive signal fired on every structural scene change; all views observe this to reconcile.
1403    pub fn item_change_signal(&self) -> Signal<SceneChange> {
1404        self.0.borrow().item_change_signal()
1405    }
1406    /// Reactive monotonic counter bumped on every AT-structure change; views re-walk accessibility on any increment.
1407    pub fn a11y_change_signal(&self) -> Signal<u64> {
1408        self.0.borrow().a11y_change_signal()
1409    }
1410    /// Monotonic counter incremented on every mutation; useful for cache invalidation without observing a signal.
1411    pub fn mutation_version(&self) -> u64 {
1412        self.0.borrow().mutation_version()
1413    }
1414    /// [`mutation_version`](Self::mutation_version) with the per-frame churn of
1415    /// [`refresh_dynamic_bounds`](Self::refresh_dynamic_bounds) excluded — the
1416    /// version to gate an expensive rebuild on. See
1417    /// [`Scene::structural_version`].
1418    pub fn structural_version(&self) -> u64 {
1419        self.0.borrow().structural_version()
1420    }
1421    /// Reactive current [`PanAxes`] restriction; updated by [`pan_axes`](Self::pan_axes).
1422    pub fn pan_axes_signal(&self) -> Signal<PanAxes> {
1423        self.0.borrow().pan_axes_signal()
1424    }
1425    /// Reactive camera-pan clamp bounds; updated by [`set_pan_bounds`](Self::set_pan_bounds).
1426    pub fn pan_bounds_signal(&self) -> Signal<Option<Rect>> {
1427        self.0.borrow().pan_bounds_signal()
1428    }
1429    /// Reactive zoom-factor clamp range; updated by [`set_zoom_range`](Self::set_zoom_range).
1430    pub fn zoom_range_signal(&self) -> Signal<Option<std::ops::RangeInclusive<f32>>> {
1431        self.0.borrow().zoom_range_signal()
1432    }
1433    /// Reactive zoom-enabled flag; updated by [`zoomable`](Self::zoomable).
1434    pub fn zoomable_signal(&self) -> Signal<bool> {
1435        self.0.borrow().zoomable_signal()
1436    }
1437
1438    // -----------------------------------------------------------------
1439    // Value queries
1440    // -----------------------------------------------------------------
1441
1442    /// Total number of items in the scene (lightweight + heavyweight).
1443    pub fn len(&self) -> usize {
1444        self.0.borrow().len()
1445    }
1446    /// Returns `true` when the scene contains no items.
1447    pub fn is_empty(&self) -> bool {
1448        self.0.borrow().is_empty()
1449    }
1450    /// All [`ItemId`]s currently in the scene, in insertion order.
1451    pub fn ids(&self) -> Vec<ItemId> {
1452        self.0.borrow().ids()
1453    }
1454    /// The local position of `id` in its parent's coordinate space; `None` if `id` is unknown.
1455    pub fn local_pos(&self, id: ItemId) -> Option<Point> {
1456        self.0.borrow().local_pos(id)
1457    }
1458    /// The local bounding rect of `id`; `None` if `id` is unknown.
1459    pub fn local_bounds(&self, id: ItemId) -> Option<Rect> {
1460        self.0.borrow().local_bounds(id)
1461    }
1462    /// The additional local-to-parent transform of `id` (beyond position); `None` if none is set.
1463    pub fn transform(&self, id: ItemId) -> Option<Transform2D> {
1464        self.0.borrow().transform(id)
1465    }
1466    /// The full local-to-scene transform for `id` (parent chain composed); identity if `id` is unknown.
1467    pub fn scene_transform(&self, id: ItemId) -> Transform2D {
1468        self.0.borrow().scene_transform(id)
1469    }
1470    /// The origin of `id` mapped into scene coordinates; `None` if `id` is unknown.
1471    pub fn scene_pos(&self, id: ItemId) -> Option<Point> {
1472        self.0.borrow().scene_pos(id)
1473    }
1474    /// The bounding rect of `id` in scene coordinates (local bounds transformed by the parent chain); `None` if unknown.
1475    pub fn scene_rect(&self, id: ItemId) -> Option<Rect> {
1476        self.0.borrow().scene_rect(id)
1477    }
1478    /// The [`ItemFlags`] bitset of `id`; `None` if `id` is unknown.
1479    pub fn flags(&self, id: ItemId) -> Option<ItemFlags> {
1480        self.0.borrow().flags(id)
1481    }
1482    /// Returns `true` if `id` and all of its ancestors are visible.
1483    pub fn is_effectively_visible(&self, id: ItemId) -> bool {
1484        self.0.borrow().is_effectively_visible(id)
1485    }
1486    /// The own opacity of `id` (ignoring ancestors); `None` if `id` is unknown.
1487    pub fn opacity(&self, id: ItemId) -> Option<f32> {
1488        self.0.borrow().opacity(id)
1489    }
1490    /// Accumulated opacity for `id` (own × each ancestor's opacity).
1491    pub fn effective_opacity(&self, id: ItemId) -> f32 {
1492        self.0.borrow().effective_opacity(id)
1493    }
1494    /// The z-order value of `id` within its layer; `None` if `id` is unknown.
1495    pub fn z(&self, id: ItemId) -> Option<f32> {
1496        self.0.borrow().z(id)
1497    }
1498    /// The [`SceneLayer`] of `id`; `None` if `id` is unknown.
1499    pub fn layer(&self, id: ItemId) -> Option<SceneLayer> {
1500        self.0.borrow().layer(id)
1501    }
1502    /// The direct parent of `id`, or `None` if it is a root item (or unknown).
1503    pub fn parent_of(&self, id: ItemId) -> Option<ItemId> {
1504        self.0.borrow().parent_of(id)
1505    }
1506    /// Returns `true` if `id` is anywhere in `ancestor`'s subtree.
1507    pub fn is_descendant_of(&self, id: ItemId, ancestor: ItemId) -> bool {
1508        self.0.borrow().is_descendant_of(id, ancestor)
1509    }
1510    /// The logical extent set via [`set_scene_rect`](Self::set_scene_rect); `None` = unbounded.
1511    pub fn scene_rect_extent(&self) -> Option<Rect> {
1512        self.0.borrow().scene_rect_extent()
1513    }
1514    /// The current pan-axis restriction without subscribing to its signal.
1515    pub fn current_pan_axes(&self) -> PanAxes {
1516        self.0.borrow().current_pan_axes()
1517    }
1518    /// Returns `true` if zoom is currently enabled (snapshot; use [`zoomable_signal`](Self::zoomable_signal) for reactivity).
1519    pub fn is_zoomable(&self) -> bool {
1520        self.0.borrow().is_zoomable()
1521    }
1522    /// Current pan-clamp bounds without subscribing to its signal.
1523    pub fn current_pan_bounds(&self) -> Option<Rect> {
1524        self.0.borrow().current_pan_bounds()
1525    }
1526    /// Current zoom-factor clamp range without subscribing to its signal.
1527    pub fn current_zoom_range(&self) -> Option<std::ops::RangeInclusive<f32>> {
1528        self.0.borrow().current_zoom_range()
1529    }
1530    /// All items whose bounding rects overlap `scene_rect` (spatial-index query).
1531    pub fn items_in_rect(&self, scene_rect: Rect) -> Vec<ItemId> {
1532        self.0.borrow().items_in_rect(scene_rect)
1533    }
1534    /// Items matching `region` under `mode`; see [`Scene::items_in_region`].
1535    ///
1536    /// Screen-anchored
1537    /// ([`IGNORES_TRANSFORMATIONS`](crate::flags::ItemFlags::IGNORES_TRANSFORMATIONS))
1538    /// items are skipped, for the same reason [`SceneModel::item_at`] skips
1539    /// them: a scene-space region cannot place them.
1540    pub fn items_in_region(
1541        &self,
1542        region: &SceneRegion,
1543        mode: ItemSelectionMode,
1544        view_scale: f32,
1545    ) -> Vec<ItemId> {
1546        self.0.borrow().items_in_region(region, mode, view_scale)
1547    }
1548    /// An entry's geometry in local coordinates; see [`Scene::item_shape`].
1549    pub fn item_shape(&self, id: ItemId) -> Option<ItemShape> {
1550        self.0.borrow().item_shape(id)
1551    }
1552    /// An entry's own shape as a scene-space region; see [`Scene::item_region`].
1553    pub fn item_region(&self, id: ItemId) -> Option<SceneRegion> {
1554        self.0.borrow().item_region(id)
1555    }
1556    /// Whether `id`'s shape contains `scene_pt`; see [`Scene::item_contains`].
1557    pub fn item_contains(&self, id: ItemId, scene_pt: Point, view_scale: f32) -> bool {
1558        self.0.borrow().item_contains(id, scene_pt, view_scale)
1559    }
1560    /// Where `id` sits in this scene's single paint order; see
1561    /// [`Scene::paint_key`].
1562    ///
1563    /// Defined for **both tiers**, so it is the value to compare when an app
1564    /// needs to ask "which of these two is on top?" without re-deriving the
1565    /// band/z/insertion rule.
1566    pub fn paint_key(&self, id: ItemId) -> Option<crate::pick::PaintKey> {
1567        self.0.borrow().paint_key(id)
1568    }
1569    /// Whether `id` takes part in pointer hit-testing — visible along its whole
1570    /// ancestor chain AND enabled; see [`Scene::is_hit_testable`].
1571    ///
1572    /// The public form of the filter every picker here applies, so an app can
1573    /// ask why one of its items is not answering.
1574    pub fn is_hit_testable(&self, id: ItemId) -> bool {
1575        self.0.borrow().is_hit_testable(id)
1576    }
1577    /// The topmost **lightweight** item whose shape contains `scene_pt`, at
1578    /// unit view scale; see [`Scene::item_at`] for the full contract.
1579    ///
1580    /// "Topmost" is [`Scene::paint_key`] order, so an
1581    /// [`Over`](crate::SceneLayer::Over)-band item beats a higher-`z`
1582    /// [`Under`](crate::SceneLayer::Under) one and an equal-`z` tie goes to the
1583    /// later-inserted entry. Hidden and disabled entries are excluded, and
1584    /// heavyweight widget entries and screen-anchored
1585    /// ([`IGNORES_TRANSFORMATIONS`](crate::flags::ItemFlags::IGNORES_TRANSFORMATIONS))
1586    /// items are skipped — see [`SceneModel::item_at_in_view`] for the query
1587    /// that places the latter.
1588    pub fn item_at(&self, scene_pt: Point) -> Option<ItemId> {
1589        self.0.borrow().item_at(scene_pt)
1590    }
1591    /// [`Scene::item_at`] at an explicit view zoom. The zoom reaches exactly
1592    /// one thing: a cosmetic stroke band's width in scene units.
1593    pub fn item_at_scaled(&self, scene_pt: Point, view_scale: f32) -> Option<ItemId> {
1594        self.0.borrow().item_at_scaled(scene_pt, view_scale)
1595    }
1596    /// All lightweight items whose shape contains `scene_pt`, topmost-first in
1597    /// [`Scene::paint_key`] order. Same tier, flag and
1598    /// `IGNORES_TRANSFORMATIONS` rules as [`SceneModel::item_at`].
1599    pub fn items_at(&self, scene_pt: Point) -> Vec<ItemId> {
1600        self.0.borrow().items_at(scene_pt)
1601    }
1602    /// [`Scene::items_at`] at an explicit view zoom.
1603    pub fn items_at_scaled(&self, scene_pt: Point, view_scale: f32) -> Vec<ItemId> {
1604        self.0.borrow().items_at_scaled(scene_pt, view_scale)
1605    }
1606    /// The topmost item under a **screen** point, resolving both hit spaces;
1607    /// see [`Scene::item_at_in_view`].
1608    pub fn item_at_in_view(&self, screen_pt: Point, view_transform: Transform2D) -> Option<ItemId> {
1609        self.0.borrow().item_at_in_view(screen_pt, view_transform)
1610    }
1611    /// Items overlapping `id`'s shape, excluding `id`; see
1612    /// [`Scene::colliding_items`].
1613    pub fn colliding_items(&self, id: ItemId) -> Vec<ItemId> {
1614        self.0.borrow().colliding_items(id)
1615    }
1616    /// [`Scene::colliding_items`] under an explicit mode.
1617    pub fn colliding_items_with(&self, id: ItemId, mode: ItemSelectionMode) -> Vec<ItemId> {
1618        self.0.borrow().colliding_items_with(id, mode)
1619    }
1620    /// Items lying along `path`; see [`Scene::items_along_path`]. (This was
1621    /// unreachable from a `SceneModel` before — the facade never forwarded it.)
1622    pub fn items_along_path(&self, path: &teksilo_canvas::Path) -> Vec<ItemId> {
1623        self.0.borrow().items_along_path(path)
1624    }
1625    /// [`Scene::items_along_path`] with an explicit stroke width and mode.
1626    pub fn items_along_path_with(
1627        &self,
1628        path: &teksilo_canvas::Path,
1629        stroke_width: f32,
1630        mode: ItemSelectionMode,
1631    ) -> Vec<ItemId> {
1632        self.0
1633            .borrow()
1634            .items_along_path_with(path, stroke_width, mode)
1635    }
1636    /// The AT-tree parent of `child` as set by [`set_a11y_parent`](Self::set_a11y_parent); `None` = visual default.
1637    pub fn a11y_parent_of(&self, child: A11yNode) -> Option<A11yNode> {
1638        self.0.borrow().a11y_parent_of(child)
1639    }
1640    /// Every declared AT relation, in declaration order.
1641    pub fn a11y_relations(&self) -> Vec<(A11yNode, A11yRelation, A11yNode)> {
1642        self.0.borrow().a11y_relations().to_vec()
1643    }
1644    /// A node's declared live-region politeness; `None` when it is not one.
1645    pub fn a11y_live_of(&self, node: A11yNode) -> Option<accesskit::Live> {
1646        self.0.borrow().a11y_live_of(node)
1647    }
1648    /// A node's declared landmark role; `None` when it is not a landmark.
1649    pub fn a11y_landmark_of(&self, node: A11yNode) -> Option<accesskit::Role> {
1650        self.0.borrow().a11y_landmark_of(node)
1651    }
1652    /// A node's declared rotor / quick-nav categories.
1653    pub fn a11y_categories_of(&self, node: A11yNode) -> Vec<A11yCategory> {
1654        self.0
1655            .borrow()
1656            .a11y_categories_of(node)
1657            .map(|c| c.to_vec())
1658            .unwrap_or_default()
1659    }
1660
1661    // -----------------------------------------------------------------
1662    // Build-support (consumed by SceneView::build)
1663    // -----------------------------------------------------------------
1664
1665    /// Drain every still-pending single-view (`Once`) widget, in entry order.
1666    pub(crate) fn drain_all_once(&self) -> Vec<(ItemId, Box<dyn Widget>)> {
1667        self.write(|s| s.drain_all_once())
1668    }
1669    /// `(id, payload)` for every multi-view (`Delegated`) item, in entry order.
1670    pub(crate) fn delegated_payloads(&self) -> Vec<(ItemId, Rc<dyn std::any::Any>)> {
1671        self.0.borrow().delegated_payloads()
1672    }
1673    /// Ids of every heavyweight widget entry, in entry order.
1674    pub(crate) fn heavyweight_ids(&self) -> Vec<ItemId> {
1675        self.0.borrow().heavyweight_ids()
1676    }
1677}
1678
1679#[cfg(test)]
1680mod tests {
1681    use super::*;
1682    use crate::items::RectItem;
1683    use teksilo_canvas::Point;
1684
1685    fn rect() -> Rect {
1686        Rect::new(0.0, 0.0, 10.0, 10.0)
1687    }
1688
1689    #[test]
1690    fn clone_shares_data() {
1691        let m1 = SceneModel::new();
1692        let m2 = m1.clone();
1693        let id = m1.add_item(RectItem::new(rect()), Point::ZERO);
1694        assert_eq!(m2.len(), 1);
1695        assert_eq!(m2.local_pos(id), Some(Point::ZERO));
1696        m1.set_local_pos(id, Point::new(10.0, 0.0));
1697        assert_eq!(m2.local_pos(id), Some(Point::new(10.0, 0.0)));
1698        assert_eq!(m1.handle_count(), 2);
1699    }
1700
1701    #[test]
1702    fn payload_round_trip_and_signal_fires() {
1703        let m1 = SceneModel::new();
1704        let m2 = m1.clone();
1705        let fired = Rc::new(std::cell::Cell::new(false));
1706        let f = fired.clone();
1707        let _h = m1.item_change_signal().observe(move |c| {
1708            if matches!(c.change, crate::scene::ItemChange::PayloadChanged { .. }) {
1709                f.set(true);
1710            }
1711        });
1712        let id = m1.add_widget_item(42u32, rect());
1713        assert_eq!(
1714            m1.payload(id)
1715                .and_then(|p| p.downcast_ref::<u32>().copied()),
1716            Some(42)
1717        );
1718        assert_eq!(
1719            m2.payload(id)
1720                .and_then(|p| p.downcast_ref::<u32>().copied()),
1721            Some(42)
1722        );
1723        m1.set_payload(id, 99u32);
1724        assert!(fired.get());
1725        assert_eq!(
1726            m2.payload(id)
1727                .and_then(|p| p.downcast_ref::<u32>().copied()),
1728            Some(99)
1729        );
1730    }
1731
1732    #[test]
1733    fn remove_drops_payload_rc() {
1734        let m = SceneModel::new();
1735        let id = m.add_widget_item(42u32, rect());
1736        let weak = Rc::downgrade(&m.payload(id).unwrap());
1737        assert!(weak.upgrade().is_some());
1738        m.remove(id);
1739        assert!(weak.upgrade().is_none(), "payload Rc leaked after remove");
1740    }
1741
1742    #[test]
1743    fn delegated_storage_and_build_helpers() {
1744        // The `Once` drain-with-a-real-widget path is covered by the view
1745        // multi-view tests (which have an arena); here we exercise the model
1746        // bookkeeping for `Delegated` entries without needing a `Widget`.
1747        let m = SceneModel::new();
1748        let a = m.add_widget_item(7u8, rect());
1749        let b = m.add_widget_item(8u8, rect());
1750        assert!(m.payload(a).is_some());
1751        assert!(m.payload(b).is_some());
1752        assert_eq!(m.heavyweight_ids(), vec![a, b]);
1753        assert_eq!(m.delegated_payloads().len(), 2);
1754        assert!(m.drain_all_once().is_empty(), "no Once entries to drain");
1755    }
1756
1757    #[test]
1758    fn mutation_version_advances() {
1759        let m = SceneModel::new();
1760        let v0 = m.mutation_version();
1761        let id = m.add_widget_item(0u32, rect());
1762        let v1 = m.mutation_version();
1763        assert_ne!(v1, v0);
1764        m.set_payload(id, 1u32);
1765        let v2 = m.mutation_version();
1766        assert_ne!(v2, v1);
1767        m.remove(id);
1768        assert_ne!(m.mutation_version(), v2);
1769    }
1770
1771    #[test]
1772    fn item_change_signal_shared_across_handles() {
1773        let m1 = SceneModel::new();
1774        let m2 = m1.clone();
1775        assert!(Signal::same(
1776            &m1.item_change_signal(),
1777            &m2.item_change_signal()
1778        ));
1779    }
1780
1781    /// The two queries that name the picker's own rules are reachable from the
1782    /// handle apps actually hold.
1783    ///
1784    /// `Scene` is behind a `pub(crate)` field, so a `pub fn` on it that the
1785    /// facade does not forward is reachable only from inside this crate — and
1786    /// these two are documented as "the value every picker compares" and "the
1787    /// public form" of the hit filter, which is a promise to a caller who has a
1788    /// `SceneModel` and nothing else.
1789    #[test]
1790    fn the_facade_forwards_the_two_picker_queries() {
1791        let m = SceneModel::new();
1792        let under = m.add_item(RectItem::new(rect()), Point::ZERO);
1793        let over = m.add_item(RectItem::new(rect()), Point::ZERO);
1794        m.set_layer(over, crate::scene::SceneLayer::Over);
1795
1796        let (ku, ko) = (m.paint_key(under), m.paint_key(over));
1797        assert!(ku.is_some() && ko.is_some());
1798        assert!(ku < ko, "the Over band outranks the Under band");
1799        assert_eq!(m.paint_key(ItemId::next()), None, "unknown id");
1800
1801        assert!(m.is_hit_testable(under));
1802        m.set_flag(under, crate::flags::ItemFlags::IS_ENABLED, false);
1803        assert!(
1804            !m.is_hit_testable(under),
1805            "a disabled item passes clicks through",
1806        );
1807        assert!(!m.is_hit_testable(ItemId::next()), "unknown id");
1808    }
1809}