1use frus_core::{Color, Point, Rect, Scene, Size, TextStyle};
11
12use crate::interaction::WidgetId;
13use crate::theme::Theme;
14
15#[derive(Clone, Copy, Debug)]
18pub struct InspectorNode {
19 pub id: WidgetId,
21 pub rect: Rect,
23 pub name: &'static str,
25 pub depth: usize,
27}
28
29pub fn node_at(nodes: &[InspectorNode], point: Point) -> Option<&InspectorNode> {
32 nodes
33 .iter()
34 .filter(|n| n.rect.contains(point))
35 .max_by_key(|n| n.depth)
36}
37
38fn short_id(id: WidgetId) -> String {
41 format!("#{:08x}", id.as_u64() as u32)
42}
43
44pub fn dump_tree(nodes: &[InspectorNode]) -> String {
48 let mut out = String::new();
49 for node in nodes {
50 let r = node.rect;
51 out.push_str(&format!(
52 "{:indent$}{} {:.0},{:.0} {:.0}×{:.0} {}\n",
53 "",
54 node.name,
55 r.x,
56 r.y,
57 r.width,
58 r.height,
59 short_id(node.id),
60 indent = node.depth * 2,
61 ));
62 }
63 out
64}
65
66const OUTLINE_COLORS: [Color; 4] = [
68 Color {
69 r: 0.35,
70 g: 0.65,
71 b: 1.0,
72 a: 0.55,
73 }, Color {
75 r: 0.45,
76 g: 0.85,
77 b: 0.55,
78 a: 0.55,
79 }, Color {
81 r: 1.0,
82 g: 0.75,
83 b: 0.35,
84 a: 0.55,
85 }, Color {
87 r: 0.85,
88 g: 0.55,
89 b: 0.95,
90 a: 0.55,
91 }, ];
93
94pub fn paint_overlay(
99 nodes: &[InspectorNode],
100 hover: Option<Point>,
101 window: Size,
102 theme: &Theme,
103 scene: &mut Scene,
104) {
105 scene.set_clip(Rect::UNBOUNDED);
106 scene.set_owner(0);
107
108 for node in nodes {
109 let color = OUTLINE_COLORS[node.depth % OUTLINE_COLORS.len()];
110 scene.draw_rect(node.rect, Color::TRANSPARENT, 0.0, 1.0, color);
111 }
112
113 let Some(target) = hover.and_then(|point| node_at(nodes, point)) else {
114 return;
115 };
116
117 scene.draw_rect(
119 target.rect,
120 theme.primary.with_alpha(0.18),
121 0.0,
122 2.0,
123 theme.primary,
124 );
125
126 let r = target.rect;
128 let title = target.name.to_string();
129 let details = format!(
130 "{:.0}×{:.0} @ {:.0},{:.0} {}",
131 r.width,
132 r.height,
133 r.x,
134 r.y,
135 short_id(target.id)
136 );
137 let title_style = TextStyle::new(13.0).weight(frus_core::FontWeight::Bold);
138 let detail_style = TextStyle::new(12.0);
139 let title_size = frus_text::measure_styled(&title, 13.0, title_style.resolved().weight, false);
140 let detail_size =
141 frus_text::measure_styled(&details, 12.0, detail_style.resolved().weight, false);
142
143 const PAD: f32 = 8.0;
144 let card_w = title_size.width.max(detail_size.width) + PAD * 2.0;
145 let card_h = title_size.height + detail_size.height + PAD * 2.0;
146 let x = r.x.clamp(0.0, (window.width - card_w).max(0.0));
148 let below = r.y + r.height + 4.0;
149 let y = if below + card_h <= window.height {
150 below
151 } else {
152 (r.y - card_h - 4.0).max(0.0)
153 };
154
155 let card = Rect::new(x, y, card_w, card_h);
156 scene.draw_rect(
157 card,
158 theme.scheme.inverse_surface,
159 6.0,
160 0.0,
161 Color::TRANSPARENT,
162 );
163 let on = theme.scheme.on_inverse_surface;
164 scene.text(
165 Point::new(x + PAD, y + PAD),
166 title,
167 &title_style.resolved(),
168 on,
169 );
170 scene.text(
171 Point::new(x + PAD, y + PAD + title_size.height),
172 details,
173 &detail_style.resolved(),
174 on,
175 );
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::widget::Widget;
182 use crate::{build_ui_inspected, Container, Flex, Runtime, Text};
183
184 #[test]
187 fn collects_names_rects_and_depths() {
188 let root: Container<()> = Container::new().width(200.0).height(100.0).child(
189 Flex::column()
190 .child(Text::new("a"))
191 .child(crate::Keyed::new(7, Text::new("b"))),
192 );
193 let runtime = Runtime::default();
194 let theme = Theme::default();
195 let (_, nodes) = build_ui_inspected(&root, Size::new(200.0, 100.0), &runtime, &theme);
196
197 let names: Vec<&str> = nodes.iter().map(|n| n.name).collect();
198 assert_eq!(
199 names,
200 ["Container", "Flex", "Text", "Text"],
201 "Keyed is transparent"
202 );
203 let depths: Vec<usize> = nodes.iter().map(|n| n.depth).collect();
204 assert_eq!(depths, [0, 1, 2, 2]);
205 assert_eq!(
206 nodes[0].rect,
207 Rect::new(0.0, 0.0, 200.0, 100.0),
208 "the root = its box"
209 );
210 let ui = crate::build_ui(&root, Size::new(200.0, 100.0), &runtime, &theme);
212 assert!(!ui.scene().is_empty());
213 }
214
215 #[test]
217 fn node_at_picks_the_deepest() {
218 let root: Container<()> = Container::new()
219 .width(200.0)
220 .height(100.0)
221 .child(Container::new().width(50.0).height(40.0));
222 let runtime = Runtime::default();
223 let theme = Theme::default();
224 let (_, nodes) = build_ui_inspected(&root, Size::new(200.0, 100.0), &runtime, &theme);
225
226 let inner = node_at(&nodes, Point::new(10.0, 10.0)).expect("a widget under the point");
227 assert_eq!(inner.depth, 1, "the child, not the root");
228 let outer = node_at(&nodes, Point::new(150.0, 80.0)).expect("the root alone here");
229 assert_eq!(outer.depth, 0);
230 assert!(
231 node_at(&nodes, Point::new(500.0, 500.0)).is_none(),
232 "outside everything"
233 );
234 }
235
236 #[test]
238 fn dump_tree_indents_by_depth() {
239 let root: Container<()> = Container::new()
240 .width(80.0)
241 .height(40.0)
242 .child(Text::new("x"));
243 let runtime = Runtime::default();
244 let theme = Theme::default();
245 let (_, nodes) = build_ui_inspected(&root, Size::new(80.0, 40.0), &runtime, &theme);
246 let dump = dump_tree(&nodes);
247 let lines: Vec<&str> = dump.lines().collect();
248 assert!(
249 lines[0].starts_with("Container"),
250 "the root is not indented: {dump}"
251 );
252 assert!(
253 lines[1].starts_with(" Text"),
254 "child indented by 2: {dump}"
255 );
256 assert!(lines[0].contains("80×40"), "geometry in the dump: {dump}");
257 }
258
259 #[test]
262 fn overlay_paints_outlines_and_hover_card() {
263 let root: Container<()> = Container::new()
264 .width(120.0)
265 .height(60.0)
266 .child(Text::new("x"));
267 let runtime = Runtime::default();
268 let theme = Theme::default();
269 let (ui, nodes) = build_ui_inspected(&root, Size::new(120.0, 60.0), &runtime, &theme);
270
271 let base = ui.scene().len();
272 let mut scene = ui.scene().clone();
273 paint_overlay(&nodes, None, Size::new(120.0, 60.0), &theme, &mut scene);
274 assert_eq!(scene.len(), base + nodes.len(), "one outline per widget");
275
276 let mut scene = ui.scene().clone();
277 paint_overlay(
278 &nodes,
279 Some(Point::new(5.0, 5.0)),
280 Size::new(120.0, 60.0),
281 &theme,
282 &mut scene,
283 );
284 assert!(
285 scene.len() > base + nodes.len() + 2,
286 "highlight + card + 2 texts on top of the outlines"
287 );
288 }
289
290 #[test]
292 fn debug_names_are_short_and_delegated() {
293 let text = Text::new("x");
294 assert_eq!(Widget::<()>::debug_name(&text), "Text");
295 let boxed: Box<dyn Widget<()>> = Box::new(Text::new("x"));
296 assert_eq!(boxed.debug_name(), "Text", "Box delegates to its content");
297 let container: Container<()> = Container::new();
298 assert_eq!(
299 Widget::<()>::debug_name(&container),
300 "Container",
301 "generics stripped"
302 );
303 }
304}