Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-widgets/src/inspector.rs
StratoSDK / crates / strato-widgets / src / inspector.rs
1//! In-app inspector overlay for visualizing widget trees, state snapshots, and performance timelines.
2 
3use std::collections::HashMap;
4 
5use glam::Vec2;
6use strato_core::event::{Event, EventResult, KeyCode, KeyboardEvent, Modifiers};
7use strato_core::inspector::{self, ComponentNodeSnapshot, InspectorSnapshot, LayoutBoxSnapshot};
8use strato_core::layout::{Constraints, Layout, Size};
9use strato_core::types::{Color, Rect, Transform};
10use strato_renderer::batch::RenderBatch;
11 
12use crate::container::Container;
13use crate::layout::Column;
14use crate::scroll_view::ScrollView;
15use crate::text::Text;
16use crate::widget::{generate_id, Widget, WidgetId};
17use slotmap::Key;
18 
19const DEFAULT_PANEL_WIDTH: f32 = 340.0;
20const DEFAULT_PANEL_HEIGHT: f32 = 320.0;
21 
22/// Overlay widget that renders the inspector panel and captures instrumentation data.
23#[derive(Debug)]
24pub struct InspectorOverlay {
25 id: WidgetId,
26 child: Box<dyn Widget>,
27 shortcut: (KeyCode, Modifiers),
28 pub visible: bool,
29 cached_child_size: Size,
30 panel: Option<Box<dyn Widget>>,
31 panel_size: Option<Size>,
32}
33 
34impl InspectorOverlay {
35 /// Create a new overlay wrapping the provided child widget.
36 pub fn new(child: impl Widget + 'static) -> Self {
37 Self {
38 id: generate_id(),
39 child: Box::new(child),
40 shortcut: (
41 KeyCode::I,
42 Modifiers {
43 control: true,
44 shift: true,
45 alt: false,
46 super_key: false,
47 },
48 ),
49 visible: false,
50 cached_child_size: Size::zero(),
51 panel: None,
52 panel_size: None,
53 }
54 }
55 
56 /// Override the keyboard shortcut used to toggle visibility.
57 pub fn shortcut(mut self, key: KeyCode, modifiers: Modifiers) -> Self {
58 self.shortcut = (key, modifiers);
59 self
60 }
61 
62 fn shortcut_pressed(&self, key: &KeyboardEvent) -> bool {
63 key.key_code == self.shortcut.0
64 && key.modifiers.control == self.shortcut.1.control
65 && key.modifiers.shift == self.shortcut.1.shift
66 && key.modifiers.alt == self.shortcut.1.alt
67 && key.modifiers.super_key == self.shortcut.1.super_key
68 }
69 
70 fn collect_components(
71 &self,
72 widget: &(dyn Widget + '_),
73 depth: usize,
74 nodes: &mut Vec<ComponentNodeSnapshot>,
75 ) {
76 nodes.push(ComponentNodeSnapshot {
77 id: strato_core::widget::WidgetId(widget.id()),
78 name: format!("{:?}", widget),
79 depth,
80 props: HashMap::new(),
81 state: HashMap::new(),
82 });
83 
84 for child in widget.children() {
85 self.collect_components(child, depth + 1, nodes);
86 }
87 }
88 
89 fn build_panel(&self, snapshot: &InspectorSnapshot) -> Box<dyn Widget> {
90 let mut lines: Vec<Box<dyn Widget>> = Vec::new();
91 lines.push(Box::new(
92 Text::new("Inspector (Ctrl+Shift+I)")
93 .font_size(16.0)
94 .color(Color::rgb(1.0, 1.0, 1.0)),
95 ));
96 
97 lines.push(Box::new(
98 Text::new("Component hierarchy")
99 .font_size(14.0)
100 .color(Color::rgb(0.8, 0.9, 1.0)),
101 ));
102 if snapshot.components.is_empty() {
103 lines.push(Box::new(
104 Text::new("(no widgets rendered yet)").font_size(12.0),
105 ));
106 } else {
107 for node in &snapshot.components {
108 let indent = " ".repeat(node.depth);
109 let line = format!("{}• {} #{:?}", indent, node.name, node.id);
110 lines.push(Box::new(
111 Text::new(line)
112 .font_size(12.0)
113 .color(Color::rgb(0.9, 0.9, 0.9)),
114 ));
115 }
116 }
117 
118 lines.push(Box::new(
119 Text::new("State snapshots")
120 .font_size(14.0)
121 .color(Color::rgb(0.8, 0.9, 1.0)),
122 ));
123 if snapshot.state_snapshots.is_empty() {
124 lines.push(Box::new(
125 Text::new("(no state mutations captured)").font_size(12.0),
126 ));
127 } else {
128 for snapshot in snapshot.state_snapshots.iter().take(8) {
129 let line = format!("• {:?} => {}", snapshot.state_id.data(), snapshot.detail);
130 lines.push(Box::new(Text::new(line).font_size(12.0)));
131 }
132 }
133 
134 lines.push(Box::new(
135 Text::new("Layout boxes")
136 .font_size(14.0)
137 .color(Color::rgb(0.8, 0.9, 1.0)),
138 ));
139 lines.push(Box::new(
140 Text::new(format!(
141 "{} boxes captured this frame",
142 snapshot.layout_boxes.len()
143 ))
144 .font_size(12.0),
145 ));
146 
147 lines.push(Box::new(
148 Text::new("Performance timeline")
149 .font_size(14.0)
150 .color(Color::rgb(0.8, 0.9, 1.0)),
151 ));
152 if snapshot.frame_timelines.is_empty() {
153 lines.push(Box::new(
154 Text::new("(no frames recorded yet)").font_size(12.0),
155 ));
156 } else {
157 for frame in snapshot.frame_timelines.iter().rev().take(5) {
158 let note = frame.notes.clone().unwrap_or_else(|| "".to_string());
159 let line = format!(
160 "• Frame {}: {:.2}ms cpu / {:.2}ms gpu {}",
161 frame.frame_id, frame.cpu_time_ms, frame.gpu_time_ms, note
162 );
163 lines.push(Box::new(Text::new(line).font_size(12.0)));
164 }
165 }
166 
167 let column = Column::new().spacing(4.0).children(lines);
168 let scrollable = ScrollView::new(column);
169 
170 Box::new(
171 Container::new()
172 .padding(12.0)
173 .background(Color::rgba(0.08, 0.1, 0.14, 0.92))
174 .border(1.0, Color::rgba(0.4, 0.6, 1.0, 0.4))
175 .child(scrollable),
176 )
177 }
178}
179 
180impl Widget for InspectorOverlay {
181 fn id(&self) -> WidgetId {
182 self.id
183 }
184 
185 fn as_any(&self) -> &dyn std::any::Any {
186 self
187 }
188 
189 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
190 self
191 }
192 
193 fn clone_widget(&self) -> Box<dyn Widget> {
194 // Since we can't easily clone the boxed child trait object without more bounds,
195 // and InspectorOverlay is likely a singleton/special widget,
196 // we might need a specific strategy.
197 // For now, assuming `InspectorOverlay` needs to be Clone but `child` is `Box<dyn Widget>`.
198 // `Box<dyn Widget>` isn't automatically cloneable unless `Widget` has `clone_widget`.
199 // We can use the child's `clone_widget` method.
200 Box::new(Self {
201 id: generate_id(), // Generate new ID on clone? Or copy? Usually clone implies new ID for widgets or copy?
202 // BaseWidget generates new ID.
203 child: self.child.clone_widget(),
204 shortcut: self.shortcut,
205 visible: self.visible,
206 cached_child_size: self.cached_child_size,
207 panel: self.panel.as_ref().map(|p| p.clone_widget()),
208 panel_size: self.panel_size,
209 })
210 }
211 
212 fn layout(&mut self, constraints: Constraints) -> Size {
213 let inspector = inspector::inspector();
214 if inspector.is_enabled() && self.visible {
215 inspector.begin_frame();
216 let mut nodes = Vec::new();
217 self.collect_components(self.child.as_ref(), 0, &mut nodes);
218 inspector.record_component_tree(nodes);
219 }
220 
221 self.cached_child_size = self.child.layout(constraints);
222 
223 if inspector::inspector().is_enabled() && self.visible {
224 let snapshot = inspector::inspector().snapshot();
225 let mut panel = self.build_panel(&snapshot);
226 let panel_constraints = Constraints {
227 min_width: DEFAULT_PANEL_WIDTH,
228 max_width: DEFAULT_PANEL_WIDTH,
229 min_height: 0.0,
230 max_height: constraints.max_height.min(DEFAULT_PANEL_HEIGHT),
231 };
232 self.panel_size = Some(panel.layout(panel_constraints));
233 self.panel = Some(panel);
234 } else {
235 self.panel = None;
236 self.panel_size = None;
237 }
238 
239 self.cached_child_size
240 }
241 
242 fn render(&self, batch: &mut RenderBatch, layout: Layout) {
243 let child_layout = Layout::new(layout.position, self.cached_child_size);
244 self.child.render(batch, child_layout);
245 
246 if inspector::inspector().is_enabled() && self.visible {
247 inspector::inspector().record_layout_box(LayoutBoxSnapshot {
248 widget_id: strato_core::widget::WidgetId(self.child.id()),
249 bounds: Rect::new(
250 layout.position.x,
251 layout.position.y,
252 self.cached_child_size.width,
253 self.cached_child_size.height,
254 ),
255 });
256 
257 let snapshot = inspector::inspector().snapshot();
258 for layout_box in &snapshot.layout_boxes {
259 batch.add_rect(
260 layout_box.bounds,
261 Color::rgba(0.1, 0.7, 1.0, 0.15),
262 Transform::identity(),
263 );
264 }
265 
266 if let (Some(panel), Some(panel_size)) = (&self.panel, self.panel_size) {
267 let panel_pos = Vec2::new(
268 layout.position.x + layout.size.width - panel_size.width - 12.0,
269 layout.position.y + 12.0,
270 );
271 let panel_layout = Layout::new(panel_pos, panel_size);
272 panel.render(batch, panel_layout);
273 
274 inspector::inspector().record_layout_box(LayoutBoxSnapshot {
275 widget_id: strato_core::widget::WidgetId(panel.id()),
276 bounds: Rect::new(
277 panel_pos.x,
278 panel_pos.y,
279 panel_size.width,
280 panel_size.height,
281 ),
282 });
283 }
284 }
285 }
286 
287 fn handle_event(&mut self, event: &Event) -> EventResult {
288 if let Event::KeyDown(key) = event {
289 if self.shortcut_pressed(key) {
290 let now_visible = !self.visible;
291 self.visible = now_visible;
292 inspector::inspector().set_enabled(now_visible);
293 return EventResult::Handled;
294 }
295 }
296 
297 self.child.handle_event(event)
298 }
299}
300