Skip to main content

kui_core/runtime/
inspect.rs

1//! The frame as a list a tool can read back: every node the last finished
2//! frame laid out, with what it is, where layout put it, and the handful
3//! of declarations that explain the rest. What a devtools tree view and
4//! node inspector are built from (`docs/adr/0021`, the harness's dock).
5//!
6//! Off unless asked: a view runs every frame and the copy is O(nodes),
7//! so a shipped app pays nothing. `Core::set_inspect(true)` turns the
8//! snapshot on, and it is taken at the end of every finished frame from
9//! then on — the tree itself is rebuilt from scratch next frame, so this
10//! is the only reading of a frame that outlives it.
11
12use super::*;
13use crate::access::Role;
14use crate::geom::{Edges, Rect};
15use crate::spec::{Align, Dir, Sizing};
16use crate::tree::{NIL, NodeContent, OriginId};
17
18/// What kind of node a snapshot row is.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum NodeKind {
21    Box,
22    Text,
23    Edit,
24    Image,
25    Line,
26    Cells,
27    Fragment,
28    Polygon,
29}
30
31impl NodeKind {
32    pub fn name(self) -> &'static str {
33        match self {
34            NodeKind::Box => "box",
35            NodeKind::Text => "text",
36            NodeKind::Edit => "edit",
37            NodeKind::Image => "image",
38            NodeKind::Line => "line",
39            NodeKind::Cells => "cells",
40            NodeKind::Fragment => "fragment",
41            NodeKind::Polygon => "polygon",
42        }
43    }
44}
45
46/// One node of the last finished frame.
47#[derive(Clone, Debug)]
48pub struct NodeInfo {
49    pub key: Key,
50    pub parent: Option<Key>,
51    /// Nesting depth; the root is 0.
52    pub depth: u16,
53    pub kind: NodeKind,
54    /// The label it was opened under, when it was opened by one.
55    pub label: Option<String>,
56    /// Where layout put it, logical px — the host's viewport through
57    /// [`Core::nodes`], the window's in the snapshot the panel reads.
58    pub rect: Rect,
59    pub dir: Dir,
60    pub width: Sizing,
61    pub height: Sizing,
62    pub bg: Color,
63    pub float: bool,
64    /// The role the access tree reads for it — declared, or derived from
65    /// what it is and does (a box with a click is a button). Plain
66    /// structure has none.
67    pub role: Option<Role>,
68    /// A text node's content, cut to a line's worth.
69    pub text: Option<String>,
70    /// The declarations that make it interactive or special, by name:
71    /// `click`, `drag`, `key`, `hover`, `hoverable`, `context-menu`,
72    /// `modal`, `selectable`, `focusable`, `disabled`, `scroll`, `clip`,
73    /// `transition`.
74    pub flags: Vec<&'static str>,
75    /// The paint layer it is in (ADR 0023): 0 in flow, else the rank of
76    /// its float layer from the bottom, 1 being the first layer over the
77    /// flow. What decides which of two nodes under one point is on top.
78    pub layer: u16,
79    /// Who declared it: the host, an extension, or the devtools.
80    pub origin: OriginId,
81    /// How many children it has.
82    pub children: u32,
83    /// The rest of the layout spec, for the inspector.
84    pub padding: Edges,
85    pub gap: f32,
86    pub main_align: Align,
87    pub cross_align: Align,
88    pub wrap: bool,
89    /// Whether it is a table (`LayoutSpec::table`): a column whose rows'
90    /// cells line up.
91    pub table: bool,
92    /// The size floors and ceilings, as layout left them: a floor is the
93    /// declared px, or the number a `fit` floor resolved to in the fit
94    /// pass (the pass writes it back into the spec, so a declared `"fit"`
95    /// reads as its measurement here, not as the word); `None` only for a
96    /// fit floor the pass never measured. A ceiling is `None` when
97    /// unbounded.
98    pub min_w: Option<f32>,
99    pub min_h: Option<f32>,
100    pub max_w: Option<f32>,
101    pub max_h: Option<f32>,
102    /// The rest of the paint spec.
103    pub radius: [f32; 4],
104    pub border_w: f32,
105    pub border_color: Color,
106    pub opacity: f32,
107    /// A scroller's offset, `None` for a node that does not scroll.
108    pub scroll: Option<Vec2>,
109    /// Every handler it declared, with the payload it would post:
110    /// `click`, `drag`, `key` (the sink's tag), `hover`, `context-menu`,
111    /// `force-click`, `layout`, `modal`.
112    pub events: Vec<(&'static str, Value)>,
113}
114
115impl NodeInfo {
116    /// The row as plain data, every field under its snake_case name —
117    /// the key and parent spelled by `h`, sizing as [`Sizing::describe`],
118    /// colours as hex, enums by their schema names, `events` a map of
119    /// handler name to payload (backlog AR1).
120    pub fn to_value(&self, h: crate::value::Handles) -> Value {
121        Value::map([
122            ("key", (h.key)(self.key)),
123            ("parent", h.opt_key(self.parent)),
124            ("depth", Value::Int(self.depth as i64)),
125            ("kind", Value::str(self.kind.name())),
126            ("label", Value::opt_str(&self.label)),
127            ("rect", self.rect.to_value()),
128            ("dir", Value::str(self.dir.name())),
129            ("width", Value::Str(self.width.describe())),
130            ("height", Value::Str(self.height.describe())),
131            ("bg", Value::Int(self.bg.to_hex() as i64)),
132            ("float", Value::Bool(self.float)),
133            ("role", Value::opt(self.role, |r| Value::str(r.name()))),
134            ("text", Value::opt_str(&self.text)),
135            (
136                "flags",
137                Value::list(self.flags.iter().map(|f| Value::str(*f))),
138            ),
139            ("layer", Value::Int(self.layer as i64)),
140            ("origin", Value::Int(self.origin.0 as i64)),
141            ("children", Value::Int(self.children as i64)),
142            (
143                "padding",
144                Value::map([
145                    ("t", Value::float(self.padding.t)),
146                    ("r", Value::float(self.padding.r)),
147                    ("b", Value::float(self.padding.b)),
148                    ("l", Value::float(self.padding.l)),
149                ]),
150            ),
151            ("gap", Value::float(self.gap)),
152            ("main_align", Value::str(self.main_align.name())),
153            ("cross_align", Value::str(self.cross_align.name())),
154            ("wrap", Value::Bool(self.wrap)),
155            ("table", Value::Bool(self.table)),
156            ("min_width", Value::opt_float(self.min_w)),
157            ("min_height", Value::opt_float(self.min_h)),
158            ("max_width", Value::opt_float(self.max_w)),
159            ("max_height", Value::opt_float(self.max_h)),
160            ("radius", Value::floats(&self.radius)),
161            ("border_width", Value::float(self.border_w)),
162            (
163                "border_color",
164                Value::Int(self.border_color.to_hex() as i64),
165            ),
166            ("opacity", Value::float(self.opacity)),
167            ("scroll", Value::opt(self.scroll, Vec2::to_value)),
168            (
169                "events",
170                Value::Map(
171                    self.events
172                        .iter()
173                        .map(|(name, v)| (name.to_string(), v.clone()))
174                        .collect(),
175                ),
176            ),
177        ])
178    }
179}
180
181const TEXT_CUT: usize = 60;
182
183impl Core {
184    /// Turns the per-frame snapshot on or off (see the module doc). Off by
185    /// default; a devtool that reads [`Self::nodes`] turns it on once.
186    /// The host's ask alone: the core's own devtools panel asks for the
187    /// snapshot separately, per frame, while its tree tab shows or it is
188    /// picking, and neither ask turns the other off (backlog AR38).
189    pub fn set_inspect(&mut self, on: bool) {
190        self.inspect = on;
191        if !on && !self.dt_inspect {
192            self.inspected.clear();
193        }
194    }
195
196    /// Whether the host asked for the snapshot.
197    pub fn inspect(&self) -> bool {
198        self.inspect
199    }
200
201    /// The last finished frame's nodes, in tree order — empty until
202    /// [`Self::set_inspect`] asked for them and a frame has finished since.
203    /// Rects in the host's viewport coordinates, like every other readback
204    /// (`layout_of`, `scroll_geometry`, `text_hit`): under a left dock the
205    /// snapshot itself is kept in window px for the panel's outlines, and
206    /// this is the translated copy (backlog AR36).
207    pub fn nodes(&self) -> Vec<NodeInfo> {
208        let mut out = self.snapshot().to_vec();
209        let shift = self.dt_shift();
210        if shift != Vec2::ZERO {
211            for n in &mut out {
212                n.rect.x -= shift.x;
213                n.rect.y -= shift.y;
214            }
215        }
216        out
217    }
218
219    /// The snapshot as kept: rects in window px, which is what the
220    /// devtools panel outlines with, docked or not.
221    pub(crate) fn snapshot(&self) -> &[NodeInfo] {
222        &self.inspected
223    }
224
225    /// Called at the end of `finish_frame`, after layout.
226    pub(crate) fn snapshot_nodes(&mut self) {
227        if !self.inspect && !self.dt_inspect {
228            // Nobody asked this frame: no copy, and nothing stale to read.
229            self.inspected.clear();
230            return;
231        }
232        let tree = &self.tree;
233        let n = tree.len();
234        let mut out = Vec::with_capacity(n);
235        let mut depth = vec![0u16; n];
236        // A float root's rank in the paint stack, bottom to top — the stack
237        // `emit_frame` left, which is the order it painted the layers in.
238        let layer_of: rustc_hash::FxHashMap<Key, u16> = self
239            .float_stack
240            .iter()
241            .enumerate()
242            .map(|(pos, &(k, _))| (k, pos as u16 + 1))
243            .collect();
244        let mut children = vec![0u32; n];
245        for i in 1..n {
246            children[tree.parent[i] as usize] += 1;
247        }
248        for i in 0..n {
249            let parent = if i == 0 {
250                None
251            } else {
252                let p = tree.parent[i] as usize;
253                depth[i] = depth[p] + 1;
254                Some(tree.keys[p])
255            };
256            let spec = &tree.specs[i];
257            let (kind, text) = match tree.content[i] {
258                NodeContent::Container => (NodeKind::Box, None),
259                NodeContent::Text(id) => {
260                    let s = self.text.content(id);
261                    let cut = s.char_indices().nth(TEXT_CUT).map_or(s.len(), |(i, _)| i);
262                    let mut t = s[..cut].to_string();
263                    if cut < s.len() {
264                        t.push('…');
265                    }
266                    (NodeKind::Text, Some(t))
267                }
268                NodeContent::Edit(_) => (NodeKind::Edit, None),
269                NodeContent::Image(..) => (NodeKind::Image, None),
270                NodeContent::Line(_) => (NodeKind::Line, None),
271                NodeContent::Cells(_) => (NodeKind::Cells, None),
272                NodeContent::Fragment(_) => (NodeKind::Fragment, None),
273                NodeContent::Polygon(_) => (NodeKind::Polygon, None),
274            };
275            let rect = if i == 0 {
276                Rect::new(0.0, 0.0, self.viewport.w, self.viewport.h)
277            } else {
278                Rect::from_pos_size(tree.pos[i], tree.size[i])
279            };
280            let ev = spec.events();
281            let it = spec.interact();
282            let mut flags = Vec::new();
283            if ev.on_click.is_some() {
284                flags.push("click");
285            }
286            if ev.on_drag.is_some() {
287                flags.push("drag");
288            }
289            if ev.on_key.is_some() {
290                flags.push("key");
291            }
292            if ev.on_hover.is_some() {
293                flags.push("hover");
294            }
295            if ev.on_drop.is_some() {
296                flags.push("drop");
297            }
298            if ev.on_context_menu.is_some() {
299                flags.push("context-menu");
300            }
301            if ev.modal.is_some() {
302                flags.push("modal");
303            }
304            if spec.hoverable {
305                flags.push("hoverable");
306            }
307            if it.selectable {
308                flags.push("selectable");
309            }
310            if spec.focusable {
311                flags.push("focusable");
312            }
313            if spec.disabled {
314                flags.push("disabled");
315            }
316            if spec.layout.scroll_x || spec.layout.scroll_y {
317                flags.push("scroll");
318            } else if spec.layout.clip {
319                flags.push("clip");
320            }
321            if spec.transition.is_some() {
322                flags.push("transition");
323            }
324            let mut events = Vec::new();
325            for (name, v) in [
326                ("click", &ev.on_click),
327                ("drag", &ev.on_drag),
328                ("key", &ev.on_key),
329                ("hover", &ev.on_hover),
330                ("drop", &ev.on_drop),
331                ("context-menu", &ev.on_context_menu),
332                ("force-click", &ev.on_force_click),
333                ("button", &ev.on_button),
334                ("layout", &ev.on_layout),
335                ("modal", &ev.modal),
336            ] {
337                if let Some(v) = v {
338                    events.push((name, v.clone()));
339                }
340            }
341            let layer =
342                if self.tree.any_float && self.float_root.len() > i && self.float_root[i] != NIL {
343                    layer_of
344                        .get(&tree.keys[self.float_root[i] as usize])
345                        .copied()
346                        .unwrap_or(0)
347                } else {
348                    0
349                };
350            let floor = |m: crate::spec::Min| (!m.is_fit()).then(|| m.resolved());
351            let ceiling = |v: f32| v.is_finite().then_some(v);
352            let l = &spec.layout;
353            out.push(NodeInfo {
354                layer,
355                origin: tree.origins[i],
356                children: children[i],
357                padding: l.padding,
358                gap: l.gap,
359                main_align: l.main_align,
360                cross_align: l.cross_align,
361                wrap: l.wrap,
362                table: l.is_table(),
363                min_w: floor(l.min_w),
364                min_h: floor(l.min_h),
365                max_w: ceiling(l.max_w_px()),
366                max_h: ceiling(l.max_h_px()),
367                radius: spec.style.radius,
368                border_w: spec.style.border_w,
369                border_color: spec.style.border_color,
370                opacity: spec.style.opacity,
371                scroll: (l.scroll_x || l.scroll_y).then(|| self.scroll.drawn(tree.keys[i])),
372                events,
373                key: tree.keys[i],
374                parent,
375                depth: depth[i],
376                kind,
377                label: self.key_labels.label_of(tree.keys[i]).map(str::to_string),
378                rect,
379                dir: spec.layout.dir,
380                width: spec.layout.width,
381                height: spec.layout.height,
382                bg: spec.style.bg,
383                float: spec.layout.float.is_some(),
384                role: crate::access::derived_role(tree, i),
385                text,
386                flags,
387            });
388        }
389        self.inspected = out;
390    }
391}