pub struct SceneWriteGuard<'a> { /* private fields */ }Expand description
Write guard over a SceneModel’s Scene: Deref/DerefMut to the
scene, with the deferred-notification contract of a SceneModel mutator.
Holding one keeps a write scope open, so every notification the edits
produce queues in emission order. On drop the guard closes the scope,
releases the RefCell borrow, and then fans the whole batch out — so a
block of edits is one notification round, and an observer may read the
scene or write it back.
let model = SceneModel::new();
let a = model.add_item(RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0)), Point::ZERO);
{
let mut scene = model.write_guard();
scene.set_local_pos(a, Point::new(10.0, 10.0));
scene.set_visible(a, false);
} // both changes fan out here, in this orderIf the thread is unwinding when the guard drops, the scope is closed and
the borrow released but the fan-out is skipped: running observers from
a Drop during an unwind would abort the process the moment one of them
panicked. The queued changes survive and are delivered by the next
flush_changes — which a catch_unwind
recovery path can call explicitly.
Methods from Deref<Target = Scene>§
Sourcepub fn add_widget<W: Widget + 'static>(
&mut self,
widget: W,
local_rect: Rect,
) -> ItemId
pub fn add_widget<W: Widget + 'static>( &mut self, widget: W, local_rect: Rect, ) -> ItemId
Place a heavyweight Widget at local_rect’s origin, sized
local_rect.size. The rect is interpreted as
(local_pos = local_rect.origin, local_bounds = (0, 0, w, h)).
Returns the ItemId for later mutation. The widget is
consumed at SceneView build time and added to the arena.
Sourcepub fn add_item<I: SceneItem + 'static>(
&mut self,
item: I,
local_pos: Point,
) -> ItemId
pub fn add_item<I: SceneItem + 'static>( &mut self, item: I, local_pos: Point, ) -> ItemId
Place a lightweight SceneItem at local_pos. The item’s
local_bounds and initial_flags are read once at insert
time. The item is not added to the arena — it’s painted
directly from SceneView::paint.
Sourcepub fn add_item_dynamic<I: SceneItem + 'static>(
&mut self,
item: I,
local_pos: Point,
) -> ItemId
pub fn add_item_dynamic<I: SceneItem + 'static>( &mut self, item: I, local_pos: Point, ) -> ItemId
Like add_item but flags the entry as
having signal-driven local_bounds. The Scene re-reads
item.local_bounds() each rebuild via
refresh_dynamic_bounds — the
SceneView calls that at the start of every build pass. The
spatial index gets re-bucketed when the read-back differs
from the cached value, so items_in_rect / hit-test stay
correct without app-side set_local_bounds plumbing.
Use only when the bounds genuinely depend on a Signal<T>
the item reads in local_bounds. Static items pay an
unnecessary per-rebuild bounds read otherwise; prefer
add_item for the common case.
Sourcepub fn refresh_dynamic_bounds(&mut self) -> bool
pub fn refresh_dynamic_bounds(&mut self) -> bool
Re-read every dynamic item’s current local_bounds, applying
set_local_bounds (and re-bucketing the spatial index) for
any entry whose value has changed. No-op for static entries.
Called by SceneView at the start of each
build() so signal-driven bounds propagate to bucketing
without explicit app-side calls.
Returns true if at least one dynamic entry’s bounds changed this call.
SceneView uses the true → false transition (an animation settling) as
the one moment to walk the final animated bounds into the AccessKit tree,
since it otherwise suppresses per-frame AT re-walks during the animation.
Sourcepub fn item_change_signal(&self) -> Signal<SceneChange>
pub fn item_change_signal(&self) -> Signal<SceneChange>
Reactive notification stream for every Scene mutation. Apps observe via
signal.observe(|change| …) to wire validation, persistence,
telemetry, or mirroring into a data layer without polling the Scene
each frame. The signal fires after the mutation has been applied — by
the time the observer runs, the Scene already reflects the new state.
§Notification timing
Where the fan-out happens depends on which door mutated the scene, and
the difference is exactly the RefCell an observer would have to
re-enter:
-
Through a
SceneModel(the normal path, includingSceneWriteGuard) the mutation runs inside a write scope: changes queue in emission order and fan out once the model has released itsborrow_mut(). An observer may therefore read the scene, and write it back — a write made from an observer queues in turn and is delivered by the same drain, after the changes already queued ahead of it. -
Through a bare
&mut Scenethere is nothing holding aRefCellopen to escape from, so the fan-out is synchronous, inside the mutator, exactly as before.This is safe for a
Sceneyou own outright — no second handle to it can exist. It is not safe for a&mut Scenereborrowed out of aSceneModel: aRefMuttaken from the shared cell leavesdefer_depthat zero, so observers still run under the live exclusive borrow and one that re-enters the model panics. TakeSceneModel::write_guardinstead — it derefs to&mut Scene, so the call sites are identical, and it opens the write scope the observers need.
Either way the notification has been delivered by the time the mutator call returns.
§What is not deferred
The scene’s constraint signals — pan_axes_signal,
zoomable_signal,
pan_bounds_signal,
zoom_range_signal — carry state, not
events (current_pan_axes and friends read
them back), so queueing their writes would make the scene contradict
itself inside an open write scope. They still fan out synchronously
under the borrow: an observer on one of those four must not re-enter
the SceneModel.
Sourcepub fn a11y_change_signal(&self) -> Signal<u64>
pub fn a11y_change_signal(&self) -> Signal<u64>
Reactive notification for logical-AT-structure mutations
(add_a11y_group / remove_a11y_group / set_a11y_parent /
add_a11y_relation / set_a11y_live / set_a11y_landmark /
set_a11y_categories). A monotonic counter bumped after each such
mutation. SceneView observes this to re-walk the AccessKit tree —
these changes don’t flow through item_change_signal
because they aren’t item geometry, and the AT tree is separate from the
visual scene.
Sourcepub fn cascade_budget(&self) -> CascadeBudget
pub fn cascade_budget(&self) -> CascadeBudget
The runaway-detection budget for this scene’s change fan-out.
Sourcepub fn set_cascade_budget(&self, budget: CascadeBudget)
pub fn set_cascade_budget(&self, budget: CascadeBudget)
Replace the runaway-detection budget for this scene’s change fan-out.
The default is tuned for the graph shapes this tier runs; see
CascadeBudget for the rule and for when raising it is the right
answer rather than a way to silence a real cycle.
A budget of zero is clamped to the smallest usable one rather than stored: it would forbid the reactive write-back this channel exists for, and would trip with no cascade for the diagnostic to describe.
Takes effect on the next drain — a budget raised from inside an observer does not enlarge the drain already running, which is why the budget is read once at its start.
Sourcepub fn open_transaction_depth(&self) -> u32
pub fn open_transaction_depth(&self) -> u32
How many transaction / write scopes are open on this scene.
Zero between mutations. A non-zero value outside a mutator means a
SceneTransaction is being held, which is
only correct within one synchronous scope — see
SceneModel::transaction.
Sourcepub fn transaction_signal(&self) -> Signal<TxnId>
pub fn transaction_signal(&self) -> Signal<TxnId>
Fires once per committed transaction, after the edit sink, with the
scene unborrowed. See
SceneModel::transaction_signal.
Sourcepub fn mutation_version(&self) -> u64
pub fn mutation_version(&self) -> u64
Monotonic counter of every model mutation applied so far — item geometry
/ visibility / structure (each ItemChange) and logical-AT
structure (groups, parents, relations, live, landmarks, categories).
Includes the per-frame churn of
refresh_dynamic_bounds; a consumer
gating an expensive rebuild on “did anything meaningful change” wants
structural_version instead. The counter
wraps; compare for equality, not ordering.
Sourcepub fn structural_version(&self) -> u64
pub fn structural_version(&self) -> u64
mutation_version with the per-frame
dynamic-bounds churn subtracted out: it advances on every mutation
except the LocalBoundsChanged events
refresh_dynamic_bounds emits.
The version to gate an expensive rebuild on.
SceneView snapshots it each build() and only
re-walks the (separate, expensive) AccessKit tree when it has advanced
since the previous walk — so an actively-animating
add_item_dynamic item, which rebuilds every
frame, does not issue an AT re-walk per frame for sub-pixel bounds drift
a screen reader cannot use.
Naming the exclusion is the point. The alternative — snapshotting
mutation_version before refresh_dynamic_bounds and again after, and
treating the difference as churn — is wrong now that the refresh fans
its changes out: an observer may legally mutate the scene from that
fan-out, and its mutation lands inside the bracket where it is
indistinguishable from churn. Folded into the baseline, an AT-structural
change made there would never un-gate a re-walk, in that build or any
later one. The counter wraps; compare for equality, not ordering.
Sourcepub fn local_pos(&self, id: ItemId) -> Option<Point>
pub fn local_pos(&self, id: ItemId) -> Option<Point>
Read an item’s local_pos (its anchor in parent coords).
Sourcepub fn set_local_pos(&mut self, id: ItemId, local_pos: Point)
pub fn set_local_pos(&mut self, id: ItemId, local_pos: Point)
Move an item to a new local_pos in its parent’s coordinate
frame. Re-buckets the item and every descendant in the
spatial index since the descendants’ scene-AABBs shift along.
No-op if the id is unknown.
Sourcepub fn local_bounds(&self, id: ItemId) -> Option<Rect>
pub fn local_bounds(&self, id: ItemId) -> Option<Rect>
Read an item’s local_bounds (its AABB in local coords).
Sourcepub fn set_local_bounds(&mut self, id: ItemId, local_bounds: Rect)
pub fn set_local_bounds(&mut self, id: ItemId, local_bounds: Rect)
Update an item’s local_bounds. For lightweight items this
also calls SceneItem::set_local_bounds on the item so its
next paint reflects the new geometry. The spatial index is
re-bucketed; only this item moves (descendants’ local frames
are unchanged). No-op if the id is unknown.
§The item has the last word, and says it once
An item whose box is derived from its own geometry — a
PathItem — treats this as “fit yourself to this
rectangle” rather than “adopt this rectangle”, and the box stored
here is what it settled on, read back from the item. It lands on the
request on every axis the geometry has extent on; on an axis it has
none (a perfectly horizontal stroke has no height) the box stays the
stroke’s own thickness, because there is nothing to stretch.
Either way the call is idempotent: asking twice for the same
rectangle emits one ItemChange::LocalBoundsChanged and re-buckets
the index once. An app driving this per frame — a resize handle, a
layout pass — therefore goes quiet as soon as it stops moving, rather
than emitting an endless series of nearly-identical changes.
Sourcepub fn size_policy(&self, id: ItemId) -> SizePolicy
pub fn size_policy(&self, id: ItemId) -> SizePolicy
Which axes of this entry’s local_bounds its widget decides.
SizePolicy::Fixed for an unknown id, and for every lightweight item.
Sourcepub fn set_size_policy(&mut self, id: ItemId, policy: SizePolicy) -> bool
pub fn set_size_policy(&mut self, id: ItemId, policy: SizePolicy) -> bool
Hand one or both axes of id’s box to its widget. See SizePolicy.
Returns false — and changes nothing — for an unknown id or a
lightweight entry, which has no layout_response to ask. That is a
refusal rather than a silent no-op because the two tiers already have
different answers to “who decides my box”
(add_item_dynamic is the lightweight one),
and a policy that quietly did nothing on one of them would read as a
bug in the view rather than a misuse of the model.
Emits ItemChange::SizePolicyChanged when the value actually changes —
an edit, carrying no geometry. The geometry it enables arrives separately
as ItemChange::MeasuredSizeChanged, which is not an edit.
Sourcepub fn set_measured_size(&mut self, id: ItemId, size: Size) -> bool
pub fn set_measured_size(&mut self, id: ItemId, size: Size) -> bool
Write a derived size into the model: the same geometry update and
index re-bucketing set_local_bounds does,
reported as ItemChange::MeasuredSizeChanged instead — not an edit,
not structural, answered by a relayout rather than a rebuild.
Returns whether anything moved. The origin of local_bounds is kept;
only the size is written, because that is the whole of what a
measurement can know.
§When this is the right door, and when it is not
Use it for a size that is a pure function of content the app already owns: the height a paragraph wraps to at a given width, the bounding box of an ink stroke after a segment dried, the extent of a generated diagram. Recording such a write as an edit is what makes a text reflow an undo step; bumping the structural version for it is what buys an AccessKit re-walk for a two-pixel line-height change. This door does neither, and the view that observes it relayouts instead of rebuilding.
Use set_local_bounds for anything the
user did: a resize handle, an import, a paste. Those are edits, and
a history above the scene must see them.
The SizePolicy machinery drives this from inside the view; an app
measuring something the framework cannot measure calls it directly.
Sourcepub fn transform(&self, id: ItemId) -> Option<Transform2D>
pub fn transform(&self, id: ItemId) -> Option<Transform2D>
Read an item’s local→parent transform (rotation/scale around the local origin). Identity by default.
Sourcepub fn set_transform(&mut self, id: ItemId, transform: Transform2D)
pub fn set_transform(&mut self, id: ItemId, transform: Transform2D)
Set an item’s local→parent transform. Re-buckets the item’s subtree in the spatial index. No-op if the id is unknown.
Sourcepub fn scene_transform(&self, id: ItemId) -> Transform2D
pub fn scene_transform(&self, id: ItemId) -> Transform2D
The composed local→scene transform for this item, walking up the parent chain. Identity for an item that doesn’t exist.
Sourcepub fn scene_pos(&self, id: ItemId) -> Option<Point>
pub fn scene_pos(&self, id: ItemId) -> Option<Point>
The item’s anchor in scene coords (its local origin transformed through the parent chain).
Sourcepub fn scene_rect(&self, id: ItemId) -> Option<Rect>
pub fn scene_rect(&self, id: ItemId) -> Option<Rect>
The AABB enclosing the item’s local_bounds after composing
through the parent chain — i.e. the rectangle the spatial
index buckets on. None if the id is unknown.
Sourcepub fn map_to_scene(&self, id: ItemId, local_pt: Point) -> Option<Point>
pub fn map_to_scene(&self, id: ItemId, local_pt: Point) -> Option<Point>
Map a point in the item’s local frame to scene coords.
Sourcepub fn map_from_scene(&self, id: ItemId, scene_pt: Point) -> Option<Point>
pub fn map_from_scene(&self, id: ItemId, scene_pt: Point) -> Option<Point>
Map a point in scene coords to the item’s local frame.
Returns None if the item is unknown or its scene transform
is degenerate (zero scale).
Sourcepub fn set_flags(&mut self, id: ItemId, flags: ItemFlags)
pub fn set_flags(&mut self, id: ItemId, flags: ItemFlags)
Replace an item’s flags wholesale. No-op if unknown.
Announces exactly what set_flag announces for the
same net change: the derived ItemChange::VisibilityChanged when
IS_VISIBLE flipped, then the ItemChange::FlagsChanged that
describes the mutation.
Sourcepub fn set_flag(&mut self, id: ItemId, flag: ItemFlags, on: bool)
pub fn set_flag(&mut self, id: ItemId, flag: ItemFlags, on: bool)
Set or clear a single flag on an item. No-op if unknown.
Sourcepub fn set_visible(&mut self, id: ItemId, visible: bool)
pub fn set_visible(&mut self, id: ItemId, visible: bool)
Toggle the ItemFlags::IS_VISIBLE bit. Convenience for
the common “hide this item” operation.
Sourcepub fn is_effectively_visible(&self, id: ItemId) -> bool
pub fn is_effectively_visible(&self, id: ItemId) -> bool
Whether the item is visible AND every ancestor in its chain
is visible. Returns true when nothing in the chain has
IS_VISIBLE cleared. false for unknown ids.
Sourcepub fn opacity(&self, id: ItemId) -> Option<f32>
pub fn opacity(&self, id: ItemId) -> Option<f32>
Read an item’s local opacity multiplier (1.0 by default).
Sourcepub fn set_opacity(&mut self, id: ItemId, opacity: f32)
pub fn set_opacity(&mut self, id: ItemId, opacity: f32)
Set an item’s local opacity, clamped to [0.0, 1.0].
Sourcepub fn set_item_fill(&mut self, id: ItemId, fill: impl Into<ColorProp>)
pub fn set_item_fill(&mut self, id: ItemId, fill: impl Into<ColorProp>)
Replace a lightweight item’s fill colour live, emitting
ItemChange::AppearanceChanged — always repaint-only, never a
relayout, rebuild, or AccessKit re-walk. The colour is a [ColorProp],
so it accepts a plain Color, a theme role, a
Signal<Color>, or a Signal<Role>. No-op for item kinds without a fill
(e.g. ImageItem).
§Reactivity contract
A colour becomes continuously reactive by being registered at build
time (SceneItem::register_bindings). So:
- Construct the item with a
Signal/role colour (.fill(my_signal)) for a colour that tracks its signal forever. This is the recommended path and needs no mutator at all. - This mutator installs a snapshot: it repaints immediately, which
is all a static colour ever needs. If you pass a
Signal/dynamic role here, it paints the signal’s current value now and starts tracking it continuously from the owning view’s next rebuild (whenever some other structural change re-runsregister_bindings). Deliberately not forced: a colour change must never cost a rebuild + AT re-walk.
Sourcepub fn clear_item_fill(&mut self, id: ItemId)
pub fn clear_item_fill(&mut self, id: ItemId)
Clear a lightweight item’s fill (Rect/Path/Group become fill-less),
emitting ItemChange::AppearanceChanged (repaint-only). No-op for items
whose fill can’t be cleared (e.g. TextItem, which always has a
foreground colour).
Sourcepub fn set_item_stroke(
&mut self,
id: ItemId,
color: impl Into<ColorProp>,
style: StrokeStyle,
)
pub fn set_item_stroke( &mut self, id: ItemId, color: impl Into<ColorProp>, style: StrokeStyle, )
Replace a lightweight item’s stroke (colour + [StrokeStyle]) live,
emitting ItemChange::AppearanceChanged (repaint-only). No-op for item
kinds without a stroke slot (TextItem / ImageItem). See
set_item_fill for the reactivity contract.
Sourcepub fn clear_item_stroke(&mut self, id: ItemId)
pub fn clear_item_stroke(&mut self, id: ItemId)
Clear a lightweight item’s stroke, emitting
ItemChange::AppearanceChanged (repaint-only). No-op for item kinds
without a stroke.
Sourcepub fn add_boxed_item(
&mut self,
item: Box<dyn SceneItem>,
local_pos: Point,
) -> ItemId
pub fn add_boxed_item( &mut self, item: Box<dyn SceneItem>, local_pos: Point, ) -> ItemId
Insert an already-boxed lightweight item at local_pos, returning its
id. The boxed-dyn counterpart of add_item — used by
SceneListAdapter whose delegate yields
Box<dyn SceneItem>.
Sourcepub fn set_item_handlers(
&mut self,
id: ItemId,
handlers: Option<SceneItemHandlerSet>,
)
pub fn set_item_handlers( &mut self, id: ItemId, handlers: Option<SceneItemHandlerSet>, )
Replace an item’s handler set. Pass None to clear.
Fires ItemChange::HandlersChanged carrying both sides
(HandlerReplacement), like every other mutator on this type: a
consumer that caches handlers has no other way to learn of it, and a
data layer inverting a transaction has no other way to put them back.
This is the handler door that produces a reversible edit; see
handlers_mut for the one that cannot.
Sourcepub fn handlers_mut(&mut self, id: ItemId) -> Option<&mut SceneItemHandlerSet>
pub fn handlers_mut(&mut self, id: ItemId) -> Option<&mut SceneItemHandlerSet>
Mutably borrow an item’s handler set, lazily creating an
empty one if none exists. Returns None for unknown ids.
Allows fluent chains: scene.handlers_mut(id).unwrap().on_tap(…).cursor(…);.
Fires ItemChange::HandlersChanged before handing the set out, with
no HandlerReplacement — &mut cannot report back what the caller
does with it, so the notification means “no longer what you last read”
and nothing finer. A caller that takes the borrow and changes nothing
therefore costs one spurious invalidation, which is the right way round:
the alternative is a consumer serving stale handlers.
Because it describes nothing, it is not recorded as an edit
(ItemChange::is_edit) — a transaction record whose every entry
carries both sides of what it replaced must not carry one that carries
neither. Make a handler change the history should be able to reverse
through set_item_handlers, which knows both
sides.
Sourcepub fn handlers(&self, id: ItemId) -> Option<&SceneItemHandlerSet>
pub fn handlers(&self, id: ItemId) -> Option<&SceneItemHandlerSet>
Read-only access to an item’s handler set, if one is set.
Sourcepub fn effective_opacity(&self, id: ItemId) -> f32
pub fn effective_opacity(&self, id: ItemId) -> f32
Effective opacity composed up the parent chain — the product
of every ancestor’s opacity and this item’s. 1.0 for an
unknown id (so callers don’t end up multiplying by a stale
value).
Sourcepub fn set_scene_rect(&mut self, rect: Option<Rect>)
pub fn set_scene_rect(&mut self, rect: Option<Rect>)
Declare the scene’s logical extent. None (the default)
means “auto-compute from items each query”; Some(rect)
fixes the extent regardless of item placement. Used by
SceneView for pan clamping and fit_to_content.
Sourcepub fn scene_rect_extent(&self) -> Option<Rect>
pub fn scene_rect_extent(&self) -> Option<Rect>
The resolved scene extent — user-declared via
Scene::set_scene_rect if set, otherwise the AABB
enclosing every item’s scene rect. None when neither is
available (the user didn’t declare and the scene is empty).
Sourcepub fn pan_axes(&mut self, axes: PanAxes)
pub fn pan_axes(&mut self, axes: PanAxes)
Set the axes the view may pan along. Default
PanAxes::Both. Writes to the reactive signal; gesture
closures pick the change up on the next event.
Sourcepub fn current_pan_axes(&self) -> PanAxes
pub fn current_pan_axes(&self) -> PanAxes
The currently-declared pan axes. Live read of the signal.
Sourcepub fn zoomable(&mut self, on: bool)
pub fn zoomable(&mut self, on: bool)
Set whether the view honors zoom gestures. Default true.
Writes to the reactive signal.
Sourcepub fn is_zoomable(&self) -> bool
pub fn is_zoomable(&self) -> bool
Whether the scene currently allows zoom. Live read.
Sourcepub fn set_pan_bounds(&mut self, bounds: Option<Rect>)
pub fn set_pan_bounds(&mut self, bounds: Option<Rect>)
Clamp the visible viewport to this scene-coord rect. None
(default) leaves pan unconstrained. When Some(r), the
SceneView’s pan is clamped so the
visible scene region overlaps r. When r is smaller than
the visible viewport, the rect is centered.
Distinct from set_scene_rect:
scene_rect declares the scene’s logical extent (used by
adopt_scene_size); pan_bounds controls what region the
user can scroll to. A doc-style app typically sets both to
the same rect.
Sourcepub fn current_pan_bounds(&self) -> Option<Rect>
pub fn current_pan_bounds(&self) -> Option<Rect>
The currently-declared pan-bounds rect. Live read.
Sourcepub fn set_zoom_range(&mut self, range: Option<RangeInclusive<f32>>)
pub fn set_zoom_range(&mut self, range: Option<RangeInclusive<f32>>)
Inclusive [min, max] zoom-factor clamp. None (default)
is unconstrained from the Scene side — the SceneView
may still impose its own override.
The effective range applied by the SceneView is the
intersection of Scene + view-level override, so apps
cannot loosen a Scene-declared range by setting a wider
override on the view.
Sourcepub fn current_zoom_range(&self) -> Option<RangeInclusive<f32>>
pub fn current_zoom_range(&self) -> Option<RangeInclusive<f32>>
The currently-declared zoom range. Live read.
Sourcepub fn pan_axes_signal(&self) -> Signal<PanAxes>
pub fn pan_axes_signal(&self) -> Signal<PanAxes>
Reactive accessors for live observation.
Sourcepub fn pan_bounds_signal(&self) -> Signal<Option<Rect>>
pub fn pan_bounds_signal(&self) -> Signal<Option<Rect>>
Reactive pan-bounds signal.
Sourcepub fn zoom_range_signal(&self) -> Signal<Option<RangeInclusive<f32>>>
pub fn zoom_range_signal(&self) -> Signal<Option<RangeInclusive<f32>>>
Reactive zoom-range signal.
Sourcepub fn zoomable_signal(&self) -> Signal<bool>
pub fn zoomable_signal(&self) -> Signal<bool>
Reactive zoomable on/off signal.
Sourcepub fn constraints(&self) -> &SceneConstraints
pub fn constraints(&self) -> &SceneConstraints
Read-only view of the full constraint bundle. Useful when passing all four signals to a custom view implementation.
Sourcepub fn set_z(&mut self, id: ItemId, z: f32)
pub fn set_z(&mut self, id: ItemId, z: f32)
Set paint z-order for an entry. Higher z paints later (on top); equal-z falls back to insertion order. Default 0.0.
Works for both tiers: lightweight items re-sort within their
band on the next paint, and heavyweight widget entries restack the
arena children on the next rebuild (the SceneView reorders
node.children by z without recreating the widgets, so focus /
text-edit / animation state survives the restack). No-op for
unknown ids.
§The no-op test is exact
A write is ignored only when the entry already holds that value.
The guard used to be an absolute |old - z| < f32::EPSILON, which is
the wrong metric in both directions: f32::EPSILON is the spacing of
the representable numbers at 1.0, so out at z = 1e6 (where the real
spacing is ~0.06) no two distinct floats are ever within it and the
guard never fires, while near zero (where the spacing is ~1e-45) it
swallows millions of distinct values.
That second half was not theoretical: z_between
bisects relatively and existed precisely so a caller is never told
“there is room” and then gets a silent no-op — and near zero the two
disagreed, so z_between returned Some(6.25e-8) and set_z dropped
it, emitting no ItemChange::ZChanged and leaving the order wrong
with nothing to observe. One metric now, and it is the one z_between
already used: two zs are the same iff they are the same float.
Sourcepub fn bring_to_front(&mut self, id: ItemId)
pub fn bring_to_front(&mut self, id: ItemId)
Raise an entry above all current entries by giving it a z one
greater than the current maximum. The drag-to-front primitive —
call it on drag-start so the grabbed card (and its text) renders
over the others. Works for both tiers (see set_z).
Sourcepub fn send_to_back(&mut self, id: ItemId)
pub fn send_to_back(&mut self, id: ItemId)
Lower an entry below all current entries by giving it a z one less
than the current minimum. Works for both tiers (see
set_z).
Sourcepub fn set_layer(&mut self, id: ItemId, layer: SceneLayer)
pub fn set_layer(&mut self, id: ItemId, layer: SceneLayer)
Set the paint band for a lightweight entry — see SceneLayer for
what each one means and what Interleaved
costs. Within a band, set_z still orders items among
themselves. No-op for unknown ids.
Sourcepub fn layer(&self, id: ItemId) -> Option<SceneLayer>
pub fn layer(&self, id: ItemId) -> Option<SceneLayer>
Read an entry’s paint band. None for unknown ids.
Sourcepub fn set_item_parent(&mut self, child: ItemId, parent: Option<ItemId>)
pub fn set_item_parent(&mut self, child: ItemId, parent: Option<ItemId>)
Declare a parent/child relationship. child’s local_pos
and transform are reinterpreted as relative to the new
parent’s local frame — the visual position changes unless
the caller compensates. Re-buckets child’s subtree.
Pass parent = None to detach (child’s local frame becomes
scene-rooted again).
Cycle guard: if the proposed parent is child itself
or a descendant of child, the call is a no-op (no parent
change, no rebucket, no signal fire). Without this guard
the downstream rebucket_subtree walk loops indefinitely.
Sourcepub fn is_descendant_of(&self, id: ItemId, ancestor: ItemId) -> bool
pub fn is_descendant_of(&self, id: ItemId, ancestor: ItemId) -> bool
Whether id’s ancestor chain contains ancestor.
Sourcepub fn collect_descendants(&self, id: ItemId, out: &mut Vec<ItemId>)
pub fn collect_descendants(&self, id: ItemId, out: &mut Vec<ItemId>)
Append every direct + transitive descendant of id into out, each
parent before its own children. The id itself is not included.
Costs the subtree, not the scene: the walk steps through the kept
SceneEntry::children adjacency rather than rescanning every entry per
visited node. A cycle in the parent graph is bounded by the visited set
rather than looping forever.
Sourcepub fn set_geometry_constraint(
&mut self,
f: impl Fn(&ProposedChange<'_>) -> ChangeVerdict + 'static,
)
pub fn set_geometry_constraint( &mut self, f: impl Fn(&ProposedChange<'_>) -> ChangeVerdict + 'static, )
Install the document’s standing geometry rule — snap-to-grid, axis lock, page-bounds clamp — consulted before a user-driven gesture applies anything.
Replaces any previous constraint; there is one per scene, because a
document has one geometry. See ProposedChange
for what the closure is handed, and
SceneModel::set_geometry_constraint
for the door an app normally uses.
use teksilo_canvas::{Point, Rect};
use teksilo_scene::{ChangeVerdict, RectItem, Scene};
let mut scene = Scene::new();
scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 40.0, 40.0)), Point::ZERO);
// A 25-unit grid, snapping the moved box's own top-leading corner.
scene.set_geometry_constraint(|c| {
let mut f = c.proposed;
f.rect.x = (f.rect.x / 25.0).round() * 25.0;
f.rect.y = (f.rect.y / 25.0).round() * 25.0;
ChangeVerdict::Adjust(f)
});
assert!(scene.has_geometry_constraint());Sourcepub fn clear_geometry_constraint(&mut self)
pub fn clear_geometry_constraint(&mut self)
Remove the geometry constraint. Gestures then apply their raw proposal.
Sourcepub fn has_geometry_constraint(&self) -> bool
pub fn has_geometry_constraint(&self) -> bool
Whether a geometry constraint is installed.
Sourcepub fn in_geometry_constraint(&self) -> bool
pub fn in_geometry_constraint(&self) -> bool
Whether a geometry constraint is running right now on this thread.
The probe SceneModel::write_guard
uses to turn “a constraint tried to write the scene” into a diagnostic
that names the hook.
Sourcepub fn selection_roots(&self, ids: &[ItemId]) -> Vec<ItemId>
pub fn selection_roots(&self, ids: &[ItemId]) -> Vec<ItemId>
ids pruned to its roots: any item whose ancestor is also in ids
is dropped.
Moving an ancestor already moves its descendants — their local_pos is
parent-relative and is not touched — so transforming both would apply
the change twice. This is the mutation-side twin of the filter the paint
preview already runs over the same set.
Order is preserved. O(n·depth): the set is hashed once and each id
then walks its own ancestor chain, asking the set rather than asking
every other id whether it is an ancestor. The hop cap is the one
is_descendant_of uses, so a malformed parent
cycle bounds rather than hangs, and an id that is its own ancestor is
kept (a cycle has no root to prefer).
The complexity is load-bearing, not incidental: every selection-transform
recompute runs this once (plus one
transformable_roots per operation), and so
does the item-drag group and the keyboard nudge — so a quadratic form
here is a frozen window on a large selection rather than a slow one.
selection_roots_is_linear_in_the_selection, in
tests/selection_roots_scaling_probe.rs, pins it: 7.03 ms against
36.3 µs for a 1 000-item selection, measured.
Sourcepub fn transformable_roots(
&self,
ids: &[ItemId],
op: TransformOp,
) -> Vec<ItemId>
pub fn transformable_roots( &self, ids: &[ItemId], op: TransformOp, ) -> Vec<ItemId>
The roots of ids that may take part in op.
Three filters, in this order. The item must carry the operation’s flag
(TransformOp::required_flag).
For TransformOp::Rotate it must be a
lightweight entry, because a heavyweight card is sized from the AABB of
its transformed bounds and a rotation there inflates its layout box
without turning anything. For
TransformOp::Resize it must own at least
one axis of its own box: an entry on SizePolicy::Intrinsic has
handed both to its widget, so every number a resize could write is
one the next layout pass measures back over — a handle that produces a
reversible step and no visible change is worse than no handle.
SizePolicy::HeightForWidth still owns its width and is offered the
full set; the height half of a corner drag is neutralised at the model
door, in apply_transform_delta.
The flag is checked before the descendant pruning, so a selected child of a selected-but-locked parent still takes part on its own.
Sourcepub fn scene_rotation(&self, id: ItemId) -> Option<f32>
pub fn scene_rotation(&self, id: ItemId) -> Option<f32>
The item’s own rotation in scene space, in radians — the angle of its
composed local → scene basis. None for an unknown id.
Sourcepub fn transform_frame(&self, roots: &[ItemId]) -> Option<TransformFrame>
pub fn transform_frame(&self, roots: &[ItemId]) -> Option<TransformFrame>
The selection frame enclosing roots — the box a transform controller
draws its handles on. None when roots is empty or none of them
resolve.
A single root’s frame takes that item’s own rotation, so resizing a rotated item happens along its own axes and is exact. A multi-item frame is axis-aligned, because the union of differently-rotated boxes has no well-defined angle — Konva does the same.
Sourcepub fn apply_transform_delta(
&mut self,
roots: &[ItemId],
delta: &TransformDelta,
) -> usize
pub fn apply_transform_delta( &mut self, roots: &[ItemId], delta: &TransformDelta, ) -> usize
Apply delta to roots as a single operation, and the only place a
transform controller writes the model.
Returns how many entry fields actually changed.
Three writes per root at most, and each is the existing setter, so the change stream, the spatial index and every observer see nothing new:
- position — the item’s scene anchor is mapped through the delta and restated in its parent’s frame, so a scale about a pivot moves it and a rotation orbits it.
- extent —
local_boundsis scaled, which makes the item reflow into the new box rather than be drawn at a stretched scale. The scale is resolved along the item’s own axes, so a rotated item stays a rotated rectangle instead of shearing into a parallelogram; where the scale is non-uniform and the item is rotated relative to the frame, that is an approximation of the true (unrepresentable) result, and it is exact whenever the item is aligned with the frame — which includes every single-item selection. An axis the entry’sSizePolicyhands to its widget is not scaled, and is not scaled for the anchor either, so a card whose height its content owns neither resizes nor moves when someone drags its top edge; see the comment at the loop head. - orientation — the item’s own
Transform2Dis post-rotated. Skipped for a heavyweight entry, whose layout box is the AABB of its transformed bounds: a rotation there would inflate the box and turn nothing.
The transaction boundary is this call. One gesture is one call, so an app-level reversible-edit layer has exactly one thing to record — and this crate ships no history of its own.
Sourcepub fn item(&self, id: ItemId) -> Option<&dyn SceneItem>
pub fn item(&self, id: ItemId) -> Option<&dyn SceneItem>
Borrow a lightweight SceneItem by id. None for unknown
ids and for heavyweight widget entries.
Sourcepub fn paint_key(&self, id: ItemId) -> Option<PaintKey>
pub fn paint_key(&self, id: ItemId) -> Option<PaintKey>
Where id sits in this scene’s single paint order — the value every
picker in the crate compares. None for unknown ids.
Defined for both tiers: a lightweight entry’s rank is its
SceneLayer band, and a heavyweight widget entry’s is
RANK_WIDGET — which is exactly where the
arena’s child walk paints it, between the Under and Over bands. An
Interleaved item shares that rank, so it
sorts against the cards by z. See PaintKey for the ordering and
for the equal-z tie-break.
Sourcepub fn remove(&mut self, id: ItemId)
pub fn remove(&mut self, id: ItemId)
Remove an item by id, recursively dropping every descendant.
Mirrors Qt’s QGraphicsScene::removeItem semantics: deleting
a parent deletes its children too. No-op if id is unknown.
Fires one ItemChange::Removed per id, descendants first
then the named parent — observers see a consistent
“leaves-then-root” order.
To remove id without deleting its children, call
Scene::orphan first to promote them to root-level, then
remove(id).
§What happens to what it destroyed
This is Scene::take with the salvage routed to the edit sink
instead of to the caller — identical work, identical events. With a
sink installed
(SceneModel::set_edit_sink), each
removed entry arrives as a
SceneEdit::Removed carrying its
RemovedItem, which Scene::restore puts back. With none, the
salvage is dropped, which is what a removal has always done.
Sourcepub fn take(&mut self, id: ItemId) -> Vec<RemovedItem>
pub fn take(&mut self, id: ItemId) -> Vec<RemovedItem>
Remove id and every descendant, handing the salvage back instead
of dropping it — item box, handlers, magnets and logical-AT decorations
intact, each still carrying its original ItemId.
The same work and the same events as Scene::remove; the difference
is who ends up owning what was removed. Feed a result to
Scene::restore_all to put the subtree back.
§Order
Deepest descendants first, the named root last — the order remove
announces in. Scene::restore_all reverses it; restoring by hand
means roots first.
use teksilo_canvas::{Point, Rect};
use teksilo_scene::{RectItem, Scene};
let mut scene = Scene::new();
let card = scene.add_item(RectItem::new(Rect::new(0.0, 0.0, 40.0, 40.0)), Point::ZERO);
let salvage = scene.take(card);
assert_eq!(scene.len(), 0);
// Back at the same id, so selection, magnets and AT parenting still resolve.
let restored = scene.restore_all(salvage).unwrap();
assert_eq!(restored, vec![card]);
assert_eq!(scene.len(), 1);Sourcepub fn restore(&mut self, salvage: RemovedItem) -> Result<ItemId, RestoreError>
pub fn restore(&mut self, salvage: RemovedItem) -> Result<ItemId, RestoreError>
Re-insert a salvaged entry at its original ItemId, with its
magnets and its logical-AT decorations.
§Identity is the point
SceneSelection is keyed by ItemId,
MagnetId → ItemId is keyed by it, the whole logical AT tree is keyed
by A11yNode::Item(ItemId), and — because a SceneItem has no
downcast — an app’s own side map is the only way to reach item-specific
state, so it is keyed by it too. A restore that minted a fresh id would
restore the pixels and lose every one of them.
§Order
Roots before children: a salvage naming a parent that is not in the
scene fails with RestoreError::MissingParent.
Scene::restore_all orders a whole Scene::take result for you.
§What comes back, and what does not
Geometry, transform, z, layer, parent link, flags, opacity, handlers, magnets (ids included) and every logical-AT decoration whose other endpoint is still alive. The entry returns to its recorded position in declaration order when that index still exists, so the accessibility reading order is preserved rather than the item being appended last.
An edge to a node that has since been removed is not re-attached:
re-inserting it would name something the AT walker cannot resolve. Edges
are therefore attached after the entry is in, and — under
restore_all — after the whole batch is in, so an
edge between two items of one removed subtree is not dropped merely
because its far end had not arrived yet.
A single-view heavyweight widget (Scene::add_widget) whose one
Box<dyn Widget> a view already drained restores as an entry with no
instance to materialise — unless the take and the restore happen inside
one build cycle, in which case the view’s orphan reap never runs and the
arena instance survives. The restore is accepted either way, because
refusing it would refuse the restores that work; see
RemovedItem::widget_instance_present. Content that must survive an
undo in every view goes in through
SceneModel::add_widget_item.
§Restoring into a different scene is supported
A RemovedItem taken from one scene may be restored into another,
and that is the move-between-documents door: cut a card out of one
board with take, restore it into a second, and it
arrives whole — item box, geometry, flags, handlers and magnets, at the
same ItemId.
Nothing special makes it work and nothing needs to refuse it.
ItemId is minted from a process-global counter, so an id from
another scene can never collide with one here; a salvage carries its
whole entry rather than an index into the scene it came from; and the
three consequences of crossing the boundary are exactly the rules that
already apply within one scene:
- A parented salvage is refused with
RestoreError::MissingParent, because the parent it names is still in the other scene. Move the parent first and the child follows — the same roots-before-children rule, doing the same job — or callRemovedItem::detachto bring the item over as a root. - Logical-AT edges whose far end is not here are dropped, by the same test that drops an edge to a since-removed node.
- Declaration order is clamped: the recorded index is where the entry sat in the source scene’s reading order, and a shorter target takes it at the end.
The salvage is consumed either way, so a refused move does not leave a
second copy behind — but it does drop the item, so check the Result.
Sourcepub fn restore_all(
&mut self,
salvage: Vec<RemovedItem>,
) -> Result<Vec<ItemId>, RestoreError>
pub fn restore_all( &mut self, salvage: Vec<RemovedItem>, ) -> Result<Vec<ItemId>, RestoreError>
Restore a whole Scene::take result, roots first.
take hands back leaves-then-root; restoring in that order would fail
the first child on RestoreError::MissingParent, so this reverses it
for you — which also puts each parent’s children list back in its
original order.
Logical-AT edges are attached once the whole batch is in, so an edge between two items of the removed subtree survives whatever order the two ends land in.
Stops at the first failure and returns it; everything restored before it stays restored, because a rollback here would be the framework applying an inverse, which is the data layer’s job.
Sourcepub fn replace_item(
&mut self,
id: ItemId,
item: Box<dyn SceneItem>,
) -> Result<Box<dyn SceneItem>, ReplaceRejected>
pub fn replace_item( &mut self, id: ItemId, item: Box<dyn SceneItem>, ) -> Result<Box<dyn SceneItem>, ReplaceRejected>
Swap the lightweight item box at id, keeping the entry — and the
ItemId. Returns the box that was there.
Position, transform, z, layer, parent, flags, opacity, handlers, magnets
and logical-AT decorations all survive, so a data row whose content
changed does not lose its selection membership, its connections or its
place in the accessibility tree. Emits one
ItemChange::ItemReplaced, not Removed + Added.
§Bounds and the spatial index
The entry’s AABB is derived from the item, so the new item’s
local_bounds is read and the subtree re-bucketed. Skipping that would
leave the old rectangle in the index and quietly break
items_in_rect, item_at, the
cull path and every cached hit snapshot.
§Flags
The entry’s flags are kept; the replacement’s
initial_flags are not consulted. They are
initial flags — they apply where an item is inserted — and an app that
has since called set_visible or
set_flag would otherwise have that silently undone
by a content refresh. Call set_flags afterwards to
adopt the new item’s instead.
§Errors
ReplaceItemError::UnknownItem for an id that is not in the scene,
ReplaceItemError::NotLightweight for a heavyweight widget entry —
swap a Delegated entry’s data with
SceneModel::set_payload instead.
Either way the ReplaceRejected hands the item back: a refused
write must not eat a value the caller cannot clone and may have paid to
build.
Sourcepub fn placement(&self, id: ItemId) -> Option<Placement>
pub fn placement(&self, id: ItemId) -> Option<Placement>
Where id sits, as one value. None for an unknown id.
Sourcepub fn set_placement(&mut self, id: ItemId, placement: Placement)
pub fn set_placement(&mut self, id: ItemId, placement: Placement)
Write parent, z, position and transform together, emitting one
ItemChange::PlacementChanged.
The atomic alternative to four mutators and four events. No-op when the
placement is unchanged, when id is unknown, and — as with
set_item_parent — when the proposed parent is
id itself or one of its own descendants, which would make a cycle.
The whole write is refused in that last case rather than applied with
the old parent, because a partly-applied atomic write is the thing this
door exists to prevent.
Sourcepub fn reparent_keeping_scene_pos(&mut self, id: ItemId, parent: Option<ItemId>)
pub fn reparent_keeping_scene_pos(&mut self, id: ItemId, parent: Option<ItemId>)
Reparent id while holding it visually still — “drag this card into
that group”, correctly.
set_item_parent reinterprets local_pos and
transform in the new parent’s frame, so the item jumps unless the
caller compensates. This derives the local frame that leaves the item’s
scene transform where it is, and applies parent and frame as one
set_placement write.
The derived frame is normalised: the rotation/scale go in transform
(around the local origin, which is what that field means) and all of the
translation in local_pos. An item whose transform carried a
translation of its own comes back with the same visual result and that
translation folded into its position.
No-op for an unknown id, for a parent that is already the current one, for a cycle, and for a new parent whose scene transform is degenerate (a zero scale somewhere in its chain) — there is no frame under it to land in.
Sourcepub fn z_between(&self, below: ItemId, above: ItemId) -> Option<f32>
pub fn z_between(&self, below: ItemId, above: ItemId) -> Option<f32>
A z strictly between two items’ — the fractional insert that puts one
item between two others without renumbering a single sibling.
None when there is no such value: the two are at the same z, either
id is unknown, or f32 precision is exhausted at that locus. An f32
carries ~24 mantissa bits, so about two dozen bisections at one point in
the order run out — reachable in any card-shuffling UI.
Asking first is what makes that visible. A caller that computed an
exhausted midpoint itself would get mid == lo, hand it to
set_z, and be given a silent no-op: the card simply
would not move, with no error and no event. The answer to None is to
renumber the band (a pass of evenly-spaced z values) and bisect again.
The two agree on one metric, and it is this one. set_z ignores a write
only when the entry already holds that exact float, so every Some
this returns is a value set_z will apply — which is the whole
contract. (An absolute epsilon there used to break it near zero: with
lo = 0.0 and hi = 1.25e-7 this answers Some(6.25e-8) and the write
was dropped.)
use teksilo_canvas::{Point, Rect};
use teksilo_scene::{RectItem, Scene};
let mut scene = Scene::new();
let r = || RectItem::new(Rect::new(0.0, 0.0, 10.0, 10.0));
let lower = scene.add_item(r(), Point::ZERO);
let upper = scene.add_item(r(), Point::ZERO);
scene.set_z(lower, 1.0);
scene.set_z(upper, 2.0);
let middle = scene.add_item(r(), Point::ZERO);
let z = scene.z_between(lower, upper).expect("room between 1.0 and 2.0");
scene.set_z(middle, z);
assert!(scene.z(middle).unwrap() > 1.0 && scene.z(middle).unwrap() < 2.0);
// No room between an item and itself.
assert_eq!(scene.z_between(lower, lower), None);Sourcepub fn orphan(&mut self, id: ItemId)
pub fn orphan(&mut self, id: ItemId)
Promote id’s direct children to root-level (clear their
parent field). Used when an app wants to remove id without
dropping its children — call orphan(id) then remove(id).
No-op when id is unknown or has no children.
Fires one ItemChange::ParentChanged per detached child and
re-buckets every detached subtree in the spatial index — the
children’s scene_transform shifts (no longer composes
id’s) so their scene-space AABBs change. Without re-bucketing
the index, items_in_rect and
item_at would return stale results.
Apps wanting visual stability across the orphan call should
first bake id’s scene_transform into each child’s
local_pos + transform; otherwise children visibly jump.
Sourcepub fn items_in_rect(&self, scene_rect: Rect) -> Vec<ItemId>
pub fn items_in_rect(&self, scene_rect: Rect) -> Vec<ItemId>
All items whose scene-AABB intersects scene_rect.
Broad phase: the spatial index returns every id bucketed in
any cell touched by scene_rect. Narrow phase: each candidate
goes through scene_rect, which itself
dispatches via entry_index (an HashMap<ItemId, usize>),
so the per-candidate cost is O(parent-chain-depth) — not
O(N). Total query is O(visible × chain) instead of O(N).
Sourcepub fn item_thumbnails(&self) -> Vec<(Rect, Color)>
pub fn item_thumbnails(&self) -> Vec<(Rect, Color)>
Snapshot every visible item — both tiers — as a (scene_rect, color) pair suitable for a minimap thumbnail. Filters out items with
HAS_NO_CONTENTS (logical-only) and items hidden by IS_VISIBLE / a
hidden ancestor — the visible-effective set matches what the SceneView’s
paint walk renders.
Ordered by insertion (low z first). A lightweight item’s color comes
from SceneItem::thumbnail_color (its fill / stroke / a neutral grey);
a heavyweight widget entry has no SceneItem, so it’s shown in a neutral
tint — a minimap that omitted the heavyweight tier would misrepresent a
widget-heavy scene (cards, nodes), so both tiers are included.
Sourcepub fn item_shape(&self, id: ItemId) -> Option<ItemShape>
pub fn item_shape(&self, id: ItemId) -> Option<ItemShape>
The shape of any entry, in its local coordinates.
For a lightweight item this is SceneItem::shape. A heavyweight
widget entry has no SceneItem at all — Scene::item returns None
for it — so its shape is defined to be
ItemShape::bounds of its local_bounds. That is not a placeholder:
a widget’s silhouette is its layout box, the arena hit-tests it as one,
and the marquee has always selected heavyweight entries through the
same index query as lightweight ones. Stating it here is what keeps a
…ItemShape selection mode meaningful for a scene whose primary
objects are cards.
None for an unknown id.
Sourcepub fn item_contains(
&self,
id: ItemId,
scene_pt: Point,
view_scale: f32,
) -> bool
pub fn item_contains( &self, id: ItemId, scene_pt: Point, view_scale: f32, ) -> bool
Whether id’s shape contains scene_pt.
Works for both tiers (see Scene::item_shape). view_scale is the
live view zoom, consulted only by a cosmetic stroke band; pass 1.0
when there is no view.
Sourcepub fn item_region(&self, id: ItemId) -> Option<SceneRegion>
pub fn item_region(&self, id: ItemId) -> Option<SceneRegion>
The item’s own shape re-published as a scene-space region — what a
collision query asks the rest of the scene about. None for an unknown
id, a shape of ItemShape::none, or a screen-anchored
(IGNORES_TRANSFORMATIONS)
item, whose silhouette is not in scene space at all.
Sourcepub fn items_in_region(
&self,
region: &SceneRegion,
mode: ItemSelectionMode,
view_scale: f32,
) -> Vec<ItemId>
pub fn items_in_region( &self, region: &SceneRegion, mode: ItemSelectionMode, view_scale: f32, ) -> Vec<ItemId>
Items matching region under mode — both tiers, exactly like
Scene::items_in_rect, which this generalises.
Broad-phased by the spatial index on region.bounding_rect(), then
narrow-phased per mode: the …ItemBoundingRect modes compare the
item’s scene AABB against the region in scene space; the
…ItemShape modes map the region into the item’s local frame and
compare it against Scene::item_shape. For an item with a
non-identity transform those are different tests — see
ItemSelectionMode.
view_scale is the live view zoom, consulted only by a cosmetic stroke
band; pass 1.0 when there is no view. Visibility and selectability
are not filtered here — this is a pure geometry query, and the
caller (e.g. SceneSelection::commit_marquee)
applies its own flag policy.
Items flagged
IGNORES_TRANSFORMATIONS
are skipped, for the reason Scene::item_at skips them: they are
anchored in screen space, so their local_bounds is a screen rectangle
and the scene AABB the index holds for them is a fiction. Comparing a
scene-space region against that fiction is a guess, and the two query
families used to disagree about whether to make it — the point queries
declined and the marquee did not. Reaching screen-pinned chrome needs a
region that arrives in screen space, which is what
Scene::item_at_in_view does for a point; there is no region twin of
it yet, so a marquee cannot select pinned chrome at all.
Sourcepub fn item_at(&self, scene_pt: Point) -> Option<ItemId>
pub fn item_at(&self, scene_pt: Point) -> Option<ItemId>
Topmost lightweight item whose shape contains scene_pt, at unit
view scale. See Scene::item_at_scaled for the zoom-aware form and
Scene::item_at_in_view for the one that also places screen-anchored
items.
Heavyweight widget entries are skipped: their hit-testing is the arena’s job, and a scene-space answer would contradict it.
Items flagged
IGNORES_TRANSFORMATIONS
are also skipped, deterministically. They are anchored in screen
space, so a scene-space point cannot place them at all; answering with
a coin-flip (which is what comparing them against a scene AABB amounts
to) is worse than not answering. Use Scene::item_at_in_view, or
SceneView dispatch, when the query needs them.
“Topmost” is Scene::paint_key order, so an
Over-band item beats a higher-z
Under one, and two equal-z items resolve to the
later-inserted one — the same answer SceneView dispatch gives.
Hidden and disabled entries are excluded: this is a hit test, and
ItemFlags::IS_VISIBLE and ItemFlags::IS_ENABLED both say so. See
Scene::is_hit_testable. For a pure geometry query that ignores flags,
use Scene::items_in_region or Scene::item_contains.
Sourcepub fn item_at_scaled(&self, scene_pt: Point, view_scale: f32) -> Option<ItemId>
pub fn item_at_scaled(&self, scene_pt: Point, view_scale: f32) -> Option<ItemId>
Scene::item_at at an explicit view zoom.
The zoom reaches exactly one thing: a cosmetic stroke band, whose
width is in device pixels and therefore covers fewer scene units the
further you zoom in. Passing the live scale is what makes this agree
with SceneView’s own dispatch, which has always had it.
Sourcepub fn entry_at(&self, scene_pt: Point, view_scale: f32) -> Option<ItemId>
pub fn entry_at(&self, scene_pt: Point, view_scale: f32) -> Option<ItemId>
Topmost entry of either tier whose geometry contains scene_pt, in
this scene’s one paint order.
The deliberate counterpart to Scene::item_at_scaled, which skips
heavyweight entries because the arena owns their hit-testing. This one
answers the different question a selection asks — “which entry is on
top here?” — and answers it for a card with the card’s scene rectangle,
which is exactly the rectangle SceneView laid the card out at. It is
not a substitute for arena dispatch and must not be used to route an
event to a widget; it is for deciding whether a press that already
reached the view landed on something the view is holding.
Screen-anchored entries are skipped for the same reason item_at skips
them: a scene-space point cannot place them.
Sourcepub fn items_at(&self, scene_pt: Point) -> Vec<ItemId>
pub fn items_at(&self, scene_pt: Point) -> Vec<ItemId>
All lightweight items whose shape contains scene_pt, topmost-first by
z. Same tier and IGNORES_TRANSFORMATIONS rules as Scene::item_at.
Sourcepub fn items_at_scaled(&self, scene_pt: Point, view_scale: f32) -> Vec<ItemId>
pub fn items_at_scaled(&self, scene_pt: Point, view_scale: f32) -> Vec<ItemId>
Scene::items_at at an explicit view zoom.
Sourcepub fn item_at_in_view(
&self,
screen_pt: Point,
view_transform: Transform2D,
) -> Option<ItemId>
pub fn item_at_in_view( &self, screen_pt: Point, view_transform: Transform2D, ) -> Option<ItemId>
Topmost lightweight item under a screen point, resolving both
hit spaces the way SceneView dispatch does.
A normal item is tested in scene space, at the view transform’s zoom; a
screen-anchored
(IGNORES_TRANSFORMATIONS)
item is tested against its local_bounds rooted at its projected
anchor, at unit scale — its local coordinates are screen
coordinates, so it has no zoom to convert. This is the query to use
when a scene may contain screen-pinned chrome; Scene::item_at
deliberately declines to guess.
Sourcepub fn is_hit_testable(&self, id: ItemId) -> bool
pub fn is_hit_testable(&self, id: ItemId) -> bool
Whether id takes part in pointer hit-testing — visible along its whole
ancestor chain AND enabled.
This is crate::pick::hit_testable resolved against the scene, and it
is the one place the two flag contracts in ItemFlags are honoured:
IS_VISIBLE (“neither painted nor hit-tested”)
and IS_ENABLED (“pass clicks through to items
beneath”). false for unknown ids.
Sourcepub fn colliding_items(&self, id: ItemId) -> Vec<ItemId>
pub fn colliding_items(&self, id: ItemId) -> Vec<ItemId>
Items overlapping id’s shape, excluding id itself.
Apps use this for “which other items overlap this card?” — graph
editors checking node-on-node overlap, CAD canvases finding adjacent
geometry. Backed by the spatial index, so the cost is O(visible) not
O(N).
Defaults to ItemSelectionMode::IntersectsItemShape, so a
stroke-only connector collides along its line rather than across its
bounding box. Pass
IntersectsItemBoundingRect
to Scene::colliding_items_with for the cheaper box test.
Screen-anchored items neither collide nor are collided with — see
Scene::items_in_region, which this is built on, and
Scene::item_region, which declines to publish one for them.
Sourcepub fn colliding_items_with(
&self,
id: ItemId,
mode: ItemSelectionMode,
) -> Vec<ItemId>
pub fn colliding_items_with( &self, id: ItemId, mode: ItemSelectionMode, ) -> Vec<ItemId>
Scene::colliding_items under an explicit ItemSelectionMode.
Sourcepub fn items_along_path(&self, path: &Path) -> Vec<ItemId>
pub fn items_along_path(&self, path: &Path) -> Vec<ItemId>
Items lying along path — a real region query, not the AABB-of-the-path
approximation this used to be.
The path is treated as a zero-width closed region: an item is picked
when the path crosses it or encloses it. For a connector — a line
with a width — pass that width to Scene::items_along_path_with, so
the query asks about the band the user can see.
Sourcepub fn items_along_path_with(
&self,
path: &Path,
stroke_width: f32,
mode: ItemSelectionMode,
) -> Vec<ItemId>
pub fn items_along_path_with( &self, path: &Path, stroke_width: f32, mode: ItemSelectionMode, ) -> Vec<ItemId>
Scene::items_along_path with an explicit stroke width and
ItemSelectionMode.
stroke_width greater than zero makes the region the path’s band
rather than its interior: “what does this 4 dp connector touch?”.
Sourcepub fn index(&self) -> &dyn SpatialIndex
pub fn index(&self) -> &dyn SpatialIndex
Borrow the spatial index (diagnostics / tests).
Sourcepub fn add_magnet(&mut self, item: ItemId, magnet: Magnet) -> MagnetId
pub fn add_magnet(&mut self, item: ItemId, magnet: Magnet) -> MagnetId
Attach a Magnet to item and return its MagnetId.
Magnets are local to their item (their local_pos is in the
item’s frame), so they follow the item under any move / rotate /
scale via the same scene_transform the item uses. No-op
returning a fresh-but-unowned id if item is unknown — callers
add magnets to items they just created.
Bumps the AT-structure change counter (magnets are AT structure)
so a SceneView with magnetism enabled re-walks its synthetic
magnet nodes.
Sourcepub fn remove_magnet(&mut self, magnet: MagnetId)
pub fn remove_magnet(&mut self, magnet: MagnetId)
Remove a magnet by id. No-op if the id is unknown.
Sourcepub fn clear_magnets(&mut self, item: ItemId)
pub fn clear_magnets(&mut self, item: ItemId)
Remove every magnet attached to item. No-op if none.
Sourcepub fn set_magnet_local_pos(&mut self, magnet: MagnetId, local_pos: Point)
pub fn set_magnet_local_pos(&mut self, magnet: MagnetId, local_pos: Point)
Move a magnet to a new position in its owning item’s local frame. No-op if the id is unknown.
Sourcepub fn set_magnet_enabled(&mut self, magnet: MagnetId, enabled: bool)
pub fn set_magnet_enabled(&mut self, magnet: MagnetId, enabled: bool)
Enable or disable a magnet. Disabled magnets are skipped by broad-phase, feedback, the keyboard cycle, and AT emission. No-op if the id is unknown.
Sourcepub fn magnet_ids_of(&self, item: ItemId) -> Vec<MagnetId>
pub fn magnet_ids_of(&self, item: ItemId) -> Vec<MagnetId>
The ids of every magnet attached to item, in insertion order
(enabled and disabled alike). Empty if item is unknown or has
no magnets.
Sourcepub fn magnet_owner(&self, magnet: MagnetId) -> Option<ItemId>
pub fn magnet_owner(&self, magnet: MagnetId) -> Option<ItemId>
The owning item of a magnet, or None if the id is unknown.
Sourcepub fn magnet_enabled(&self, magnet: MagnetId) -> bool
pub fn magnet_enabled(&self, magnet: MagnetId) -> bool
Whether a magnet is enabled. false for an unknown id.
Sourcepub fn magnet_scene_pos(&self, magnet: MagnetId) -> Option<Point>
pub fn magnet_scene_pos(&self, magnet: MagnetId) -> Option<Point>
A magnet’s position in scene coordinates (its local position
projected through its owning item’s scene_transform). None
for an unknown id or a degenerate item transform.
Sourcepub fn magnet(&self, magnet: MagnetId) -> Option<MagnetRef>
pub fn magnet(&self, magnet: MagnetId) -> Option<MagnetRef>
Resolve a magnet to a borrow-free MagnetRef snapshot (id,
owning item, role, payload clone, current scene position).
None for an unknown id or a degenerate item transform.
Sourcepub fn compute_item_snap(
&self,
dragged: ItemId,
drag_delta: Vec2,
capture_radius: f32,
predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
) -> Option<MagnetSnap>
pub fn compute_item_snap( &self, dragged: ItemId, drag_delta: Vec2, capture_radius: f32, predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict, ) -> Option<MagnetSnap>
Compute the best item-drag snap: the dragged item is visually
offset by drag_delta, and each of its enabled magnets seeks the
nearest accepting magnet on another item within capture_radius
(in scene units). Returns the globally closest accepting pair, or
None if nothing accepts within range.
Pure mechanism: it collects candidates under a brief read, then
runs the consumer predicate with no scene borrow held, so the
predicate may inspect payloads freely. snap_vector added to
drag_delta aligns the dragged magnet onto its target.
Sourcepub fn compute_port_snap(
&self,
source: MagnetId,
cursor_scene: Point,
capture_radius: f32,
predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict,
) -> Option<(MagnetRef, Option<Rc<dyn Any>>)>
pub fn compute_port_snap( &self, source: MagnetId, cursor_scene: Point, capture_radius: f32, predicate: &dyn Fn(&MagnetRef, &MagnetRef) -> MagnetVerdict, ) -> Option<(MagnetRef, Option<Rc<dyn Any>>)>
Compute the best port-drag snap: a single source magnet is
dragging a transient wire whose free end is at cursor_scene.
Finds the nearest accepting target magnet within
capture_radius (scene units), excluding the source’s own
magnet. Returns the target MagnetRef and the accepting
verdict’s payload, or None.
Sourcepub fn nearest_magnet(&self, scene_pt: Point, radius: f32) -> Option<MagnetId>
pub fn nearest_magnet(&self, scene_pt: Point, radius: f32) -> Option<MagnetId>
The nearest enabled magnet to scene_pt within radius (scene
units), or None. Used by the view to start a port-drag from a
grabbed magnet handle (the handle’s grab area is a screen-pixel
disc, converted to scene units by the caller).
Sourcepub fn add_a11y_group(&mut self, builder: A11yGroupBuilder) -> A11yGroupId
pub fn add_a11y_group(&mut self, builder: A11yGroupBuilder) -> A11yGroupId
Declare a virtual AT group. The group has no visual counterpart — it exists so the AT walker can emit an AT node under which items / other groups / widgets can be reparented.
Sourcepub fn remove_a11y_group(&mut self, id: A11yGroupId)
pub fn remove_a11y_group(&mut self, id: A11yGroupId)
Remove a logical group; orphaned references fall back to SceneView root. Relations / live / landmarks / categories targeting this group are cleaned up too.
Sourcepub fn a11y_group(&self, id: A11yGroupId) -> Option<&A11yGroup>
pub fn a11y_group(&self, id: A11yGroupId) -> Option<&A11yGroup>
Borrow a logical group by id.
Sourcepub fn set_a11y_parent(&mut self, child: A11yNode, parent: Option<A11yNode>)
pub fn set_a11y_parent(&mut self, child: A11yNode, parent: Option<A11yNode>)
Declare a logical-parent relationship for AT (independent of visual placement).
Sourcepub fn a11y_parent_of(&self, child: A11yNode) -> Option<A11yNode>
pub fn a11y_parent_of(&self, child: A11yNode) -> Option<A11yNode>
The currently-declared logical parent of a node.
Sourcepub fn add_a11y_relation(
&mut self,
from: A11yNode,
kind: A11yRelation,
to: A11yNode,
)
pub fn add_a11y_relation( &mut self, from: A11yNode, kind: A11yRelation, to: A11yNode, )
Declare an AT relationship between two nodes.
Sourcepub fn a11y_relations(&self) -> &[(A11yNode, A11yRelation, A11yNode)]
pub fn a11y_relations(&self) -> &[(A11yNode, A11yRelation, A11yNode)]
All declared AT relations.
Sourcepub fn set_a11y_live(&mut self, node: A11yNode, live: Live)
pub fn set_a11y_live(&mut self, node: A11yNode, live: Live)
Mark a node as a live region. Pass Live::Off to clear.
Sourcepub fn set_a11y_landmark(&mut self, node: A11yNode, role: Role)
pub fn set_a11y_landmark(&mut self, node: A11yNode, role: Role)
Mark a node as a landmark by overriding its role. Pass
Role::Unknown to clear.
Sourcepub fn set_a11y_categories(
&mut self,
node: A11yNode,
categories: &[A11yCategory],
)
pub fn set_a11y_categories( &mut self, node: A11yNode, categories: &[A11yCategory], )
Tag a node with rotor / quick-nav categories.
Sourcepub fn a11y_categories_of(&self, node: A11yNode) -> Option<&[A11yCategory]>
pub fn a11y_categories_of(&self, node: A11yNode) -> Option<&[A11yCategory]>
Read declared categories for a node.
Sourcepub fn a11y_live_of(&self, node: A11yNode) -> Option<Live>
pub fn a11y_live_of(&self, node: A11yNode) -> Option<Live>
Read a node’s declared live-region politeness. None when it is not a
live region.
The read half of set_a11y_live, added because a
consumer restoring a RemovedItem has to be able to check that the
semantics came back — and because the other four decorations already
had one.
Sourcepub fn a11y_landmark_of(&self, node: A11yNode) -> Option<Role>
pub fn a11y_landmark_of(&self, node: A11yNode) -> Option<Role>
Read a node’s declared landmark role. None when it is not a landmark.
Trait Implementations§
Source§impl Debug for SceneWriteGuard<'_>
impl Debug for SceneWriteGuard<'_>
Source§impl Deref for SceneWriteGuard<'_>
impl Deref for SceneWriteGuard<'_>
Source§impl DerefMut for SceneWriteGuard<'_>
impl DerefMut for SceneWriteGuard<'_>
Source§impl Drop for SceneWriteGuard<'_>
impl Drop for SceneWriteGuard<'_>
Auto Trait Implementations§
impl<'a> !RefUnwindSafe for SceneWriteGuard<'a>
impl<'a> !Send for SceneWriteGuard<'a>
impl<'a> !Sync for SceneWriteGuard<'a>
impl<'a> !UnwindSafe for SceneWriteGuard<'a>
impl<'a> Freeze for SceneWriteGuard<'a>
impl<'a> Unpin for SceneWriteGuard<'a>
impl<'a> UnsafeUnpin for SceneWriteGuard<'a>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.impl<T> ErasedDestructor for Twhere
T: 'static,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more