Skip to main content

eui_core/
tree_guides.rs

1//! Round 3 (VAPP-102, `docs/round-3-contract.md` §2): the tree connector rule
2//! the CORE computes for nested rows, mirroring
3//! `packages/exponential-ui/src/tree-guides.ts` line by line. `tree_guides`
4//! is the ×4 rule of the app (`packages/ui/src/tree-guides.ts`, desktop
5//! `domain::tree_guides`, iOS `TreeGuides.swift`, Android `TreeGuides.kt`),
6//! copied verbatim so a Section of Rows connects like a session tree or a PR
7//! stack. `apply_tree_guides` is the ONE post-expansion pass (the end of
8//! [`crate::macros::expand_macros`]): over every node's children in order,
9//! each Row root contributes its numeric `recipe.props.depth` (anything else
10//! counts as 0 and ends every subtree), and the rule fills the Row's `guides`
11//! part (a TreeGuides native) with `elbowAt` / `tee` / `passThrough`. Locked
12//! by `fixtures/tree-guides.json` and the Row cases of `catalog-macros.json`.
13
14use serde_json::Value;
15
16use crate::types::{Props, UiNode};
17
18/// Guide geometry for ONE row, in gutter LEVELS (not pixels).
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20pub struct TreeGuide {
21    /// The gutter level the elbow sits in (`depth - 1`); `None` for a root.
22    pub elbow_at: Option<usize>,
23    /// The elbow's vertical continues past the centre: a later sibling follows.
24    pub tee: bool,
25    /// Ancestor gutter levels carrying a straight full-height line, ascending.
26    pub pass_through: Vec<usize>,
27}
28
29/// Whether the subtree hanging off gutter `level` continues AFTER row
30/// `index`: another row at depth `level + 1` follows before the walk leaves
31/// that subtree (a row at depth `≤ level`).
32fn continues_after(depths: &[usize], index: usize, level: usize) -> bool {
33    for &depth in &depths[index + 1..] {
34        if depth <= level {
35            return false;
36        }
37        if depth == level + 1 {
38            return true;
39        }
40    }
41    false
42}
43
44/// The guides for a flat list of VISIBLE rows, given their depths.
45pub fn tree_guides(depths: &[usize]) -> Vec<TreeGuide> {
46    depths
47        .iter()
48        .enumerate()
49        .map(|(index, &depth)| {
50            if depth == 0 {
51                return TreeGuide::default();
52            }
53            let pass_through = (0..depth - 1).filter(|&level| continues_after(depths, index, level)).collect();
54            TreeGuide { elbow_at: Some(depth - 1), tee: continues_after(depths, index, depth - 1), pass_through }
55        })
56        .collect()
57}
58
59/// The expanded root of a `Row` macro.
60pub fn is_row_root(node: &UiNode) -> bool {
61    node.recipe.as_ref().is_some_and(|r| r.macro_ == "Row" && r.part == "root")
62}
63
64/// A Row root's depth: its recipe prop when a finite number > 0 (floored),
65/// else 0 (a bound depth cannot be computed against its siblings).
66/// A sibling's depth: a Row root's recipe prop, else any node's own numeric
67/// `depth` prop (an extension row such as SessionRow or PrRow), when a finite
68/// number > 0, else 0 (a bound depth cannot be computed against its siblings).
69fn sibling_depth(node: &UiNode) -> usize {
70    let depth = if is_row_root(node) { node.recipe.as_ref().and_then(|r| r.props.get("depth")) } else { node.props.get("depth") }.and_then(Value::as_f64);
71    match depth {
72        Some(d) if d.is_finite() && d > 0.0 => d.floor() as usize,
73        _ => 0,
74    }
75}
76
77/// A non-Row sibling that takes part: its own numeric `depth` prop.
78fn has_depth(node: &UiNode) -> bool {
79    !is_row_root(node) && node.component != "TreeGuides" && node.props.get("depth").is_some_and(Value::is_number)
80}
81
82/// The filled guide as props: `{depth, elbowAt?, tee, passThrough}` (TS `guideProps`).
83fn guide_props(depth: usize, g: &TreeGuide) -> Props {
84    let mut props = Props::new();
85    props.insert("depth".into(), Value::from(depth as u64));
86    if let Some(elbow) = g.elbow_at {
87        props.insert("elbowAt".into(), Value::from(elbow as u64));
88    }
89    props.insert("tee".into(), Value::Bool(g.tee));
90    props.insert("passThrough".into(), Value::Array(g.pass_through.iter().map(|&l| Value::from(l as u64)).collect()));
91    props
92}
93
94/// Fill the guides of ONE sibling list (TS `fillSiblings`): a Row root's
95/// `guides` part takes the props; any other node with a numeric `depth` gets
96/// them (minus `depth`) as its `guide` prop for an extension painter to draw.
97fn fill_siblings(nodes: &mut [UiNode]) {
98    let depths: Vec<usize> = nodes.iter().map(sibling_depth).collect();
99    let guides = tree_guides(&depths);
100    for (i, node) in nodes.iter_mut().enumerate() {
101        let g = &guides[i];
102        if is_row_root(node) {
103            if let Some(part) = guides_part(node) {
104                part.props = guide_props(depths[i], g);
105            }
106        } else if has_depth(node) {
107            let mut guide = guide_props(depths[i], g);
108            guide.shift_remove("depth");
109            node.props.insert("guide".into(), Value::Object(guide));
110        }
111    }
112}
113
114/// The Row's `guides` part, when its depth emitted one.
115fn guides_part(row: &mut UiNode) -> Option<&mut UiNode> {
116    row.children
117        .iter_mut()
118        .find(|c| c.component == "TreeGuides" && c.recipe.as_ref().is_some_and(|r| r.part == "guides"))
119}
120
121/// Fill every Row's guides from the rows around it. Mutates `root` in place
122/// (the expander hands it a fresh tree).
123/// TS `applyTreeGuides`: the root itself and every slot value are one-element
124/// lists; every node's children are one list; recurse slots then children.
125pub fn apply_tree_guides(root: &mut UiNode) {
126    fill_siblings(std::slice::from_mut(root));
127    visit(root);
128}
129
130fn visit(node: &mut UiNode) {
131    fill_siblings(&mut node.children);
132    if let Some(slots) = node.slots.as_mut() {
133        for slot in slots.values_mut() {
134            fill_siblings(std::slice::from_mut(slot));
135            visit(slot);
136        }
137    }
138    for child in &mut node.children {
139        visit(child);
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn a_sibling_after_the_subtree_tees_and_passes_through() {
149        let g = tree_guides(&[0, 1, 2, 1]);
150        assert_eq!(g[0], TreeGuide::default());
151        assert_eq!(g[1], TreeGuide { elbow_at: Some(0), tee: true, pass_through: vec![] });
152        assert_eq!(g[2], TreeGuide { elbow_at: Some(1), tee: false, pass_through: vec![0] });
153        assert_eq!(g[3], TreeGuide { elbow_at: Some(0), tee: false, pass_through: vec![] });
154    }
155}