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