nandi/jolt-nativepublic Fork 0
6a3304ddddcc7d3e9486b470fea5933a1f81f8e8
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

tree.rs · 753 lines · 24.4 KBRust Blame HistoryRaw
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago1//! A retained node tree, painted into a character grid.
2//!
3//! The same arena glimmer's reconciler expects everywhere else: nodes are
4//! integer handles, `create` / `apply-props!` / `append-child!` mutate them,
5//! and nothing is drawn until the frame call walks the whole thing at once.
6//! Interactions come back as a queue the caller drains, because a jolt closure
7//! cannot be a callback down here — identity crosses the boundary instead.
8//!
9//! This module knows nothing about terminals. It is the data; [`crate::layout`]
10//! measures it and [`crate::paint`] draws it.
11
12use std::collections::{HashMap, VecDeque};
13
14/// A prop value: the three types the ABI can carry, which is all glimmer needs.
15/// Keywords and colours arrive as strings, numbers as doubles, flags as ints.
16#[derive(Clone, Debug, PartialEq)]
17pub enum Value {
18 Str(String),
19 Num(f64),
20 Bool(bool),
21}
22
23/// What a node renders as.
24///
25/// An unknown tag is kept rather than refused — it paints as a vertical box, so
26/// a component written against a tag this backend has not grown yet still shows
27/// its children instead of nothing.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub enum Tag {
30 Window,
31 Box,
32 Frame,
33 Scroll,
34 Overlay,
35 Label,
36 Title,
37 DimLabel,
38 Button,
39 CheckButton,
40 Entry,
41 Separator,
42 Spacer,
43 Listbox,
44 Progress,
45 Spinner,
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago46 Reaction,
47 Emoji,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago48 Image,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago49 Unknown(String),
50}
51
52impl Default for Tag {
53 fn default() -> Self {
54 Self::Unknown(String::new())
55 }
56}
57
58impl Tag {
59 fn parse(name: &str) -> Self {
60 match name {
61 "window" => Self::Window,
62 "box" | "hbox" | "vbox" => Self::Box,
63 "frame" => Self::Frame,
64 "scroll" => Self::Scroll,
65 "overlay" => Self::Overlay,
66 "label" => Self::Label,
67 "title" | "title-2" => Self::Title,
68 "dim-label" => Self::DimLabel,
69 "button" => Self::Button,
70 "checkbutton" | "checkbox" => Self::CheckButton,
71 "entry" => Self::Entry,
72 "separator" => Self::Separator,
73 "spacer" | "gap" => Self::Spacer,
74 "listbox" => Self::Listbox,
75 "progress" => Self::Progress,
76 "spinner" => Self::Spinner,
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago77 // The two glyph nodes. A reaction is a pill somebody can press —
78 // the count of who is on it, and whether you are one of them; an
79 // emoji is the same glyph with none of that, a character in a
80 // sentence. Both carry what to draw in `:emoji` rather than in a
81 // label, which is why an unknown tag painted neither.
82 "reaction" => Self::Reaction,
83 "emoji" => Self::Emoji,
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago84 // A picture, which a cell grid cannot hold: the painter reserves
85 // the cells and the terminal draws it over them, where it has the
86 // protocol for that. See `crate::graphics`.
87 "image" => Self::Image,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago88 other => Self::Unknown(other.to_owned()),
89 }
90 }
91
92 /// The canonical name: `hbox` and `vbox` are one node, so both answer
93 /// `box` and carry their orientation in a prop.
94 pub fn name(&self) -> &str {
95 match self {
96 Self::Window => "window",
97 Self::Box => "box",
98 Self::Frame => "frame",
99 Self::Scroll => "scroll",
100 Self::Overlay => "overlay",
101 Self::Label => "label",
102 Self::Title => "title",
103 Self::DimLabel => "dim-label",
104 Self::Button => "button",
105 Self::CheckButton => "checkbutton",
106 Self::Entry => "entry",
107 Self::Separator => "separator",
108 Self::Spacer => "spacer",
109 Self::Listbox => "listbox",
110 Self::Progress => "progress",
111 Self::Spinner => "spinner",
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago112 Self::Reaction => "reaction",
113 Self::Emoji => "emoji",
Draw a picture in a terminal, over the Kitty graphics protocol c2d912f nandi 16d ago114 Self::Image => "image",
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago115 Self::Unknown(name) => name,
116 }
117 }
118
119 /// Whether the focus ring stops here. A container never takes focus of its
120 /// own; a control that does nothing with a key does not either.
121 pub fn focusable(&self) -> bool {
122 matches!(
123 self,
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago124 Self::Button | Self::CheckButton | Self::Entry | Self::Listbox | Self::Reaction
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago125 )
126 }
127}
128
129/// One interaction, waiting to be drained by the caller. Names are glimmer's
130/// handler props with the `on-` dropped.
131#[derive(Clone, Debug, PartialEq)]
132pub struct Event {
133 pub node: u32,
134 pub name: &'static str,
135 pub text: String,
136 pub num: f64,
137}
138
139#[derive(Clone, Debug, Default)]
140struct Node {
141 tag: Tag,
Measure a node once, and paint only what is on screen c41903b nandi 16d ago142 props: Props,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago143 children: Vec<u32>,
Measure a node once, and paint only what is on screen c41903b nandi 16d ago144 /// When this node or anything under it last changed. A size measured of
145 /// this subtree is good for exactly as long as this number holds still,
146 /// which is what lets one outlive the frame it was taken in.
147 rev: u64,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago148 /// 0 when unparented. The root's parent is 0 as well, which is what stops
149 /// the ancestor walk in [`Tree::would_cycle`].
150 parent: u32,
151}
152
Measure a node once, and paint only what is on screen c41903b nandi 16d ago153/// A node's props.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago154///
155/// Reading them through this rather than the map means a missing prop and a
156/// prop of the wrong type answer the same thing: the default. Nothing a caller
157/// can write should be able to make a widget vanish.
158#[derive(Clone, Debug, Default)]
159pub struct Props(pub HashMap<String, Value>);
160
161impl Props {
162 pub fn str(&self, key: &str) -> &str {
163 match self.0.get(key) {
164 Some(Value::Str(s)) => s,
165 _ => "",
166 }
167 }
168
169 pub fn num(&self, key: &str, fallback: f64) -> f64 {
170 match self.0.get(key) {
171 Some(Value::Num(n)) => *n,
172 Some(Value::Bool(b)) => {
173 if *b {
174 1.0
175 } else {
176 0.0
177 }
178 }
179 _ => fallback,
180 }
181 }
182
183 /// A count of cells. Negative and absurd values are clamped rather than
184 /// cast, since `as u16` on a negative double is a silent 0 or 65535.
185 pub fn cells(&self, key: &str, fallback: u16) -> u16 {
186 match self.0.get(key) {
187 Some(Value::Num(n)) if n.is_finite() => n.clamp(0.0, u16::MAX as f64) as u16,
188 _ => fallback,
189 }
190 }
191
192 pub fn bool(&self, key: &str, fallback: bool) -> bool {
193 match self.0.get(key) {
194 Some(Value::Bool(b)) => *b,
195 Some(Value::Num(n)) => *n != 0.0,
196 Some(Value::Str(s)) => s == "true",
197 _ => fallback,
198 }
199 }
200
201 pub fn has(&self, key: &str) -> bool {
202 self.0.contains_key(key)
203 }
204
205 /// The text a widget shows. `:label` and `:text` are the same prop to every
206 /// glimmer backend; whichever the caller wrote is the one that shows.
207 pub fn label(&self) -> &str {
208 if self.has("label") {
209 self.str("label")
210 } else {
211 self.str("text")
212 }
213 }
214}
215
216/// One prop value as EDN. Whole numbers print without a trailing `.0`: every
217/// number crossed the boundary as a double, and `{:spacing 8}` reads better
218/// than `{:spacing 8.0}`.
219fn write_value(value: &Value, out: &mut String) {
220 match value {
221 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
222 Value::Num(n) => {
223 if n.is_finite() && n.fract() == 0.0 && n.abs() < 1e15 {
224 out.push_str(&format!("{}", *n as i64));
225 } else if n.is_finite() {
226 out.push_str(&format!("{n}"));
227 } else {
228 // EDN has no infinity or NaN literal; say nil rather than emit
229 // something no reader will take.
230 out.push_str("nil");
231 }
232 }
233 Value::Str(text) => {
234 out.push('"');
235 for c in text.chars() {
236 match c {
237 '"' => out.push_str("\\\""),
238 '\\' => out.push_str("\\\\"),
239 '\n' => out.push_str("\\n"),
240 '\r' => out.push_str("\\r"),
241 '\t' => out.push_str("\\t"),
242 _ => out.push(c),
243 }
244 }
245 out.push('"');
246 }
247 }
248}
249
250pub struct Tree {
251 /// Index 0 is never handed out: 0 is "no node" throughout the ABI.
252 nodes: Vec<Option<Node>>,
253 free: Vec<u32>,
254 root: u32,
255 pending: VecDeque<Event>,
256 current: Option<Event>,
Measure a node once, and paint only what is on screen c41903b nandi 16d ago257 /// Counts every change to any node. Never read for itself — it is what
258 /// stamps `Node::rev`, so that two changes are never confused for one.
259 revision: u64,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago260}
261
262impl Default for Tree {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267
268impl Tree {
269 pub fn new() -> Self {
270 let mut tree = Self {
271 nodes: vec![None],
272 free: Vec::new(),
273 root: 0,
274 pending: VecDeque::new(),
275 current: None,
Measure a node once, and paint only what is on screen c41903b nandi 16d ago276 revision: 0,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago277 };
278 tree.root = tree.new_node("window");
279 tree
280 }
281
282 pub fn root(&self) -> u32 {
283 self.root
284 }
285
286 fn slot(&self, id: u32) -> Option<&Node> {
287 self.nodes.get(id as usize).and_then(|n| n.as_ref())
288 }
289
Measure a node once, and paint only what is on screen c41903b nandi 16d ago290 /// The one way to a node that can be changed — so it is the one place a
291 /// change has to be recorded. Handing the caller a `&mut Node` is handing
292 /// them everything a measurement of it depended on; whether they write to
293 /// it or not, this is where a cache has to assume they did.
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago294 fn slot_mut(&mut self, id: u32) -> Option<&mut Node> {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago295 self.touch(id);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago296 self.nodes.get_mut(id as usize).and_then(|n| n.as_mut())
297 }
298
Measure a node once, and paint only what is on screen c41903b nandi 16d ago299 /// Mark `id` and every node above it as changed.
300 ///
301 /// Upwards, because that is the direction sizes travel: how tall a message
302 /// is depends on its own words, and how tall the backlog is depends on the
303 /// message — so a word typed into one line makes every ancestor's measured
304 /// size a lie, and no sibling's. Walking the parents is what keeps a new
305 /// message at the bottom of a long backlog from throwing away the
306 /// measurements of the hundred above it that did not move.
307 fn touch(&mut self, id: u32) {
308 self.revision = self.revision.wrapping_add(1);
309 let now = self.revision;
310 let mut at = id;
311 // Bounded by the depth of the tree, and by the node count besides: a
312 // parent chain cannot revisit a node, since `would_cycle` is what stops
313 // one being built.
314 while at != 0 {
315 match self.nodes.get_mut(at as usize).and_then(|n| n.as_mut()) {
316 Some(node) => {
317 node.rev = now;
318 at = node.parent;
319 }
320 None => break,
321 }
322 }
323 }
324
325 /// When this node's subtree last changed. A measurement of it taken at the
326 /// same number is still the right answer.
327 pub fn revision_of(&self, id: u32) -> u64 {
328 self.slot(id).map_or(0, |n| n.rev)
329 }
330
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago331 pub fn exists(&self, id: u32) -> bool {
332 self.slot(id).is_some()
333 }
334
335 pub fn new_node(&mut self, tag: &str) -> u32 {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago336 self.revision = self.revision.wrapping_add(1);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago337 let node = Node {
338 tag: Tag::parse(tag),
Measure a node once, and paint only what is on screen c41903b nandi 16d ago339 rev: self.revision,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago340 ..Node::default()
341 };
342 match self.free.pop() {
343 Some(id) => {
344 self.nodes[id as usize] = Some(node);
345 id
346 }
347 None => {
348 self.nodes.push(Some(node));
349 (self.nodes.len() - 1) as u32
350 }
351 }
352 }
353
354 /// Free `id` and everything under it. The root is refused: the window node
355 /// is the one thing a caller cannot drop out from under itself.
356 pub fn free_node(&mut self, id: u32) {
357 if id == self.root || !self.exists(id) {
358 return;
359 }
360 let parent = self.slot(id).map(|n| n.parent).unwrap_or(0);
361 if parent != 0 {
362 if let Some(node) = self.slot_mut(parent) {
363 node.children.retain(|c| *c != id);
364 }
365 }
366 self.free_subtree(id);
367 }
368
369 fn free_subtree(&mut self, id: u32) {
370 let children = self
371 .slot(id)
372 .map(|n| n.children.clone())
373 .unwrap_or_default();
374 for child in children {
375 self.free_subtree(child);
376 }
377 if self.nodes[id as usize].take().is_some() {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago378 self.revision = self.revision.wrapping_add(1);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago379 self.free.push(id);
380 }
381 // An event queued against a node that has since gone would be routed to
382 // a handler the reconciler has already dropped. Drop it here instead.
383 self.pending.retain(|e| e.node != id);
384 }
385
386 /// Whether making `child` a child of `parent` would make a loop — `child`
387 /// being `parent` or one of its ancestors.
388 fn would_cycle(&self, parent: u32, child: u32) -> bool {
389 let mut at = parent;
390 while at != 0 {
391 if at == child {
392 return true;
393 }
394 at = match self.slot(at) {
395 Some(node) => node.parent,
396 None => return false,
397 };
398 }
399 false
400 }
401
402 fn unparent(&mut self, child: u32) {
403 let parent = self.slot(child).map(|n| n.parent).unwrap_or(0);
404 if parent != 0 {
405 if let Some(node) = self.slot_mut(parent) {
406 node.children.retain(|c| *c != child);
407 }
408 }
409 if let Some(node) = self.slot_mut(child) {
410 node.parent = 0;
411 }
412 }
413
414 pub fn append(&mut self, parent: u32, child: u32) -> bool {
415 if !self.exists(parent) || !self.exists(child) || self.would_cycle(parent, child) {
416 return false;
417 }
418 self.unparent(child);
419 self.slot_mut(parent).unwrap().children.push(child);
420 self.slot_mut(child).unwrap().parent = parent;
421 true
422 }
423
424 /// Unparent *and* free `child`, which is what the reconciler means by
425 /// remove: a node it has taken out of the tree is a node it has dropped.
426 pub fn remove(&mut self, parent: u32, child: u32) {
427 if self.slot(child).map(|n| n.parent) == Some(parent) {
428 self.free_node(child);
429 }
430 }
431
432 /// Move `child` after `sibling`; `sibling` 0 means the first position.
433 pub fn insert_after(&mut self, parent: u32, child: u32, sibling: u32) -> bool {
434 if !self.exists(parent) || !self.exists(child) || self.would_cycle(parent, child) {
435 return false;
436 }
437 if sibling != 0 && self.slot(sibling).map(|n| n.parent) != Some(parent) {
438 return false;
439 }
440 self.unparent(child);
441 let at = match sibling {
442 0 => 0,
443 _ => {
444 let children = &self.slot(parent).unwrap().children;
445 children
446 .iter()
447 .position(|c| *c == sibling)
448 .map_or(0, |i| i + 1)
449 }
450 };
451 self.slot_mut(parent).unwrap().children.insert(at, child);
452 self.slot_mut(child).unwrap().parent = parent;
453 true
454 }
455
456 /// Put `new` where `old` was, and free `old`.
457 pub fn replace(&mut self, parent: u32, old: u32, new: u32) -> bool {
458 if self.slot(old).map(|n| n.parent) != Some(parent) || !self.exists(new) {
459 return false;
460 }
461 if self.would_cycle(parent, new) {
462 return false;
463 }
464 self.unparent(new);
465 let at = self
466 .slot(parent)
467 .and_then(|n| n.children.iter().position(|c| *c == old));
468 let Some(at) = at else { return false };
469 self.slot_mut(parent).unwrap().children[at] = new;
470 self.slot_mut(new).unwrap().parent = parent;
471 if let Some(node) = self.slot_mut(old) {
472 node.parent = 0;
473 }
474 self.free_subtree(old);
475 true
476 }
477
478 // ── reading it back ─────────────────────────────────────────────────────
479
480 pub fn tag(&self, id: u32) -> Tag {
481 self.slot(id).map(|n| n.tag.clone()).unwrap_or_default()
482 }
483
484 pub fn tag_name(&self, id: u32) -> &str {
485 self.slot(id).map_or("", |n| n.tag.name())
486 }
487
488 pub fn children(&self, id: u32) -> Vec<u32> {
489 self.slot(id)
490 .map(|n| n.children.clone())
491 .unwrap_or_default()
492 }
493
494 pub fn child_count(&self, id: u32) -> usize {
495 self.slot(id).map_or(0, |n| n.children.len())
496 }
497
498 pub fn child_at(&self, id: u32, index: usize) -> u32 {
499 self.slot(id)
500 .and_then(|n| n.children.get(index).copied())
501 .unwrap_or(0)
502 }
503
504 pub fn parent(&self, id: u32) -> u32 {
505 self.slot(id).map_or(0, |n| n.parent)
506 }
507
Measure a node once, and paint only what is on screen c41903b nandi 16d ago508 /// A node's props, borrowed. A node that is not there lends the empty set,
509 /// which reads as every default — the same answer `props` has always given
510 /// for a missing node, without the copy.
511 ///
512 /// The copying `props` below is what a caller that needs to keep them past
513 /// the borrow uses. Measuring and painting do not: they read a handful of
514 /// values and are done, and cloning a whole map per node per question was
515 /// most of what a frame cost.
516 pub fn props_of(&self, id: u32) -> &Props {
517 static NONE: std::sync::OnceLock<Props> = std::sync::OnceLock::new();
518 match self.slot(id) {
519 Some(node) => &node.props,
520 None => NONE.get_or_init(Props::default),
521 }
522 }
523
524 /// A node's tag, borrowed. `Tag::Unknown` carries the name it was given,
525 /// so `tag` is a string copy per call where this is a look.
526 pub fn tag_of(&self, id: u32) -> &Tag {
527 static NONE: std::sync::OnceLock<Tag> = std::sync::OnceLock::new();
528 match self.slot(id) {
529 Some(node) => &node.tag,
530 None => NONE.get_or_init(Tag::default),
531 }
532 }
533
534 /// A node's children, borrowed.
535 pub fn children_of(&self, id: u32) -> &[u32] {
536 self.slot(id).map_or(&[], |n| n.children.as_slice())
537 }
538
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago539 pub fn props(&self, id: u32) -> Props {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago540 self.props_of(id).clone()
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago541 }
542
543 pub fn set(&mut self, id: u32, key: &str, value: Value) {
544 if let Some(node) = self.slot_mut(id) {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago545 node.props.0.insert(key.to_owned(), value);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago546 }
547 }
548
549 pub fn clear_props(&mut self, id: u32) {
550 if let Some(node) = self.slot_mut(id) {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago551 node.props.0.clear();
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago552 }
553 }
554
555 pub fn get(&self, id: u32, key: &str) -> Option<&Value> {
Measure a node once, and paint only what is on screen c41903b nandi 16d ago556 self.slot(id).and_then(|n| n.props.0.get(key))
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago557 }
558
559 /// The subtree at `id` as pretty-printed hiccup — what the tree *is*, read
560 /// back from the arena, rather than what a component meant to build.
561 ///
562 /// Props are sorted, so two dumps of the same tree compare as text.
563 pub fn dump(&self, id: u32) -> String {
564 let mut out = String::new();
565 self.dump_into(id, 0, &mut out);
566 out
567 }
568
569 fn dump_into(&self, id: u32, depth: usize, out: &mut String) {
570 let Some(node) = self.slot(id) else {
571 out.push_str("nil");
572 return;
573 };
574 let indent = " ".repeat(depth);
575 out.push_str("[:");
576 out.push_str(node.tag.name());
577
Measure a node once, and paint only what is on screen c41903b nandi 16d ago578 let mut keys: Vec<&String> = node.props.0.keys().collect();
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago579 keys.sort();
580 out.push_str(" {");
581 for (i, key) in keys.iter().enumerate() {
582 if i > 0 {
583 out.push(' ');
584 }
585 out.push(':');
586 out.push_str(key);
587 out.push(' ');
Measure a node once, and paint only what is on screen c41903b nandi 16d ago588 write_value(&node.props.0[*key], out);
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 17d ago589 }
590 out.push('}');
591
592 for child in &node.children {
593 out.push('\n');
594 out.push_str(&indent);
595 out.push_str(" ");
596 self.dump_into(*child, depth + 1, out);
597 }
598 out.push(']');
599 }
600
601 // ── events ──────────────────────────────────────────────────────────────
602
603 pub fn emit(&mut self, node: u32, name: &'static str, text: String, num: f64) {
604 self.pending.push_back(Event {
605 node,
606 name,
607 text,
608 num,
609 });
610 }
611
612 /// Dequeue one event into the accessor slot. False when the queue is empty.
613 pub fn poll(&mut self) -> bool {
614 self.current = self.pending.pop_front();
615 self.current.is_some()
616 }
617
618 pub fn current(&self) -> Option<&Event> {
619 self.current.as_ref()
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 fn tree_with_button() -> (Tree, u32) {
628 let mut tree = Tree::new();
629 let button = tree.new_node("button");
630 tree.set(button, "label", Value::Str("go".into()));
631 let root = tree.root();
632 tree.append(root, button);
633 (tree, button)
634 }
635
636 #[test]
637 fn a_dump_is_the_tree_as_hiccup_with_sorted_props() {
638 let (mut tree, button) = tree_with_button();
639 tree.set(button, "kind", Value::Str("primary".into()));
640 assert_eq!(
641 tree.dump(tree.root()),
642 "[:window {}\n [:button {:kind \"primary\" :label \"go\"}]]"
643 );
644 }
645
646 #[test]
647 fn hbox_and_vbox_are_one_node() {
648 let mut tree = Tree::new();
649 let h = tree.new_node("hbox");
650 let v = tree.new_node("vbox");
651 assert_eq!(tree.tag_name(h), "box");
652 assert_eq!(tree.tag_name(v), "box");
653 }
654
655 #[test]
656 fn an_unknown_tag_keeps_its_name() {
657 let mut tree = Tree::new();
658 let node = tree.new_node("sparkline");
659 assert_eq!(tree.tag_name(node), "sparkline");
660 assert_eq!(tree.tag(node), Tag::Unknown("sparkline".into()));
661 }
662
663 #[test]
664 fn removing_a_node_frees_its_subtree_and_reuses_the_handles() {
665 let mut tree = Tree::new();
666 let outer = tree.new_node("vbox");
667 let inner = tree.new_node("label");
668 tree.append(outer, inner);
669 tree.append(tree.root(), outer);
670 tree.remove(tree.root(), outer);
671 assert!(!tree.exists(outer));
672 assert!(!tree.exists(inner));
673 assert_eq!(tree.child_count(tree.root()), 0);
674 // The arena hands the slots back out rather than growing forever.
675 assert!([outer, inner].contains(&tree.new_node("label")));
676 }
677
678 #[test]
679 fn a_node_cannot_become_its_own_ancestor() {
680 let mut tree = Tree::new();
681 let outer = tree.new_node("vbox");
682 let inner = tree.new_node("vbox");
683 tree.append(outer, inner);
684 assert!(!tree.append(inner, outer));
685 assert_eq!(tree.parent(outer), 0);
686 }
687
688 #[test]
689 fn insert_after_zero_is_the_first_position() {
690 let mut tree = Tree::new();
691 let (a, b, c) = (
692 tree.new_node("label"),
693 tree.new_node("label"),
694 tree.new_node("label"),
695 );
696 let root = tree.root();
697 tree.append(root, a);
698 tree.append(root, b);
699 tree.insert_after(root, c, 0);
700 assert_eq!(tree.children(root), vec![c, a, b]);
701 tree.insert_after(root, c, a);
702 assert_eq!(tree.children(root), vec![a, c, b]);
703 }
704
705 #[test]
706 fn replace_keeps_the_position_and_frees_the_old_node() {
707 let mut tree = Tree::new();
708 let root = tree.root();
709 let (a, b) = (tree.new_node("label"), tree.new_node("label"));
710 tree.append(root, a);
711 tree.append(root, b);
712 let fresh = tree.new_node("button");
713 assert!(tree.replace(root, a, fresh));
714 assert_eq!(tree.children(root), vec![fresh, b]);
715 assert!(!tree.exists(a));
716 }
717
718 #[test]
719 fn the_root_cannot_be_freed() {
720 let mut tree = Tree::new();
721 let root = tree.root();
722 tree.free_node(root);
723 assert!(tree.exists(root));
724 }
725
726 #[test]
727 fn an_event_for_a_freed_node_never_reaches_the_caller() {
728 let (mut tree, button) = tree_with_button();
729 tree.emit(button, "click", String::new(), 0.0);
730 tree.remove(tree.root(), button);
731 assert!(!tree.poll());
732 }
733
734 #[test]
735 fn props_of_the_wrong_type_read_as_the_default() {
736 let mut tree = Tree::new();
737 let node = tree.new_node("progress");
738 tree.set(node, "value", Value::Str("lots".into()));
739 let props = tree.props(node);
740 assert_eq!(props.num("value", 0.5), 0.5);
741 assert_eq!(props.cells("width-request", 7), 7);
742 }
743
744 #[test]
745 fn label_and_text_are_the_same_prop() {
746 let mut tree = Tree::new();
747 let node = tree.new_node("label");
748 tree.set(node, "text", Value::Str("hello".into()));
749 assert_eq!(tree.props(node).label(), "hello");
750 tree.set(node, "label", Value::Str("hi".into()));
751 assert_eq!(tree.props(node).label(), "hi");
752 }
753}