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    /// Its `backdrop_blur` radius in logical px, 0 for none (backlog
110    /// F129).
111    pub backdrop_blur: f32,
112    /// Its turn in turns and its scale (ADR 0043): `0.0` and `1.0` for a
113    /// node that declares none — what it is drawn through this frame,
114    /// eased.
115    pub rotate: f32,
116    pub scale: f32,
117    /// A scroller's offset, `None` for a node that does not scroll.
118    pub scroll: Option<Vec2>,
119    /// Every handler it declared, with the payload it would post:
120    /// `click`, `drag`, `key` (the sink's tag), `hover`, `context-menu`,
121    /// `force-click`, `layout`, `modal`.
122    pub events: Vec<(&'static str, Value)>,
123}
124
125impl NodeInfo {
126    /// The row as plain data, every field under its snake_case name —
127    /// the key and parent spelled by `h`, sizing as [`Sizing::describe`],
128    /// colours as hex, enums by their schema names, `events` a map of
129    /// handler name to payload.
130    pub fn to_value(&self, h: crate::value::Handles) -> Value {
131        Value::map([
132            ("key", (h.key)(self.key)),
133            ("parent", h.opt_key(self.parent)),
134            ("depth", Value::Int(self.depth as i64)),
135            ("kind", Value::str(self.kind.name())),
136            ("label", Value::opt_str(&self.label)),
137            ("rect", self.rect.to_value()),
138            ("dir", Value::str(self.dir.name())),
139            ("width", Value::Str(self.width.describe())),
140            ("height", Value::Str(self.height.describe())),
141            ("bg", Value::Int(self.bg.to_hex() as i64)),
142            ("float", Value::Bool(self.float)),
143            ("role", Value::opt(self.role, |r| Value::str(r.name()))),
144            ("text", Value::opt_str(&self.text)),
145            (
146                "flags",
147                Value::list(self.flags.iter().map(|f| Value::str(*f))),
148            ),
149            ("layer", Value::Int(self.layer as i64)),
150            ("origin", Value::Int(self.origin.0 as i64)),
151            ("children", Value::Int(self.children as i64)),
152            (
153                "padding",
154                Value::map([
155                    ("t", Value::float(self.padding.t)),
156                    ("r", Value::float(self.padding.r)),
157                    ("b", Value::float(self.padding.b)),
158                    ("l", Value::float(self.padding.l)),
159                ]),
160            ),
161            ("gap", Value::float(self.gap)),
162            ("main_align", Value::str(self.main_align.name())),
163            ("cross_align", Value::str(self.cross_align.name())),
164            ("wrap", Value::Bool(self.wrap)),
165            ("table", Value::Bool(self.table)),
166            ("min_width", Value::opt_float(self.min_w)),
167            ("min_height", Value::opt_float(self.min_h)),
168            ("max_width", Value::opt_float(self.max_w)),
169            ("max_height", Value::opt_float(self.max_h)),
170            ("radius", Value::floats(&self.radius)),
171            ("border_width", Value::float(self.border_w)),
172            (
173                "border_color",
174                Value::Int(self.border_color.to_hex() as i64),
175            ),
176            ("opacity", Value::float(self.opacity)),
177            ("backdrop_blur", Value::float(self.backdrop_blur)),
178            ("rotate", Value::float(self.rotate)),
179            ("scale", Value::float(self.scale)),
180            ("scroll", Value::opt(self.scroll, Vec2::to_value)),
181            (
182                "events",
183                Value::Map(
184                    self.events
185                        .iter()
186                        .map(|(name, v)| (name.to_string(), v.clone()))
187                        .collect(),
188                ),
189            ),
190        ])
191    }
192}
193
194const TEXT_CUT: usize = 60;
195
196impl Core {
197    /// Turns the per-frame snapshot on or off (see the module doc). Off by
198    /// default; a devtool that reads [`Self::nodes`] turns it on once.
199    /// The host's ask alone: the core's own devtools panel asks for the
200    /// snapshot separately, per frame, while its tree tab shows or it is
201    /// picking, and neither ask turns the other off.
202    pub fn set_inspect(&mut self, on: bool) {
203        self.inspect = on;
204        if !on && !self.dt_inspect {
205            self.inspected.clear();
206        }
207    }
208
209    /// Whether the host asked for the snapshot.
210    pub fn inspect(&self) -> bool {
211        self.inspect
212    }
213
214    /// The last finished frame's nodes, in tree order — empty until
215    /// [`Self::set_inspect`] asked for them and a frame has finished since.
216    /// Rects in the host's viewport coordinates, like every other readback
217    /// (`layout_of`, `scroll_geometry`, `text_hit`): under a left dock the
218    /// snapshot itself is kept in window px for the panel's outlines, and
219    /// this is the translated copy.
220    pub fn nodes(&self) -> Vec<NodeInfo> {
221        let mut out = self.snapshot().to_vec();
222        let shift = self.dt_shift();
223        if shift != Vec2::ZERO {
224            for n in &mut out {
225                n.rect.x -= shift.x;
226                n.rect.y -= shift.y;
227            }
228        }
229        out
230    }
231
232    /// The snapshot as kept: rects in window px, which is what the
233    /// devtools panel outlines with, docked or not.
234    pub(crate) fn snapshot(&self) -> &[NodeInfo] {
235        &self.inspected
236    }
237
238    /// Called at the end of `finish_frame`, after layout.
239    pub(crate) fn snapshot_nodes(&mut self) {
240        if !self.inspect && !self.dt_inspect {
241            // Nobody asked this frame: no copy, and nothing stale to read.
242            self.inspected.clear();
243            return;
244        }
245        let tree = &self.tree;
246        let n = tree.len();
247        let mut out = Vec::with_capacity(n);
248        let mut depth = vec![0u16; n];
249        // A float root's rank in the paint stack, bottom to top — the stack
250        // `emit_frame` left, which is the order it painted the layers in.
251        let layer_of: rustc_hash::FxHashMap<Key, u16> = self
252            .float_stack
253            .iter()
254            .enumerate()
255            .map(|(pos, &(k, _))| (k, pos as u16 + 1))
256            .collect();
257        let mut children = vec![0u32; n];
258        for i in 1..n {
259            children[tree.parent[i] as usize] += 1;
260        }
261        for i in 0..n {
262            let parent = if i == 0 {
263                None
264            } else {
265                let p = tree.parent[i] as usize;
266                depth[i] = depth[p] + 1;
267                Some(tree.keys[p])
268            };
269            let spec = &tree.specs[i];
270            let (kind, text) = match tree.content[i] {
271                NodeContent::Container => (NodeKind::Box, None),
272                NodeContent::Text(id) => {
273                    let s = self.text.content(id);
274                    let cut = s.char_indices().nth(TEXT_CUT).map_or(s.len(), |(i, _)| i);
275                    let mut t = s[..cut].to_string();
276                    if cut < s.len() {
277                        t.push('…');
278                    }
279                    (NodeKind::Text, Some(t))
280                }
281                NodeContent::Edit(_) => (NodeKind::Edit, None),
282                NodeContent::Image(..) => (NodeKind::Image, None),
283                NodeContent::Line(_) => (NodeKind::Line, None),
284                NodeContent::Cells(_) => (NodeKind::Cells, None),
285                NodeContent::Fragment(_) => (NodeKind::Fragment, None),
286                NodeContent::Polygon(_) => (NodeKind::Polygon, None),
287                NodeContent::Path(_) => (NodeKind::Path, None),
288            };
289            let rect = if i == 0 {
290                Rect::new(0.0, 0.0, self.viewport.w, self.viewport.h)
291            } else {
292                Rect::from_pos_size(tree.pos[i], tree.size[i])
293            };
294            let ev = spec.events();
295            let it = spec.interact();
296            let mut flags = Vec::new();
297            if ev.on_click.is_some() {
298                flags.push("click");
299            }
300            if ev.on_drag.is_some() {
301                flags.push("drag");
302            }
303            if ev.on_key.is_some() {
304                flags.push("key");
305            }
306            if ev.on_hover.is_some() {
307                flags.push("hover");
308            }
309            if ev.on_drop.is_some() {
310                flags.push("drop");
311            }
312            if ev.on_context_menu.is_some() {
313                flags.push("context-menu");
314            }
315            if ev.modal.is_some() {
316                flags.push("modal");
317            }
318            if spec.hoverable {
319                flags.push("hoverable");
320            }
321            if it.selectable {
322                flags.push("selectable");
323            }
324            if spec.focusable {
325                flags.push("focusable");
326            }
327            if spec.disabled {
328                flags.push("disabled");
329            }
330            if spec.layout.scroll_x || spec.layout.scroll_y {
331                flags.push("scroll");
332            } else if spec.layout.clip {
333                flags.push("clip");
334            }
335            if spec.transition.is_some() {
336                flags.push("transition");
337            }
338            let mut events = Vec::new();
339            for (name, v) in [
340                ("click", &ev.on_click),
341                ("drag", &ev.on_drag),
342                ("key", &ev.on_key),
343                ("hover", &ev.on_hover),
344                ("drop", &ev.on_drop),
345                ("context-menu", &ev.on_context_menu),
346                ("force-click", &ev.on_force_click),
347                ("button", &ev.on_button),
348                ("layout", &ev.on_layout),
349                ("modal", &ev.modal),
350            ] {
351                if let Some(v) = v {
352                    events.push((name, v.clone()));
353                }
354            }
355            let layer =
356                if self.tree.any_float && self.float_root.len() > i && self.float_root[i] != NIL {
357                    layer_of
358                        .get(&tree.keys[self.float_root[i] as usize])
359                        .copied()
360                        .unwrap_or(0)
361                } else {
362                    0
363                };
364            let floor = |m: crate::spec::Min| (!m.is_fit()).then(|| m.resolved());
365            let ceiling = |v: f32| v.is_finite().then_some(v);
366            let l = &spec.layout;
367            out.push(NodeInfo {
368                layer,
369                origin: tree.origins[i],
370                children: children[i],
371                padding: l.padding,
372                gap: l.gap,
373                main_align: l.main_align,
374                cross_align: l.cross_align,
375                wrap: l.wrap,
376                table: l.is_table(),
377                min_w: floor(l.min_w),
378                min_h: floor(l.min_h),
379                max_w: ceiling(l.max_w_px()),
380                max_h: ceiling(l.max_h_px()),
381                radius: spec.style.radius,
382                border_w: spec.style.border_w,
383                border_color: spec.style.border_color,
384                opacity: spec.style.opacity,
385                backdrop_blur: spec.interact().backdrop_blur,
386                rotate: spec.transform_spec().map_or(0.0, |t| t.rotate),
387                scale: spec.transform_spec().map_or(1.0, |t| t.scale),
388                scroll: (l.scroll_x || l.scroll_y).then(|| self.scroll.drawn(tree.keys[i])),
389                events,
390                key: tree.keys[i],
391                parent,
392                depth: depth[i],
393                kind,
394                label: self.key_labels.label_of(tree.keys[i]).map(str::to_string),
395                rect,
396                dir: spec.layout.dir,
397                width: spec.layout.width,
398                height: spec.layout.height,
399                bg: spec.style.bg,
400                float: spec.layout.float.is_some(),
401                role: crate::access::derived_role(tree, i),
402                text,
403                flags,
404            });
405        }
406        self.inspected = out;
407    }
408}