Skip to main content

repose_devtools/
lib.rs

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}