pub struct RenderRoot<State: 'static, V: View<State>> { /* private fields */ }Expand description
Owns the retained tree and drives the rebuild/layout/paint passes for a single-root application.
Generic over the application State and the concrete root view type V
returned by the build closure.
Implementations§
Source§impl<State: 'static, V: View<State>> RenderRoot<State, V>
impl<State: 'static, V: View<State>> RenderRoot<State, V>
Sourcepub fn set_theme(&mut self, theme: Box<dyn Any>)
pub fn set_theme(&mut self, theme: Box<dyn Any>)
Store the app’s active theme, threaded into every subsequent
layout/paint pass as Option<&dyn Any> and recovered by widgets via
crate::widget::PaintCtx::theme_as/crate::widget::LayoutCtx::theme_as.
The theme is boxed type-erased (Box<dyn Any>) so this crate stays
independent of frust-theme; the shell boxes the concrete Theme
(and re-boxes it on a live appearance change, e.g. dark-mode toggle).
Calling again replaces the stored theme.
Marks LAYOUT | PAINT pending (drained by
RenderRoot::take_change_flags): a theme swap can change baked-in
paint state a widget resolves at layout time (e.g. Text’s themed
glyph color, cached into its TextLayout — see
frust-widgets::text), so a shell that later gates layout/paint on
this seam must still see a bare set_theme as dirty even though no
view changed.
Sourcepub fn set_insets(&mut self, insets: WindowInsets)
pub fn set_insets(&mut self, insets: WindowInsets)
Store the window’s insets (WindowInsets), threaded into every
subsequent layout/paint pass and recovered by widgets via
crate::widget::LayoutCtx::window_insets/
crate::widget::PaintCtx::window_insets.
Mirrors RenderRoot::set_theme’s dirty-tracking contract: a change
marks LAYOUT | PAINT pending (drained by
RenderRoot::take_change_flags) so a shell gating layout/paint on that
seam still relayouts when the insets move — a SafeArea widget resolves
its inset at layout time, so the mobile layout-skip gate must see a bare
set_insets as dirty even though no view changed (the same reasoning as
the theme swap — see docs/ARCHITECTURE.md’s Theme delivery and Frame
gate). A change also bumps the semantics generation, since a moved inset
shifts laid-out node bounds.
No-op guarded by WindowInsets’s PartialEq: pushing the current
value marks nothing dirty, so a shell that polls the platform insets
every frame and forwards unconditionally never forces a needless
relayout. (A shell may also skip the call itself by comparing first —
this is the same guard, held on the core side.)
Sourcepub fn insets(&self) -> WindowInsets
pub fn insets(&self) -> WindowInsets
The window’s insets currently threaded into the layout/paint passes.
Sourcepub fn set_presented_frames(&mut self, presented: u64)
pub fn set_presented_frames(&mut self, presented: u64)
Store the shell’s running count of frames the render thread has actually
presented, threaded into every subsequent paint pass and recovered by
widgets via crate::widget::PaintCtx::presented_frames. A shell loads
the atomic its render side increments (once per presented frame) and
pushes it here once per UI frame, before paint.
Deliberately dirties nothing. Unlike RenderRoot::set_theme and
RenderRoot::set_insets — which mark LAYOUT | PAINT pending because a
widget bakes their value in at layout time — this setter marks NO
ChangeFlags and bumps NO semantics generation. The presented count is
a paint-only observation a widget reads live every paint (never baked at
layout), so treating it as dirty would be wrong twice over: it would force
a needless relayout, and — critically — on the mobile shells a
monotonically ticking counter would keep the frame gate’s pending-flags
input perpetually true, so the menu would never idle (the 32s-idle
behavior must survive). Keeping this setter dirt-free
is exactly what keeps the frame gate unaware of it (see
docs/ARCHITECTURE.md’s Frame gate).
Sourcepub fn presented_frames(&self) -> Option<u64>
pub fn presented_frames(&self) -> Option<u64>
The presented-frame count currently threaded into the paint pass, or
None if no shell has pushed one.
Sourcepub fn set_surface_translucent(&mut self, translucent: bool)
pub fn set_surface_translucent(&mut self, translucent: bool)
Store whether the shell’s GPU surface is translucent (alpha-channel,
“Mode B”), threaded into every subsequent paint pass and recovered by
widgets through crate::widget::PaintCtx::is_translucent. A shell
pushes the surface’s resolved translucency here — what the GPU
backend reports after the surface is installed, not what the app
requested via frust-shell-common::surface_mode’s latch: a translucency
request the platform refuses must degrade to the opaque contract, or
every platform_view slot punches a hole in an opaque swapchain
(black rectangles). Every desktop app leaves the default false
(opaque, “Mode A”).
Marks PAINT pending on an actual change (PartialEq-guarded, mirroring
RenderRoot::set_insets’s no-op guard): translucency is read purely at
paint time (the hole-punch runs in paint, never baked at layout), so a
flip must repaint but need not relayout. A flip is rare but real: a
surface (re)install can resolve differently from the previous one, and
both mobile shells re-push this every frame (the no-op-if-unchanged
guard is what makes that free).
Sourcepub fn is_surface_translucent(&self) -> bool
pub fn is_surface_translucent(&self) -> bool
Whether the shell’s GPU surface is currently marked translucent.
Sourcepub fn is_pointer_captured(&self) -> bool
pub fn is_pointer_captured(&self) -> bool
Whether a captured pointer gesture is currently in flight.
Sourcepub fn pointer_capture_claimant(&self) -> Option<PointerId>
pub fn pointer_capture_claimant(&self) -> Option<PointerId>
The contact that claimed the pointer capture in flight — the only one
whose Up/Cancel can end it — or None while nothing is captured.
Sourcepub fn pointer_capture_contacts(&self) -> bool
pub fn pointer_capture_contacts(&self) -> bool
Whether the capture in flight routes the gesture’s other contacts to
its captor — the captor opted in with EventCtx::capture_contacts on
the Down it captured with, and no container has since taken the
gesture over from it (EventCtx::release_captured_child). false
while nothing is captured.
Sourcepub fn is_focus_active(&self) -> bool
pub fn is_focus_active(&self) -> bool
Whether some widget in the tree currently holds keyboard/IME focus.
Sourcepub fn is_hover_active(&self) -> bool
pub fn is_hover_active(&self) -> bool
Whether some widget in the tree currently holds the hover link — i.e.
whether the last hover pass (an uncaptured PointerPhase::Move) left the
pointer over a widget that claimed it.
The hover analog of RenderRoot::is_focus_active, and a level accessor
like it: hover is not a session (nothing has to be released), so there is no
generation counterpart. false for any app whose widgets never call
EventCtx::claim_hover. A touch app
can still see it go true transiently: nothing distinguishes a touch
contact from a mouse here, so an uncaptured touch drag over a
non-capturing claimant is an ordinary hover pass — ended by the Up at
lift (see docs/LIMITATIONS.md’s hover-window-leave-standing).
A RenderRoot::rebuild that removes the claimant ends the link too, so
this never reports a hover held by a widget that no longer exists — the
hover counterpart of the unmount focus release (see that method).
Sourcepub fn cursor(&self) -> CursorIcon
pub fn cursor(&self) -> CursorIcon
The cursor the tree last asked the host to show — what a desktop shell
pushes to its window (frust-shell-desktop maps it onto winit’s own
cursor icons).
A level accessor like RenderRoot::is_hover_active, not an edge one:
the value re-resolves on every pointer PointerPhase::Move and stands
unchanged through every other pass, so a shell caches what it last applied
and calls the platform only when this differs. There is deliberately no
generation counter — a cursor is a value, not a session, and an unmoved
cursor is indistinguishable from one re-resolved to the same shape.
CursorIcon::Default before the first Move, and after any Move in
which no widget called
EventCtx::set_cursor — so any app
whose widgets never request a cursor reads Default forever, and the mobile
shells never read this at all regardless of what resolves here.
Residual: a widget that is torn down (or moves out from under a
stationary pointer) while its request stands leaves the last shape in
place until the next Move re-resolves it — the same self-correction
window hover has, and for the same reason: nothing re-resolves without
pointer motion.
Sourcepub fn take_clipboard_write(&mut self) -> Option<String>
pub fn take_clipboard_write(&mut self) -> Option<String>
Take (and clear) the text the tree asked the shell to put on the host
clipboard — the drain a shell performs immediately after every
RenderRoot::event, beside cursor() and
ime_state().
Some exactly when some widget called
EventCtx::write_clipboard
during a pass since the last drain (answering a
EditCommand::Copy/Cut,
or a chord the widget decoded itself). The shell hands the text to its host
clipboard — winit’s arboard on desktop, ClipboardManager on Android,
UIPasteboard on iOS — and does nothing at all on None.
Destructive, unlike cursor(): a clipboard write
is an edge, not a standing level, so a caller that drains and drops the
result loses that write. Draining twice after one pass yields None the
second time.
A widget that never copies leaves this None forever, so a shell with no
clipboard (the mobile shells before their own clipboard work lands) may
call it and discard the result, or not call it at all.
Sourcepub fn take_paste_request(&mut self) -> bool
pub fn take_paste_request(&mut self) -> bool
Take (and clear) whether the tree asked the shell to read the host
clipboard back to it — drained beside
take_clipboard_write after every
RenderRoot::event.
true exactly when some widget called
EventCtx::request_paste during a
pass since the last drain. The shell answers by reading its host clipboard
and dispatching
InputEvent::EditCommand(EditCommand::Paste(text))
— a new dispatch, because the read may be asynchronous and the pass that
asked is over. That answer carries no identity of its own and is
focus-routed to whoever holds focus when it lands: a release in between
drops it harmlessly, but a focus move in between lands it in the new
field rather than the one that asked. A synchronous read has no such
window; an asynchronous one snapshots
focus_epoch at this drain and discards an
answer whose epoch no longer matches — not
focus_ime_generation, which also
moves within a single session.
Destructive, for take_clipboard_write’s
reason. A pass may both write and request (a cut that immediately re-reads,
or a widget answering two chords) — the two drains are independent.
Sourcepub fn ime_state(&self) -> Option<ImeState>
pub fn ime_state(&self) -> Option<ImeState>
The IME surface the focused widget published, for the shell to drive the
platform input method (winit set_ime_cursor_area, Android
updateSelection, iOS inputDelegate). None when nothing is focused or
the focused widget publishes no IME surface.
Written by the focused widget through EventCtx::publish_ime_state during
the event pass and refreshed on every event; it survives a rebuild (so the
shell can query it between frames) and is cleared when focus is lost.
§None is the only “no session” form — an inactive surface is never stored
A widget publishing ImeState { active: false, .. } is ending the session,
not describing it, so both publish paths turn that into a full release
(see RenderRoot::paint) and this returns None rather than
Some(inactive). A shell therefore never has to distinguish the two, and
is_some() means “a live IME session” with no second check.
The platform still sees the keyboard-hide. All three shells already
map None onto the inactive form on the way out, so the observable wire
behavior is unchanged: frust-shell-android‘s ime_state_to_json returns
ImeJsonState::default() (active:false, empty text, -1 indices, null
caret, "normal") and frust-shell-ios’ returns the byte-identical
ime_state_json(false, "", -1, -1, -1, -1, None, "normal") — exactly what
the navigator’s own cleared surface serialised to before. Kotlin’s
pollImeAfterDispatch and Swift’s syncImeFocus both branch on active
alone (an inactive surface’s text/caret/content-type are ignored), and the
desktop shell’s sync_ime reads is_some_and(|s| s.active). Dropping the
inactive surface’s payload also stops a disabled secret field’s text
riding to the platform after its session ended.
Sourcepub fn focus_ime_generation(&self) -> u64
pub fn focus_ime_generation(&self) -> u64
The focus/IME session generation — bumped on every actual change of
is_focus_active or
ime_state, and on nothing else.
The edge counterpart of those two level accessors, for a shell that
needs “did the focus/IME session move since I last looked?” rather than
“is something focused?”. A shell caches the value it last saw and
compares (mirroring semantics_generation’s
cheap dirty gate) — that comparison is the mobile frame gate’s
FrameInputs::focus_or_ime_changed input.
A same-value write never moves it: re-publishing an identical IME surface (which the paint pass does on every frame a field stays focused) or re-blurring an already-blurred root is not an edge. Wrapping is deliberate and harmless — a comparison, never an ordering.
§Not the session’s identity
This counts changes to the published surface, not sessions, and the
two come apart in both directions — see
focus_epoch, which is what to reach for when
the question is “is this still the same focus session?”. Answering that
one from this counter is wrong whenever focus moves between two fields
without the published value changing.
Sourcepub fn focus_epoch(&self) -> u64
pub fn focus_epoch(&self) -> u64
The live focus session’s identity — advanced once per honoured focus claim and once per session release, and by nothing else.
The neighbour of focus_ime_generation
and easy to mistake for it, so: that one counts changes to the published
surface (the focus flag, or the ImeState value), this one counts
sessions. They come apart in both directions, which is why both exist:
- Focus moving from one field to another moves this one and can leave
that one completely still. Claiming focus while some field already
holds it writes
trueovertrue, and the surface the new field publishes may compare equal to the old field’s (ImeStateis{active, editing, caret, content_type}and names no widget) — or may not be published at all, since a widget is free to take focus and publish nothing, which leaves the previous field’s surface standing. - An edit landing, a caret moving, or the field being repositioned under the user moves that one and leaves this one still: the session is the same session throughout.
So a caller binding an asynchronous answer to the session that asked for it wants this one; a caller asking “must I run a frame, or re-sync the platform IME?” wants that one.
Never 0. The counter is built at 1 and steps past 0 on wrap,
because 0 is a never-claimed ChildPod’s
stamp and a root publishing it would hand every unclaimed pod in the tree
a live link. A caller is therefore free to use 0 as its own “no root /
no answer” sentinel with no risk of colliding with a live value. Wrapping
is otherwise deliberate and harmless: the value is compared for equality,
never ordered.
Sourcepub fn platform_view_frames(&self) -> &[PlatformViewFrame]
pub fn platform_view_frames(&self) -> &[PlatformViewFrame]
The PlatformViewFrames published during the most recent
RenderRoot::paint, in paint order.
Replaced wholesale every pass (see the platform_view_frames field
doc), so a pass with no publishers yields an empty slice — a shell
never sees a stale frame for a slot that stopped painting.
Sourcepub fn input_shields(&self) -> &[Rect]
pub fn input_shields(&self) -> &[Rect]
The z-shield rects reported during the most recent RenderRoot::paint
(see crate::widget::PaintCtx::report_input_shield), in paint order.
Replaced wholesale every pass, exactly like
RenderRoot::platform_view_frames — a shell feeds both into the same
differ ingest call, and the differ intersects these against each
interactive slot’s rect.
Sourcepub fn take_retired_platform_views(&mut self) -> Vec<u64>
pub fn take_retired_platform_views(&mut self) -> Vec<u64>
Drain the slot ids whose platform_view widgets were torn down since the
last call (View::teardown ran on them — see
crate::widget::report_retired_slot).
The prompt-teardown channel: a shell calls this once per frame, right
after its rebuild, and retires each id in its platform-view differ
(PlatformViewState::retire) so a disposed slot’s native view goes away
immediately instead of waiting out the differ’s missing-streak
heuristic. Draining is destructive, mirroring
RenderRoot::take_change_flags: an id is reported exactly once, so a
shell that drains and drops the result loses the prompt path (the
missing-streak backstop still covers it).
A merely culled slot (scrolled offscreen, a parent skipping paint)
never appears here — culling doesn’t run teardown — which is what
keeps the camera keep-alive contract intact.
Sourcepub fn take_change_flags(&mut self) -> ChangeFlags
pub fn take_change_flags(&mut self) -> ChangeFlags
Take (and clear) the dirtiness accumulated since the last call.
A shell can consult this to skip the layout/paint passes when nothing has
changed and no redraw was requested (a desktop optimisation; the mobile
continuous-loop shells may ignore it and repaint every tick). Each
RenderRoot::rebuild merges its result here; this drains it.
Sourcepub fn has_pending_change_flags(&self) -> bool
pub fn has_pending_change_flags(&self) -> bool
Non-draining peek at the dirtiness accumulated since the last
RenderRoot::take_change_flags — true when any LAYOUT/PAINT
bit is pending, without clearing it.
Complements take_change_flags for a
shell frame gate: the gate reads this as one of its
“should this frame run” inputs before deciding, so a frame it chooses
to skip leaves pending intact for the next non-skipped frame to drain
and act on. Draining stays the job of take_change_flags, called only
on a frame that actually runs its layout/paint passes. No behavioral
change to rebuild/layout/paint.
Sourcepub fn tree(&self) -> &WidgetTree
pub fn tree(&self) -> &WidgetTree
Shared access to the retained tree (for the shell / tests).
Sourcepub fn inspect(&self) -> Vec<InspectNode>
pub fn inspect(&self) -> Vec<InspectNode>
A read-only, pre-order snapshot of the retained tree for tooling: per node an id, its parent and children, the concrete widget’s type name, an optional debug label, and its absolute border box in logical px.
Computed on demand in O(nodes) and takes &self — no per-frame
bookkeeping, no mutation, and nothing here participates in
build/layout/paint. Bounds reflect the last layout pass, so call it
after one (before the first, every rect is zero-sized).
Scope: the walk covers the WidgetTree arena and the
ChildPods containers own, reached through
Widget::visit_children — so it
is the real retained hierarchy, not just the arena (which holds little
more than the root pod). A container that leaves that seam defaulted
reads as a leaf.
Sourcepub fn rebuild(
&mut self,
build: &mut impl FnMut(&mut State) -> V,
state: &mut State,
) -> ChangeFlags
pub fn rebuild( &mut self, build: &mut impl FnMut(&mut State) -> V, state: &mut State, ) -> ChangeFlags
Run the build closure, then build (first call) or rebuild (subsequent calls) the root widget, returning what changed.
the build closure is expected to be cheap and re-entrant: it is re-run in full every rebuild.
§Deferred-callback flush
The view diff itself is state-free (rebuild_view below takes no
State), so a widget applying a structural op there — the navigator
draining its queued push/pop is the shipped case — cannot run an app
callback that needs &mut State. It instead queues the callback and calls
mark_pending_result_flush;
this method drains that flag and dispatches an
InputEvent::Housekeeping broadcast through the ordinary
event plumbing, where state is in scope. This
is the only unconditional per-frame pass that holds &mut State, which is
why the dispatch lives here and not in a shell (flushing on the next
real input meant waiting seconds for a touch, or forever when the next
touch went to chrome outside the navigator).
A flushed callback mutates State, so the view built before it ran is
stale — the build closure + rebuild_view cycle therefore re-runs after
each flush, and the same frame shows the result. Results can queue further
nav ops, so the loop is bounded; past the cap the flag is left standing
and one more frame is requested rather than spinning (see
MAX_PENDING_RESULT_FLUSH_PASSES, this module’s private cap constant).
The broadcast’s EventOutcome is propagated, not discarded: a
needs_redraw coming back from the dispatch folds into this rebuild’s
ChangeFlags::PAINT and the deferred frame request, so a callback
whose only effect is EventCtx::request_redraw
— invisible to the re-diff, since no view-visible state changed — still
wakes both the mobile frame gate and the desktop Wait loop.
Sourcepub fn layout(&mut self, window_size: Size) -> Size
pub fn layout(&mut self, window_size: Size) -> Size
Lay out the root widget against window_size and record its geometry.
The root receives loose constraints (zero up to the window size) and is
placed at the origin. Returns the size the root chose. No text context is
threaded in (use RenderRoot::layout_with_text when the tree contains
text widgets); the stored theme, if any, is still threaded down.
Sourcepub fn layout_with_text(
&mut self,
window_size: Size,
text_ctx: &mut dyn Any,
) -> Size
pub fn layout_with_text( &mut self, window_size: Size, text_ctx: &mut dyn Any, ) -> Size
Lay out the root widget, threading a shared text-shaping context down to text widgets.
text_ctx is the shell-owned frust_text::TextContext, passed
type-erased so this crate needs no frust-text dependency. Text
widgets recover it via crate::widget::LayoutCtx::text_context. The
stored theme, if any, is threaded down alongside it.
Sourcepub fn paint(
&mut self,
scene: &mut dyn PaintScene,
frame_time: FrameTime,
) -> PaintOutcome
pub fn paint( &mut self, scene: &mut dyn PaintScene, frame_time: FrameTime, ) -> PaintOutcome
Paint the root widget into scene, returning whether the tree wants
another frame to continue an animation.
A widget whose paint advances animation state (e.g. a scroll fling) signals
PaintCtx::request_frame; that flag bubbles up through the container
ChildPods and out here as
PaintOutcome::needs_frame, which the shell honors by scheduling the next
frame (desktop window.request_redraw(); the mobile continuous loops
already do so). Mirrors how RenderRoot::event surfaces needs_redraw.
A widget whose animation changes its layout signals
PaintCtx::request_layout instead (or as well); that bubbles up the same
way and is folded here into the render root’s pending ChangeFlags
(LAYOUT), so the next frame’s
take_change_flags().needs_layout()
reports it and the mobile intra-frame layout skip relayouts while the
animation is in flight. It is also surfaced on the returned
PaintOutcome::needs_layout.
frame_time is the shell’s shared monotonic clock for this frame
(time enters frust-core from the shell, never Instant::now() here). It
is seeded onto the root PaintCtx and threaded unchanged to every child
(crate::widget::ChildPod::paint_child), so an animating widget advances
against one consistent timestamp — see PaintCtx::frame_time.
§The overlay post-pass
Painting the main tree is only the first half. Widgets registering a
floated surface during that walk (PaintCtx::register_overlay) are
drained here and painted after it, in band order — which is the only
way a popover, menu or tooltip escapes its owner’s paint order and every
ancestor’s clip. Their routing rects are retained (see
RenderRoot::overlay_hits) for the next event pass to hit-test first, and
their paint outcomes merge into this pass’s own, so an animating overlay
keeps the frames coming exactly like an animating widget in the tree.
Sourcepub fn selection_toolbar(&self) -> Option<SelectionToolbarRequest>
pub fn selection_toolbar(&self) -> Option<SelectionToolbarRequest>
The selection-toolbar request the focused field published during the most
recent RenderRoot::paint, or None when no field is focused at all.
The shell half of the platform edit-menu route
(SelectionToolbarPolicy::Native):
a shell reads it beside RenderRoot::ime_state, answers “may I offer
this verb?” from SelectionToolbarRequest::actions whenever the platform
asks, and presents the host’s own menu at
SelectionToolbarRequest::anchor when
SelectionToolbarRequest::present_menu says so. A level, not an edge
— re-read it as often as you like; pair it with
RenderRoot::selection_toolbar_generation to notice the changes worth
presenting or dismissing for.
Present for a focused field with no selection at all, which is not a wasted answer: paste applies to a bare caret, and a platform asking whether it may offer one needs a reply before any bar exists.
A field under the framework policy publishes this too (it floats its own
toolbar through crate::overlay as well), so a shell that drives the
platform menu must decide on the policy, not on the presence of a request.
Sourcepub fn selection_toolbar_generation(&self) -> u64
pub fn selection_toolbar_generation(&self) -> u64
A monotonically-increasing generation bumped on every change to the
menu-significant part of RenderRoot::selection_toolbar — its
present_menu flag and its
actions — including the clearing that
a blur produces, so a shell sees the menu going away as an edge too.
A moved anchor is not an edge: it
is republished (and recomputed) every painted frame, so a shell re-reads it
from RenderRoot::selection_toolbar rather than waiting for this to move
— bumping on it would re-present a menu on every touch sample of a drag
that widens a selection.
The focus_ime_generation contract one channel over: a shell caches the
last value it acted on and acts only when it moves, which is what keeps a
standing selection — republished every single frame — from asking the
platform to re-present its menu on every vsync.
Sourcepub fn semantics(&self) -> SemanticsUpdate
pub fn semantics(&self) -> SemanticsUpdate
Collect the accessibility tree for the current frame,
returning a SemanticsUpdate a platform adapter (accesskit_*)
can consume.
Pull-based and stateless: the shell calls this when a platform a11y client
asks for the tree (or after a change), never per frame — this crate owns
no scheduling. Must run after RenderRoot::layout, since node bounds
come from the pods’ post-layout geometry.
The result is always rooted at a synthetic accesskit::Role::Window
node covering the window, whose children are whatever the root widget
contributed. An unbuilt tree yields a bare window node with no children.
Sourcepub fn semantics_generation(&self) -> u64
pub fn semantics_generation(&self) -> u64
The current semantics generation — bumped by every rebuild/theme swap that could have changed the accessibility tree (the semantics dirty gate).
A shell records the value it last pushed and compares; see
RenderRoot::semantics_if_changed.
Sourcepub fn semantics_if_changed(&self, last_seen: u64) -> Option<SemanticsUpdate>
pub fn semantics_if_changed(&self, last_seen: u64) -> Option<SemanticsUpdate>
Pull a fresh SemanticsUpdate only if the semantics tree may have
changed since generation last_seen.
Returns None when nothing relevant changed, letting a shell skip both the
tree walk and the platform accesskit_* push. Call it post-layout (bounds
must be valid). A shell threads its stored generation in and, on Some,
updates it from RenderRoot::semantics_generation. v1 pushes the whole
tree when it does recompute (stable ids make that valid); finer-grained
diffing is a later optimization.
Sourcepub fn event(&mut self, state: &mut State, event: &InputEvent) -> EventOutcome
pub fn event(&mut self, state: &mut State, event: &InputEvent) -> EventOutcome
Deliver an input event to the widget tree, returning what happened.
Builds a root EventCtx over the (type-erased) state, dispatches to
the root widget — which routes the event down through its container
children — and folds the result into an EventOutcome. The outcome’s
needs_redraw is set whenever a widget consumed the event or explicitly
requested a redraw; the shell turns that into a window.request_redraw().
Root capture bookkeeping mirrors the per-container active-child model: a
Down whose dispatch requested capture marks a gesture in flight and
latches the contact that sent it as the gesture’s claimant; the
claimant’s Up and Cancel release it (never a window-leave, and never
another contact’s release).
Pointer contacts are gated first, before anything else runs: an
InputEvent::PointerContact is unwrapped into the plain
InputEvent::Pointer every widget matches on, with
EventCtx::pointer_id reporting its
id, and a contact the multi-contact contract does not route — an
additional contact with nothing captured, or one the captor did not opt
into — is dropped here with an empty outcome (see that variant’s
Multi-contact contract). A bare InputEvent::Pointer is the mouse.
Root hover bookkeeping is the third recorded path, and the one this pass
derives rather than merely mirrors: an uncaptured Move opens a hover
pass (widgets on the hit-tested path may claim it — see
EventCtx::claim_hover), a
Down/Up/Cancel ends whatever hover stood, and every other event leaves
it alone. There is nothing to release and no generation to bump: the epoch
advance strands the previous claimant’s path by itself, and the outcome’s
needs_redraw carries the one repaint no widget can ask for — a hover
that ended with nothing taking it. A hover that begins or moves from one
claimant to another is repainted by the new claimant’s own change-gated
request_redraw, which is why keeping an internal hover flag is part of the
consumer contract rather than an optimization (see claim_hover).
No shell change is required for hover — the desktop shell already
dispatches a Move on every cursor move.
The cursor is hover’s sibling channel and the fourth thing this pass
resolves: any pointer Move (captured included) re-resolves
RenderRoot::cursor from the pass’s last
EventCtx::set_cursor, defaulting to
CursorIcon::Default when nothing asked. It deliberately does not
fold into the outcome’s needs_redraw: applying a cursor is a platform
call a desktop shell makes straight after this pass returns, with no frame
involved, and folding it in would repaint the tree on every hover move.
The clipboard channel rides the same bracket and is the fifth thing
this pass resolves: whatever the dispatch asked for through
EventCtx::write_clipboard and
EventCtx::request_paste lands in
RenderRoot::take_clipboard_write / RenderRoot::take_paste_request,
which a shell drains immediately after this returns, beside
RenderRoot::cursor and RenderRoot::ime_state. Unlike the cursor,
both commit on every pass rather than on a pointer Move alone — a
copy can be answered from a key chord, a context-menu tap, or an
InputEvent::EditCommand — and both are one-shot drains rather than
standing levels. Like the cursor, neither folds into needs_redraw:
talking to the host clipboard paints nothing (a Cut that mutates the
document asks for its own redraw, for the mutation).
§Reentrancy
This pass never rebuilds or repaints. Event handlers mutate state
synchronously through the context; the shell is expected to run a single
RenderRoot::rebuild (then layout/paint) after the event pass returns,
driven by the outcome. Rebuilding re-entrantly here would invalidate the
widget references the dispatch still holds and turn the event→state→view
feedback into recursion.
RenderRoot::rebuild calls this itself with
InputEvent::Housekeeping to flush deferred state-bearing callbacks.
That is sequential, not re-entrant — the dispatch fully returns before
the next diff starts — so the rule above is intact.
Sourcepub fn perform_accessibility_action(
&mut self,
state: &mut State,
node_id: NodeId,
action: Action,
) -> EventOutcome
pub fn perform_accessibility_action( &mut self, state: &mut State, node_id: NodeId, action: Action, ) -> EventOutcome
Perform a platform accessibility action, returning the same
EventOutcome the synthesized input produced.
A platform accesskit_* adapter delivers an ActionRequest(node_id, action); a shell forwards it here. v1 routes actions through the normal
event path by synthesizing pointer events at the target node’s absolute
bounds center (recovered from a fresh semantics pass — the id → bounds map
the pass produces), so every fire-on-up-inside widget is operable with
zero widget-side changes:
accesskit::Action::Click→ aDownthen anUpat the center, activating any button/switch/checkbox exactly as a real tap would.accesskit::Action::Focus→ aDownthen a syntheticCancelat the center: theDownclaims focus for a widget that opts in onDown(the recorded-focus contract), and theCancelreleases the capture that sameDownopened without touching the recorded focus path — so the action claims focus without leaving the widget permanently capturing every later pointer event. A widget that does not claim focus onDownis unaffected, andCancelnever fires an on-press callback.- any other action → ignored (a no-op
EventOutcome); richer actions are deferred.
An unknown node_id (not in the current tree) is a benign no-op. Requires
a prior RenderRoot::layout so the bounds are valid. Synthetic-pointer
activation cannot drive widgets that require a real drag (e.g. a slider) —
an accepted v1 limitation.