frust_core/app/mod.rs
1//! The render root: the object each platform shell drives each frame.
2//!
3//! [`RenderRoot`] owns the retained [`WidgetTree`] and the previous [`View`], and runs the
4//! [`RenderRoot::rebuild`], [`RenderRoot::layout`], [`RenderRoot::paint`] and
5//! [`RenderRoot::event`] passes.
6//!
7//! Single-root: the build closure returns one `impl View<State>` of a fixed type, stored typed.
8
9mod config;
10mod dispatch;
11mod layout;
12mod metrics;
13mod paint;
14mod rebuild;
15mod semantics;
16mod session;
17#[cfg(test)]
18mod tests;
19
20use std::any::Any;
21use std::cell::{Cell, RefCell};
22use std::num::NonZeroU64;
23use std::rc::{Rc, Weak};
24use std::sync::atomic::{AtomicU64, Ordering};
25
26use kurbo::{Rect, Size};
27
28use crate::event::{CursorIcon, EventOutcome, ImeState, PointerId};
29use crate::insets::WindowInsets;
30use crate::overlay::{OverlayHit, OverlayKey};
31use crate::selection_toolbar::SelectionToolbarRequest;
32use crate::shortcuts::{ShortcutHint, ShortcutId, ShortcutTable};
33use crate::tree::WidgetTree;
34use crate::view::{ChangeFlags, View, WidgetId};
35use crate::widget::{ChildPod, PlatformViewFrame};
36
37pub use config::FocusRingPolicy;
38use layout::LayoutAudit;
39pub use metrics::{Orientation, WindowMetrics};
40
41/// Rebuild flush passes per frame: a result that navigates once, plus that page's own result.
42const MAX_PENDING_RESULT_FLUSH_PASSES: usize = 3;
43
44/// Allocates `root_identity` (`0` = no root); module-level so distinct `<State, V>` never collide.
45static NEXT_ROOT_IDENTITY: AtomicU64 = AtomicU64::new(1);
46
47/// The next focus epoch after `epoch`, skipping `0` (a never-claimed pod's stamp).
48fn advance_focus_epoch(epoch: u64) -> u64 {
49 match epoch.wrapping_add(1) {
50 0 => 1,
51 next => next,
52 }
53}
54
55/// What [`RenderRoot::event`]'s overlay pre-pass decided about one incoming event.
56enum OverlayRoute {
57 /// The overlay layer consumed the event; return this outcome unchanged.
58 Consumed(EventOutcome),
59 /// The main dispatch continues; merge this outcome into its own.
60 Continue(EventOutcome),
61}
62
63/// Owns the retained tree and drives the rebuild/layout/paint/event passes of a single-root app.
64pub struct RenderRoot<State: 'static, V: View<State>> {
65 tree: WidgetTree,
66 root_id: Option<WidgetId>,
67 prev_view: Option<V>,
68 /// Widget-id counter lent to each `BuildCtx`.
69 next_id: u64,
70 window_size: Size,
71 /// The contact whose `Down` claimed the live capture; only its own `Up`/`Cancel` clears it.
72 capture_claimant: Option<PointerId>,
73 /// Whether the live captor opted into the gesture's other contacts; `false` when uncaptured.
74 capture_contacts: bool,
75 /// Whether that opt-in came from the root widget itself rather than a pod below it.
76 contacts_captor_is_root: bool,
77 /// Whether some widget holds focus; set by a focus claim, cleared only by a session release.
78 focus_active: bool,
79 /// The branch owning the live focus session: `None` for the main tree, `Some(key)` for a
80 /// floated surface. Resolved by the paint pass from live links; other passes only clear it.
81 focus_surface: Option<OverlayKey>,
82 /// The live focus session's identity, stamped beside each pod's focus link; never `0`.
83 focus_epoch: u64,
84 /// The pod declined keys are forwarded to, with the `focus_epoch` it was recorded under.
85 key_forward: Option<(OverlayKey, u64)>,
86 /// The carrier id of this root's traversal grants (`widget::FocusGrant`); declared by no one.
87 focus_grant: ShortcutId,
88 /// Whether traversal granted the live focus; any other claim, a release or a `Down` clears it.
89 keyboard_focus: bool,
90 focus_ring_policy: FocusRingPolicy,
91 /// Whether a declined bare Tab/Shift+Tab runs traversal ([`RenderRoot::set_tab_traversal`]).
92 tab_traversal: bool,
93 hover_active: bool,
94 /// Identity of the latest hover pass; a pod's hover stamp counts only while it equals this.
95 hover_epoch: u64,
96 /// Process-unique id qualifying this root's thread-local focus and hover channels.
97 root_identity: u64,
98 cursor: CursorIcon,
99 /// Undrained clipboard write; a newer write replaces one nobody has taken yet.
100 pending_clipboard_write: Option<String>,
101 /// Whether a widget asked to read the host clipboard since the last drain.
102 pending_paste_request: bool,
103 ime_state: Option<ImeState>,
104 /// Bumped on every actual change of `focus_active` or `ime_state`, never on a same-value write.
105 focus_ime_gen: u64,
106 /// The frames the last paint published; replaced wholesale every pass.
107 platform_view_frames: Vec<PlatformViewFrame>,
108 /// The z-shield rects the last paint reported; replaced wholesale every pass.
109 input_shields: Vec<Rect>,
110 /// Dirtiness accumulated since the last [`RenderRoot::take_change_flags`].
111 pending: ChangeFlags,
112 /// The active theme, type-erased so this crate needs no `frust-theme` dependency.
113 theme: Option<Box<dyn Any>>,
114 insets: WindowInsets,
115 /// The shell's presented-frame count: a paint-time observation that dirties nothing.
116 presented_frames: Option<u64>,
117 /// Whether the GPU surface is translucent ("Mode B"); read by the platform-view hole-punch.
118 surface_translucent: bool,
119 /// Next never-reused semantics base id; starts at `2` (`0` keeps `NonZeroU64` valid, `1` is
120 /// the window node). A `Cell` because [`RenderRoot::semantics`] takes `&self`.
121 semantics_alloc: Cell<u64>,
122 root_semantics_id: Cell<Option<NonZeroU64>>,
123 /// Bumped when a rebuild, theme swap or inset change may have changed the semantics tree.
124 semantics_gen: u64,
125 /// Set when the deferred-callback flush owes a frame; the next paint reports `needs_frame`.
126 deferred_frame: bool,
127 /// The root widget's own trailing-paint bit, since it sits in no pod.
128 painted_animating: bool,
129 /// Routing half of the overlay entries the last paint registered, in paint order; no pods.
130 overlay_hits: Vec<OverlayHit>,
131 /// The shortcut declarations the last paint made, matched against unhandled keys.
132 shortcuts: ShortcutTable,
133 /// `shortcuts` as the snapshot this root's build and paint contexts hand out.
134 last_hints: Rc<[ShortcutHint]>,
135 /// The focused field's selection-toolbar request from the last paint; a release clears it.
136 selection_toolbar: Option<SelectionToolbarRequest>,
137 /// Bumped on every actual change of `selection_toolbar`, a clear included.
138 selection_toolbar_gen: u64,
139 /// Bumped by a theme swap, an inset change, a resize, or a dirty floated surface.
140 layout_epoch: u64,
141 /// A pod painted under another paint epoch is never reported clean.
142 paint_epoch: u64,
143 /// Serial of the most recent paint (`0` before the first).
144 paint_serial: u64,
145 /// The hover claimant's `inspect_id` (`0` for the root widget); `None` while nothing hovers.
146 hover_link: Option<u64>,
147 /// `(hover_link, focus_epoch)` as the last paint saw them; a change bumps `paint_epoch`.
148 painted_links: (Option<u64>, u64),
149 retained_layers: bool,
150 /// The renderer's resident-key ledger as of the previous frame.
151 retained_ledger: crate::retained::RetainedLedger,
152 layout_skip: bool,
153 layout_audit: LayoutAudit,
154 /// Weak handles to the floated pods the last paint drew. Their owners lay them out, so
155 /// [`RenderRoot::layout`] bumps the layout epoch when any is dirty.
156 overlay_pods: Vec<Weak<RefCell<ChildPod>>>,
157 _state: core::marker::PhantomData<fn(&mut State)>,
158}
159
160impl<State: 'static, V: View<State>> RenderRoot<State, V> {
161 /// Create an empty render root with no widget yet built.
162 pub fn new() -> Self {
163 Self {
164 tree: WidgetTree::new(),
165 root_id: None,
166 prev_view: None,
167 next_id: 0,
168 window_size: Size::ZERO,
169 capture_claimant: None,
170 capture_contacts: false,
171 contacts_captor_is_root: false,
172 focus_active: false,
173 focus_surface: None,
174 focus_epoch: 1,
175 key_forward: None,
176 focus_grant: ShortcutId::next(),
177 keyboard_focus: false,
178 focus_ring_policy: FocusRingPolicy::KeyboardOnly,
179 tab_traversal: true,
180 hover_active: false,
181 hover_epoch: 1,
182 root_identity: NEXT_ROOT_IDENTITY.fetch_add(1, Ordering::Relaxed),
183 cursor: CursorIcon::Default,
184 pending_clipboard_write: None,
185 pending_paste_request: false,
186 ime_state: None,
187 focus_ime_gen: 0,
188 platform_view_frames: Vec::new(),
189 input_shields: Vec::new(),
190 pending: ChangeFlags::NONE,
191 theme: None,
192 insets: WindowInsets::default(),
193 presented_frames: None,
194 surface_translucent: false,
195 semantics_alloc: Cell::new(2),
196 root_semantics_id: Cell::new(None),
197 semantics_gen: 0,
198 deferred_frame: false,
199 painted_animating: false,
200 overlay_hits: Vec::new(),
201 shortcuts: ShortcutTable::default(),
202 last_hints: Rc::from(Vec::new()),
203 selection_toolbar: None,
204 selection_toolbar_gen: 0,
205 layout_epoch: 1,
206 paint_epoch: 1,
207 paint_serial: 0,
208 hover_link: None,
209 painted_links: (None, 1),
210 retained_layers: true,
211 retained_ledger: crate::retained::RetainedLedger::empty(),
212 layout_skip: true,
213 layout_audit: LayoutAudit::default(),
214 overlay_pods: Vec::new(),
215 _state: core::marker::PhantomData,
216 }
217 }
218
219 /// The last paint's shortcut declarations `filter` keeps, in paint order: a palette's list.
220 pub fn shortcut_hints(
221 &self,
222 mut filter: impl FnMut(&ShortcutHint) -> bool,
223 ) -> Vec<ShortcutHint> {
224 self.last_hints
225 .iter()
226 .filter(|hint| filter(hint))
227 .cloned()
228 .collect()
229 }
230}
231
232impl<State: 'static, V: View<State>> Default for RenderRoot<State, V> {
233 fn default() -> Self {
234 Self::new()
235 }
236}