frust_core/app/config.rs
1//! Configuration knobs and shell-pushed inputs on [`RenderRoot`]: layout skip and audit, retained
2//! layers, focus ring, Tab traversal, theme, insets, presented frames and surface translucency.
3
4use std::any::Any;
5
6use super::RenderRoot;
7use crate::insets::WindowInsets;
8use crate::view::{ChangeFlags, View};
9
10/// When [`PaintCtx::is_focused_for_ring`](crate::widget::PaintCtx::is_focused_for_ring) reports
11/// a ring on the focused widget ([`RenderRoot::set_focus_ring_policy`]).
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13pub enum FocusRingPolicy {
14 /// No ring, whatever focused the widget.
15 Never,
16 /// While the focus came from keyboard traversal and no pointer `Down` has landed since.
17 #[default]
18 KeyboardOnly,
19 /// A ring whenever the widget holds focus.
20 Always,
21}
22
23impl<State: 'static, V: View<State>> RenderRoot<State, V> {
24 /// Choose when the focused widget draws a ring (default [`FocusRingPolicy::KeyboardOnly`]).
25 pub fn set_focus_ring_policy(&mut self, policy: FocusRingPolicy) {
26 let visible = self.focus_ring_visible();
27 self.focus_ring_policy = policy;
28 self.repaint_if_ring_moved(visible);
29 }
30
31 /// The policy [`RenderRoot::set_focus_ring_policy`] last set.
32 pub fn focus_ring_policy(&self) -> FocusRingPolicy {
33 self.focus_ring_policy
34 }
35
36 /// Whether the policy shows a ring now, before ANDing a pod's own focus path.
37 pub(super) fn focus_ring_visible(&self) -> bool {
38 match self.focus_ring_policy {
39 FocusRingPolicy::Never => false,
40 FocusRingPolicy::KeyboardOnly => self.keyboard_focus,
41 FocusRingPolicy::Always => true,
42 }
43 }
44
45 /// Record whether the live focus came from the keyboard: a landing traversal grant sets it; a
46 /// claim honoured outside a grant, a session release and a pointer `Down` clear it.
47 pub(super) fn set_keyboard_focus(&mut self, keyboard: bool) {
48 let visible = self.focus_ring_visible();
49 self.keyboard_focus = keyboard;
50 self.repaint_if_ring_moved(visible);
51 }
52
53 /// Switch Tab traversal on (the default) or off. Off, a bare Tab/Shift+Tab the tree, the
54 /// forward and the shortcut table declined stays unhandled, for the host's own tab order.
55 /// On, each such Tab rebuilds the walk's stops (accepted: Tab is rare).
56 pub fn set_tab_traversal(&mut self, enabled: bool) {
57 self.tab_traversal = enabled;
58 }
59
60 /// Whether a declined bare Tab runs traversal ([`RenderRoot::set_tab_traversal`]).
61 pub fn tab_traversal(&self) -> bool {
62 self.tab_traversal
63 }
64
65 /// A moved ring changes pod reads without dirtying them, so no retained subtree stays clean.
66 fn repaint_if_ring_moved(&mut self, was_visible: bool) {
67 if self.focus_ring_visible() != was_visible {
68 self.paint_epoch = self.paint_epoch.wrapping_add(1);
69 self.pending |= ChangeFlags::PAINT;
70 }
71 }
72
73 /// Switch the layout skip on (the default) or off; `false` lays out every pod every pass.
74 pub fn set_layout_skip(&mut self, skip: bool) {
75 self.layout_skip = skip;
76 }
77
78 /// Whether layout passes may skip clean pods ([`RenderRoot::set_layout_skip`]).
79 pub fn layout_skip(&self) -> bool {
80 self.layout_skip
81 }
82
83 /// Turn the layout audit (a debug aid that doubles layout cost) on or off; off by default.
84 /// With the skip on, layout re-runs forced and warns `layout-audit:` per pod moved >1e-3 px.
85 pub fn set_layout_audit(&mut self, audit: bool) {
86 self.layout_audit.enabled = audit;
87 }
88
89 /// Whether the layout audit is on ([`RenderRoot::set_layout_audit`]).
90 pub fn layout_audit(&self) -> bool {
91 self.layout_audit.enabled
92 }
93
94 /// Pods the layout audit has found with mismatched geometry since creation, every occurrence
95 /// counted (unlike the rate-limited log).
96 pub fn layout_audit_mismatches(&self) -> u64 {
97 self.layout_audit.mismatches
98 }
99
100 /// The global layout epoch the next layout pass runs under, for tooling and tests.
101 pub fn layout_epoch(&self) -> u64 {
102 self.layout_epoch
103 }
104
105 /// Invalidate every cached layout and paint: the next pass lays out the whole tree.
106 pub(super) fn bump_layout_epoch(&mut self) {
107 self.layout_epoch = self.layout_epoch.wrapping_add(1);
108 self.paint_epoch = self.paint_epoch.wrapping_add(1);
109 }
110
111 /// Enable or disable retained layers (default on); off, every boundary repaints its child.
112 pub fn set_retained_layers(&mut self, enabled: bool) {
113 self.retained_layers = enabled;
114 }
115
116 /// Hand the root the renderer's resident-key ledger from the previous frame (one frame stale
117 /// by design); cloned only when it differs.
118 pub fn set_retained_ledger(&mut self, ledger: &crate::retained::RetainedLedger) {
119 if self.retained_ledger != *ledger {
120 self.retained_ledger.clone_from(ledger);
121 }
122 }
123
124 /// Whether retained layers are enabled ([`RenderRoot::set_retained_layers`]).
125 pub fn retained_layers(&self) -> bool {
126 self.retained_layers
127 }
128
129 /// Whether any floated pod the last paint drew is layout-dirty (a borrowed one counts).
130 pub(super) fn overlay_pod_dirty(&self) -> bool {
131 self.overlay_pods.iter().any(|weak| {
132 weak.upgrade()
133 .is_some_and(|pod| pod.try_borrow().map_or(true, |pod| pod.is_layout_dirty()))
134 })
135 }
136
137 /// Store (replace) the app's type-erased theme, recovered by widgets via `theme_as`. Marks
138 /// `LAYOUT | PAINT` and invalidates every cached layout: widgets bake themed state at layout.
139 pub fn set_theme(&mut self, theme: Box<dyn Any>) {
140 self.theme = Some(theme);
141 self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
142 self.bump_layout_epoch();
143 self.semantics_gen = self.semantics_gen.wrapping_add(1);
144 }
145
146 /// Store the window's insets, recovered by widgets via `window_insets`. An actual change
147 /// dirties exactly like [`RenderRoot::set_theme`]; pushing the current value is a no-op.
148 pub fn set_insets(&mut self, insets: WindowInsets) {
149 if self.insets == insets {
150 return;
151 }
152 self.insets = insets;
153 self.pending |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
154 self.bump_layout_epoch();
155 self.semantics_gen = self.semantics_gen.wrapping_add(1);
156 }
157
158 /// The window's insets currently threaded into the layout/paint passes.
159 pub fn insets(&self) -> WindowInsets {
160 self.insets
161 }
162
163 /// Store the shell's presented-frame count (pushed once per UI frame, before `paint`). Dirties
164 /// nothing: a ticking count marked dirty would keep the mobile frame gate from ever idling.
165 pub fn set_presented_frames(&mut self, presented: u64) {
166 self.presented_frames = Some(presented);
167 }
168
169 /// The presented-frame count currently threaded into the paint pass, or
170 /// `None` if no shell has pushed one.
171 pub fn presented_frames(&self) -> Option<u64> {
172 self.presented_frames
173 }
174
175 /// Store the surface's *resolved* translucency (what the backend granted, not what the app
176 /// asked: a refused request must stay opaque, or every platform-view slot punches a black
177 /// hole). An actual change marks `PAINT` and invalidates retained paint; no relayout.
178 pub fn set_surface_translucent(&mut self, translucent: bool) {
179 if self.surface_translucent == translucent {
180 return;
181 }
182 self.surface_translucent = translucent;
183 self.paint_epoch = self.paint_epoch.wrapping_add(1);
184 self.pending |= ChangeFlags::PAINT;
185 }
186
187 /// Whether the shell's GPU surface is currently marked translucent.
188 pub fn is_surface_translucent(&self) -> bool {
189 self.surface_translucent
190 }
191}