1use std::sync::Arc;
2
3use web_time::Instant;
4
5use repose_core::{Brush, Color, Rect, Scene, SceneNode};
6
7const FPS_HISTORY_LEN: usize = 60;
8
9pub struct Hud {
10 pub inspector_enabled: bool,
11 pub hovered: Option<Rect>,
12 pub hovered_semantics: Option<HoveredInfo>,
13 frame_count: u64,
14 last_frame: Option<Instant>,
15 fps_smooth: f32,
16 fps_history: [f32; FPS_HISTORY_LEN],
17 fps_history_idx: usize,
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: [0.0; FPS_HISTORY_LEN],
53 fps_history_idx: 0,
54 metrics: None,
55 selected_widget: None,
56 }
57 }
58 pub fn toggle_inspector(&mut self) {
59 self.inspector_enabled = !self.inspector_enabled;
60 }
61 pub fn set_hovered(&mut self, r: Option<Rect>, info: Option<HoveredInfo>) {
62 self.hovered = r;
63 self.hovered_semantics = info;
64 }
65 pub fn select_widget(&mut self, info: SelectedWidget) {
66 self.selected_widget = Some(info);
67 }
68 pub fn clear_selection(&mut self) {
69 self.selected_widget = None;
70 }
71
72 fn update_fps(&mut self, now: Instant) {
73 if let Some(prev) = self.last_frame.replace(now) {
74 let dt = (now - prev).as_secs_f32();
75 if dt > 0.0 && dt < 1.0 {
76 let fps = 1.0 / dt;
77 let a = 0.3;
78 self.fps_smooth = if self.fps_smooth == 0.0 {
79 fps
80 } else {
81 (1.0 - a) * self.fps_smooth + a * fps
82 };
83 self.fps_history[self.fps_history_idx] = fps;
84 self.fps_history_idx = (self.fps_history_idx + 1) % FPS_HISTORY_LEN;
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: 4.0,
108 });
109
110 let fps_norm = (self.fps_smooth / 60.0).min(1.0);
111 let bar_fill = bar_w * fps_norm;
112 scene.nodes.push(SceneNode::Rect {
113 rect: Rect {
114 x: bar_x + 2.0,
115 y: bar_y + 2.0,
116 w: bar_fill,
117 h: bar_h - 4.0,
118 },
119 brush: Brush::Solid(if self.fps_smooth >= 50.0 {
120 Color::from_hex("#44FF44")
121 } else if self.fps_smooth >= 30.0 {
122 Color::from_hex("#FFAA00")
123 } else {
124 Color::from_hex("#FF4444")
125 }),
126 radius: 2.0,
127 });
128
129 let mut text_y = bar_y + bar_h + 4.0;
130 let line = format!("{:.0} fps", self.fps_smooth);
131 scene.nodes.push(SceneNode::Text {
132 rect: Rect {
133 x: bar_x,
134 y: text_y,
135 w: 80.0,
136 h: 14.0,
137 },
138 text: Arc::<str>::from(line),
139 color: Color::from_hex("#AAAAAA"),
140 size: 12.0,
141 font_family: None,
142 });
143 text_y += 16.0;
144
145 let line = format!("frame: {}", self.frame_count);
146 scene.nodes.push(SceneNode::Text {
147 rect: Rect {
148 x: bar_x,
149 y: text_y,
150 w: 80.0,
151 h: 14.0,
152 },
153 text: Arc::<str>::from(line),
154 color: Color::from_hex("#888888"),
155 size: 11.0,
156 font_family: None,
157 });
158 text_y += 14.0;
159
160 let line = format!("build: {:.1}ms", m.build_ms);
161 scene.nodes.push(SceneNode::Text {
162 rect: Rect {
163 x: bar_x,
164 y: text_y,
165 w: 80.0,
166 h: 14.0,
167 },
168 text: Arc::<str>::from(line),
169 color: Color::from_hex("#888888"),
170 size: 11.0,
171 font_family: None,
172 });
173 text_y += 14.0;
174
175 let line = format!("layout: {:.1}ms", m.layout_ms);
176 scene.nodes.push(SceneNode::Text {
177 rect: Rect {
178 x: bar_x,
179 y: text_y,
180 w: 80.0,
181 h: 14.0,
182 },
183 text: Arc::<str>::from(line),
184 color: Color::from_hex("#888888"),
185 size: 11.0,
186 font_family: None,
187 });
188 text_y += 14.0;
189
190 let line = format!("widgets: {}", m.widget_count);
191 scene.nodes.push(SceneNode::Text {
192 rect: Rect {
193 x: bar_x,
194 y: text_y,
195 w: 80.0,
196 h: 14.0,
197 },
198 text: Arc::<str>::from(line),
199 color: Color::from_hex("#888888"),
200 size: 11.0,
201 font_family: None,
202 });
203 text_y += 14.0;
204
205 let line = format!("signals: {}", m.signal_count);
206 scene.nodes.push(SceneNode::Text {
207 rect: Rect {
208 x: bar_x,
209 y: text_y,
210 w: 80.0,
211 h: 14.0,
212 },
213 text: Arc::<str>::from(line),
214 color: Color::from_hex("#888888"),
215 size: 11.0,
216 font_family: None,
217 });
218 text_y += 14.0;
219
220 let line = format!("scene nodes: {}", m.scene_nodes);
221 scene.nodes.push(SceneNode::Text {
222 rect: Rect {
223 x: bar_x,
224 y: text_y,
225 w: 100.0,
226 h: 14.0,
227 },
228 text: Arc::<str>::from(line),
229 color: Color::from_hex("#888888"),
230 size: 11.0,
231 font_family: None,
232 });
233
234 if let Some(hover) = &self.hovered_semantics {
235 text_y += 20.0;
236 let line = format!("↳ {}: {:?}", hover.id, hover.role);
237 scene.nodes.push(SceneNode::Text {
238 rect: Rect {
239 x: bar_x,
240 y: text_y,
241 w: 150.0,
242 h: 14.0,
243 },
244 text: Arc::<str>::from(line),
245 color: Color::from_hex("#44AAFF"),
246 size: 11.0,
247 font_family: None,
248 });
249 if let Some(lbl) = &hover.label {
250 text_y += 14.0;
251 scene.nodes.push(SceneNode::Text {
252 rect: Rect {
253 x: bar_x,
254 y: text_y,
255 w: 150.0,
256 h: 14.0,
257 },
258 text: Arc::<str>::from(format!(" \"{}\"", lbl)),
259 color: Color::from_hex("#66CCFF"),
260 size: 10.0,
261 font_family: None,
262 });
263 }
264 }
265 }
266
267 if let Some(r) = self.hovered {
268 scene.nodes.push(SceneNode::Border {
269 rect: r,
270 color: Color::from_hex("#44AAFF"),
271 width: 2.0,
272 radius: 2.0,
273 });
274 }
275
276 if let Some(sel) = &self.selected_widget {
277 scene.nodes.push(SceneNode::Border {
278 rect: sel.bounds,
279 color: Color::from_hex("#FFAA00"),
280 width: 2.0,
281 radius: 2.0,
282 });
283 }
284 }
285}
286
287#[derive(Clone, Debug, Default)]
288pub struct Metrics {
289 pub build_ms: f32,
290 pub layout_ms: f32,
291 pub scene_nodes: usize,
292 pub widget_count: usize,
293 pub signal_count: usize,
294}
295
296pub struct Inspector {
297 pub hud: Hud,
298}
299impl Default for Inspector {
300 fn default() -> Self {
301 Self::new()
302 }
303}
304
305impl Inspector {
306 pub fn new() -> Self {
307 Self { hud: Hud::new() }
308 }
309 pub fn frame(&mut self, scene: &mut Scene) {
310 if self.hud.inspector_enabled {
311 self.hud.overlay(scene);
312 }
313 }
314}