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}