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, 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/// 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 semantics tree using the same policy as its platform bridge.
106pub type InspectorProjector = fn(&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    /// The window's edges the system draws on; the controls stay inside.
114    insets: EdgeInsets,
115    dirty: bool,
116    pub(crate) pointer_captured: bool,
117    keyboard: bool,
118    installed: bool,
119    drag: Option<InspectorDrag>,
120}
121
122struct InspectorDrag {
123    action: InspectorAction,
124    start: Point,
125    origin: Point,
126    moved: bool,
127}
128
129impl DeveloperInspector {
130    /// The part of the window outside the system's edges, where the
131    /// controls go.
132    fn area(&self) -> Rect {
133        let viewport = self.viewport.unwrap_or_default();
134        let insets = self.insets;
135        Rect {
136            x: insets.left.min(viewport.width),
137            y: insets.top.min(viewport.height),
138            width: (viewport.width - insets.left - insets.right).max(0.0),
139            height: (viewport.height - insets.top - insets.bottom).max(0.0),
140        }
141    }
142
143    fn apply(&mut self, action: InspectorAction) {
144        match action {
145            InspectorAction::Move => return,
146            InspectorAction::Toggle => {
147                self.state.open = !self.state.open;
148                self.state.picking = false;
149                self.keyboard = self.state.open;
150                if !self.state.open {
151                    self.state.mode = InspectorMode::Normal;
152                    self.state.nodes.clear();
153                    self.state.selected = None;
154                    self.revision = None;
155                }
156            }
157            InspectorAction::Normal => self.state.mode = InspectorMode::Normal,
158            InspectorAction::Overlay => self.state.mode = InspectorMode::Overlay,
159            InspectorAction::Accessibility => self.state.mode = InspectorMode::Accessibility,
160            InspectorAction::Pick => self.state.picking = !self.state.picking,
161            InspectorAction::Previous => self.select_relative(false),
162            InspectorAction::Next => self.select_relative(true),
163            InspectorAction::DetailsUp => {
164                self.state.detail_offset = self.state.detail_offset.saturating_sub(3);
165            }
166            InspectorAction::DetailsDown => {
167                let count = draw::detail_line_count(&self.state, self.area());
168                self.state.detail_offset =
169                    (self.state.detail_offset + 3).min(count.saturating_sub(1));
170            }
171            InspectorAction::Select(index) => {
172                self.state.selected = (index < self.state.nodes.len()).then_some(index);
173                self.state.detail_offset = 0;
174            }
175        }
176        self.dirty = true;
177    }
178
179    fn select_relative(&mut self, forward: bool) {
180        self.state.detail_offset = 0;
181        let len = self.state.nodes.len();
182        if len == 0 {
183            self.state.selected = None;
184            return;
185        }
186        self.state.selected = Some(match self.state.selected {
187            Some(index) if forward => (index + 1) % len,
188            Some(index) => (index + len - 1) % len,
189            None if forward => 0,
190            None => len - 1,
191        });
192    }
193
194    fn replace_nodes(&mut self, nodes: Vec<InspectorNode>) {
195        if self.state.nodes == nodes {
196            return;
197        }
198        let identity = self
199            .state
200            .selected
201            .and_then(|index| self.state.nodes.get(index))
202            .map(|node| (node.node_id, node.canvas_key));
203        self.state.selected = identity.and_then(|identity| {
204            nodes
205                .iter()
206                .position(|node| (node.node_id, node.canvas_key) == identity)
207        });
208        self.state.detail_offset = 0;
209        self.state.nodes = nodes;
210        self.dirty = true;
211    }
212
213    fn pick(&mut self, x: f32, y: f32) {
214        self.state.selected = self
215            .state
216            .nodes
217            .iter()
218            .enumerate()
219            .filter(|(_, node)| node.bounds.contains(x, y))
220            .min_by(|(_, a), (_, b)| {
221                (a.bounds.width * a.bounds.height).total_cmp(&(b.bounds.width * b.bounds.height))
222            })
223            .map(|(index, _)| index);
224        self.state.picking = false;
225        self.state.detail_offset = 0;
226        self.keyboard = true;
227        self.dirty = true;
228    }
229
230    fn start_drag(&mut self, action: InspectorAction, x: f32, y: f32) {
231        let area = self.area();
232        let bounds = if action == InspectorAction::Move {
233            draw::panel_bounds(&self.state, area)
234        } else {
235            draw::launcher_bounds(&self.state, area)
236        };
237        self.drag = Some(InspectorDrag {
238            action,
239            start: Point { x, y },
240            origin: Point {
241                x: bounds.x,
242                y: bounds.y,
243            },
244            moved: false,
245        });
246    }
247
248    fn move_pointer(&mut self, x: f32, y: f32) -> bool {
249        let Some(drag) = &mut self.drag else {
250            return false;
251        };
252        let dx = x - drag.start.x;
253        let dy = y - drag.start.y;
254        drag.moved |= dx * dx + dy * dy >= 16.0;
255        if !drag.moved {
256            return false;
257        }
258        let position = Some(Point {
259            x: drag.origin.x + dx,
260            y: drag.origin.y + dy,
261        });
262        if drag.action == InspectorAction::Move {
263            self.state.panel_position = position;
264        } else {
265            self.state.launcher_position = position;
266        }
267        self.dirty = true;
268        true
269    }
270
271    pub(crate) fn release_pointer(&mut self) -> bool {
272        if !std::mem::take(&mut self.pointer_captured) {
273            return false;
274        }
275        if let Some(drag) = self.drag.take()
276            && !drag.moved
277            && drag.action != InspectorAction::Move
278        {
279            self.apply(drag.action);
280        }
281        true
282    }
283
284    pub(crate) fn cancel_pointer(&mut self) {
285        self.pointer_captured = false;
286        self.drag = None;
287    }
288}
289
290impl<R: Renderer> AppShell<R>
291where
292    R::Error: Debug,
293{
294    /// Installs or disables the developer inspector without adding application nodes.
295    ///
296    /// Hosts install the platform projection in debug builds. `None` removes all
297    /// inspector drawing and input handling, including on secondary surfaces.
298    pub fn set_inspector_projector(&mut self, projector: Option<InspectorProjector>) {
299        self.app.inspector_projector = projector;
300        for surface in &mut self.surfaces {
301            surface.inspector = DeveloperInspector {
302                insets: surface.inspector.insets,
303                dirty: true,
304                ..Default::default()
305            };
306            if projector.is_none() {
307                surface.renderer.set_inspector_overlay(None);
308            }
309            surface.is_dirty = true;
310        }
311    }
312
313    /// The edges of the primary window the system draws on, such as a
314    /// phone's status bar, its rounded corners and its home indicator. The
315    /// developer inspector keeps its controls inside them, where they can be
316    /// reached.
317    pub fn set_safe_area(&mut self, insets: EdgeInsets) {
318        let surface = &mut self.surfaces[0];
319        if surface.inspector.insets != insets {
320            surface.inspector.insets = insets;
321            surface.inspector.dirty = true;
322            surface.is_dirty = true;
323        }
324    }
325
326    /// The primary surface's inspector state, independent of application semantics.
327    pub fn inspector_state(&self) -> &InspectorState {
328        &self.surfaces[0].inspector.state
329    }
330}
331
332impl<R: Renderer> SurfaceMut<'_, R>
333where
334    R::Error: Debug,
335{
336    /// This surface's developer UI and projected application elements.
337    pub fn inspector_state(&self) -> &InspectorState {
338        &self.surface().inspector.state
339    }
340
341    pub(crate) fn inspector_owns_keyboard(&self) -> bool {
342        let inspector = &self.surface().inspector;
343        inspector.state.open && inspector.keyboard
344    }
345
346    pub(crate) fn inspector_blocks_pointer(&self, x: f32, y: f32) -> bool {
347        let inspector = &self.surface().inspector;
348        self.shell_app_ref().inspector_projector.is_some()
349            && (inspector.pointer_captured
350                || inspector.state.picking
351                || inspector
352                    .state
353                    .controls
354                    .iter()
355                    .any(|control| control.bounds.contains(x, y))
356                || (inspector.state.open
357                    && draw::panel_bounds(&inspector.state, inspector.area()).contains(x, y)))
358    }
359
360    pub(crate) fn inspector_move(&mut self, x: f32, y: f32) -> bool {
361        self.surface_mut().inspector.move_pointer(x, y)
362    }
363
364    pub(crate) fn inspector_scroll(&mut self, delta: f32) -> bool {
365        let (x, y) = self.surface().cursor;
366        if !self.inspector_blocks_pointer(x, y) {
367            return false;
368        }
369        let inspector = &mut self.surface_mut().inspector;
370        if inspector.state.open && delta != 0.0 {
371            inspector.apply(if delta < 0.0 {
372                InspectorAction::DetailsDown
373            } else {
374                InspectorAction::DetailsUp
375            });
376            self.mark_dirty();
377        }
378        true
379    }
380
381    pub(crate) fn inspector_press(&mut self, x: f32, y: f32) -> bool {
382        if self.shell_app_ref().inspector_projector.is_none() {
383            return false;
384        }
385        let inspector = &mut self.surface_mut().inspector;
386        let action = inspector
387            .state
388            .controls
389            .iter()
390            .find(|control| control.bounds.contains(x, y))
391            .map(|control| control.action);
392        let consumed = if let Some(action) = action {
393            if action == InspectorAction::Move || !inspector.state.open || inspector.state.picking {
394                inspector.start_drag(action, x, y);
395            } else {
396                inspector.apply(action);
397            }
398            inspector.keyboard = inspector.state.open;
399            true
400        } else if inspector.state.open && inspector.state.picking {
401            inspector.pick(x, y);
402            true
403        } else {
404            inspector.keyboard = false;
405            inspector.state.open
406                && draw::panel_bounds(&inspector.state, inspector.area()).contains(x, y)
407        };
408        if consumed {
409            inspector.keyboard = inspector.state.open;
410            inspector.pointer_captured = true;
411            self.mark_dirty();
412        }
413        consumed
414    }
415
416    pub(crate) fn inspector_key(&mut self, event: &KeyEvent) -> bool {
417        if self.shell_app_ref().inspector_projector.is_none() {
418            return false;
419        }
420        let inspector = &mut self.surface_mut().inspector;
421        let action = if inspector.state.open && inspector.keyboard {
422            match event.key_code {
423                KeyCode::Escape => Some(InspectorAction::Toggle),
424                KeyCode::ArrowUp | KeyCode::ArrowLeft => Some(InspectorAction::Previous),
425                KeyCode::ArrowDown | KeyCode::ArrowRight => Some(InspectorAction::Next),
426                KeyCode::Digit1 => Some(InspectorAction::Normal),
427                KeyCode::Digit2 => Some(InspectorAction::Overlay),
428                KeyCode::Digit3 => Some(InspectorAction::Accessibility),
429                KeyCode::P => Some(InspectorAction::Pick),
430                KeyCode::PageUp => Some(InspectorAction::DetailsUp),
431                KeyCode::PageDown => Some(InspectorAction::DetailsDown),
432                _ => None,
433            }
434        } else {
435            None
436        };
437        let Some(action) = action else {
438            return inspector.state.open && inspector.keyboard;
439        };
440        if event.event_type == KeyEventType::KeyDown {
441            inspector.apply(action);
442            self.mark_dirty();
443        }
444        true
445    }
446}
447
448pub(crate) fn refresh<R: Renderer>(
449    app: &mut ShellApp,
450    surface: &mut RootSurface<R>,
451    revision: u64,
452) -> bool {
453    let Some(projector) = app.inspector_projector else {
454        return false;
455    };
456    let viewport = surface.viewport_size();
457    if surface.inspector.viewport != Some(viewport) {
458        surface.inspector.viewport = Some(viewport);
459        surface.inspector.dirty = true;
460    }
461    if surface.inspector.state.open && surface.inspector.revision != Some(revision) {
462        surface.semantics_tree_in_context(app);
463        let nodes = surface
464            .semantics_tree
465            .as_ref()
466            .map_or_else(Vec::new, projector);
467        surface.inspector.replace_nodes(nodes);
468        surface.inspector.revision = Some(revision);
469    }
470    if !surface.inspector.dirty && surface.inspector.installed {
471        return false;
472    }
473    let area = surface.inspector.area();
474    let graph = draw::build(&mut surface.inspector.state, viewport, area);
475    surface.renderer.set_inspector_overlay(Some(graph));
476    surface.inspector.installed = true;
477    surface.inspector.dirty = false;
478    true
479}
480
481#[cfg(test)]
482#[path = "tests/inspector_tests.rs"]
483mod tests;