1use std::fmt::Debug;
4
5use cranpose_core::{NodeId, collections::map::HashMap};
6use cranpose_render_common::{RenderScene, Renderer};
7use cranpose_ui::{Key, KeyEvent, KeyEventType, SemanticsTree};
8use cranpose_ui_graphics::{EdgeInsets, 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(&SemanticsTree, &HashMap<NodeId, Rect>) -> Vec<InspectorNode>;
108
109#[derive(Default)]
110pub(crate) struct DeveloperInspector {
111 pub(crate) state: InspectorState,
112 revision: Option<u64>,
113 viewport: Option<Size>,
114 insets: EdgeInsets,
116 dirty: bool,
117 pub(crate) pointer_captured: bool,
118 keyboard: bool,
119 installed: bool,
120 drag: Option<InspectorDrag>,
121}
122
123struct InspectorDrag {
124 action: InspectorAction,
125 start: Point,
126 origin: Point,
127 moved: bool,
128}
129
130impl DeveloperInspector {
131 fn area(&self) -> Rect {
134 let viewport = self.viewport.unwrap_or_default();
135 let insets = self.insets;
136 Rect {
137 x: insets.left.min(viewport.width),
138 y: insets.top.min(viewport.height),
139 width: (viewport.width - insets.left - insets.right).max(0.0),
140 height: (viewport.height - insets.top - insets.bottom).max(0.0),
141 }
142 }
143
144 fn apply(&mut self, action: InspectorAction) {
145 match action {
146 InspectorAction::Move => return,
147 InspectorAction::Toggle => {
148 self.state.open = !self.state.open;
149 self.state.picking = false;
150 self.keyboard = self.state.open;
151 if !self.state.open {
152 self.state.mode = InspectorMode::Normal;
153 self.state.nodes.clear();
154 self.state.selected = None;
155 self.revision = None;
156 }
157 }
158 InspectorAction::Normal => self.state.mode = InspectorMode::Normal,
159 InspectorAction::Overlay => self.state.mode = InspectorMode::Overlay,
160 InspectorAction::Accessibility => self.state.mode = InspectorMode::Accessibility,
161 InspectorAction::Pick => self.state.picking = !self.state.picking,
162 InspectorAction::Previous => self.select_relative(false),
163 InspectorAction::Next => self.select_relative(true),
164 InspectorAction::DetailsUp => {
165 self.state.detail_offset = self.state.detail_offset.saturating_sub(3);
166 }
167 InspectorAction::DetailsDown => {
168 let count = draw::detail_line_count(&self.state, self.area());
169 self.state.detail_offset =
170 (self.state.detail_offset + 3).min(count.saturating_sub(1));
171 }
172 InspectorAction::Select(index) => {
173 self.state.selected = (index < self.state.nodes.len()).then_some(index);
174 self.state.detail_offset = 0;
175 }
176 }
177 self.dirty = true;
178 }
179
180 fn select_relative(&mut self, forward: bool) {
181 self.state.detail_offset = 0;
182 let len = self.state.nodes.len();
183 if len == 0 {
184 self.state.selected = None;
185 return;
186 }
187 self.state.selected = Some(match self.state.selected {
188 Some(index) if forward => (index + 1) % len,
189 Some(index) => (index + len - 1) % len,
190 None if forward => 0,
191 None => len - 1,
192 });
193 }
194
195 fn replace_nodes(&mut self, nodes: Vec<InspectorNode>) {
196 if self.state.nodes == nodes {
197 return;
198 }
199 let identity = self
200 .state
201 .selected
202 .and_then(|index| self.state.nodes.get(index))
203 .map(|node| (node.node_id, node.canvas_key));
204 self.state.selected = identity.and_then(|identity| {
205 nodes
206 .iter()
207 .position(|node| (node.node_id, node.canvas_key) == identity)
208 });
209 self.state.detail_offset = 0;
210 self.state.nodes = nodes;
211 self.dirty = true;
212 }
213
214 fn pick(&mut self, x: f32, y: f32) {
215 self.state.selected = self
216 .state
217 .nodes
218 .iter()
219 .enumerate()
220 .filter(|(_, node)| node.bounds.contains(x, y))
221 .min_by(|(_, a), (_, b)| {
222 (a.bounds.width * a.bounds.height).total_cmp(&(b.bounds.width * b.bounds.height))
223 })
224 .map(|(index, _)| index);
225 self.state.picking = false;
226 self.state.detail_offset = 0;
227 self.keyboard = true;
228 self.dirty = true;
229 }
230
231 fn start_drag(&mut self, action: InspectorAction, x: f32, y: f32) {
232 let area = self.area();
233 let bounds = if action == InspectorAction::Move {
234 draw::panel_bounds(&self.state, area)
235 } else {
236 draw::launcher_bounds(&self.state, area)
237 };
238 self.drag = Some(InspectorDrag {
239 action,
240 start: Point { x, y },
241 origin: Point {
242 x: bounds.x,
243 y: bounds.y,
244 },
245 moved: false,
246 });
247 }
248
249 fn move_pointer(&mut self, x: f32, y: f32) -> bool {
250 let Some(drag) = &mut self.drag else {
251 return false;
252 };
253 let dx = x - drag.start.x;
254 let dy = y - drag.start.y;
255 drag.moved |= dx * dx + dy * dy >= 16.0;
256 if !drag.moved {
257 return false;
258 }
259 let position = Some(Point {
260 x: drag.origin.x + dx,
261 y: drag.origin.y + dy,
262 });
263 if drag.action == InspectorAction::Move {
264 self.state.panel_position = position;
265 } else {
266 self.state.launcher_position = position;
267 }
268 self.dirty = true;
269 true
270 }
271
272 pub(crate) fn release_pointer(&mut self) -> bool {
273 if !std::mem::take(&mut self.pointer_captured) {
274 return false;
275 }
276 if let Some(drag) = self.drag.take()
277 && !drag.moved
278 && drag.action != InspectorAction::Move
279 {
280 self.apply(drag.action);
281 }
282 true
283 }
284
285 pub(crate) fn cancel_pointer(&mut self) {
286 self.pointer_captured = false;
287 self.drag = None;
288 }
289}
290
291#[expect(clippy::multiple_inherent_impl, reason = "one file per shell concern")]
292impl<R: Renderer> AppShell<R>
293where
294 R::Error: Debug,
295{
296 pub fn set_inspector_projector(&mut self, projector: Option<InspectorProjector>) {
301 self.app.inspector_projector = projector;
302 for surface in &mut self.surfaces {
303 surface.inspector = DeveloperInspector {
304 insets: surface.inspector.insets,
305 dirty: true,
306 ..Default::default()
307 };
308 if projector.is_none() {
309 surface.renderer.set_inspector_overlay(None);
310 }
311 surface.is_dirty = true;
312 }
313 }
314
315 pub fn set_safe_area(&mut self, insets: EdgeInsets) {
320 let surface = &mut self.surfaces[0];
321 if surface.inspector.insets != insets {
322 surface.inspector.insets = insets;
323 surface.inspector.dirty = true;
324 surface.is_dirty = true;
325 }
326 }
327
328 pub fn inspector_state(&self) -> &InspectorState {
330 &self.surfaces[0].inspector.state
331 }
332}
333
334impl<R: Renderer> SurfaceMut<'_, R>
335where
336 R::Error: Debug,
337{
338 pub fn inspector_state(&self) -> &InspectorState {
340 &self.surface().inspector.state
341 }
342
343 pub(crate) fn inspector_owns_keyboard(&self) -> bool {
344 let inspector = &self.surface().inspector;
345 inspector.state.open && inspector.keyboard
346 }
347
348 pub(crate) fn inspector_blocks_pointer(&self, x: f32, y: f32) -> bool {
349 let inspector = &self.surface().inspector;
350 self.shell_app_ref().inspector_projector.is_some()
351 && (inspector.pointer_captured
352 || inspector.state.picking
353 || inspector
354 .state
355 .controls
356 .iter()
357 .any(|control| control.bounds.contains(x, y))
358 || (inspector.state.open
359 && draw::panel_bounds(&inspector.state, inspector.area()).contains(x, y)))
360 }
361
362 pub(crate) fn inspector_move(&mut self, x: f32, y: f32) -> bool {
363 self.surface_mut().inspector.move_pointer(x, y)
364 }
365
366 pub(crate) fn inspector_scroll(&mut self, delta: f32) -> bool {
367 let (x, y) = self.surface().cursor;
368 if !self.inspector_blocks_pointer(x, y) {
369 return false;
370 }
371 let inspector = &mut self.surface_mut().inspector;
372 if inspector.state.open && delta != 0.0 {
373 inspector.apply(if delta < 0.0 {
374 InspectorAction::DetailsDown
375 } else {
376 InspectorAction::DetailsUp
377 });
378 self.mark_dirty();
379 }
380 true
381 }
382
383 pub(crate) fn inspector_press(&mut self, x: f32, y: f32) -> bool {
384 if self.shell_app_ref().inspector_projector.is_none() {
385 return false;
386 }
387 let inspector = &mut self.surface_mut().inspector;
388 let action = inspector
389 .state
390 .controls
391 .iter()
392 .find(|control| control.bounds.contains(x, y))
393 .map(|control| control.action);
394 let consumed = if let Some(action) = action {
395 if action == InspectorAction::Move || !inspector.state.open || inspector.state.picking {
396 inspector.start_drag(action, x, y);
397 } else {
398 inspector.apply(action);
399 }
400 inspector.keyboard = inspector.state.open;
401 true
402 } else if inspector.state.open && inspector.state.picking {
403 inspector.pick(x, y);
404 true
405 } else {
406 inspector.keyboard = false;
407 inspector.state.open
408 && draw::panel_bounds(&inspector.state, inspector.area()).contains(x, y)
409 };
410 if consumed {
411 inspector.keyboard = inspector.state.open;
412 inspector.pointer_captured = true;
413 self.mark_dirty();
414 }
415 consumed
416 }
417
418 pub(crate) fn inspector_key(&mut self, event: &KeyEvent) -> bool {
419 if self.shell_app_ref().inspector_projector.is_none() {
420 return false;
421 }
422 let inspector = &mut self.surface_mut().inspector;
423 let action = if inspector.state.open && inspector.keyboard {
424 match event.key() {
425 Key::Escape => Some(InspectorAction::Toggle),
426 Key::DirectionUp | Key::DirectionLeft => Some(InspectorAction::Previous),
427 Key::DirectionDown | Key::DirectionRight => Some(InspectorAction::Next),
428 Key::One => Some(InspectorAction::Normal),
429 Key::Two => Some(InspectorAction::Overlay),
430 Key::Three => Some(InspectorAction::Accessibility),
431 Key::P => Some(InspectorAction::Pick),
432 Key::PageUp => Some(InspectorAction::DetailsUp),
433 Key::PageDown => Some(InspectorAction::DetailsDown),
434 _ => None,
435 }
436 } else {
437 None
438 };
439 let Some(action) = action else {
440 return inspector.state.open && inspector.keyboard;
441 };
442 if event.type_() == KeyEventType::KeyDown {
443 inspector.apply(action);
444 self.mark_dirty();
445 }
446 true
447 }
448}
449
450pub(crate) fn refresh<R: Renderer>(
451 app: &mut ShellApp,
452 surface: &mut RootSurface<R>,
453 revision: u64,
454) -> bool {
455 match app.inspector_projector {
456 Some(projector) => refresh_with(app, surface, revision, projector),
457 None => false,
458 }
459}
460
461#[inline(never)]
464fn refresh_with<R: Renderer>(
465 app: &mut ShellApp,
466 surface: &mut RootSurface<R>,
467 revision: u64,
468 projector: InspectorProjector,
469) -> bool {
470 let viewport = surface.viewport_size();
471 if surface.inspector.viewport != Some(viewport) {
472 surface.inspector.viewport = Some(viewport);
473 surface.inspector.dirty = true;
474 }
475 if surface.inspector.state.open && surface.inspector.revision != Some(revision) {
476 surface.semantics_tree_in_context(app);
477 let mut bounds = HashMap::default();
478 surface
479 .renderer
480 .scene()
481 .node_window_bounds(&mut |node_id, rect| {
482 bounds.insert(node_id, rect);
483 });
484 let nodes = surface
485 .semantics_tree
486 .as_ref()
487 .map_or_else(Vec::new, |tree| projector(tree, &bounds));
488 surface.inspector.replace_nodes(nodes);
489 surface.inspector.revision = Some(revision);
490 }
491 if !surface.inspector.dirty && surface.inspector.installed {
492 return false;
493 }
494 let area = surface.inspector.area();
495 let graph = draw::build(&mut surface.inspector.state, viewport, area);
496 surface.renderer.set_inspector_overlay(Some(graph));
497 surface.inspector.installed = true;
498 surface.inspector.dirty = false;
499 true
500}
501
502#[cfg(test)]
503#[path = "tests/inspector_tests.rs"]
504mod tests;