Skip to main content

cranpose_app_shell/
inspector.rs

1//! Developer inspection drawn outside the application's composition and semantics.
2
3use 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/// The application's visual presentation while inspecting accessibility.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub enum InspectorMode {
18    /// Draw the application normally.
19    #[default]
20    Normal,
21    /// Draw accessibility bounds and reading-order numbers over the application.
22    Overlay,
23    /// Cover the application with its accessible controls and labels.
24    Accessibility,
25}
26
27/// An inspector control, independent of the application's actions.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum InspectorAction {
30    /// Open or close the inspector.
31    Toggle,
32    /// Drag the floating panel by its title bar.
33    Move,
34    /// Show the normal application.
35    Normal,
36    /// Show accessibility outlines.
37    Overlay,
38    /// Show only the accessibility representation.
39    Accessibility,
40    /// Select a control by its screen bounds without activating it.
41    Pick,
42    /// Select the previous accessible element.
43    Previous,
44    /// Select the next accessible element.
45    Next,
46    /// Scroll the selected element's properties toward the beginning.
47    DetailsUp,
48    /// Scroll the selected element's properties toward the end.
49    DetailsDown,
50    /// Select an element at its reading-order index.
51    Select(usize),
52}
53
54/// A sanitized platform-projection element for developer inspection.
55#[derive(Clone, Debug, PartialEq)]
56pub struct InspectorNode {
57    /// The application node owning this accessible element.
58    pub node_id: NodeId,
59    /// The identity of a virtual canvas child, when present.
60    pub canvas_key: Option<u64>,
61    /// Accessible bounds in surface-local logical pixels.
62    pub bounds: Rect,
63    /// Accessible name and role for the reading-order list.
64    pub label: String,
65    /// Accessible properties and actions, with password values excluded.
66    pub details: String,
67    /// Whether the application reports this element as focused.
68    pub focused: bool,
69    /// An actionable naming issue to mark in the overlay.
70    pub issue: bool,
71}
72
73/// A visible inspector control and its logical hit bounds.
74#[derive(Clone, Debug, PartialEq)]
75pub struct InspectorControl {
76    /// The operation performed by this control.
77    pub action: InspectorAction,
78    /// Bounds shared by rendering and pointer dispatch.
79    pub bounds: Rect,
80}
81
82/// A read-only snapshot of developer UI, separate from application semantics.
83#[derive(Clone, Debug, Default, PartialEq)]
84pub struct InspectorState {
85    /// Whether the inspector panel is open.
86    pub open: bool,
87    /// The selected visual mode.
88    pub mode: InspectorMode,
89    /// Whether the next application press selects an element.
90    pub picking: bool,
91    /// Elements in the shared projection's reading order.
92    pub nodes: Vec<InspectorNode>,
93    /// The selected element's index in `nodes`.
94    pub selected: Option<usize>,
95    /// The first visible line of the selected element's properties.
96    pub detail_offset: usize,
97    /// Visible developer controls for deterministic robot input.
98    pub controls: Vec<InspectorControl>,
99    /// User-positioned launcher origin in logical pixels, clamped to the surface.
100    pub launcher_position: Option<Point>,
101    /// User-positioned panel origin in logical pixels, clamped to the surface.
102    pub panel_position: Option<Point>,
103}
104
105/// Projects a surface's trees using the same policy as its platform bridge.
106pub 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    /// Installs or disables the developer inspector without adding application nodes.
280    ///
281    /// Hosts install the platform projection in debug builds. `None` removes all
282    /// inspector drawing and input handling, including on secondary surfaces.
283    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    /// The primary surface's inspector state, independent of application semantics.
298    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    /// This surface's developer UI and projected application elements.
308    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;