1use std::fmt::Debug;
4
5use cranpose_core::NodeId;
6use cranpose_render_common::Renderer;
7use cranpose_ui::{KeyCode, KeyEvent, KeyEventType, LayoutTree, SemanticsTree};
8use cranpose_ui_graphics::{Point, Rect, Size};
9
10use crate::{AppShell, RootSurface, ShellApp, SurfaceMut};
11
12#[path = "inspector_draw.rs"]
13mod draw;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub enum InspectorMode {
18 #[default]
20 Normal,
21 Overlay,
23 Accessibility,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum InspectorAction {
30 Toggle,
32 Move,
34 Normal,
36 Overlay,
38 Accessibility,
40 Pick,
42 Previous,
44 Next,
46 DetailsUp,
48 DetailsDown,
50 Select(usize),
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct InspectorNode {
57 pub node_id: NodeId,
59 pub canvas_key: Option<u64>,
61 pub bounds: Rect,
63 pub label: String,
65 pub details: String,
67 pub focused: bool,
69 pub issue: bool,
71}
72
73#[derive(Clone, Debug, PartialEq)]
75pub struct InspectorControl {
76 pub action: InspectorAction,
78 pub bounds: Rect,
80}
81
82#[derive(Clone, Debug, Default, PartialEq)]
84pub struct InspectorState {
85 pub open: bool,
87 pub mode: InspectorMode,
89 pub picking: bool,
91 pub nodes: Vec<InspectorNode>,
93 pub selected: Option<usize>,
95 pub detail_offset: usize,
97 pub controls: Vec<InspectorControl>,
99 pub launcher_position: Option<Point>,
101 pub panel_position: Option<Point>,
103}
104
105pub type InspectorProjector = fn(&LayoutTree, &SemanticsTree) -> Vec<InspectorNode>;
107
108#[derive(Default)]
109pub(crate) struct DeveloperInspector {
110 pub(crate) state: InspectorState,
111 revision: Option<u64>,
112 viewport: Option<Size>,
113 dirty: bool,
114 pub(crate) pointer_captured: bool,
115 keyboard: bool,
116 installed: bool,
117 drag: Option<InspectorDrag>,
118}
119
120struct InspectorDrag {
121 action: InspectorAction,
122 start: Point,
123 origin: Point,
124 moved: bool,
125}
126
127impl DeveloperInspector {
128 fn apply(&mut self, action: InspectorAction) {
129 match action {
130 InspectorAction::Move => return,
131 InspectorAction::Toggle => {
132 self.state.open = !self.state.open;
133 self.state.picking = false;
134 self.keyboard = self.state.open;
135 if !self.state.open {
136 self.state.mode = InspectorMode::Normal;
137 self.state.nodes.clear();
138 self.state.selected = None;
139 self.revision = None;
140 }
141 }
142 InspectorAction::Normal => self.state.mode = InspectorMode::Normal,
143 InspectorAction::Overlay => self.state.mode = InspectorMode::Overlay,
144 InspectorAction::Accessibility => self.state.mode = InspectorMode::Accessibility,
145 InspectorAction::Pick => self.state.picking = !self.state.picking,
146 InspectorAction::Previous => self.select_relative(false),
147 InspectorAction::Next => self.select_relative(true),
148 InspectorAction::DetailsUp => {
149 self.state.detail_offset = self.state.detail_offset.saturating_sub(3)
150 }
151 InspectorAction::DetailsDown => {
152 let count = draw::detail_line_count(&self.state, self.viewport.unwrap_or_default());
153 self.state.detail_offset =
154 (self.state.detail_offset + 3).min(count.saturating_sub(1));
155 }
156 InspectorAction::Select(index) => {
157 self.state.selected = (index < self.state.nodes.len()).then_some(index);
158 self.state.detail_offset = 0;
159 }
160 }
161 self.dirty = true;
162 }
163
164 fn select_relative(&mut self, forward: bool) {
165 self.state.detail_offset = 0;
166 let len = self.state.nodes.len();
167 if len == 0 {
168 self.state.selected = None;
169 return;
170 }
171 self.state.selected = Some(match self.state.selected {
172 Some(index) if forward => (index + 1) % len,
173 Some(index) => (index + len - 1) % len,
174 None if forward => 0,
175 None => len - 1,
176 });
177 }
178
179 fn replace_nodes(&mut self, nodes: Vec<InspectorNode>) {
180 if self.state.nodes == nodes {
181 return;
182 }
183 let identity = self
184 .state
185 .selected
186 .and_then(|index| self.state.nodes.get(index))
187 .map(|node| (node.node_id, node.canvas_key));
188 self.state.selected = identity.and_then(|identity| {
189 nodes
190 .iter()
191 .position(|node| (node.node_id, node.canvas_key) == identity)
192 });
193 self.state.detail_offset = 0;
194 self.state.nodes = nodes;
195 self.dirty = true;
196 }
197
198 fn pick(&mut self, x: f32, y: f32) {
199 self.state.selected = self
200 .state
201 .nodes
202 .iter()
203 .enumerate()
204 .filter(|(_, node)| node.bounds.contains(x, y))
205 .min_by(|(_, a), (_, b)| {
206 (a.bounds.width * a.bounds.height).total_cmp(&(b.bounds.width * b.bounds.height))
207 })
208 .map(|(index, _)| index);
209 self.state.picking = false;
210 self.state.detail_offset = 0;
211 self.keyboard = true;
212 self.dirty = true;
213 }
214
215 fn start_drag(&mut self, action: InspectorAction, x: f32, y: f32) {
216 let viewport = self.viewport.unwrap_or_default();
217 let bounds = if action == InspectorAction::Move {
218 draw::panel_bounds(&self.state, viewport)
219 } else {
220 draw::launcher_bounds(&self.state, viewport)
221 };
222 self.drag = Some(InspectorDrag {
223 action,
224 start: Point { x, y },
225 origin: Point {
226 x: bounds.x,
227 y: bounds.y,
228 },
229 moved: false,
230 });
231 }
232
233 fn move_pointer(&mut self, x: f32, y: f32) -> bool {
234 let Some(drag) = &mut self.drag else {
235 return false;
236 };
237 let dx = x - drag.start.x;
238 let dy = y - drag.start.y;
239 drag.moved |= dx * dx + dy * dy >= 16.0;
240 if !drag.moved {
241 return false;
242 }
243 let position = Some(Point {
244 x: drag.origin.x + dx,
245 y: drag.origin.y + dy,
246 });
247 if drag.action == InspectorAction::Move {
248 self.state.panel_position = position;
249 } else {
250 self.state.launcher_position = position;
251 }
252 self.dirty = true;
253 true
254 }
255
256 pub(crate) fn release_pointer(&mut self) -> bool {
257 if !std::mem::take(&mut self.pointer_captured) {
258 return false;
259 }
260 if let Some(drag) = self.drag.take()
261 && !drag.moved
262 && drag.action != InspectorAction::Move
263 {
264 self.apply(drag.action);
265 }
266 true
267 }
268
269 pub(crate) fn cancel_pointer(&mut self) {
270 self.pointer_captured = false;
271 self.drag = None;
272 }
273}
274
275impl<R: Renderer> AppShell<R>
276where
277 R::Error: Debug,
278{
279 pub fn set_inspector_projector(&mut self, projector: Option<InspectorProjector>) {
284 self.app.inspector_projector = projector;
285 for surface in &mut self.surfaces {
286 surface.inspector = DeveloperInspector {
287 dirty: true,
288 ..Default::default()
289 };
290 if projector.is_none() {
291 surface.renderer.set_inspector_overlay(None);
292 }
293 surface.is_dirty = true;
294 }
295 }
296
297 pub fn inspector_state(&self) -> &InspectorState {
299 &self.surfaces[0].inspector.state
300 }
301}
302
303impl<R: Renderer> SurfaceMut<'_, R>
304where
305 R::Error: Debug,
306{
307 pub fn inspector_state(&self) -> &InspectorState {
309 &self.surface().inspector.state
310 }
311
312 pub(crate) fn inspector_owns_keyboard(&self) -> bool {
313 let inspector = &self.surface().inspector;
314 inspector.state.open && inspector.keyboard
315 }
316
317 pub(crate) fn inspector_blocks_pointer(&self, x: f32, y: f32) -> bool {
318 let inspector = &self.surface().inspector;
319 self.shell_app_ref().inspector_projector.is_some()
320 && (inspector.pointer_captured
321 || inspector.state.picking
322 || inspector
323 .state
324 .controls
325 .iter()
326 .any(|control| control.bounds.contains(x, y))
327 || (inspector.state.open
328 && draw::panel_bounds(
329 &inspector.state,
330 inspector.viewport.unwrap_or_default(),
331 )
332 .contains(x, y)))
333 }
334
335 pub(crate) fn inspector_move(&mut self, x: f32, y: f32) -> bool {
336 self.surface_mut().inspector.move_pointer(x, y)
337 }
338
339 pub(crate) fn inspector_scroll(&mut self, delta: f32) -> bool {
340 let (x, y) = self.surface().cursor;
341 if !self.inspector_blocks_pointer(x, y) {
342 return false;
343 }
344 let inspector = &mut self.surface_mut().inspector;
345 if inspector.state.open && delta != 0.0 {
346 inspector.apply(if delta < 0.0 {
347 InspectorAction::DetailsDown
348 } else {
349 InspectorAction::DetailsUp
350 });
351 self.mark_dirty();
352 }
353 true
354 }
355
356 pub(crate) fn inspector_press(&mut self, x: f32, y: f32) -> bool {
357 if self.shell_app_ref().inspector_projector.is_none() {
358 return false;
359 }
360 let inspector = &mut self.surface_mut().inspector;
361 let action = inspector
362 .state
363 .controls
364 .iter()
365 .find(|control| control.bounds.contains(x, y))
366 .map(|control| control.action);
367 let consumed = if let Some(action) = action {
368 if action == InspectorAction::Move || !inspector.state.open || inspector.state.picking {
369 inspector.start_drag(action, x, y);
370 } else {
371 inspector.apply(action);
372 }
373 inspector.keyboard = inspector.state.open;
374 true
375 } else if inspector.state.open && inspector.state.picking {
376 inspector.pick(x, y);
377 true
378 } else {
379 inspector.keyboard = false;
380 inspector.state.open
381 && draw::panel_bounds(&inspector.state, inspector.viewport.unwrap_or_default())
382 .contains(x, y)
383 };
384 if consumed {
385 inspector.keyboard = inspector.state.open;
386 inspector.pointer_captured = true;
387 self.mark_dirty();
388 }
389 consumed
390 }
391
392 pub(crate) fn inspector_key(&mut self, event: &KeyEvent) -> bool {
393 if self.shell_app_ref().inspector_projector.is_none() {
394 return false;
395 }
396 let inspector = &mut self.surface_mut().inspector;
397 let action = if inspector.state.open && inspector.keyboard {
398 match event.key_code {
399 KeyCode::Escape => Some(InspectorAction::Toggle),
400 KeyCode::ArrowUp | KeyCode::ArrowLeft => Some(InspectorAction::Previous),
401 KeyCode::ArrowDown | KeyCode::ArrowRight => Some(InspectorAction::Next),
402 KeyCode::Digit1 => Some(InspectorAction::Normal),
403 KeyCode::Digit2 => Some(InspectorAction::Overlay),
404 KeyCode::Digit3 => Some(InspectorAction::Accessibility),
405 KeyCode::P => Some(InspectorAction::Pick),
406 KeyCode::PageUp => Some(InspectorAction::DetailsUp),
407 KeyCode::PageDown => Some(InspectorAction::DetailsDown),
408 _ => None,
409 }
410 } else {
411 None
412 };
413 let Some(action) = action else {
414 return inspector.state.open && inspector.keyboard;
415 };
416 if event.event_type == KeyEventType::KeyDown {
417 inspector.apply(action);
418 self.mark_dirty();
419 }
420 true
421 }
422}
423
424pub(crate) fn refresh<R: Renderer>(
425 app: &mut ShellApp,
426 surface: &mut RootSurface<R>,
427 revision: u64,
428) -> bool {
429 let Some(projector) = app.inspector_projector else {
430 return false;
431 };
432 let viewport = surface.viewport_size();
433 if surface.inspector.viewport != Some(viewport) {
434 surface.inspector.viewport = Some(viewport);
435 surface.inspector.dirty = true;
436 }
437 if surface.inspector.state.open && surface.inspector.revision != Some(revision) {
438 surface.layout_tree_in_context(app);
439 surface.semantics_tree_in_context(app);
440 let nodes = match (&surface.layout_tree, &surface.semantics_tree) {
441 (Some(layout), Some(semantics)) => projector(layout, semantics),
442 _ => Vec::new(),
443 };
444 surface.inspector.replace_nodes(nodes);
445 surface.inspector.revision = Some(revision);
446 }
447 if !surface.inspector.dirty && surface.inspector.installed {
448 return false;
449 }
450 let graph = draw::build(&mut surface.inspector.state, viewport);
451 surface.renderer.set_inspector_overlay(Some(graph));
452 surface.inspector.installed = true;
453 surface.inspector.dirty = false;
454 true
455}
456
457#[cfg(test)]
458#[path = "tests/inspector_tests.rs"]
459mod tests;