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, 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/// 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, given
106/// the window bounds of each node the surface's scene draws: where its layers show it.
107pub 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    /// The window's edges the system draws on; the controls stay inside.
115    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    /// The part of the window outside the system's edges, where the
132    /// controls go.
133    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    /// Installs or disables the developer inspector without adding application nodes.
297    ///
298    /// Hosts install the platform projection in debug builds. `None` removes all
299    /// inspector drawing and input handling, including on secondary surfaces.
300    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    /// The edges of the primary window the system draws on, such as a
316    /// phone's status bar, its rounded corners and its home indicator. The
317    /// developer inspector keeps its controls inside them, where they can be
318    /// reached.
319    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    /// The primary surface's inspector state, independent of application semantics.
329    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    /// This surface's developer UI and projected application elements.
339    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/// Brings the inspector overlay of `surface` up to date. Out of line, as an
462/// app seldom installs the inspector.
463#[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;