1use 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#[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 pub fill: bool,
42 pub tight: bool,
46 pub fill_each: Option<[bool; 2]>,
50}
51
52impl Constraints {
53 pub fn definite(size: Size) -> Self {
55 Self {
56 w: size.width,
57 h: size.height,
58 free_x: false,
59 free_y: false,
60 fill: false,
61 tight: false,
62 fill_each: None,
63 }
64 }
65
66 pub fn filled(size: Size) -> Self {
68 Self {
69 fill: true,
70 ..Self::definite(size)
71 }
72 }
73
74 pub fn scroll(w: f32, h: f32, free_x: bool, free_y: bool) -> Self {
76 Self {
77 w,
78 h,
79 free_x,
80 free_y,
81 fill: false,
82 tight: false,
83 fill_each: None,
84 }
85 }
86
87 pub fn pinned(w: Option<f32>, h: Option<f32>, available: Size) -> Self {
90 Self {
91 w: w.unwrap_or(available.width),
92 h: h.unwrap_or(available.height),
93 free_x: w.is_none(),
94 free_y: h.is_none(),
95 fill: false,
96 tight: true,
97 fill_each: None,
98 }
99 }
100
101 pub fn axes(w: f32, h: f32, free: [bool; 2], fill: [bool; 2]) -> Self {
103 Self {
104 fill_each: Some(fill),
105 ..Self::scroll(w, h, free[0], free[1])
106 }
107 }
108}
109
110struct Entry {
113 signature: u64,
114 constraints: Constraints,
115 rects: Vec<Rect>,
116 overflows: Vec<Overflowing>,
120}
121
122#[derive(Default)]
124pub struct LayoutCache {
125 entries: HashMap<WidgetId, Entry>,
126 touched: HashSet<WidgetId>,
128 last_hits: u32,
130 last_misses: u32,
131 hits: u32,
132 misses: u32,
133}
134
135impl LayoutCache {
136 pub(crate) fn rects<Msg>(
140 &mut self,
141 key: WidgetId,
142 root: &dyn Widget<Msg>,
143 runtime: &Runtime,
144 theme: &crate::theme::Theme,
145 c: Constraints,
146 ) -> (Vec<Rect>, Vec<Overflowing>) {
147 self.touched.insert(key);
148 let (signature, volatile) = signature_of(root, key, runtime, theme);
149 if !volatile {
150 if let Some(entry) = self.entries.get(&key) {
151 if entry.signature == signature && entry.constraints == c {
152 self.hits += 1;
153 return (entry.rects.clone(), entry.overflows.clone());
154 }
155 }
156 }
157 self.misses += 1;
158 let (rects, overflows) = compute_rects(root, key, runtime, theme, c);
159 if volatile {
160 self.entries.remove(&key);
163 return (rects, overflows);
164 }
165 self.entries.insert(
166 key,
167 Entry {
168 signature,
169 constraints: c,
170 rects: rects.clone(),
171 overflows: overflows.clone(),
172 },
173 );
174 (rects, overflows)
175 }
176
177 pub(crate) fn end_frame(&mut self) {
180 let touched = std::mem::take(&mut self.touched);
181 self.entries.retain(|id, _| touched.contains(id));
182 self.last_hits = self.hits;
183 self.last_misses = self.misses;
184 self.hits = 0;
185 self.misses = 0;
186 }
187
188 pub fn last_frame_stats(&self) -> (u32, u32) {
190 (self.last_hits, self.last_misses)
191 }
192}
193
194fn compute_rects<Msg>(
197 root: &dyn Widget<Msg>,
198 key: WidgetId,
199 runtime: &Runtime,
200 theme: &crate::theme::Theme,
201 c: Constraints,
202) -> (Vec<Rect>, Vec<Overflowing>) {
203 let mut layout = frus_layout::Layout::new();
204 let node = build_layout(root, key, runtime, theme, &mut layout);
205 if c.tight {
208 layout.compute_tight(node, c.w, c.h, c.free_x, c.free_y);
209 } else if let Some(fill) = c.fill_each {
210 layout.compute_axes(node, c.w, c.h, [c.free_x, c.free_y], fill);
211 } else if c.fill {
212 layout.compute_filled(node, c.w, c.h);
213 } else {
214 layout.compute_scroll(node, c.w, c.h, c.free_x, c.free_y);
215 }
216 let rects = layout
217 .absolute_rects(node)
218 .into_iter()
219 .map(|(rect, _)| rect)
220 .collect();
221 (rects, layout.overflows(node))
222}
223
224fn signature_of<Msg>(
237 root: &dyn Widget<Msg>,
238 id: WidgetId,
239 runtime: &Runtime,
240 theme: &crate::theme::Theme,
241) -> (u64, bool) {
242 let mut hasher = std::collections::hash_map::DefaultHasher::new();
243 frus_core::text_scale().to_bits().hash(&mut hasher);
248 let mut volatile = false;
249 hash_node(root, id, runtime, theme, &mut hasher, &mut volatile);
250 (hasher.finish(), volatile)
251}
252
253fn hash_node<Msg, H: Hasher>(
254 widget: &dyn Widget<Msg>,
255 id: WidgetId,
256 runtime: &Runtime,
257 theme: &crate::theme::Theme,
258 hasher: &mut H,
259 volatile: &mut bool,
260) {
261 let scoped = crate::ui::scoped_theme(widget, id, runtime, theme);
266 let theme = scoped.as_deref().unwrap_or(theme);
267 let surface = widget.media_override(crate::MediaQuery::of());
271 let _surface = surface.map(crate::MediaQuery::install);
272 if let Some(mq) = surface {
273 mq.measure_hash(hasher);
274 }
275 let shell = widget.scaffold_override();
278 if let Some(info) = &shell {
279 info.shape_hash(hasher);
280 }
281 let _shell = shell.map(crate::ScaffoldInfo::install);
282 let scrolling = widget.scroll_behavior_override();
285 if let Some(behavior) = &scrolling {
286 behavior.shape_hash(hasher);
287 }
288 let _scrolling = scrolling.map(crate::ScrollConfiguration::install);
289 widget.build_in(id, runtime, theme);
293 if let Some(q) = widget.rotated_quarter_turns() {
301 4u8.hash(hasher);
302 q.hash(hasher);
303 effective_style(widget, id, runtime, theme).layout_hash(hasher);
304 if let Some(child) = widget.children().first() {
305 hash_node(
306 child.as_ref(),
307 child_id(id, 0, child.as_ref()),
308 runtime,
309 theme,
310 hasher,
311 volatile,
312 );
313 }
314 return;
315 }
316 if widget.scroll_content().is_some()
317 || widget.interactive().is_some()
318 || widget.fitted().is_some()
319 || widget.navigator().is_some()
320 || widget.virtual_list(frus_core::Size::ZERO).is_some()
323 || widget.page_view().is_some()
324 || widget.overflow_box().is_some()
325 || widget.stack()
326 || widget.custom_layout().is_some()
327 {
328 1u8.hash(hasher);
329 effective_style(widget, id, runtime, theme).layout_hash(hasher);
330 return;
331 }
332 if let Some(transform) = widget.constraints_transform() {
337 5u8.hash(hasher);
338 transform.layout_hash(hasher);
339 let fills = widget.fill_axes(theme);
340 (fills.horizontal, fills.vertical).hash(hasher);
341 if let Some((across, down)) = widget.size_factor() {
344 across.to_bits().hash(hasher);
345 down.to_bits().hash(hasher);
346 }
347 if widget.animated_size().is_some() {
350 if let Some((now, to)) = runtime.size_anims.borrow_mut().fingerprint(id) {
351 for v in [now.width, now.height, to.width, to.height] {
352 v.to_bits().hash(hasher);
353 }
354 }
355 }
356 effective_style(widget, id, runtime, theme).layout_hash(hasher);
357 if let Some(child) = widget.children().first() {
358 hash_node(
359 child.as_ref(),
360 child_id(id, 0, child.as_ref()),
361 runtime,
362 theme,
363 hasher,
364 volatile,
365 );
366 }
367 return;
368 }
369 if widget.layout_builder().is_some() {
373 1u8.hash(hasher);
374 effective_style(widget, id, runtime, theme).layout_hash(hasher);
375 *volatile = true;
376 return;
377 }
378 if widget.overlay().is_some() {
379 2u8.hash(hasher);
380 effective_style(widget, id, runtime, theme).layout_hash(hasher);
381 let anchor = widget.children()[0].as_ref();
382 hash_node(
383 anchor,
384 child_id(id, 0, anchor),
385 runtime,
386 theme,
387 hasher,
388 volatile,
389 );
390 return;
391 }
392 let children = widget.children();
393 3u8.hash(hasher);
394 effective_style(widget, id, runtime, theme).layout_hash(hasher);
395 widget.measure_key(theme).hash(hasher);
399 let fills = widget.fill_axes(theme);
402 (fills.horizontal, fills.vertical).hash(hasher);
403 widget.main_axis_floor(theme).map(f32::to_bits).hash(hasher);
404 widget.tile_shape().map(f32::to_bits).hash(hasher);
405 children.len().hash(hasher);
406 for (i, child) in children.iter().enumerate() {
407 hash_node(
408 child.as_ref(),
409 child_id(id, i, child.as_ref()),
410 runtime,
411 theme,
412 hasher,
413 volatile,
414 );
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::{Container, Flex};
422
423 fn sig<Msg>(w: &dyn Widget<Msg>) -> u64 {
424 signature_of(
425 w,
426 WidgetId::ROOT,
427 &Runtime::default(),
428 &crate::theme::Theme::default(),
429 )
430 .0
431 }
432
433 #[test]
434 fn identical_trees_share_a_signature() {
435 let a: Container<()> = Container::new().width(100.0).height(40.0);
436 let b: Container<()> = Container::new().width(100.0).height(40.0);
437 assert_eq!(sig(&a), sig(&b));
438 }
439
440 #[test]
441 fn a_size_change_changes_the_signature() {
442 let a: Container<()> = Container::new().width(100.0).height(40.0);
443 let b: Container<()> = Container::new().width(101.0).height(40.0);
444 assert_ne!(sig(&a), sig(&b));
445 }
446
447 #[test]
448 fn child_count_changes_the_signature() {
449 let one: Flex<()> = Flex::row().child(Container::new());
450 let two: Flex<()> = Flex::row().child(Container::new()).child(Container::new());
451 assert_ne!(sig(&one), sig(&two));
452 }
453
454 #[test]
459 fn a_change_of_main_axis_size_changes_the_signature() {
460 let fills: crate::Row<()> = crate::Row::new().child(Container::new());
461 let hugs: crate::Row<()> = crate::Row::new().shrink_wrap().child(Container::new());
462 assert_ne!(sig(&fills), sig(&hugs));
463 }
464
465 #[test]
468 fn a_measured_box_can_be_reused_and_still_notices_a_change() {
469 let rt = Runtime::default();
470 let theme = crate::theme::Theme::default();
471 let c = Constraints::definite(Size::new(300.0, 200.0));
472 let page = |label: &'static str, rule: crate::constraints::AxisConstraint| -> Flex<()> {
473 Flex::column()
474 .child(Container::new().width(10.0).height(20.0))
477 .child(crate::ConstraintsTransformBox::new(crate::Text::new(label)).width(rule))
478 };
479 let unbounded = crate::constraints::AxisConstraint::Unbounded;
480 let mut cache = LayoutCache::default();
481 let first = cache.rects(WidgetId::ROOT, &page("one", unbounded), &rt, &theme, c);
482 let again = cache.rects(WidgetId::ROOT, &page("one", unbounded), &rt, &theme, c);
483 assert_eq!(first, again);
484 assert_eq!(
485 (cache.hits, cache.misses),
486 (1, 1),
487 "the second frame is reused"
488 );
489
490 let longer = page("a much longer label", unbounded);
492 let changed = cache.rects(WidgetId::ROOT, &longer, &rt, &theme, c);
493 assert_eq!(cache.misses, 2, "a different child is a different box");
494 assert!(changed.0.last().unwrap().width > first.0.last().unwrap().width);
495
496 let fixed = page(
497 "a much longer label",
498 crate::constraints::AxisConstraint::Fixed(250.0),
499 );
500 let ruled = cache.rects(WidgetId::ROOT, &fixed, &rt, &theme, c);
501 assert_eq!(cache.misses, 3, "a different rule is a different box");
502 assert_eq!(ruled.0.last().unwrap().width, 250.0);
503
504 let wider = page(
505 "a much longer label",
506 crate::constraints::AxisConstraint::Fixed(280.0),
507 );
508 let rewidth = cache.rects(WidgetId::ROOT, &wider, &rt, &theme, c);
509 assert_eq!(cache.misses, 4, "a different number is a different rule");
510 assert_eq!(rewidth.0.last().unwrap().width, 280.0);
511 }
512
513 #[test]
515 fn a_layout_builder_still_poisons_its_root() {
516 let rt = Runtime::default();
517 let theme = crate::theme::Theme::default();
518 let c = Constraints::definite(Size::new(300.0, 200.0));
519 let tree: crate::LayoutBuilder<()> = crate::LayoutBuilder::new(|_| Container::new());
520 let mut cache = LayoutCache::default();
521 cache.rects(WidgetId::ROOT, &tree, &rt, &theme, c);
522 cache.rects(WidgetId::ROOT, &tree, &rt, &theme, c);
523 assert_eq!((cache.hits, cache.misses), (0, 2));
524 }
525
526 #[test]
527 fn cache_hits_on_repeated_identical_root() {
528 let mut cache = LayoutCache::default();
529 let rt = Runtime::default();
530 let key = WidgetId::ROOT;
531 let c = Constraints::definite(Size::new(200.0, 100.0));
532
533 let tree: Container<()> = Container::new().width(200.0).height(100.0);
534 let first = cache.rects(key, &tree, &rt, &crate::theme::Theme::default(), c);
535 let second = cache.rects(key, &tree, &rt, &crate::theme::Theme::default(), c);
536 assert_eq!(first, second, "the same rectangles");
537 assert_eq!((cache.hits, cache.misses), (1, 1));
539 }
540
541 #[test]
542 fn changed_constraints_miss() {
543 let mut cache = LayoutCache::default();
544 let rt = Runtime::default();
545 let key = WidgetId::ROOT;
546 let tree: Container<()> = Container::new();
547 let theme = crate::theme::Theme::default();
548 cache.rects(
549 key,
550 &tree,
551 &rt,
552 &theme,
553 Constraints::definite(Size::new(200.0, 100.0)),
554 );
555 cache.rects(
556 key,
557 &tree,
558 &rt,
559 &theme,
560 Constraints::definite(Size::new(300.0, 100.0)),
561 );
562 assert_eq!(
563 (cache.hits, cache.misses),
564 (0, 2),
565 "size changed → recomputed"
566 );
567 }
568
569 #[test]
570 fn end_frame_evicts_untouched_roots() {
571 let mut cache = LayoutCache::default();
572 let rt = Runtime::default();
573 let c = Constraints::definite(Size::new(10.0, 10.0));
574 let tree: Container<()> = Container::new();
575 cache.rects(
576 WidgetId::ROOT,
577 &tree,
578 &rt,
579 &crate::theme::Theme::default(),
580 c,
581 );
582 cache.rects(
583 WidgetId::ROOT.child(0),
584 &tree,
585 &rt,
586 &crate::theme::Theme::default(),
587 c,
588 );
589 cache.end_frame();
590 assert_eq!(cache.entries.len(), 2);
591
592 cache.rects(
594 WidgetId::ROOT,
595 &tree,
596 &rt,
597 &crate::theme::Theme::default(),
598 c,
599 );
600 cache.end_frame();
601 assert_eq!(cache.entries.len(), 1);
602 assert!(cache.entries.contains_key(&WidgetId::ROOT));
603 }
604
605 #[test]
616 fn a_text_style_in_flight_moves_the_signature() {
617 let tree = |size: f32| {
618 crate::AnimatedDefaultTextStyle::<()>::new(
619 0.10,
620 frus_core::Curve::Linear,
621 frus_core::TextStyle::NONE.size(size),
622 crate::Text::new("Ag"),
623 )
624 };
625 let theme = crate::theme::Theme::default();
626 let mut rt = Runtime::default();
627 rt.advance_text_styles(&tree(24.0), 1.0);
628 rt.advance_text_styles(&tree(12.0), 0.02);
629 let early = signature_of(&tree(12.0), WidgetId::ROOT, &rt, &theme).0;
630 rt.advance_text_styles(&tree(12.0), 0.04);
631 let later = signature_of(&tree(12.0), WidgetId::ROOT, &rt, &theme).0;
632 assert_ne!(
633 early, later,
634 "the same tree at two points of one movement must not share a fingerprint"
635 );
636 }
637}