Skip to main content

repose_devtools/
lib.rs

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