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