pub struct FrameInputs {
pub signals_dirty: bool,
pub events_since_last_frame: bool,
pub pointer_capture_active: bool,
pub focus_or_ime_changed: bool,
pub last_needs_frame: bool,
pub last_needs_frame_paced_only: bool,
pub change_flags_pending: bool,
pub deferred_callbacks_pending: bool,
pub theme_or_appearance_changed: bool,
pub surface_changed_or_resized: bool,
pub a11y_action_performed: bool,
pub resumed_recently: bool,
}Expand description
The per-frame OR-list a shell gathers and hands to FrameGate::decide.
Every field is a “something that needs this frame to run” signal; the gate
runs the frame if any is true, with three deliberate deltas noted
in the field docs:
-
focus_or_ime_changedis an edge, not a level: it reports that the focus/IME session moved since the shell last looked, so a steady focus session no longer forces a frame every vsync (and, alone among the wake inputs, it does not disqualify pacing). -
Semantics adapter needs is not a field here: semantics-publish gating stays shell-side (both Android and iOS are generation-gated via
AppTree::semantics_if_changed), so it never gates whole-frame production. -
resumed_recentlyis the added input for the resume warmup (seeWARMUP_FRAMES); the gate also drives this same condition internally viaFrameGate::note_resumed, so a shell may leave the fieldfalseand rely on the counter (both force aRun).
Default is all-false (the “nothing changed” baseline a
FrameGate::decide turns into a FrameDecision::Skip).
Fields§
§signals_dirty: booltracked-signal dirty. A tracked signal changed since the last
frame. Source: frust_reactive::ReactiveRuntime::take_signals_dirty,
read by the shell after its per-frame pump_local per
that crate’s pump-first ordering contract.
events_since_last_frame: boolevents dispatched since last frame. A pointer/scroll/key/IME
event reached RenderRoot::event between frames. Source: a shell-side
latch set by the nativeOnTouch/IME entry points and cleared each
frame.
pointer_capture_active: boolactive pointer capture. A gesture is mid-drag and the captured
widget may animate/track the pointer. Source:
AppTree/RenderRoot::is_pointer_captured.
focus_or_ime_changed: boolfocus/IME session moved. The root’s focus flag or its published IME
surface changed since the shell last produced a frame — an edge,
not a level. Source:
AppTree::focus_ime_generation
compared against the shell’s cached copy, which the shell commits only
once the gate has decided to run this frame (a peek at gather time —
see the deferral note at the end of this doc).
Why an edge. This was a level input
(RenderRoot::is_focus_active || ime_state().is_some()) — the
original conservative default. Because it forces a Run through
any_set and disqualified
is_paced_only_frame, any screen holding
root focus rendered every single vsync for as long as the focus lasted,
and caret pacing was unreachable while a caret blinked: measured at
62–120 fps on a static screen whose only live input was focus (Xiaomi
12). As an edge it still forces one frame per transition (focus gained /
lost, IME surface published / cleared) while a steady focus session
leaves the gate free to idle or pace.
Why one frame is enough. Every IME event the platform delivers also
trips events_since_last_frame (both
shells latch it in their ime_apply entry points), and the per-frame
platform IME reconcile (Kotlin doFrame / Swift renderFrame) polls the
published Rust state on its own cadence, independent of whether Rust
produced a frame — all it needs is that state to be current, which the
edge guarantees by forcing the frame after every change.
Unlike the other wake inputs this one is not an
is_paced_only_frame disqualifier, so an
edge landing on a tick whose only other dirtiness is a paced loop is
consumed by whichever tick’s pacing decision resolves to Run: the
repaint then lands with the loop’s next paced frame (bounded by one
cosmetic_loop_rate interval), and the platform’s IME poll is unaffected
either way. This is exactly why the shells peek the generation rather
than draining it at gather time — an edge that a Skip erased would make
the next repaint wait out the loop’s full effective interval instead of
the bound below. A per-request
paced interval (FramePacing::requested_interval) never widens that
bound — FrameGate::decide_paced tightens an edge-carrying tick back
to the theme cap on purpose, so a 500ms caret cannot turn a focus
transition into a 500ms lag.
last_needs_frame: boollast paint’s needs_frame. The previous paint advanced an
animation/transition and asked for another frame. Source:
PaintOutcome::needs_frame, latched by the shell from the prior
frame’s AppTree::paint return.
last_needs_frame_paced_only: boollast paint’s needs_frame_paced_only: the prior frame’s frame request
aggregated to frust_core::TickClass::CosmeticLoop alone — a pacable
decorative loop with no concurrent transition. Latched by the shell from
PaintOutcome::needs_frame_paced_only. Meaningful only alongside
last_needs_frame; with every other input
clear (is_paced_only_frame) it is the sole
signal FrameGate::decide_paced throttles to the theme’s cadence. Any
concurrent transition/input clears it, so the frame runs immediately.
change_flags_pending: boolpending ChangeFlags. A rebuild (or set_theme) left layout/
paint dirtiness undrained. Source:
AppTree::has_pending_change_flags
(a non-draining peek, so a skipped frame preserves the flags).
deferred_callbacks_pending: booldeferred callbacks owed a flush. A widget queued a state-bearing
callback during a state-free pass and raised
frust_core::mark_pending_result_flush, which only
RenderRoot::rebuild can drain (it holds the &mut State the callback
needs). Source: frust_core::has_pending_result_flush, the
non-draining peek — draining stays the rebuild’s job on a frame that
actually runs.
Hardening under the default-to-run rule rather than a reproduced stall:
every mark raised today is also covered by another input (a mark from
inside a rebuild is drained by that same rebuild, whose leftover
pending |= PAINT reaches change_flags_pending
and whose deferred_frame reaches last_needs_frame;
frust-widgets’ paint-time long-press latch pairs its mark with a
request_frame). This input closes the general case those two happen to
cover — a mark raised with nothing else dirty must never wait for the
next stray touch.
theme_or_appearance_changed: booltheme-override/appearance change. The app-facing theme override
or the platform light/dark preference changed this tick. Source: the
shell’s per-frame ThemeOverrideWatcher/appearance poll (see
[crate::theme_override]).
surface_changed_or_resized: boolsurface resize/recreation. The GPU surface was created, resized,
or recreated (rotation/backgrounding). Source: the shell’s
nativeOnSurfaceChanged/frust_resize path.
a11y_action_performed: boolaccessibility actions. A platform accesskit_* action was
performed this tick, mutating state. Source: the shell’s a11y-action
drain feeding AppTree::perform_accessibility_action.
resumed_recently: boolAdded input: resume warmup. The app resumed / the surface was
(re)created within the last WARMUP_FRAMES frames. A shell may set
this explicitly, or leave it false and let FrameGate::note_resumed
drive the same condition through the gate’s own countdown — both force
a Run.
Implementations§
Source§impl FrameInputs
impl FrameInputs
Sourcepub fn any_set(&self) -> bool
pub fn any_set(&self) -> bool
Whether any input signals that this frame must run. The gate ORs the
full field set — the single decision rule the whole type exists to
feed (see FrameGate::decide).
Sourcepub fn is_paced_only_frame(&self) -> bool
pub fn is_paced_only_frame(&self) -> bool
Whether the only dirtiness this frame is a paced (CosmeticLoop)
frame request — the throttleable case FrameGate::decide_paced paces.
True iff last_needs_frame and
last_needs_frame_paced_only are
both set and every other OR-list input is clear. Any
input/signal/change-flag/transition alongside it makes this false, so
the gate runs the frame immediately rather than pacing it (the
default-to-run rule — see docs/CODE_STANDARDS.md’s Frame-Gate
conventions).
One deliberate exception:
focus_or_ime_changed is not a
disqualifier. Its level-input predecessor was one, and — being true for
a whole focus session — that is what made caret pacing unreachable: a
blinking caret in a focused field is precisely the paced loop this gate
must be able to throttle (that field’s doc has the device measurement).
The edge that replaced it reports one transition, not a session, so an
edge arriving mid-loop is simply carried by the loop’s next paced frame
instead of pre-empting it. It is one repaint per transition, not
per tick: the shells peek it, so it keeps reporting across every skipped
tick in between and is cleared by the frame that finally runs.
Trait Implementations§
Source§impl Clone for FrameInputs
impl Clone for FrameInputs
impl Copy for FrameInputs
Source§impl Debug for FrameInputs
impl Debug for FrameInputs
Source§impl Default for FrameInputs
impl Default for FrameInputs
impl Eq for FrameInputs
Source§impl PartialEq for FrameInputs
impl PartialEq for FrameInputs
impl StructuralPartialEq for FrameInputs
Auto Trait Implementations§
impl Freeze for FrameInputs
impl RefUnwindSafe for FrameInputs
impl Send for FrameInputs
impl Sync for FrameInputs
impl Unpin for FrameInputs
impl UnsafeUnpin for FrameInputs
impl UnwindSafe for FrameInputs
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<T> Brush for T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.