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