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