use std::fmt::Debug;
use cranpose_core::NodeId;
use cranpose_render_common::Renderer;
use cranpose_ui::{KeyCode, KeyEvent, KeyEventType, LayoutTree, SemanticsTree};
use cranpose_ui_graphics::{Point, Rect, Size};
use crate::{AppShell, RootSurface, ShellApp, SurfaceMut};
#[path = "inspector_draw.rs"]
mod draw;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum InspectorMode {
#[default]
Normal,
Overlay,
Accessibility,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InspectorAction {
Toggle,
Move,
Normal,
Overlay,
Accessibility,
Pick,
Previous,
Next,
DetailsUp,
DetailsDown,
Select(usize),
}
#[derive(Clone, Debug, PartialEq)]
pub struct InspectorNode {
pub node_id: NodeId,
pub canvas_key: Option<u64>,
pub bounds: Rect,
pub label: String,
pub details: String,
pub focused: bool,
pub issue: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct InspectorControl {
pub action: InspectorAction,
pub bounds: Rect,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct InspectorState {
pub open: bool,
pub mode: InspectorMode,
pub picking: bool,
pub nodes: Vec<InspectorNode>,
pub selected: Option<usize>,
pub detail_offset: usize,
pub controls: Vec<InspectorControl>,
pub launcher_position: Option<Point>,
pub panel_position: Option<Point>,
}
pub type InspectorProjector = fn(&LayoutTree, &SemanticsTree) -> Vec<InspectorNode>;
#[derive(Default)]
pub(crate) struct DeveloperInspector {
pub(crate) state: InspectorState,
revision: Option<u64>,
viewport: Option<Size>,
dirty: bool,
pub(crate) pointer_captured: bool,
keyboard: bool,
installed: bool,
drag: Option<InspectorDrag>,
}
struct InspectorDrag {
action: InspectorAction,
start: Point,
origin: Point,
moved: bool,
}
impl DeveloperInspector {
fn apply(&mut self, action: InspectorAction) {
match action {
InspectorAction::Move => return,
InspectorAction::Toggle => {
self.state.open = !self.state.open;
self.state.picking = false;
self.keyboard = self.state.open;
if !self.state.open {
self.state.mode = InspectorMode::Normal;
self.state.nodes.clear();
self.state.selected = None;
self.revision = None;
}
}
InspectorAction::Normal => self.state.mode = InspectorMode::Normal,
InspectorAction::Overlay => self.state.mode = InspectorMode::Overlay,
InspectorAction::Accessibility => self.state.mode = InspectorMode::Accessibility,
InspectorAction::Pick => self.state.picking = !self.state.picking,
InspectorAction::Previous => self.select_relative(false),
InspectorAction::Next => self.select_relative(true),
InspectorAction::DetailsUp => {
self.state.detail_offset = self.state.detail_offset.saturating_sub(3)
}
InspectorAction::DetailsDown => {
let count = draw::detail_line_count(&self.state, self.viewport.unwrap_or_default());
self.state.detail_offset =
(self.state.detail_offset + 3).min(count.saturating_sub(1));
}
InspectorAction::Select(index) => {
self.state.selected = (index < self.state.nodes.len()).then_some(index);
self.state.detail_offset = 0;
}
}
self.dirty = true;
}
fn select_relative(&mut self, forward: bool) {
self.state.detail_offset = 0;
let len = self.state.nodes.len();
if len == 0 {
self.state.selected = None;
return;
}
self.state.selected = Some(match self.state.selected {
Some(index) if forward => (index + 1) % len,
Some(index) => (index + len - 1) % len,
None if forward => 0,
None => len - 1,
});
}
fn replace_nodes(&mut self, nodes: Vec<InspectorNode>) {
if self.state.nodes == nodes {
return;
}
let identity = self
.state
.selected
.and_then(|index| self.state.nodes.get(index))
.map(|node| (node.node_id, node.canvas_key));
self.state.selected = identity.and_then(|identity| {
nodes
.iter()
.position(|node| (node.node_id, node.canvas_key) == identity)
});
self.state.detail_offset = 0;
self.state.nodes = nodes;
self.dirty = true;
}
fn pick(&mut self, x: f32, y: f32) {
self.state.selected = self
.state
.nodes
.iter()
.enumerate()
.filter(|(_, node)| node.bounds.contains(x, y))
.min_by(|(_, a), (_, b)| {
(a.bounds.width * a.bounds.height).total_cmp(&(b.bounds.width * b.bounds.height))
})
.map(|(index, _)| index);
self.state.picking = false;
self.state.detail_offset = 0;
self.keyboard = true;
self.dirty = true;
}
fn start_drag(&mut self, action: InspectorAction, x: f32, y: f32) {
let viewport = self.viewport.unwrap_or_default();
let bounds = if action == InspectorAction::Move {
draw::panel_bounds(&self.state, viewport)
} else {
draw::launcher_bounds(&self.state, viewport)
};
self.drag = Some(InspectorDrag {
action,
start: Point { x, y },
origin: Point {
x: bounds.x,
y: bounds.y,
},
moved: false,
});
}
fn move_pointer(&mut self, x: f32, y: f32) -> bool {
let Some(drag) = &mut self.drag else {
return false;
};
let dx = x - drag.start.x;
let dy = y - drag.start.y;
drag.moved |= dx * dx + dy * dy >= 16.0;
if !drag.moved {
return false;
}
let position = Some(Point {
x: drag.origin.x + dx,
y: drag.origin.y + dy,
});
if drag.action == InspectorAction::Move {
self.state.panel_position = position;
} else {
self.state.launcher_position = position;
}
self.dirty = true;
true
}
pub(crate) fn release_pointer(&mut self) -> bool {
if !std::mem::take(&mut self.pointer_captured) {
return false;
}
if let Some(drag) = self.drag.take()
&& !drag.moved
&& drag.action != InspectorAction::Move
{
self.apply(drag.action);
}
true
}
pub(crate) fn cancel_pointer(&mut self) {
self.pointer_captured = false;
self.drag = None;
}
}
impl<R: Renderer> AppShell<R>
where
R::Error: Debug,
{
pub fn set_inspector_projector(&mut self, projector: Option<InspectorProjector>) {
self.app.inspector_projector = projector;
for surface in &mut self.surfaces {
surface.inspector = DeveloperInspector {
dirty: true,
..Default::default()
};
if projector.is_none() {
surface.renderer.set_inspector_overlay(None);
}
surface.is_dirty = true;
}
}
pub fn inspector_state(&self) -> &InspectorState {
&self.surfaces[0].inspector.state
}
}
impl<R: Renderer> SurfaceMut<'_, R>
where
R::Error: Debug,
{
pub fn inspector_state(&self) -> &InspectorState {
&self.surface().inspector.state
}
pub(crate) fn inspector_owns_keyboard(&self) -> bool {
let inspector = &self.surface().inspector;
inspector.state.open && inspector.keyboard
}
pub(crate) fn inspector_blocks_pointer(&self, x: f32, y: f32) -> bool {
let inspector = &self.surface().inspector;
self.shell_app_ref().inspector_projector.is_some()
&& (inspector.pointer_captured
|| inspector.state.picking
|| inspector
.state
.controls
.iter()
.any(|control| control.bounds.contains(x, y))
|| (inspector.state.open
&& draw::panel_bounds(
&inspector.state,
inspector.viewport.unwrap_or_default(),
)
.contains(x, y)))
}
pub(crate) fn inspector_move(&mut self, x: f32, y: f32) -> bool {
self.surface_mut().inspector.move_pointer(x, y)
}
pub(crate) fn inspector_scroll(&mut self, delta: f32) -> bool {
let (x, y) = self.surface().cursor;
if !self.inspector_blocks_pointer(x, y) {
return false;
}
let inspector = &mut self.surface_mut().inspector;
if inspector.state.open && delta != 0.0 {
inspector.apply(if delta < 0.0 {
InspectorAction::DetailsDown
} else {
InspectorAction::DetailsUp
});
self.mark_dirty();
}
true
}
pub(crate) fn inspector_press(&mut self, x: f32, y: f32) -> bool {
if self.shell_app_ref().inspector_projector.is_none() {
return false;
}
let inspector = &mut self.surface_mut().inspector;
let action = inspector
.state
.controls
.iter()
.find(|control| control.bounds.contains(x, y))
.map(|control| control.action);
let consumed = if let Some(action) = action {
if action == InspectorAction::Move || !inspector.state.open || inspector.state.picking {
inspector.start_drag(action, x, y);
} else {
inspector.apply(action);
}
inspector.keyboard = inspector.state.open;
true
} else if inspector.state.open && inspector.state.picking {
inspector.pick(x, y);
true
} else {
inspector.keyboard = false;
inspector.state.open
&& draw::panel_bounds(&inspector.state, inspector.viewport.unwrap_or_default())
.contains(x, y)
};
if consumed {
inspector.keyboard = inspector.state.open;
inspector.pointer_captured = true;
self.mark_dirty();
}
consumed
}
pub(crate) fn inspector_key(&mut self, event: &KeyEvent) -> bool {
if self.shell_app_ref().inspector_projector.is_none() {
return false;
}
let inspector = &mut self.surface_mut().inspector;
let action = if inspector.state.open && inspector.keyboard {
match event.key_code {
KeyCode::Escape => Some(InspectorAction::Toggle),
KeyCode::ArrowUp | KeyCode::ArrowLeft => Some(InspectorAction::Previous),
KeyCode::ArrowDown | KeyCode::ArrowRight => Some(InspectorAction::Next),
KeyCode::Digit1 => Some(InspectorAction::Normal),
KeyCode::Digit2 => Some(InspectorAction::Overlay),
KeyCode::Digit3 => Some(InspectorAction::Accessibility),
KeyCode::P => Some(InspectorAction::Pick),
KeyCode::PageUp => Some(InspectorAction::DetailsUp),
KeyCode::PageDown => Some(InspectorAction::DetailsDown),
_ => None,
}
} else {
None
};
let Some(action) = action else {
return inspector.state.open && inspector.keyboard;
};
if event.event_type == KeyEventType::KeyDown {
inspector.apply(action);
self.mark_dirty();
}
true
}
}
pub(crate) fn refresh<R: Renderer>(
app: &mut ShellApp,
surface: &mut RootSurface<R>,
revision: u64,
) -> bool {
let Some(projector) = app.inspector_projector else {
return false;
};
let viewport = surface.viewport_size();
if surface.inspector.viewport != Some(viewport) {
surface.inspector.viewport = Some(viewport);
surface.inspector.dirty = true;
}
if surface.inspector.state.open && surface.inspector.revision != Some(revision) {
surface.layout_tree_in_context(app);
surface.semantics_tree_in_context(app);
let nodes = match (&surface.layout_tree, &surface.semantics_tree) {
(Some(layout), Some(semantics)) => projector(layout, semantics),
_ => Vec::new(),
};
surface.inspector.replace_nodes(nodes);
surface.inspector.revision = Some(revision);
}
if !surface.inspector.dirty && surface.inspector.installed {
return false;
}
let graph = draw::build(&mut surface.inspector.state, viewport);
surface.renderer.set_inspector_overlay(Some(graph));
surface.inspector.installed = true;
surface.inspector.dirty = false;
true
}
#[cfg(test)]
#[path = "tests/inspector_tests.rs"]
mod tests;