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