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