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