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