Skip to main content

repose_devtools/
lib.rs

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}