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}