1use std::sync::Arc;
2
3use web_time::Instant;
4
5use repose_core::{
6 Brush, Color, FontStyle, FontWeight, Rect, Scene, SceneNode, TextAlign, TextDecoration,
7};
8
9const FPS_HISTORY_LEN: usize = 60;
10
11pub struct Hud {
12 pub inspector_enabled: bool,
13 pub hovered: Option<Rect>,
14 pub hovered_semantics: Option<HoveredInfo>,
15 frame_count: u64,
16 last_frame: Option<Instant>,
17 fps_smooth: f32,
18 fps_history: [f32; FPS_HISTORY_LEN],
19 fps_history_idx: usize,
20 pub metrics: Option<Metrics>,
21 selected_widget: Option<SelectedWidget>,
22}
23
24#[derive(Clone, Debug)]
25pub struct HoveredInfo {
26 pub id: u64,
27 pub role: String,
28 pub label: Option<String>,
29}
30
31#[derive(Clone, Debug)]
32pub struct SelectedWidget {
33 pub id: u64,
34 pub role: String,
35 pub label: Option<String>,
36 pub bounds: Rect,
37}
38
39impl Default for Hud {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl Hud {
46 pub fn new() -> Self {
47 Self {
48 inspector_enabled: false,
49 hovered: None,
50 hovered_semantics: None,
51 frame_count: 0,
52 last_frame: None,
53 fps_smooth: 0.0,
54 fps_history: [0.0; FPS_HISTORY_LEN],
55 fps_history_idx: 0,
56 metrics: None,
57 selected_widget: None,
58 }
59 }
60 pub fn toggle_inspector(&mut self) {
61 self.inspector_enabled = !self.inspector_enabled;
62 }
63 pub fn set_hovered(&mut self, r: Option<Rect>, info: Option<HoveredInfo>) {
64 self.hovered = r;
65 self.hovered_semantics = info;
66 }
67 pub fn select_widget(&mut self, info: SelectedWidget) {
68 self.selected_widget = Some(info);
69 }
70 pub fn clear_selection(&mut self) {
71 self.selected_widget = None;
72 }
73
74 fn update_fps(&mut self, now: Instant) {
75 if let Some(prev) = self.last_frame.replace(now) {
76 let dt = (now - prev).as_secs_f32();
77 if dt > 0.0 && dt < 1.0 {
78 let fps = 1.0 / dt;
79 let a = 0.3;
80 self.fps_smooth = if self.fps_smooth == 0.0 {
81 fps
82 } else {
83 (1.0 - a) * self.fps_smooth + a * fps
84 };
85 self.fps_history[self.fps_history_idx] = fps;
86 self.fps_history_idx = (self.fps_history_idx + 1) % FPS_HISTORY_LEN;
87 }
88 }
89 }
90
91 pub fn overlay(&mut self, scene: &mut Scene) {
92 self.frame_count += 1;
93 self.update_fps(Instant::now());
94
95 let bar_x = 8.0;
96 let bar_y = 8.0;
97 let bar_w = 120.0;
98 let bar_h = 24.0;
99
100 if let Some(m) = &self.metrics {
101 scene.nodes.push(SceneNode::Rect {
102 rect: Rect {
103 x: bar_x,
104 y: bar_y,
105 w: bar_w,
106 h: bar_h,
107 },
108 brush: Brush::Solid(Color::from_hex("#1A1A1ACC")),
109 radius: [4.0; 4],
110 });
111
112 Self::draw_fps_sparkline(
113 scene,
114 bar_x + 2.0,
115 bar_y + bar_h + 4.0,
116 bar_w - 4.0,
117 16.0,
118 &self.fps_history,
119 self.fps_history_idx,
120 );
121
122 let fps_norm = (self.fps_smooth / 60.0).min(1.0);
123 let bar_fill = bar_w * fps_norm;
124 scene.nodes.push(SceneNode::Rect {
125 rect: Rect {
126 x: bar_x + 2.0,
127 y: bar_y + 2.0,
128 w: bar_fill,
129 h: bar_h - 4.0,
130 },
131 brush: Brush::Solid(if self.fps_smooth >= 50.0 {
132 Color::from_hex("#44FF44")
133 } else if self.fps_smooth >= 30.0 {
134 Color::from_hex("#FFAA00")
135 } else {
136 Color::from_hex("#FF4444")
137 }),
138 radius: [2.0; 4],
139 });
140
141 let mut text_y = bar_y + bar_h + 24.0;
142 Self::push_text(
143 scene,
144 bar_x,
145 text_y,
146 100.0,
147 &format!("{:.0} fps", self.fps_smooth),
148 "#AAAAAA",
149 12.0,
150 );
151 text_y += 16.0;
152
153 Self::push_text(
154 scene,
155 bar_x,
156 text_y,
157 100.0,
158 &format!("frame: {}", self.frame_count),
159 "#888888",
160 11.0,
161 );
162 text_y += 14.0;
163 Self::push_text(
164 scene,
165 bar_x,
166 text_y,
167 120.0,
168 &format!("build: {:.1}ms", m.build_ms),
169 "#888888",
170 11.0,
171 );
172 text_y += 14.0;
173 Self::push_text(
174 scene,
175 bar_x,
176 text_y,
177 120.0,
178 &format!("layout: {:.1}ms", m.layout_ms),
179 "#888888",
180 11.0,
181 );
182 text_y += 14.0;
183 Self::push_text(
184 scene,
185 bar_x,
186 text_y,
187 120.0,
188 &format!("paint: {:.1}ms", m.paint_ms),
189 "#888888",
190 11.0,
191 );
192 text_y += 14.0;
193 Self::push_text(
194 scene,
195 bar_x,
196 text_y,
197 120.0,
198 &format!("widgets: {}", m.widget_count),
199 "#888888",
200 11.0,
201 );
202 text_y += 14.0;
203 Self::push_text(
204 scene,
205 bar_x,
206 text_y,
207 120.0,
208 &format!("signals: {}", m.signal_count),
209 "#888888",
210 11.0,
211 );
212 text_y += 14.0;
213 Self::push_text(
214 scene,
215 bar_x,
216 text_y,
217 140.0,
218 &format!("scene: {}", m.scene_nodes),
219 "#888888",
220 11.0,
221 );
222 text_y += 14.0;
223 Self::push_text(
224 scene,
225 bar_x,
226 text_y,
227 200.0,
228 &format!("taffy: {:+}/{:+}", m.taffy_created, m.taffy_reused),
229 "#888888",
230 11.0,
231 );
232 text_y += 14.0;
233 Self::push_text(
234 scene,
235 bar_x,
236 text_y,
237 200.0,
238 &format!("layout: {}h/{}m", m.layout_hits, m.layout_misses),
239 "#888888",
240 11.0,
241 );
242 text_y += 14.0;
243 Self::push_text(
244 scene,
245 bar_x,
246 text_y,
247 200.0,
248 &format!(
249 "paint cache: {}h/{}m ({} culled)",
250 m.paint_cache_hits, m.paint_cache_misses, m.paint_culled
251 ),
252 "#888888",
253 11.0,
254 );
255 text_y += 14.0;
256
257 if let Some(hover) = &self.hovered_semantics {
258 text_y += 6.0;
259 Self::push_text(
260 scene,
261 bar_x,
262 text_y,
263 200.0,
264 &format!("↳ {}: {:?}", hover.id, hover.role),
265 "#44AAFF",
266 11.0,
267 );
268 if let Some(lbl) = &hover.label {
269 text_y += 14.0;
270 Self::push_text(
271 scene,
272 bar_x,
273 text_y,
274 200.0,
275 &format!(" \"{}\"", lbl),
276 "#66CCFF",
277 10.0,
278 );
279 }
280 }
281 }
282
283 if let Some(r) = self.hovered {
284 scene.nodes.push(SceneNode::Border {
285 rect: r,
286 color: Color::from_hex("#44AAFF"),
287 width: 2.0,
288 radius: [2.0; 4],
289 });
290 }
291
292 if let Some(sel) = &self.selected_widget {
293 scene.nodes.push(SceneNode::Border {
294 rect: sel.bounds,
295 color: Color::from_hex("#FFAA00"),
296 width: 2.0,
297 radius: [2.0; 4],
298 });
299 }
300 }
301
302 fn push_text(scene: &mut Scene, x: f32, y: f32, w: f32, txt: &str, color: &str, size: f32) {
304 scene.nodes.push(SceneNode::Text {
305 rect: Rect { x, y, w, h: 14.0 },
306 text: Arc::<str>::from(txt.to_string()),
307 color: Color::from_hex(color),
308 size,
309 font_family: None,
310 text_align: TextAlign::Unspecified,
311 font_weight: FontWeight::NORMAL,
312 font_style: FontStyle::Normal,
313 text_decoration: TextDecoration::default(),
314 letter_spacing: 0.0,
315 line_height: 0.0,
316 extra_style: Default::default(),
317 url: None,
318 font_variation_settings: None,
319 });
320 }
321
322 fn draw_fps_sparkline(
326 scene: &mut Scene,
327 x: f32,
328 y: f32,
329 w: f32,
330 h: f32,
331 history: &[f32],
332 idx: usize,
333 ) {
334 let n = history.len();
335 if n == 0 || idx == 0 {
336 return;
337 }
338 scene.nodes.push(SceneNode::Rect {
339 rect: Rect { x, y, w, h },
340 brush: Brush::Solid(Color::from_hex("#1A1A1ACC")),
341 radius: [2.0; 4],
342 });
343 let bin_w = w / n as f32;
344 let max_fps = 60.0f32.max(history.iter().copied().fold(0.0f32, f32::max));
345 for i in 0..n {
346 let sample = history[(i + idx) % n];
348 let frac = (sample / max_fps).min(1.0);
349 let bh = (h - 2.0) * frac;
350 let color = if frac >= 0.83 {
351 "#44FF44"
352 } else if frac >= 0.5 {
353 "#FFAA00"
354 } else {
355 "#FF4444"
356 };
357 scene.nodes.push(SceneNode::Rect {
358 rect: Rect {
359 x: x + i as f32 * bin_w,
360 y: y + (h - 2.0) - bh,
361 w: (bin_w - 1.0).max(0.5),
362 h: bh.max(1.0),
363 },
364 brush: Brush::Solid(Color::from_hex(color)),
365 radius: [0.0; 4],
366 });
367 }
368 }
369}
370
371#[derive(Clone, Debug, Default)]
372pub struct Metrics {
373 pub build_ms: f32,
374 pub layout_ms: f32,
375 pub paint_ms: f32,
376 pub scene_nodes: usize,
377 pub widget_count: usize,
378 pub signal_count: usize,
379 pub taffy_created: usize,
381 pub taffy_reused: usize,
382 pub layout_hits: usize,
383 pub layout_misses: usize,
384 pub paint_cache_hits: usize,
385 pub paint_cache_misses: usize,
386 pub paint_culled: usize,
387}
388
389pub struct Inspector {
390 pub hud: Hud,
391}
392impl Default for Inspector {
393 fn default() -> Self {
394 Self::new()
395 }
396}
397
398impl Inspector {
399 pub fn new() -> Self {
400 Self { hud: Hud::new() }
401 }
402 pub fn frame(&mut self, scene: &mut Scene) {
403 if self.hud.inspector_enabled {
404 self.hud.overlay(scene);
405 }
406 }
407}