nandi/jolt-nativepublic Fork 0
3dd441e764bc6b0123d9b33fe9871dcf7ac150b6
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 · 668 lines · 20.8 KBRust Blame HistoryRaw
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d 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,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago48 Unknown(String),
49}
50
51impl Default for Tag {
52 fn default() -> Self {
53 Self::Unknown(String::new())
54 }
55}
56
57impl Tag {
58 fn parse(name: &str) -> Self {
59 match name {
60 "window" => Self::Window,
61 "box" | "hbox" | "vbox" => Self::Box,
62 "frame" => Self::Frame,
63 "scroll" => Self::Scroll,
64 "overlay" => Self::Overlay,
65 "label" => Self::Label,
66 "title" | "title-2" => Self::Title,
67 "dim-label" => Self::DimLabel,
68 "button" => Self::Button,
69 "checkbutton" | "checkbox" => Self::CheckButton,
70 "entry" => Self::Entry,
71 "separator" => Self::Separator,
72 "spacer" | "gap" => Self::Spacer,
73 "listbox" => Self::Listbox,
74 "progress" => Self::Progress,
75 "spinner" => Self::Spinner,
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago76 // The two glyph nodes. A reaction is a pill somebody can press —
77 // the count of who is on it, and whether you are one of them; an
78 // emoji is the same glyph with none of that, a character in a
79 // sentence. Both carry what to draw in `:emoji` rather than in a
80 // label, which is why an unknown tag painted neither.
81 "reaction" => Self::Reaction,
82 "emoji" => Self::Emoji,
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago83 other => Self::Unknown(other.to_owned()),
84 }
85 }
86
87 /// The canonical name: `hbox` and `vbox` are one node, so both answer
88 /// `box` and carry their orientation in a prop.
89 pub fn name(&self) -> &str {
90 match self {
91 Self::Window => "window",
92 Self::Box => "box",
93 Self::Frame => "frame",
94 Self::Scroll => "scroll",
95 Self::Overlay => "overlay",
96 Self::Label => "label",
97 Self::Title => "title",
98 Self::DimLabel => "dim-label",
99 Self::Button => "button",
100 Self::CheckButton => "checkbutton",
101 Self::Entry => "entry",
102 Self::Separator => "separator",
103 Self::Spacer => "spacer",
104 Self::Listbox => "listbox",
105 Self::Progress => "progress",
106 Self::Spinner => "spinner",
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago107 Self::Reaction => "reaction",
108 Self::Emoji => "emoji",
Paint the same tree into a terminal, for the machines with no window a7f6202 nandi 18d ago109 Self::Unknown(name) => name,
110 }
111 }
112
113 /// Whether the focus ring stops here. A container never takes focus of its
114 /// own; a control that does nothing with a key does not either.
115 pub fn focusable(&self) -> bool {
116 matches!(
117 self,
Paint a reaction, and give an emoji the two columns it takes dce285f nandi 16d ago118 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 18d ago119 )
120 }
121}
122
123/// One interaction, waiting to be drained by the caller. Names are glimmer's
124/// handler props with the `on-` dropped.
125#[derive(Clone, Debug, PartialEq)]
126pub struct Event {
127 pub node: u32,
128 pub name: &'static str,
129 pub text: String,
130 pub num: f64,
131}
132
133#[derive(Clone, Debug, Default)]
134struct Node {
135 tag: Tag,
136 props: HashMap<String, Value>,
137 children: Vec<u32>,
138 /// 0 when unparented. The root's parent is 0 as well, which is what stops
139 /// the ancestor walk in [`Tree::would_cycle`].
140 parent: u32,
141}
142
143/// A node's props, copied out for the duration of one measure or paint.
144///
145/// Reading them through this rather than the map means a missing prop and a
146/// prop of the wrong type answer the same thing: the default. Nothing a caller
147/// can write should be able to make a widget vanish.
148#[derive(Clone, Debug, Default)]
149pub struct Props(pub HashMap<String, Value>);
150
151impl Props {
152 pub fn str(&self, key: &str) -> &str {
153 match self.0.get(key) {
154 Some(Value::Str(s)) => s,
155 _ => "",
156 }
157 }
158
159 pub fn num(&self, key: &str, fallback: f64) -> f64 {
160 match self.0.get(key) {
161 Some(Value::Num(n)) => *n,
162 Some(Value::Bool(b)) => {
163 if *b {
164 1.0
165 } else {
166 0.0
167 }
168 }
169 _ => fallback,
170 }
171 }
172
173 /// A count of cells. Negative and absurd values are clamped rather than
174 /// cast, since `as u16` on a negative double is a silent 0 or 65535.
175 pub fn cells(&self, key: &str, fallback: u16) -> u16 {
176 match self.0.get(key) {
177 Some(Value::Num(n)) if n.is_finite() => n.clamp(0.0, u16::MAX as f64) as u16,
178 _ => fallback,
179 }
180 }
181
182 pub fn bool(&self, key: &str, fallback: bool) -> bool {
183 match self.0.get(key) {
184 Some(Value::Bool(b)) => *b,
185 Some(Value::Num(n)) => *n != 0.0,
186 Some(Value::Str(s)) => s == "true",
187 _ => fallback,
188 }
189 }
190
191 pub fn has(&self, key: &str) -> bool {
192 self.0.contains_key(key)
193 }
194
195 /// The text a widget shows. `:label` and `:text` are the same prop to every
196 /// glimmer backend; whichever the caller wrote is the one that shows.
197 pub fn label(&self) -> &str {
198 if self.has("label") {
199 self.str("label")
200 } else {
201 self.str("text")
202 }
203 }
204}
205
206/// One prop value as EDN. Whole numbers print without a trailing `.0`: every
207/// number crossed the boundary as a double, and `{:spacing 8}` reads better
208/// than `{:spacing 8.0}`.
209fn write_value(value: &Value, out: &mut String) {
210 match value {
211 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
212 Value::Num(n) => {
213 if n.is_finite() && n.fract() == 0.0 && n.abs() < 1e15 {
214 out.push_str(&format!("{}", *n as i64));
215 } else if n.is_finite() {
216 out.push_str(&format!("{n}"));
217 } else {
218 // EDN has no infinity or NaN literal; say nil rather than emit
219 // something no reader will take.
220 out.push_str("nil");
221 }
222 }
223 Value::Str(text) => {
224 out.push('"');
225 for c in text.chars() {
226 match c {
227 '"' => out.push_str("\\\""),
228 '\\' => out.push_str("\\\\"),
229 '\n' => out.push_str("\\n"),
230 '\r' => out.push_str("\\r"),
231 '\t' => out.push_str("\\t"),
232 _ => out.push(c),
233 }
234 }
235 out.push('"');
236 }
237 }
238}
239
240pub struct Tree {
241 /// Index 0 is never handed out: 0 is "no node" throughout the ABI.
242 nodes: Vec<Option<Node>>,
243 free: Vec<u32>,
244 root: u32,
245 pending: VecDeque<Event>,
246 current: Option<Event>,
247}
248
249impl Default for Tree {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255impl Tree {
256 pub fn new() -> Self {
257 let mut tree = Self {
258 nodes: vec![None],
259 free: Vec::new(),
260 root: 0,
261 pending: VecDeque::new(),
262 current: None,
263 };
264 tree.root = tree.new_node("window");
265 tree
266 }
267
268 pub fn root(&self) -> u32 {
269 self.root
270 }
271
272 fn slot(&self, id: u32) -> Option<&Node> {
273 self.nodes.get(id as usize).and_then(|n| n.as_ref())
274 }
275
276 fn slot_mut(&mut self, id: u32) -> Option<&mut Node> {
277 self.nodes.get_mut(id as usize).and_then(|n| n.as_mut())
278 }
279
280 pub fn exists(&self, id: u32) -> bool {
281 self.slot(id).is_some()
282 }
283
284 pub fn new_node(&mut self, tag: &str) -> u32 {
285 let node = Node {
286 tag: Tag::parse(tag),
287 ..Node::default()
288 };
289 match self.free.pop() {
290 Some(id) => {
291 self.nodes[id as usize] = Some(node);
292 id
293 }
294 None => {
295 self.nodes.push(Some(node));
296 (self.nodes.len() - 1) as u32
297 }
298 }
299 }
300
301 /// Free `id` and everything under it. The root is refused: the window node
302 /// is the one thing a caller cannot drop out from under itself.
303 pub fn free_node(&mut self, id: u32) {
304 if id == self.root || !self.exists(id) {
305 return;
306 }
307 let parent = self.slot(id).map(|n| n.parent).unwrap_or(0);
308 if parent != 0 {
309 if let Some(node) = self.slot_mut(parent) {
310 node.children.retain(|c| *c != id);
311 }
312 }
313 self.free_subtree(id);
314 }
315
316 fn free_subtree(&mut self, id: u32) {
317 let children = self
318 .slot(id)
319 .map(|n| n.children.clone())
320 .unwrap_or_default();
321 for child in children {
322 self.free_subtree(child);
323 }
324 if self.nodes[id as usize].take().is_some() {
325 self.free.push(id);
326 }
327 // An event queued against a node that has since gone would be routed to
328 // a handler the reconciler has already dropped. Drop it here instead.
329 self.pending.retain(|e| e.node != id);
330 }
331
332 /// Whether making `child` a child of `parent` would make a loop — `child`
333 /// being `parent` or one of its ancestors.
334 fn would_cycle(&self, parent: u32, child: u32) -> bool {
335 let mut at = parent;
336 while at != 0 {
337 if at == child {
338 return true;
339 }
340 at = match self.slot(at) {
341 Some(node) => node.parent,
342 None => return false,
343 };
344 }
345 false
346 }
347
348 fn unparent(&mut self, child: u32) {
349 let parent = self.slot(child).map(|n| n.parent).unwrap_or(0);
350 if parent != 0 {
351 if let Some(node) = self.slot_mut(parent) {
352 node.children.retain(|c| *c != child);
353 }
354 }
355 if let Some(node) = self.slot_mut(child) {
356 node.parent = 0;
357 }
358 }
359
360 pub fn append(&mut self, parent: u32, child: u32) -> bool {
361 if !self.exists(parent) || !self.exists(child) || self.would_cycle(parent, child) {
362 return false;
363 }
364 self.unparent(child);
365 self.slot_mut(parent).unwrap().children.push(child);
366 self.slot_mut(child).unwrap().parent = parent;
367 true
368 }
369
370 /// Unparent *and* free `child`, which is what the reconciler means by
371 /// remove: a node it has taken out of the tree is a node it has dropped.
372 pub fn remove(&mut self, parent: u32, child: u32) {
373 if self.slot(child).map(|n| n.parent) == Some(parent) {
374 self.free_node(child);
375 }
376 }
377
378 /// Move `child` after `sibling`; `sibling` 0 means the first position.
379 pub fn insert_after(&mut self, parent: u32, child: u32, sibling: u32) -> bool {
380 if !self.exists(parent) || !self.exists(child) || self.would_cycle(parent, child) {
381 return false;
382 }
383 if sibling != 0 && self.slot(sibling).map(|n| n.parent) != Some(parent) {
384 return false;
385 }
386 self.unparent(child);
387 let at = match sibling {
388 0 => 0,
389 _ => {
390 let children = &self.slot(parent).unwrap().children;
391 children
392 .iter()
393 .position(|c| *c == sibling)
394 .map_or(0, |i| i + 1)
395 }
396 };
397 self.slot_mut(parent).unwrap().children.insert(at, child);
398 self.slot_mut(child).unwrap().parent = parent;
399 true
400 }
401
402 /// Put `new` where `old` was, and free `old`.
403 pub fn replace(&mut self, parent: u32, old: u32, new: u32) -> bool {
404 if self.slot(old).map(|n| n.parent) != Some(parent) || !self.exists(new) {
405 return false;
406 }
407 if self.would_cycle(parent, new) {
408 return false;
409 }
410 self.unparent(new);
411 let at = self
412 .slot(parent)
413 .and_then(|n| n.children.iter().position(|c| *c == old));
414 let Some(at) = at else { return false };
415 self.slot_mut(parent).unwrap().children[at] = new;
416 self.slot_mut(new).unwrap().parent = parent;
417 if let Some(node) = self.slot_mut(old) {
418 node.parent = 0;
419 }
420 self.free_subtree(old);
421 true
422 }
423
424 // ── reading it back ─────────────────────────────────────────────────────
425
426 pub fn tag(&self, id: u32) -> Tag {
427 self.slot(id).map(|n| n.tag.clone()).unwrap_or_default()
428 }
429
430 pub fn tag_name(&self, id: u32) -> &str {
431 self.slot(id).map_or("", |n| n.tag.name())
432 }
433
434 pub fn children(&self, id: u32) -> Vec<u32> {
435 self.slot(id)
436 .map(|n| n.children.clone())
437 .unwrap_or_default()
438 }
439
440 pub fn child_count(&self, id: u32) -> usize {
441 self.slot(id).map_or(0, |n| n.children.len())
442 }
443
444 pub fn child_at(&self, id: u32, index: usize) -> u32 {
445 self.slot(id)
446 .and_then(|n| n.children.get(index).copied())
447 .unwrap_or(0)
448 }
449
450 pub fn parent(&self, id: u32) -> u32 {
451 self.slot(id).map_or(0, |n| n.parent)
452 }
453
454 pub fn props(&self, id: u32) -> Props {
455 Props(self.slot(id).map(|n| n.props.clone()).unwrap_or_default())
456 }
457
458 pub fn set(&mut self, id: u32, key: &str, value: Value) {
459 if let Some(node) = self.slot_mut(id) {
460 node.props.insert(key.to_owned(), value);
461 }
462 }
463
464 pub fn clear_props(&mut self, id: u32) {
465 if let Some(node) = self.slot_mut(id) {
466 node.props.clear();
467 }
468 }
469
470 pub fn get(&self, id: u32, key: &str) -> Option<&Value> {
471 self.slot(id).and_then(|n| n.props.get(key))
472 }
473
474 /// The subtree at `id` as pretty-printed hiccup — what the tree *is*, read
475 /// back from the arena, rather than what a component meant to build.
476 ///
477 /// Props are sorted, so two dumps of the same tree compare as text.
478 pub fn dump(&self, id: u32) -> String {
479 let mut out = String::new();
480 self.dump_into(id, 0, &mut out);
481 out
482 }
483
484 fn dump_into(&self, id: u32, depth: usize, out: &mut String) {
485 let Some(node) = self.slot(id) else {
486 out.push_str("nil");
487 return;
488 };
489 let indent = " ".repeat(depth);
490 out.push_str("[:");
491 out.push_str(node.tag.name());
492
493 let mut keys: Vec<&String> = node.props.keys().collect();
494 keys.sort();
495 out.push_str(" {");
496 for (i, key) in keys.iter().enumerate() {
497 if i > 0 {
498 out.push(' ');
499 }
500 out.push(':');
501 out.push_str(key);
502 out.push(' ');
503 write_value(&node.props[*key], out);
504 }
505 out.push('}');
506
507 for child in &node.children {
508 out.push('\n');
509 out.push_str(&indent);
510 out.push_str(" ");
511 self.dump_into(*child, depth + 1, out);
512 }
513 out.push(']');
514 }
515
516 // ── events ──────────────────────────────────────────────────────────────
517
518 pub fn emit(&mut self, node: u32, name: &'static str, text: String, num: f64) {
519 self.pending.push_back(Event {
520 node,
521 name,
522 text,
523 num,
524 });
525 }
526
527 /// Dequeue one event into the accessor slot. False when the queue is empty.
528 pub fn poll(&mut self) -> bool {
529 self.current = self.pending.pop_front();
530 self.current.is_some()
531 }
532
533 pub fn current(&self) -> Option<&Event> {
534 self.current.as_ref()
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 fn tree_with_button() -> (Tree, u32) {
543 let mut tree = Tree::new();
544 let button = tree.new_node("button");
545 tree.set(button, "label", Value::Str("go".into()));
546 let root = tree.root();
547 tree.append(root, button);
548 (tree, button)
549 }
550
551 #[test]
552 fn a_dump_is_the_tree_as_hiccup_with_sorted_props() {
553 let (mut tree, button) = tree_with_button();
554 tree.set(button, "kind", Value::Str("primary".into()));
555 assert_eq!(
556 tree.dump(tree.root()),
557 "[:window {}\n [:button {:kind \"primary\" :label \"go\"}]]"
558 );
559 }
560
561 #[test]
562 fn hbox_and_vbox_are_one_node() {
563 let mut tree = Tree::new();
564 let h = tree.new_node("hbox");
565 let v = tree.new_node("vbox");
566 assert_eq!(tree.tag_name(h), "box");
567 assert_eq!(tree.tag_name(v), "box");
568 }
569
570 #[test]
571 fn an_unknown_tag_keeps_its_name() {
572 let mut tree = Tree::new();
573 let node = tree.new_node("sparkline");
574 assert_eq!(tree.tag_name(node), "sparkline");
575 assert_eq!(tree.tag(node), Tag::Unknown("sparkline".into()));
576 }
577
578 #[test]
579 fn removing_a_node_frees_its_subtree_and_reuses_the_handles() {
580 let mut tree = Tree::new();
581 let outer = tree.new_node("vbox");
582 let inner = tree.new_node("label");
583 tree.append(outer, inner);
584 tree.append(tree.root(), outer);
585 tree.remove(tree.root(), outer);
586 assert!(!tree.exists(outer));
587 assert!(!tree.exists(inner));
588 assert_eq!(tree.child_count(tree.root()), 0);
589 // The arena hands the slots back out rather than growing forever.
590 assert!([outer, inner].contains(&tree.new_node("label")));
591 }
592
593 #[test]
594 fn a_node_cannot_become_its_own_ancestor() {
595 let mut tree = Tree::new();
596 let outer = tree.new_node("vbox");
597 let inner = tree.new_node("vbox");
598 tree.append(outer, inner);
599 assert!(!tree.append(inner, outer));
600 assert_eq!(tree.parent(outer), 0);
601 }
602
603 #[test]
604 fn insert_after_zero_is_the_first_position() {
605 let mut tree = Tree::new();
606 let (a, b, c) = (
607 tree.new_node("label"),
608 tree.new_node("label"),
609 tree.new_node("label"),
610 );
611 let root = tree.root();
612 tree.append(root, a);
613 tree.append(root, b);
614 tree.insert_after(root, c, 0);
615 assert_eq!(tree.children(root), vec![c, a, b]);
616 tree.insert_after(root, c, a);
617 assert_eq!(tree.children(root), vec![a, c, b]);
618 }
619
620 #[test]
621 fn replace_keeps_the_position_and_frees_the_old_node() {
622 let mut tree = Tree::new();
623 let root = tree.root();
624 let (a, b) = (tree.new_node("label"), tree.new_node("label"));
625 tree.append(root, a);
626 tree.append(root, b);
627 let fresh = tree.new_node("button");
628 assert!(tree.replace(root, a, fresh));
629 assert_eq!(tree.children(root), vec![fresh, b]);
630 assert!(!tree.exists(a));
631 }
632
633 #[test]
634 fn the_root_cannot_be_freed() {
635 let mut tree = Tree::new();
636 let root = tree.root();
637 tree.free_node(root);
638 assert!(tree.exists(root));
639 }
640
641 #[test]
642 fn an_event_for_a_freed_node_never_reaches_the_caller() {
643 let (mut tree, button) = tree_with_button();
644 tree.emit(button, "click", String::new(), 0.0);
645 tree.remove(tree.root(), button);
646 assert!(!tree.poll());
647 }
648
649 #[test]
650 fn props_of_the_wrong_type_read_as_the_default() {
651 let mut tree = Tree::new();
652 let node = tree.new_node("progress");
653 tree.set(node, "value", Value::Str("lots".into()));
654 let props = tree.props(node);
655 assert_eq!(props.num("value", 0.5), 0.5);
656 assert_eq!(props.cells("width-request", 7), 7);
657 }
658
659 #[test]
660 fn label_and_text_are_the_same_prop() {
661 let mut tree = Tree::new();
662 let node = tree.new_node("label");
663 tree.set(node, "text", Value::Str("hello".into()));
664 assert_eq!(tree.props(node).label(), "hello");
665 tree.set(node, "label", Value::Str("hi".into()));
666 assert_eq!(tree.props(node).label(), "hi");
667 }
668}