Skip to main content

frus_widgets/
relayout.rs

1//! The **relayout boundary** cache: holds, between frames, the computed rectangles
2//! of each layout root, indexed by identity.
3//!
4//! Frus rebuilds the widget tree on every frame and, until now, re-ran taffy *from
5//! scratch* at every layout root (`build_ui`, every scrollable, every screen, every
6//! overlay…). Yet the geometry depends **only** on the tree's *style* and
7//! *structure* and on the parent's *constraints* — not on colors or text, which
8//! only affect painting. A hover, a blinking caret, an animating color → the same
9//! layout.
10//!
11//! This cache records, per root (`WidgetId`), `(fingerprint, constraints,
12//! rectangles)`. If the layout fingerprint and the constraints are unchanged, the
13//! **rectangles are reused** and taffy is not called again. The output is
14//! **bit-for-bit identical** to the full computation — only the performance
15//! changes. It is also the foundation of the future phase system: "fingerprint
16//! changed" *is* a root's "layout dirty" bit.
17
18use std::collections::{HashMap, HashSet};
19use std::hash::{Hash, Hasher};
20
21use frus_core::{Rect, Size};
22use frus_layout::Overflowing;
23
24use crate::interaction::WidgetId;
25use crate::runtime::Runtime;
26use crate::ui::{build_layout, child_id, effective_style};
27use crate::widget::Widget;
28
29/// The constraints passed to taffy for a layout root. `free_x`/`free_y` leave an
30/// axis **free** (the content takes its natural size) — the case of scrollable
31/// content.
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub(crate) struct Constraints {
34    pub w: f32,
35    pub h: f32,
36    pub free_x: bool,
37    pub free_y: bool,
38    /// **Fill** the box rather than hug the content: an unset (`Auto`) dimension of
39    /// the root becomes the available size. For content that is *given* its box —
40    /// a page of a [`crate::PageView`] — rather than asked how big it wants to be.
41    pub fill: bool,
42    /// **Force** the constrained axes rather than merely fill them: a size the content
43    /// chose for itself is overruled. What a stack layer pinned to two opposite edges
44    /// asks for — see [`frus_layout::Layout::compute_tight`].
45    pub tight: bool,
46}
47
48impl Constraints {
49    /// Both axes constrained to `size` (ordinary layout).
50    pub fn definite(size: Size) -> Self {
51        Self {
52            w: size.width,
53            h: size.height,
54            free_x: false,
55            free_y: false,
56            fill: false,
57            tight: false,
58        }
59    }
60
61    /// A box the content is **handed**: constrained on both axes, and filled.
62    pub fn filled(size: Size) -> Self {
63        Self {
64            fill: true,
65            ..Self::definite(size)
66        }
67    }
68
69    /// Scrollable content: each axis either constrained or free.
70    pub fn scroll(w: f32, h: f32, free_x: bool, free_y: bool) -> Self {
71        Self {
72            w,
73            h,
74            free_x,
75            free_y,
76            fill: false,
77            tight: false,
78        }
79    }
80
81    /// A box the content is **forced** into on the axes that were given, with the others
82    /// left free so the content's own size comes back on them.
83    pub fn pinned(w: Option<f32>, h: Option<f32>, available: Size) -> Self {
84        Self {
85            w: w.unwrap_or(available.width),
86            h: h.unwrap_or(available.height),
87            free_x: w.is_none(),
88            free_y: h.is_none(),
89            fill: false,
90            tight: true,
91        }
92    }
93}
94
95/// One cache entry: the fingerprint of the last root computed under this identity,
96/// its constraints, and the rectangles produced (in prefix order).
97struct Entry {
98    signature: u64,
99    constraints: Constraints,
100    rects: Vec<Rect>,
101    /// Boxes in this root whose children did not fit. Cached alongside the rectangles
102    /// because it is computed from the same taffy pass and would otherwise need a
103    /// second one on every cache hit.
104    overflows: Vec<Overflowing>,
105}
106
107/// The relayout cache, retained in the [`crate::Runtime`] from one frame to the next.
108#[derive(Default)]
109pub struct LayoutCache {
110    entries: HashMap<WidgetId, Entry>,
111    /// Roots touched during the current frame (to evict the vanished ones).
112    touched: HashSet<WidgetId>,
113    /// Diagnostic: reuses and recomputations of the **last** frame.
114    last_hits: u32,
115    last_misses: u32,
116    hits: u32,
117    misses: u32,
118}
119
120impl LayoutCache {
121    /// The rectangles (prefix order) of root `key` under constraints `c`. Reuses
122    /// the cache if the layout fingerprint **and** the constraints are unchanged;
123    /// otherwise re-runs taffy and records the result.
124    pub(crate) fn rects<Msg>(
125        &mut self,
126        key: WidgetId,
127        root: &dyn Widget<Msg>,
128        runtime: &Runtime,
129        theme: &crate::theme::Theme,
130        c: Constraints,
131    ) -> (Vec<Rect>, Vec<Overflowing>) {
132        self.touched.insert(key);
133        let (signature, volatile) = signature_of(root, key, runtime, theme);
134        if !volatile {
135            if let Some(entry) = self.entries.get(&key) {
136                if entry.signature == signature && entry.constraints == c {
137                    self.hits += 1;
138                    return (entry.rects.clone(), entry.overflows.clone());
139                }
140            }
141        }
142        self.misses += 1;
143        let (rects, overflows) = compute_rects(root, key, runtime, theme, c);
144        if volatile {
145            // Nothing is stored either: an entry that can never be trusted would only
146            // sit there being evicted and re-made.
147            self.entries.remove(&key);
148            return (rects, overflows);
149        }
150        self.entries.insert(
151            key,
152            Entry {
153                signature,
154                constraints: c,
155                rects: rects.clone(),
156                overflows: overflows.clone(),
157            },
158        );
159        (rects, overflows)
160    }
161
162    /// To be called at the end of a frame: forgets untouched roots (vanished
163    /// widgets) and freezes the frame's diagnostic counters.
164    pub(crate) fn end_frame(&mut self) {
165        let touched = std::mem::take(&mut self.touched);
166        self.entries.retain(|id, _| touched.contains(id));
167        self.last_hits = self.hits;
168        self.last_misses = self.misses;
169        self.hits = 0;
170        self.misses = 0;
171    }
172
173    /// Reuses and recomputations of the last completed frame (diagnostic).
174    pub fn last_frame_stats(&self) -> (u32, u32) {
175        (self.last_hits, self.last_misses)
176    }
177}
178
179/// Computes a root's absolute rectangles — the "full" path (build + taffy +
180/// collect), taken on every cache *miss*.
181fn compute_rects<Msg>(
182    root: &dyn Widget<Msg>,
183    key: WidgetId,
184    runtime: &Runtime,
185    theme: &crate::theme::Theme,
186    c: Constraints,
187) -> (Vec<Rect>, Vec<Overflowing>) {
188    let mut layout = frus_layout::Layout::new();
189    let node = build_layout(root, key, runtime, theme, &mut layout);
190    // `compute_scroll(_, _, false, false)` is equivalent to `compute` (both axes
191    // `Definite`): a single path covers both cases.
192    if c.tight {
193        layout.compute_tight(node, c.w, c.h, c.free_x, c.free_y);
194    } else if c.fill {
195        layout.compute_filled(node, c.w, c.h);
196    } else {
197        layout.compute_scroll(node, c.w, c.h, c.free_x, c.free_y);
198    }
199    let rects = layout
200        .absolute_rects(node)
201        .into_iter()
202        .map(|(rect, _)| rect)
203        .collect();
204    (rects, layout.overflows(node))
205}
206
207/// A 64-bit fingerprint of a subtree's **layout** — styles + structure, following
208/// **exactly** the branching of [`build_layout`] (scrollable/navigator/list/stack = a
209/// leaf; portal = the anchor alone), with colours, texts and messages excluded because
210/// they only affect painting — and whether that fingerprint can be **trusted between
211/// frames**.
212///
213/// A `LayoutBuilder`'s box is the size of what its closure built, and a closure cannot be
214/// fingerprinted: two frames whose styles and structure are identical can still want
215/// different geometry, because the application changed what it builds. So a root holding
216/// one is marked *volatile* and recomputed every frame — the cache's contract is that a
217/// hit is **bit-for-bit** what the full computation would have produced, and the only
218/// honest way to keep that here is to take the miss.
219fn signature_of<Msg>(
220    root: &dyn Widget<Msg>,
221    id: WidgetId,
222    runtime: &Runtime,
223    theme: &crate::theme::Theme,
224) -> (u64, bool) {
225    let mut hasher = std::collections::hash_map::DefaultHasher::new();
226    // The **reader's font size**, once, at the root. Every measured box in the tree is
227    // resolved against it, so a reader who changes the system setting changes every
228    // geometry below — and a fingerprint blind to it would answer the new frame with the
229    // old layout and nothing would move.
230    frus_core::text_scale().to_bits().hash(&mut hasher);
231    let mut volatile = false;
232    hash_node(root, id, runtime, theme, &mut hasher, &mut volatile);
233    (hasher.finish(), volatile)
234}
235
236fn hash_node<Msg, H: Hasher>(
237    widget: &dyn Widget<Msg>,
238    id: WidgetId,
239    runtime: &Runtime,
240    theme: &crate::theme::Theme,
241    hasher: &mut H,
242    volatile: &mut bool,
243) {
244    // A themed subtree, exactly as `build_layout` sees it: its styles are resolved
245    // against its own theme, so the fingerprint is taken against that one too. A
246    // fingerprint that skipped the swap would hash one geometry and the cache would
247    // store another.
248    let scoped = crate::ui::scoped_theme(widget, id, runtime, theme);
249    let theme = scoped.as_deref().unwrap_or(theme);
250    // The same for the **surface**: a scoped description changes what the subtree measures
251    // with, so it has to be in force here as well — and hashed, or two different surfaces
252    // would share one fingerprint and the cache would keep the first one's geometry.
253    let surface = widget.media_override(crate::MediaQuery::of());
254    let _surface = surface.map(crate::MediaQuery::install);
255    if let Some(mq) = surface {
256        mq.measure_hash(hasher);
257    }
258    // And the **shell** (milestone 422), hashed for the same reason: whether there is a
259    // drawer decides whether a bar composes a menu button, and a button is a different row.
260    let shell = widget.scaffold_override();
261    if let Some(info) = &shell {
262        info.shape_hash(hasher);
263    }
264    let _shell = shell.map(crate::ScaffoldInfo::install);
265    // The cache exists to **skip** `build_layout`, so this walk can be the first one down
266    // the tree: a deferred subtree has to be composed here too, or the fingerprint would
267    // be taken of a node with no children and the cache would agree with itself forever.
268    widget.build_in(id, runtime, theme);
269    // These branches must stay aligned with `build_layout`: the shape of the taffy
270    // tree (and therefore the number and order of the rectangles) depends on it.
271    // The **effective** style is hashed (animated size injected) — the same source
272    // as `build_layout`, so the fingerprint changes while the size moves.
273    // `RotatedBox`: its box depends on the child's **natural** size (swapped for an
274    // odd quarter), so the fingerprint must include the child; otherwise a modified
275    // child would leave a stale box in the cache.
276    if let Some(q) = widget.rotated_quarter_turns() {
277        4u8.hash(hasher);
278        q.hash(hasher);
279        effective_style(widget, id, runtime, theme).layout_hash(hasher);
280        if let Some(child) = widget.children().first() {
281            hash_node(
282                child.as_ref(),
283                child_id(id, 0, child.as_ref()),
284                runtime,
285                theme,
286                hasher,
287                volatile,
288            );
289        }
290        return;
291    }
292    if widget.scroll_content().is_some()
293        || widget.interactive().is_some()
294        || widget.fitted().is_some()
295        || widget.navigator().is_some()
296        // Only *whether*, never *what*: this asks if the widget windows its
297        // children at all, and a size it will not read is the honest argument.
298        || widget.virtual_list(frus_core::Size::ZERO).is_some()
299        || widget.page_view().is_some()
300        || widget.overflow_box().is_some()
301        || widget.stack()
302    {
303        1u8.hash(hasher);
304        effective_style(widget, id, runtime, theme).layout_hash(hasher);
305        return;
306    }
307    // A box that transforms its constraints hashes like a leaf and poisons the entry, for
308    // the same reason a `LayoutBuilder` does and one more: its box comes from a closure,
309    // and a closure cannot be hashed. Two frames whose trees fingerprint alike could ask
310    // two different questions of the same child.
311    if widget.constraints_transform().is_some() || widget.layout_builder().is_some() {
312        1u8.hash(hasher);
313        effective_style(widget, id, runtime, theme).layout_hash(hasher);
314        *volatile = true;
315        return;
316    }
317    if widget.overlay().is_some() {
318        2u8.hash(hasher);
319        effective_style(widget, id, runtime, theme).layout_hash(hasher);
320        let anchor = widget.children()[0].as_ref();
321        hash_node(
322            anchor,
323            child_id(id, 0, anchor),
324            runtime,
325            theme,
326            hasher,
327            volatile,
328        );
329        return;
330    }
331    let children = widget.children();
332    3u8.hash(hasher);
333    effective_style(widget, id, runtime, theme).layout_hash(hasher);
334    // A measured leaf: its **content** (text…) affects the geometry without going
335    // through the style — without this fingerprint, two different contents would
336    // be conflated and the cache would keep an old layout.
337    widget.measure_key(theme).hash(hasher);
338    // Filling the parent is resolved during the walk rather than written into the style,
339    // so the style hash alone would conflate a run that fills with one that shrink-wraps.
340    let fills = widget.fill_axes(theme);
341    (fills.horizontal, fills.vertical).hash(hasher);
342    widget.main_axis_floor(theme).map(f32::to_bits).hash(hasher);
343    widget.tile_shape().map(f32::to_bits).hash(hasher);
344    children.len().hash(hasher);
345    for (i, child) in children.iter().enumerate() {
346        hash_node(
347            child.as_ref(),
348            child_id(id, i, child.as_ref()),
349            runtime,
350            theme,
351            hasher,
352            volatile,
353        );
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::{Container, Flex};
361
362    fn sig<Msg>(w: &dyn Widget<Msg>) -> u64 {
363        signature_of(
364            w,
365            WidgetId::ROOT,
366            &Runtime::default(),
367            &crate::theme::Theme::default(),
368        )
369        .0
370    }
371
372    #[test]
373    fn identical_trees_share_a_signature() {
374        let a: Container<()> = Container::new().width(100.0).height(40.0);
375        let b: Container<()> = Container::new().width(100.0).height(40.0);
376        assert_eq!(sig(&a), sig(&b));
377    }
378
379    #[test]
380    fn a_size_change_changes_the_signature() {
381        let a: Container<()> = Container::new().width(100.0).height(40.0);
382        let b: Container<()> = Container::new().width(101.0).height(40.0);
383        assert_ne!(sig(&a), sig(&b));
384    }
385
386    #[test]
387    fn child_count_changes_the_signature() {
388        let one: Flex<()> = Flex::row().child(Container::new());
389        let two: Flex<()> = Flex::row().child(Container::new()).child(Container::new());
390        assert_ne!(sig(&one), sig(&two));
391    }
392
393    /// Filling the parent is resolved during the walk and never reaches the style, so
394    /// the fingerprint has to ask for it separately — otherwise a row that fills and one
395    /// that shrink-wraps are the same tree as far as the cache is concerned, and the
396    /// second one silently gets the first one's layout.
397    #[test]
398    fn a_change_of_main_axis_size_changes_the_signature() {
399        let fills: crate::Row<()> = crate::Row::new().child(Container::new());
400        let hugs: crate::Row<()> = crate::Row::new().shrink_wrap().child(Container::new());
401        assert_ne!(sig(&fills), sig(&hugs));
402    }
403
404    #[test]
405    fn cache_hits_on_repeated_identical_root() {
406        let mut cache = LayoutCache::default();
407        let rt = Runtime::default();
408        let key = WidgetId::ROOT;
409        let c = Constraints::definite(Size::new(200.0, 100.0));
410
411        let tree: Container<()> = Container::new().width(200.0).height(100.0);
412        let first = cache.rects(key, &tree, &rt, &crate::theme::Theme::default(), c);
413        let second = cache.rects(key, &tree, &rt, &crate::theme::Theme::default(), c);
414        assert_eq!(first, second, "the same rectangles");
415        // 1 miss (computed), then 1 hit (reused).
416        assert_eq!((cache.hits, cache.misses), (1, 1));
417    }
418
419    #[test]
420    fn changed_constraints_miss() {
421        let mut cache = LayoutCache::default();
422        let rt = Runtime::default();
423        let key = WidgetId::ROOT;
424        let tree: Container<()> = Container::new();
425        let theme = crate::theme::Theme::default();
426        cache.rects(
427            key,
428            &tree,
429            &rt,
430            &theme,
431            Constraints::definite(Size::new(200.0, 100.0)),
432        );
433        cache.rects(
434            key,
435            &tree,
436            &rt,
437            &theme,
438            Constraints::definite(Size::new(300.0, 100.0)),
439        );
440        assert_eq!(
441            (cache.hits, cache.misses),
442            (0, 2),
443            "size changed → recomputed"
444        );
445    }
446
447    #[test]
448    fn end_frame_evicts_untouched_roots() {
449        let mut cache = LayoutCache::default();
450        let rt = Runtime::default();
451        let c = Constraints::definite(Size::new(10.0, 10.0));
452        let tree: Container<()> = Container::new();
453        cache.rects(
454            WidgetId::ROOT,
455            &tree,
456            &rt,
457            &crate::theme::Theme::default(),
458            c,
459        );
460        cache.rects(
461            WidgetId::ROOT.child(0),
462            &tree,
463            &rt,
464            &crate::theme::Theme::default(),
465            c,
466        );
467        cache.end_frame();
468        assert_eq!(cache.entries.len(), 2);
469
470        // Next frame: only one root touched → the other is evicted.
471        cache.rects(
472            WidgetId::ROOT,
473            &tree,
474            &rt,
475            &crate::theme::Theme::default(),
476            c,
477        );
478        cache.end_frame();
479        assert_eq!(cache.entries.len(), 1);
480        assert!(cache.entries.contains_key(&WidgetId::ROOT));
481    }
482
483    /// **A style in flight moves the fingerprint.** The same tree twice, at two points of
484    /// one movement: nothing differs but the runtime.
485    ///
486    /// This is the cache's half of milestone 498, and it is not a formality. A font size
487    /// reaches *layout* — the box a run of words is measured into — so a fingerprint
488    /// blind to the style the theme swap put in force would answer every frame of the
489    /// movement with the first frame's geometry. The words would grow inside a box that
490    /// did not, and the layout would snap into place at the end. That failure is exactly
491    /// what this cache's contract forbids: a hit must be bit-for-bit what the full
492    /// computation would have produced.
493    #[test]
494    fn a_text_style_in_flight_moves_the_signature() {
495        let tree = |size: f32| {
496            crate::AnimatedDefaultTextStyle::<()>::new(
497                0.10,
498                frus_core::Curve::Linear,
499                frus_core::TextStyle::NONE.size(size),
500                crate::Text::new("Ag"),
501            )
502        };
503        let theme = crate::theme::Theme::default();
504        let mut rt = Runtime::default();
505        rt.advance_text_styles(&tree(24.0), 1.0);
506        rt.advance_text_styles(&tree(12.0), 0.02);
507        let early = signature_of(&tree(12.0), WidgetId::ROOT, &rt, &theme).0;
508        rt.advance_text_styles(&tree(12.0), 0.04);
509        let later = signature_of(&tree(12.0), WidgetId::ROOT, &rt, &theme).0;
510        assert_ne!(
511            early, later,
512            "the same tree at two points of one movement must not share a fingerprint"
513        );
514    }
515}