Skip to main content

frust_core/app/
paint.rs

1//! `RenderRoot`'s paint pass: the main tree, then the floated overlay pods
2//! above it, then the IME-surface and selection-toolbar resolution.
3
4use std::rc::Rc;
5
6use super::RenderRoot;
7use crate::anim::FrameTime;
8use crate::event::ImeState;
9use crate::overlay::{
10    OverlayEntry, OverlayHit, OverlayKey, OverlayPaintPass, sort_into_paint_order,
11};
12use crate::selection_toolbar::{
13    SelectionToolbarActions, SelectionToolbarPass, SelectionToolbarRequest,
14};
15use crate::shortcuts::{OverlayStamp, ShortcutPaintPass, ShortcutTable};
16use crate::view::{ChangeFlags, View};
17use crate::widget::{PaintCtx, PaintOutcome, PaintScene};
18
19impl<State: 'static, V: View<State>> RenderRoot<State, V> {
20    /// Paint the root widget into `scene`. The outcome reports a continuation
21    /// frame ([`PaintCtx::request_frame`] → [`PaintOutcome::needs_frame`]) and a
22    /// paint-time [`PaintCtx::request_layout`], which is also folded into the
23    /// pending [`ChangeFlags`] as `LAYOUT`. `frame_time` is the shell's clock for
24    /// the frame, threaded unchanged to every widget ([`PaintCtx::frame_time`]).
25    ///
26    /// # The overlay post-pass
27    ///
28    /// Surfaces registered during the main walk ([`PaintCtx::register_overlay`])
29    /// are painted after it in band order, escaping their owner's paint order and
30    /// clips; their routing rects are kept for the next event pass to hit-test
31    /// first, and their outcomes merge into this one.
32    pub fn paint(&mut self, scene: &mut dyn PaintScene, frame_time: FrameTime) -> PaintOutcome {
33        // Entering clears each channel, so the registries start every paint empty.
34        let overlay_pass = OverlayPaintPass::enter();
35        let toolbar_pass = SelectionToolbarPass::enter();
36        let shortcut_pass = ShortcutPaintPass::enter();
37        // The pods compare their stamps against this root's session.
38        self.publish_focus_session();
39        // Number this pass (see `paint_serial`).
40        self.paint_serial = self.paint_serial.wrapping_add(1);
41        // Moved links change pod reads without dirtying the loser: no old subtree is clean.
42        let links = (self.hover_link, self.focus_epoch);
43        if links != self.painted_links {
44            self.painted_links = links;
45            self.paint_epoch = self.paint_epoch.wrapping_add(1);
46        }
47
48        let (mut outcome, main_tree_ime) = self.paint_main_tree(scene, frame_time);
49        outcome.paint_serial = self.paint_serial;
50
51        // Stable sort: the band decides, registration order breaks ties.
52        let mut entries = overlay_pass.take();
53        sort_into_paint_order(&mut entries);
54        // Routing rects for the next event pass; weak pods for the next layout (`overlay_pods`).
55        self.overlay_hits = entries.iter().map(OverlayHit::of).collect();
56        self.overlay_pods = entries
57            .iter()
58            .map(|entry| Rc::downgrade(&entry.pod))
59            .collect();
60        let (holder, holder_ime) = self.paint_overlays(&entries, scene, frame_time, &mut outcome);
61        // The root holds no overlay pod at rest.
62        drop(entries);
63        // After the pods, so their (stamped) declarations are in the snapshot too.
64        self.shortcuts = ShortcutTable::new(shortcut_pass.take());
65        self.last_hints = self.shortcuts.publish();
66
67        self.resolve_session_surface(main_tree_ime, holder, holder_ime);
68        // A modal registered by this paint ends a main-tree session now, not at the next event.
69        self.trap_modal_focus();
70
71        // Last, so a field painted inside a floated pod is resolved like any other.
72        self.resolve_selection_toolbar(toolbar_pass.take());
73
74        outcome
75    }
76
77    /// Settle which branch's IME surface the shell is configured with, from the
78    /// main tree's publish and the focus-holding surface's (`holder`/`holder_ime`,
79    /// see [`RenderRoot::paint_overlays`]). Decided here, once both halves have
80    /// painted, because the main tree publishes before ownership is known.
81    ///
82    /// # The rules
83    ///
84    /// * the owner's own publish wins, whether the owner is a surface or the tree;
85    /// * an inactive publish from the owner ends the session;
86    /// * an owner that published nothing leaves a standing surface standing,
87    ///   unless ownership moved this pass, in either direction: then it is cleared.
88    fn resolve_session_surface(
89        &mut self,
90        main_tree_ime: Option<ImeState>,
91        holder: Option<OverlayKey>,
92        holder_ime: Option<ImeState>,
93    ) {
94        let published = if holder.is_some() {
95            holder_ime
96        } else {
97            main_tree_ime
98        };
99        let ownership_moved = holder != self.focus_surface;
100        match published {
101            Some(ime) if ime.active => self.store_ime_state(Some(ime)),
102            Some(_) => self.release_focus_session(),
103            None if ownership_moved => self.store_ime_state(None),
104            None => {}
105        }
106        // Resolved from the links, never from the request that opened the session.
107        self.focus_surface = if self.focus_active { holder } else { None };
108    }
109
110    /// Paint the main widget tree, returning its outcome and the IME surface it
111    /// published (if any). Split from [`RenderRoot::paint`] for borrow scoping;
112    /// the publish is settled by [`RenderRoot::resolve_session_surface`].
113    fn paint_main_tree(
114        &mut self,
115        scene: &mut dyn PaintScene,
116        frame_time: FrameTime,
117    ) -> (PaintOutcome, Option<ImeState>) {
118        let Some(root_id) = self.root_id else {
119            return (PaintOutcome::default(), None);
120        };
121        // Read out before the `&mut self.tree` borrow below.
122        let theme = self.theme.as_deref();
123        let insets = self.insets;
124        let presented_frames = self.presented_frames;
125        let surface_translucent = self.surface_translucent;
126        let (paint_epoch, retained_layers) = (self.paint_epoch, self.retained_layers);
127        let paint_serial = self.paint_serial;
128        let focus_ring = self.focus_ring_visible();
129        let retained_ledger = &self.retained_ledger;
130        let hints = Rc::clone(&self.last_hints);
131        let Some(pod) = self.tree.pod_mut(root_id) else {
132            return (PaintOutcome::default(), None);
133        };
134        let mut ctx = PaintCtx::new(pod.origin(), pod.size()).with_shortcut_hints(hints);
135        // The pass's global inputs (clock, theme, insets, frame count, translucency, retain).
136        ctx.set_frame_time(frame_time);
137        ctx.set_theme(theme);
138        ctx.set_window_insets(insets);
139        ctx.set_presented_frames(presented_frames);
140        ctx.set_translucent(surface_translucent);
141        ctx.set_paint_state(
142            paint_epoch,
143            retained_layers,
144            Some(retained_ledger),
145            paint_serial,
146        );
147        // Seed focus/hover from the root's mirrors and live epochs; `ChildPod::paint_child`
148        // counts a deeper link only while its stamp names the live session.
149        ctx.set_has_focus(self.focus_active);
150        ctx.set_focus_ring(focus_ring);
151        ctx.set_focus_epoch(self.focus_epoch);
152        ctx.set_hovered(self.hover_active);
153        ctx.set_hover_epoch(self.hover_epoch);
154        pod.widget_mut().paint(&mut ctx, scene);
155        pod.clear_flags();
156        // Collected for `resolve_session_surface`, and only while a session is live,
157        // so a lagging widget cannot resurrect the surface a blur dropped.
158        let main_tree_ime = if self.focus_active {
159            ctx.take_ime_state()
160        } else {
161            None
162        };
163        // Replaced every pass, so a stale platform-view frame or input shield
164        // never outlives the paint that stopped publishing it.
165        self.platform_view_frames = ctx.take_platform_views();
166        self.input_shields = ctx.take_input_shields();
167        let outcome = fold_main_tree_outcome(
168            &ctx,
169            &mut self.pending,
170            &mut self.painted_animating,
171            &mut self.deferred_frame,
172        );
173        (outcome, main_tree_ime)
174    }
175
176    /// Paint this pass's floated pods above the main tree, each at its own
177    /// `window_rect` with the root pod's seeding, folding outcomes into `outcome`.
178    /// Retires stale focus links
179    /// ([`ChildPod::retire_stale_focus_link`](crate::widget::ChildPod::retire_stale_focus_link))
180    /// and returns `(holder, holder's publish)` for the one surface holding a live
181    /// link; other pods' publishes are dropped, and two holders resolve to none.
182    fn paint_overlays(
183        &mut self,
184        entries: &[OverlayEntry],
185        scene: &mut dyn PaintScene,
186        frame_time: FrameTime,
187        outcome: &mut PaintOutcome,
188    ) -> (Option<OverlayKey>, Option<ImeState>) {
189        if entries.is_empty() {
190            return (None, None);
191        }
192        let (paint_epoch, retained_layers) = (self.paint_epoch, self.retained_layers);
193        let paint_serial = self.paint_serial;
194        let retained_ledger = &self.retained_ledger;
195        let theme = self.theme.as_deref();
196        let presented_frames = self.presented_frames;
197        let surface_translucent = self.surface_translucent;
198        let hover_active = self.hover_active;
199        let hover_epoch = self.hover_epoch;
200        let focus_active = self.focus_active;
201        let focus_epoch = self.focus_epoch;
202        let focus_ring = self.focus_ring_visible();
203
204        // One pair, so a publish is never attributed to an entry that did not make it.
205        let mut holder: Option<(OverlayKey, Option<ImeState>)> = None;
206        let mut contested = false;
207
208        for entry in entries {
209            let mut ctx = PaintCtx::new(entry.window_rect.origin(), entry.window_rect.size())
210                .with_shortcut_hints(Rc::clone(&self.last_hints));
211            ctx.set_frame_time(frame_time);
212            ctx.set_theme(theme);
213            ctx.set_window_insets(entry.insets);
214            ctx.set_presented_frames(presented_frames);
215            ctx.set_translucent(surface_translucent);
216            ctx.set_paint_state(
217                paint_epoch,
218                retained_layers,
219                Some(retained_ledger),
220                paint_serial,
221            );
222            // The pod's own link is ANDed onto the mirror inside `paint_child`.
223            ctx.set_has_focus(focus_active);
224            ctx.set_focus_ring(focus_ring);
225            ctx.set_focus_epoch(focus_epoch);
226            ctx.set_hovered(hover_active);
227            ctx.set_hover_epoch(hover_epoch);
228            let pod_holds_focus = {
229                let _stamp = OverlayStamp::enter(entry.key);
230                let mut pod = entry.pod.borrow_mut();
231                pod.retire_stale_focus_link();
232                let holds = pod.holds_live_focus();
233                pod.paint_child(&mut ctx, scene);
234                holds
235            };
236
237            fold_overlay_outcome(&ctx, outcome, &mut self.pending);
238            // Taken unconditionally and kept only from the live holder; an inactive
239            // publish from any other pod is refused too, not read as a release.
240            let published = ctx.take_ime_state();
241            if focus_active && pod_holds_focus {
242                if holder.is_some() {
243                    contested = true;
244                } else {
245                    holder = Some((entry.key, published));
246                }
247            }
248            // Extend, not replace: `paint_main_tree` already filled these this pass.
249            self.platform_view_frames.extend(ctx.take_platform_views());
250            self.input_shields.extend(ctx.take_input_shields());
251        }
252
253        match holder {
254            Some((key, published)) if !contested => (Some(key), published),
255            _ => (None, None),
256        }
257    }
258
259    /// Store this pass's selection-toolbar publish as a level (newest wins) and
260    /// bump [`RenderRoot::selection_toolbar_generation`] only on a menu edge. The
261    /// publish is refused while no focus session is active, as the IME one is.
262    fn resolve_selection_toolbar(&mut self, published: Option<SelectionToolbarRequest>) {
263        let next = if self.focus_active { published } else { None };
264        // An absent request reads as "no menu, no verbs": one comparison covers every edge.
265        let menu_edge = |request: &Option<SelectionToolbarRequest>| {
266            request.map_or((false, SelectionToolbarActions::default()), |request| {
267                (request.present_menu, request.actions)
268            })
269        };
270        if menu_edge(&self.selection_toolbar) != menu_edge(&next) {
271            self.selection_toolbar_gen = self.selection_toolbar_gen.wrapping_add(1);
272        }
273        self.selection_toolbar = next;
274    }
275
276    /// The selection-toolbar request the focused field published during the most
277    /// recent [`RenderRoot::paint`], or `None` when no field is focused. A level:
278    /// a shell driving the platform edit menu
279    /// ([`SelectionToolbarPolicy::Native`](crate::selection_toolbar::SelectionToolbarPolicy::Native))
280    /// reads it beside [`RenderRoot::ime_state`]. Present with a bare caret too
281    /// (paste applies), and under the framework policy as well.
282    pub fn selection_toolbar(&self) -> Option<SelectionToolbarRequest> {
283        self.selection_toolbar
284    }
285
286    /// A generation bumped on every change to the menu-significant part of
287    /// [`RenderRoot::selection_toolbar`] (`present_menu` and `actions`, clearing
288    /// included). A moved anchor is not an edge: a shell re-reads it per frame,
289    /// caching this value like [`RenderRoot::focus_ime_generation`].
290    pub fn selection_toolbar_generation(&self) -> u64 {
291        self.selection_toolbar_gen
292    }
293}
294
295/// Fold the main tree's paint-time layout dirtying into `pending` (`request_layout`, an unpaced
296/// frame, the path bit, the root's landing paint) and build its outcome. A paced-only frame folds
297/// nothing, or the frame gate would run it every vsync. Writes `painted_animating`, drains
298/// `deferred_frame`.
299fn fold_main_tree_outcome(
300    ctx: &PaintCtx<'_>,
301    pending: &mut ChangeFlags,
302    painted_animating: &mut bool,
303    deferred_frame: &mut bool,
304) -> PaintOutcome {
305    let needs_layout = ctx.needs_layout();
306    let animating = needs_layout || ctx.needs_frame();
307    let unpaced = needs_layout || (ctx.needs_frame() && !ctx.needs_frame_paced_only());
308    let landing = *painted_animating && !animating;
309    if unpaced || landing || ctx.is_layout_path_dirty() {
310        *pending |= ChangeFlags::LAYOUT;
311    }
312    *painted_animating = animating;
313    // A rebuild that ran out of flush passes owes one more frame.
314    let deferred_frame = std::mem::take(deferred_frame);
315    PaintOutcome {
316        needs_frame: ctx.needs_frame() || deferred_frame,
317        needs_layout,
318        needs_frame_paced_only: ctx.needs_frame_paced_only(),
319        paced_interval: ctx.paced_interval(),
320        // Stamped by `paint` once both halves of the pass have run.
321        paint_serial: 0,
322    }
323}
324
325/// Fold one floated pod's paint into `outcome`: the frame request ORed, its class on the max
326/// lattice, the paced interval on the MIN lattice; layout dirtying as in the tree, into `pending`
327/// too (the pod's own mark makes the next pass a full one).
328fn fold_overlay_outcome(ctx: &PaintCtx<'_>, outcome: &mut PaintOutcome, pending: &mut ChangeFlags) {
329    let stood_unpaced = outcome.needs_frame && !outcome.needs_frame_paced_only;
330    let entry_unpaced = ctx.needs_frame() && !ctx.needs_frame_paced_only();
331    outcome.needs_frame |= ctx.needs_frame();
332    outcome.needs_frame_paced_only = outcome.needs_frame && !(stood_unpaced || entry_unpaced);
333    outcome.paced_interval = match (outcome.paced_interval, ctx.paced_interval()) {
334        (Some(standing), Some(asked)) => Some(standing.min(asked)),
335        (standing, asked) => standing.or(asked),
336    };
337    if ctx.needs_layout() {
338        outcome.needs_layout = true;
339        *pending |= ChangeFlags::LAYOUT;
340    }
341    if ctx.is_layout_path_dirty() {
342        *pending |= ChangeFlags::LAYOUT;
343    }
344}