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