1use std::any::Any;
5use std::collections::HashMap;
6
7use kurbo::{Point, Size};
8
9use super::RenderRoot;
10use crate::layout::BoxConstraints;
11use crate::view::{View, WidgetId};
12use crate::widget::LayoutCtx;
13
14#[derive(Default)]
19pub(super) struct LayoutAudit {
20 pub(super) enabled: bool,
21 pub(super) mismatches: u64,
22 frame: u64,
24 last_logged: HashMap<(&'static str, Option<String>), u64>,
26}
27
28struct AuditGeom {
30 id: WidgetId,
31 type_name: &'static str,
32 label: Option<String>,
33 origin: Point,
34 size: Size,
35}
36
37impl<State: 'static, V: View<State>> RenderRoot<State, V> {
38 pub fn layout(&mut self, window_size: Size) -> Size {
43 self.layout_inner(window_size, None)
44 }
45
46 pub fn layout_with_text(&mut self, window_size: Size, text_ctx: &mut dyn Any) -> Size {
53 self.layout_inner(window_size, Some(text_ctx))
54 }
55
56 fn layout_inner(&mut self, window_size: Size, text_ctx: Option<&mut dyn Any>) -> Size {
58 if self.window_size != window_size {
61 self.bump_layout_epoch();
62 }
63 self.window_size = window_size;
64 if self.overlay_pod_dirty() {
67 self.bump_layout_epoch();
68 }
69 if self.root_id.is_none() {
70 return Size::ZERO;
71 }
72 if !(self.layout_audit.enabled && self.layout_skip) {
73 return self.layout_pass(window_size, text_ctx, !self.layout_skip);
74 }
75 let mut text_ctx = text_ctx;
77 self.layout_pass(window_size, text_ctx.as_deref_mut(), false);
78 let before = self.audit_snapshot();
79 let size = self.layout_pass(window_size, text_ctx, true);
80 let after = self.audit_snapshot();
81 self.audit_compare(&before, &after);
82 size
83 }
84
85 fn audit_snapshot(&self) -> Vec<AuditGeom> {
87 let nodes = self.tree.inspect();
88 let origins: HashMap<WidgetId, Point> =
89 nodes.iter().map(|n| (n.id, n.bounds.origin())).collect();
90 nodes
91 .into_iter()
92 .map(|n| {
93 let parent = n
94 .parent
95 .and_then(|p| origins.get(&p).copied())
96 .unwrap_or(Point::ZERO);
97 AuditGeom {
98 id: n.id,
99 type_name: n.type_name,
100 label: n.debug_label.map(|l| l.into_owned()),
101 origin: Point::new(n.bounds.x0 - parent.x, n.bounds.y0 - parent.y),
102 size: n.bounds.size(),
103 }
104 })
105 .collect()
106 }
107
108 fn audit_compare(&mut self, before: &[AuditGeom], after: &[AuditGeom]) {
110 const TOLERANCE: f64 = 1e-3;
111 let frame = self.layout_audit.frame;
112 self.layout_audit.frame += 1;
113 let by_id: HashMap<WidgetId, &AuditGeom> = before.iter().map(|g| (g.id, g)).collect();
114 for now in after {
115 let Some(was) = by_id.get(&now.id) else {
116 continue;
117 };
118 let differs = (was.origin.x - now.origin.x).abs() > TOLERANCE
119 || (was.origin.y - now.origin.y).abs() > TOLERANCE
120 || (was.size.width - now.size.width).abs() > TOLERANCE
121 || (was.size.height - now.size.height).abs() > TOLERANCE;
122 if !differs {
123 continue;
124 }
125 self.layout_audit.mismatches += 1;
126 let key = (now.type_name, now.label.clone());
127 let due = self
128 .layout_audit
129 .last_logged
130 .get(&key)
131 .is_none_or(|last| frame.saturating_sub(*last) >= 60);
132 if due {
133 self.layout_audit.last_logged.insert(key, frame);
134 log::warn!(
135 "layout-audit: {} (label {:?}) changed geometry without signalling layout: skipped pass origin {:?} size {:?}, forced pass origin {:?} size {:?}",
136 now.type_name,
137 now.label,
138 was.origin,
139 was.size,
140 now.origin,
141 now.size,
142 );
143 }
144 }
145 }
146
147 fn layout_pass(
150 &mut self,
151 window_size: Size,
152 text_ctx: Option<&mut dyn Any>,
153 force: bool,
154 ) -> Size {
155 let Some(root_id) = self.root_id else {
156 return Size::ZERO;
157 };
158 let bc = BoxConstraints::loose(window_size);
159 let theme = self.theme.as_deref();
160 let insets = self.insets;
161 let layout_epoch = self.layout_epoch;
162 let Some(pod) = self.tree.pod_mut(root_id) else {
163 return Size::ZERO;
164 };
165 let mut ctx = LayoutCtx::with_resources(text_ctx, theme);
166 ctx.set_window_insets(insets);
170 ctx.set_window_size(window_size);
171 ctx.set_layout_epoch(layout_epoch);
172 ctx.set_layout_force(force);
173 let size = pod.widget_mut().layout(&mut ctx, &bc);
174 pod.set_layout(Point::ZERO, size);
175 size
176 }
177}