Skip to main content

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}