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 let fps_norm = (self.fps_smooth / 60.0).min(1.0);
113 let bar_fill = bar_w * fps_norm;
114 scene.nodes.push(SceneNode::Rect {
115 rect: Rect {
116 x: bar_x + 2.0,
117 y: bar_y + 2.0,
118 w: bar_fill,
119 h: bar_h - 4.0,
120 },
121 brush: Brush::Solid(if self.fps_smooth >= 50.0 {
122 Color::from_hex("#44FF44")
123 } else if self.fps_smooth >= 30.0 {
124 Color::from_hex("#FFAA00")
125 } else {
126 Color::from_hex("#FF4444")
127 }),
128 radius: [2.0; 4],
129 });
130
131 let mut text_y = bar_y + bar_h + 4.0;
132 let line = format!("{:.0} fps", self.fps_smooth);
133 scene.nodes.push(SceneNode::Text {
134 rect: Rect {
135 x: bar_x,
136 y: text_y,
137 w: 80.0,
138 h: 14.0,
139 },
140 text: Arc::<str>::from(line),
141 color: Color::from_hex("#AAAAAA"),
142 size: 12.0,
143 font_family: None,
144 text_align: TextAlign::Unspecified,
145 font_weight: FontWeight::NORMAL,
146 font_style: FontStyle::Normal,
147 text_decoration: TextDecoration::default(),
148 letter_spacing: 0.0,
149 line_height: 0.0,
150 extra_style: Default::default(),
151 url: None,
152 font_variation_settings: None,
153 });
154 text_y += 16.0;
155
156 let line = format!("frame: {}", self.frame_count);
157 scene.nodes.push(SceneNode::Text {
158 rect: Rect {
159 x: bar_x,
160 y: text_y,
161 w: 80.0,
162 h: 14.0,
163 },
164 text: Arc::<str>::from(line),
165 color: Color::from_hex("#888888"),
166 size: 11.0,
167 font_family: None,
168 text_align: TextAlign::Unspecified,
169 font_weight: FontWeight::NORMAL,
170 font_style: FontStyle::Normal,
171 text_decoration: TextDecoration::default(),
172 letter_spacing: 0.0,
173 line_height: 0.0,
174 extra_style: Default::default(),
175 url: None,
176 font_variation_settings: None,
177 });
178 text_y += 14.0;
179
180 let line = format!("build: {:.1}ms", m.build_ms);
181 scene.nodes.push(SceneNode::Text {
182 rect: Rect {
183 x: bar_x,
184 y: text_y,
185 w: 80.0,
186 h: 14.0,
187 },
188 text: Arc::<str>::from(line),
189 color: Color::from_hex("#888888"),
190 size: 11.0,
191 font_family: None,
192 text_align: TextAlign::Unspecified,
193 font_weight: FontWeight::NORMAL,
194 font_style: FontStyle::Normal,
195 text_decoration: TextDecoration::default(),
196 letter_spacing: 0.0,
197 line_height: 0.0,
198 extra_style: Default::default(),
199 url: None,
200 font_variation_settings: None,
201 });
202 text_y += 14.0;
203
204 let line = format!("layout: {:.1}ms", m.layout_ms);
205 scene.nodes.push(SceneNode::Text {
206 rect: Rect {
207 x: bar_x,
208 y: text_y,
209 w: 80.0,
210 h: 14.0,
211 },
212 text: Arc::<str>::from(line),
213 color: Color::from_hex("#888888"),
214 size: 11.0,
215 font_family: None,
216 text_align: TextAlign::Unspecified,
217 font_weight: FontWeight::NORMAL,
218 font_style: FontStyle::Normal,
219 text_decoration: TextDecoration::default(),
220 letter_spacing: 0.0,
221 line_height: 0.0,
222 extra_style: Default::default(),
223 url: None,
224 font_variation_settings: None,
225 });
226 text_y += 14.0;
227
228 let line = format!("widgets: {}", m.widget_count);
229 scene.nodes.push(SceneNode::Text {
230 rect: Rect {
231 x: bar_x,
232 y: text_y,
233 w: 80.0,
234 h: 14.0,
235 },
236 text: Arc::<str>::from(line),
237 color: Color::from_hex("#888888"),
238 size: 11.0,
239 font_family: None,
240 text_align: TextAlign::Unspecified,
241 font_weight: FontWeight::NORMAL,
242 font_style: FontStyle::Normal,
243 text_decoration: TextDecoration::default(),
244 letter_spacing: 0.0,
245 line_height: 0.0,
246 extra_style: Default::default(),
247 url: None,
248 font_variation_settings: None,
249 });
250 text_y += 14.0;
251
252 let line = format!("signals: {}", m.signal_count);
253 scene.nodes.push(SceneNode::Text {
254 rect: Rect {
255 x: bar_x,
256 y: text_y,
257 w: 80.0,
258 h: 14.0,
259 },
260 text: Arc::<str>::from(line),
261 color: Color::from_hex("#888888"),
262 size: 11.0,
263 font_family: None,
264 text_align: TextAlign::Unspecified,
265 font_weight: FontWeight::NORMAL,
266 font_style: FontStyle::Normal,
267 text_decoration: TextDecoration::default(),
268 letter_spacing: 0.0,
269 line_height: 0.0,
270 extra_style: Default::default(),
271 url: None,
272 font_variation_settings: None,
273 });
274 text_y += 14.0;
275
276 let line = format!("scene nodes: {}", m.scene_nodes);
277 scene.nodes.push(SceneNode::Text {
278 rect: Rect {
279 x: bar_x,
280 y: text_y,
281 w: 100.0,
282 h: 14.0,
283 },
284 text: Arc::<str>::from(line),
285 color: Color::from_hex("#888888"),
286 size: 11.0,
287 font_family: None,
288 text_align: TextAlign::Unspecified,
289 font_weight: FontWeight::NORMAL,
290 font_style: FontStyle::Normal,
291 text_decoration: TextDecoration::default(),
292 letter_spacing: 0.0,
293 line_height: 0.0,
294 extra_style: Default::default(),
295 url: None,
296 font_variation_settings: None,
297 });
298
299 if let Some(hover) = &self.hovered_semantics {
300 text_y += 20.0;
301 let line = format!("↳ {}: {:?}", hover.id, hover.role);
302 scene.nodes.push(SceneNode::Text {
303 rect: Rect {
304 x: bar_x,
305 y: text_y,
306 w: 150.0,
307 h: 14.0,
308 },
309 text: Arc::<str>::from(line),
310 color: Color::from_hex("#44AAFF"),
311 size: 11.0,
312 font_family: None,
313 text_align: TextAlign::Unspecified,
314 font_weight: FontWeight::NORMAL,
315 font_style: FontStyle::Normal,
316 text_decoration: TextDecoration::default(),
317 letter_spacing: 0.0,
318 line_height: 0.0,
319 extra_style: Default::default(),
320 url: None,
321 font_variation_settings: None,
322 });
323 if let Some(lbl) = &hover.label {
324 text_y += 14.0;
325 scene.nodes.push(SceneNode::Text {
326 rect: Rect {
327 x: bar_x,
328 y: text_y,
329 w: 150.0,
330 h: 14.0,
331 },
332 text: Arc::<str>::from(format!(" \"{}\"", lbl)),
333 color: Color::from_hex("#66CCFF"),
334 size: 10.0,
335 font_family: None,
336 text_align: TextAlign::Unspecified,
337 font_weight: FontWeight::NORMAL,
338 font_style: FontStyle::Normal,
339 text_decoration: TextDecoration::default(),
340 letter_spacing: 0.0,
341 line_height: 0.0,
342 extra_style: Default::default(),
343 url: None,
344 font_variation_settings: None,
345 });
346 }
347 }
348 }
349
350 if let Some(r) = self.hovered {
351 scene.nodes.push(SceneNode::Border {
352 rect: r,
353 color: Color::from_hex("#44AAFF"),
354 width: 2.0,
355 radius: [2.0; 4],
356 });
357 }
358
359 if let Some(sel) = &self.selected_widget {
360 scene.nodes.push(SceneNode::Border {
361 rect: sel.bounds,
362 color: Color::from_hex("#FFAA00"),
363 width: 2.0,
364 radius: [2.0; 4],
365 });
366 }
367 }
368}
369
370#[derive(Clone, Debug, Default)]
371pub struct Metrics {
372 pub build_ms: f32,
373 pub layout_ms: f32,
374 pub scene_nodes: usize,
375 pub widget_count: usize,
376 pub signal_count: usize,
377}
378
379pub struct Inspector {
380 pub hud: Hud,
381}
382impl Default for Inspector {
383 fn default() -> Self {
384 Self::new()
385 }
386}
387
388impl Inspector {
389 pub fn new() -> Self {
390 Self { hud: Hud::new() }
391 }
392 pub fn frame(&mut self, scene: &mut Scene) {
393 if self.hud.inspector_enabled {
394 self.hud.overlay(scene);
395 }
396 }
397}