Skip to main content

urushi/component/
tree.rs

1//! Renderer-neutral trees with reusable owned nodes.
2
3use std::{fmt, sync::Arc};
4
5use crate::text::{PrintableLines, PrintableText, wrap_text};
6use crate::view::{CanvasMeasure, CanvasRequirements};
7use crate::{
8    Canvas, CanvasContext, CanvasItem, CanvasSizing, Composition, Grapheme, LineContinuations,
9    LineGlyphs, LineNetwork, Position, TextStyle, TreeRole, View,
10};
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
13struct ChildOffset {
14    start: usize,
15    end: usize,
16}
17
18/// A visible value's position in a Tree.
19///
20/// The optional root has no sibling group. Child positions are assigned after
21/// offsets and hidden nodes are removed, and top-level children have depth
22/// zero.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum TreePosition {
25    /// The Tree's optional root value.
26    Root,
27    /// One node among its visible siblings.
28    Child {
29        /// The zero-based visible sibling index.
30        index: usize,
31        /// The number of visible siblings in this group.
32        len: usize,
33        /// The zero-based nesting depth used by the canonical connectors.
34        depth: usize,
35    },
36}
37
38/// One owned value and its recursive child nodes.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct TreeNode<T> {
41    value: T,
42    children: Vec<Self>,
43    hidden: bool,
44    child_offset: ChildOffset,
45}
46
47impl<T> TreeNode<T> {
48    /// Creates a visible leaf node from a typed value.
49    pub fn new(value: T) -> Self {
50        Self {
51            value,
52            children: Vec::new(),
53            hidden: false,
54            child_offset: ChildOffset::default(),
55        }
56    }
57
58    /// Appends one child node.
59    #[must_use]
60    pub fn child(mut self, child: impl Into<Self>) -> Self {
61        self.children.push(child.into());
62        self
63    }
64
65    /// Appends child nodes in iteration order.
66    #[must_use]
67    pub fn children<I, N>(mut self, children: I) -> Self
68    where
69        I: IntoIterator<Item = N>,
70        N: Into<Self>,
71    {
72        self.children.extend(children.into_iter().map(Into::into));
73        self
74    }
75
76    /// Includes or excludes this node and all of its descendants.
77    #[must_use]
78    pub const fn hidden(mut self, hidden: bool) -> Self {
79        self.hidden = hidden;
80        self
81    }
82
83    /// Omits `start` children from the front and `end` from the back.
84    #[must_use]
85    pub const fn child_offset(mut self, start: usize, end: usize) -> Self {
86        self.child_offset = ChildOffset { start, end };
87        self
88    }
89
90    /// Returns this node's value.
91    pub const fn value(&self) -> &T {
92        &self.value
93    }
94
95    /// Returns all owned children before visibility and offset are applied.
96    pub fn child_nodes(&self) -> &[Self] {
97        &self.children
98    }
99
100    /// Returns whether this node and its descendants are excluded.
101    pub const fn is_hidden(&self) -> bool {
102        self.hidden
103    }
104
105    fn visible_children(&self) -> Vec<&Self> {
106        visible_children(&self.children, self.child_offset)
107    }
108}
109
110impl<T> From<T> for TreeNode<T> {
111    fn from(value: T) -> Self {
112        Self::new(value)
113    }
114}
115
116type NodeFormatter<'a, T> = dyn Fn(&T, TreePosition) -> String + 'a;
117type NodeStyler<'a, T> = dyn Fn(&T, TreePosition) -> Option<TextStyle> + 'a;
118
119/// Typed formatting and optional text-style overrides for Tree values.
120///
121/// This policy covers both the optional root and child nodes. It is separate
122/// from [`TreePresentation`], which remains the type-independent Tree-wide
123/// presentation stored by a Theme. Composition snapshots callback results;
124/// measurement and drawing do not evaluate application policy.
125#[derive(Clone)]
126pub struct TreeNodePresentation<'a, T> {
127    format: Arc<NodeFormatter<'a, T>>,
128    node_style: Arc<NodeStyler<'a, T>>,
129}
130
131impl<T> fmt::Debug for TreeNodePresentation<'_, T> {
132    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133        formatter.write_str("TreeNodePresentation { .. }")
134    }
135}
136
137impl<'a, T: 'a> TreeNodePresentation<'a, T> {
138    /// Creates a node presentation with a custom formatter and no style overrides.
139    pub fn new<F>(format: F) -> Self
140    where
141        F: Fn(&T, TreePosition) -> String + 'a,
142    {
143        Self {
144            format: Arc::new(format),
145            node_style: Arc::new(no_node_style::<T>),
146        }
147    }
148
149    /// Replaces the per-node text-style policy.
150    ///
151    /// `None` keeps the [`TreePresentation`] root or item default.
152    /// `Some(style)` replaces that complete style rather than layering over it.
153    #[must_use]
154    pub fn node_style<S>(mut self, node_style: S) -> Self
155    where
156        S: Fn(&T, TreePosition) -> Option<TextStyle> + 'a,
157    {
158        self.node_style = Arc::new(node_style);
159        self
160    }
161}
162
163impl<'a, T> TreeNodePresentation<'a, T>
164where
165    T: fmt::Display + 'a,
166{
167    /// Uses the value's canonical [`fmt::Display`] representation.
168    pub fn display() -> Self {
169        Self::new(display_node::<T>)
170    }
171}
172
173fn display_node<T>(value: &T, _: TreePosition) -> String
174where
175    T: fmt::Display,
176{
177    value.to_string()
178}
179
180fn no_node_style<T>(_: &T, _: TreePosition) -> Option<TextStyle> {
181    None
182}
183
184/// Presentation policy used to compose a [`Tree`] into a [`View`].
185#[derive(Debug, Clone, PartialEq)]
186pub struct TreePresentation {
187    root_style: TextStyle,
188    item_style: TextStyle,
189    connector_style: TextStyle,
190    line_glyphs: LineGlyphs,
191    indent_width: usize,
192}
193
194impl TreePresentation {
195    /// Creates the canonical tree presentation with square line glyphs.
196    pub fn new(root_style: TextStyle, item_style: TextStyle, connector_style: TextStyle) -> Self {
197        Self {
198            root_style,
199            item_style,
200            connector_style,
201            line_glyphs: LineGlyphs::NORMAL,
202            indent_width: 4,
203        }
204    }
205
206    /// Returns the style assigned to one logical tree role.
207    pub fn get_style(&self, role: TreeRole) -> &TextStyle {
208        match role {
209            TreeRole::Root => &self.root_style,
210            TreeRole::Item => &self.item_style,
211            TreeRole::Connector => &self.connector_style,
212        }
213    }
214
215    /// Replaces the style assigned to one logical tree role.
216    #[must_use]
217    pub fn style(mut self, role: TreeRole, style: TextStyle) -> Self {
218        match role {
219            TreeRole::Root => self.root_style = style,
220            TreeRole::Item => self.item_style = style,
221            TreeRole::Connector => self.connector_style = style,
222        }
223        self
224    }
225
226    /// Replaces the root style.
227    #[must_use]
228    pub fn root_style(self, style: TextStyle) -> Self {
229        self.style(TreeRole::Root, style)
230    }
231
232    /// Replaces the item style.
233    #[must_use]
234    pub fn item_style(self, style: TextStyle) -> Self {
235        self.style(TreeRole::Item, style)
236    }
237
238    /// Replaces the connector style.
239    #[must_use]
240    pub fn connector_style(self, style: TextStyle) -> Self {
241        self.style(TreeRole::Connector, style)
242    }
243
244    /// Replaces the complete one-cell connector glyph repertoire.
245    #[must_use]
246    pub const fn line_glyphs(mut self, line_glyphs: LineGlyphs) -> Self {
247        self.line_glyphs = line_glyphs;
248        self
249    }
250
251    /// Sets the cell distance from one connector column to the next.
252    ///
253    /// The width includes the junction, at least one horizontal continuation
254    /// cell, and one gap before the node body.
255    ///
256    /// # Panics
257    ///
258    /// Panics when `indent_width` is less than three cells.
259    #[must_use]
260    pub const fn indent_width(mut self, indent_width: usize) -> Self {
261        assert!(
262            indent_width >= 3,
263            "tree indentation must be at least three cells"
264        );
265        self.indent_width = indent_width;
266        self
267    }
268
269    /// Composes displayable tree data into an intrinsically sized Canvas.
270    pub fn compose<T>(&self, tree: &Tree<T>) -> View
271    where
272        T: fmt::Display,
273    {
274        self.compose_using(tree, &display_node::<T>, &no_node_style::<T>)
275    }
276
277    /// Composes tree data with typed node formatting and text-style overrides.
278    pub fn compose_with<T>(&self, tree: &Tree<T>, nodes: &TreeNodePresentation<'_, T>) -> View {
279        self.compose_using(tree, &*nodes.format, &*nodes.node_style)
280    }
281
282    fn compose_using<T, F, S>(&self, tree: &Tree<T>, format: &F, style: &S) -> View
283    where
284        F: Fn(&T, TreePosition) -> String + ?Sized,
285        S: Fn(&T, TreePosition) -> Option<TextStyle> + ?Sized,
286    {
287        if tree.hidden {
288            return View::empty();
289        }
290
291        let children = visible_children(&tree.children, tree.child_offset);
292        if tree.root.is_none() && children.is_empty() {
293            return View::empty();
294        }
295
296        let mut nodes = Vec::new();
297        let mut groups = Vec::new();
298        let root = tree.root.as_ref().map(|value| {
299            let position = TreePosition::Root;
300            BoundTreeRoot {
301                value: format(value, position),
302                style: style(value, position).unwrap_or_else(|| self.root_style.clone()),
303            }
304        });
305        bind_group(&mut nodes, &mut groups, &children, 0, self, format, style);
306        let item = TreeCanvasItem(Arc::new(TreeFrame {
307            root,
308            nodes,
309            groups,
310            connector_style: self.connector_style.clone(),
311            line_glyphs: self.line_glyphs,
312            indent_width: self.indent_width,
313        }));
314        View::canvas(Canvas::new().sizing(item.sizing()).item(item))
315    }
316}
317
318#[derive(Debug, Clone, PartialEq)]
319struct BoundTreeRoot {
320    value: String,
321    style: TextStyle,
322}
323
324#[derive(Debug, Clone, PartialEq)]
325struct BoundTreeNode {
326    value: String,
327    style: TextStyle,
328    connector_x: usize,
329    content_x: usize,
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
333struct BoundSiblingGroup {
334    connector_x: usize,
335    nodes: Vec<usize>,
336}
337
338fn bind_group<T, F, S>(
339    bound: &mut Vec<BoundTreeNode>,
340    groups: &mut Vec<BoundSiblingGroup>,
341    nodes: &[&TreeNode<T>],
342    depth: usize,
343    presentation: &TreePresentation,
344    format: &F,
345    style: &S,
346) where
347    F: Fn(&T, TreePosition) -> String + ?Sized,
348    S: Fn(&T, TreePosition) -> Option<TextStyle> + ?Sized,
349{
350    if nodes.is_empty() {
351        return;
352    }
353
354    let connector_x = depth.saturating_mul(presentation.indent_width);
355    let content_x = connector_x.saturating_add(presentation.indent_width);
356    let group_index = groups.len();
357    groups.push(BoundSiblingGroup {
358        connector_x,
359        nodes: Vec::with_capacity(nodes.len()),
360    });
361
362    for (index, node) in nodes.iter().enumerate() {
363        let position = TreePosition::Child {
364            index,
365            len: nodes.len(),
366            depth,
367        };
368        let node_index = bound.len();
369        bound.push(BoundTreeNode {
370            value: format(&node.value, position),
371            style: style(&node.value, position).unwrap_or_else(|| presentation.item_style.clone()),
372            connector_x,
373            content_x,
374        });
375        groups[group_index].nodes.push(node_index);
376
377        let children = node.visible_children();
378        bind_group(
379            bound,
380            groups,
381            &children,
382            depth.saturating_add(1),
383            presentation,
384            format,
385            style,
386        );
387    }
388}
389
390#[derive(Debug, Clone, PartialEq)]
391struct TreeFrame {
392    root: Option<BoundTreeRoot>,
393    nodes: Vec<BoundTreeNode>,
394    groups: Vec<BoundSiblingGroup>,
395    connector_style: TextStyle,
396    line_glyphs: LineGlyphs,
397    indent_width: usize,
398}
399
400impl TreeFrame {
401    fn width_requirements(&self) -> CanvasRequirements {
402        let mut demand = 0;
403        let mut floor = 0;
404        if let Some(root) = &self.root {
405            let widths = text_widths(&root.value);
406            demand = widths.0;
407            floor = widths.1;
408        }
409        for node in &self.nodes {
410            let (content_demand, content_floor) = text_widths(&node.value);
411            demand = demand.max(node.content_x.saturating_add(content_demand));
412            floor = floor.max(node.content_x.saturating_add(content_floor));
413        }
414        CanvasRequirements::new(demand, floor)
415    }
416
417    fn rows(&self, width: usize) -> TreeRows {
418        let mut rows = Vec::new();
419        if let Some(root) = &self.root {
420            rows.extend(
421                wrapped_rows(&root.value, width)
422                    .into_iter()
423                    .map(TreeRow::Root),
424            );
425        }
426
427        let mut node_y = Vec::with_capacity(self.nodes.len());
428        for (node_index, node) in self.nodes.iter().enumerate() {
429            node_y.push(rows.len());
430            let content_width = width.saturating_sub(node.content_x);
431            rows.extend(
432                wrapped_rows(&node.value, content_width)
433                    .into_iter()
434                    .map(|text| TreeRow::Node { node_index, text }),
435            );
436        }
437        TreeRows { rows, node_y }
438    }
439
440    fn draw(&self, context: &mut CanvasContext) {
441        let plan = self.rows(context.size().width());
442        let mut network = LineNetwork::new(self.line_glyphs, self.connector_style.clone());
443        let branch_length = self.indent_width - 2;
444        for group in &self.groups {
445            let first_y = plan.node_y[group.nodes[0]];
446            let last_y = plan.node_y[*group.nodes.last().expect("a bound group is non-empty")];
447            network.vertical_with(
448                position(group.connector_x),
449                position(first_y)..=position(last_y),
450                LineContinuations::START,
451            );
452            for &node_index in &group.nodes {
453                let node = &self.nodes[node_index];
454                let y = plan.node_y[node_index];
455                network.horizontal(
456                    position(y),
457                    position(node.connector_x)
458                        ..=position(node.connector_x.saturating_add(branch_length)),
459                );
460            }
461        }
462        context.line_network(network);
463
464        for (y, row) in plan.rows.into_iter().enumerate() {
465            let (x, text, style) = match row {
466                TreeRow::Root(text) => (
467                    0,
468                    text,
469                    self.root
470                        .as_ref()
471                        .expect("a root row has a bound root")
472                        .style
473                        .clone(),
474                ),
475                TreeRow::Node { node_index, text } => (
476                    self.nodes[node_index].content_x,
477                    text,
478                    self.nodes[node_index].style.clone(),
479                ),
480            };
481            context.text_with(
482                Position::new(position(x), position(y)),
483                text,
484                style,
485                Composition::Replace,
486            );
487        }
488    }
489}
490
491fn wrapped_rows(text: &str, width: usize) -> Vec<String> {
492    text.split('\n')
493        .flat_map(|line| wrap_text(PrintableLines::new(line), width))
494        .collect()
495}
496
497fn text_widths(text: &str) -> (usize, usize) {
498    text.split('\n')
499        .map(PrintableText::new)
500        .fold((0, 0), |(demand, floor), line| {
501            (
502                demand.max(line.width()),
503                floor.max(line.graphemes().map(Grapheme::width).max().unwrap_or(0)),
504            )
505        })
506}
507
508#[derive(Debug, Clone, PartialEq, Eq)]
509struct TreeRows {
510    rows: Vec<TreeRow>,
511    node_y: Vec<usize>,
512}
513
514#[derive(Debug, Clone, PartialEq, Eq)]
515enum TreeRow {
516    Root(String),
517    Node { node_index: usize, text: String },
518}
519
520#[derive(Debug, Clone, PartialEq)]
521struct TreeCanvasItem(Arc<TreeFrame>);
522
523impl TreeCanvasItem {
524    fn sizing(&self) -> CanvasSizing {
525        CanvasSizing::intrinsic(self.clone())
526    }
527}
528
529impl CanvasMeasure for TreeCanvasItem {
530    fn width_requirements(&self) -> CanvasRequirements {
531        self.0.width_requirements()
532    }
533
534    fn height_requirements(&self, width: usize) -> CanvasRequirements {
535        CanvasRequirements::new(self.0.rows(width).rows.len(), 0)
536    }
537}
538
539impl CanvasItem for TreeCanvasItem {
540    fn draw(&self, context: &mut CanvasContext) {
541        self.0.draw(context);
542    }
543}
544
545fn position(value: usize) -> i64 {
546    i64::try_from(value).unwrap_or(i64::MAX)
547}
548
549/// Owned tree data independent of presentation policy.
550///
551/// Nodes remain data-only so the model can be reused independently of one
552/// component's presentation policy.
553#[derive(Debug, Clone, PartialEq, Eq)]
554pub struct Tree<T> {
555    root: Option<T>,
556    children: Vec<TreeNode<T>>,
557    hidden: bool,
558    child_offset: ChildOffset,
559}
560
561impl<T> Default for Tree<T> {
562    fn default() -> Self {
563        Self {
564            root: None,
565            children: Vec::new(),
566            hidden: false,
567            child_offset: ChildOffset::default(),
568        }
569    }
570}
571
572impl<T> Tree<T> {
573    /// Creates an empty, rootless tree.
574    pub fn new() -> Self {
575        Self::default()
576    }
577
578    /// Sets the optional typed root value.
579    #[must_use]
580    pub fn root(mut self, root: T) -> Self {
581        self.root = Some(root);
582        self
583    }
584
585    /// Appends one top-level node.
586    #[must_use]
587    pub fn child(mut self, child: impl Into<TreeNode<T>>) -> Self {
588        self.children.push(child.into());
589        self
590    }
591
592    /// Appends top-level nodes in iteration order.
593    #[must_use]
594    pub fn children<I, N>(mut self, children: I) -> Self
595    where
596        I: IntoIterator<Item = N>,
597        N: Into<TreeNode<T>>,
598    {
599        self.children.extend(children.into_iter().map(Into::into));
600        self
601    }
602
603    /// Includes or excludes the complete tree.
604    #[must_use]
605    pub const fn hidden(mut self, hidden: bool) -> Self {
606        self.hidden = hidden;
607        self
608    }
609
610    /// Omits `start` top-level nodes from the front and `end` from the back.
611    #[must_use]
612    pub const fn child_offset(mut self, start: usize, end: usize) -> Self {
613        self.child_offset = ChildOffset { start, end };
614        self
615    }
616
617    /// Returns the optional root value.
618    pub const fn root_value(&self) -> Option<&T> {
619        self.root.as_ref()
620    }
621
622    /// Returns all owned top-level nodes before visibility and offset are applied.
623    pub fn child_nodes(&self) -> &[TreeNode<T>] {
624        &self.children
625    }
626}
627
628fn visible_children<T>(children: &[TreeNode<T>], offset: ChildOffset) -> Vec<&TreeNode<T>> {
629    let end = children.len().saturating_sub(offset.end);
630    if offset.start >= end {
631        return Vec::new();
632    }
633    children[offset.start..end]
634        .iter()
635        .filter(|child| !child.hidden)
636        .collect()
637}
638
639#[cfg(test)]
640mod tests {
641    use super::*;
642    use crate::test_support::{plain, plain_rows, style_at};
643    use crate::{Color, ComponentTheme, SemanticTokens, measure};
644
645    fn styles() -> ComponentTheme {
646        ComponentTheme::from_tokens(&SemanticTokens {
647            text: Color::WHITE,
648            text_muted: Color::BRIGHT_BLACK,
649            background: Color::BLACK,
650            surface: Color::BLACK,
651            accent: Color::CYAN,
652            accent_text: Color::BLACK,
653            success: Color::GREEN,
654            warning: Color::YELLOW,
655            error: Color::RED,
656            border: Color::BRIGHT_BLACK,
657        })
658    }
659
660    #[test]
661    fn renders_empty_and_root_only_trees() {
662        assert!(measure(&styles().tree().compose(&Tree::<String>::new())).is_empty());
663        assert_eq!(
664            plain(&styles().tree().compose(&Tree::new().root("root"))),
665            "root"
666        );
667    }
668
669    #[test]
670    fn renders_flat_and_nested_default_branches() {
671        let tree = Tree::new()
672            .child("alpha")
673            .child(TreeNode::new("beta").child("nested"))
674            .child("omega");
675
676        assert_eq!(
677            plain(&styles().tree().compose(&tree)),
678            "├── alpha\n├── beta\n│   └── nested\n└── omega"
679        );
680    }
681
682    #[test]
683    fn keeps_descendants_connected_when_the_last_node_is_a_subtree() {
684        let tree = Tree::new()
685            .child("first")
686            .child(TreeNode::new("last").children(["one", "two"]));
687
688        assert_eq!(
689            plain(&styles().tree().compose(&tree)),
690            "├── first\n└── last\n    ├── one\n    └── two"
691        );
692    }
693
694    #[test]
695    fn excludes_hidden_nodes_before_assigning_branch_markers() {
696        let tree = Tree::new()
697            .child("visible")
698            .child(TreeNode::new("hidden").hidden(true));
699
700        assert_eq!(plain(&styles().tree().compose(&tree)), "└── visible");
701        assert!(measure(&styles().tree().compose(&tree.hidden(true))).is_empty());
702    }
703
704    #[test]
705    fn offsets_children_from_both_ends() {
706        let tree = Tree::new()
707            .children(["skip", "one", "two", "drop"])
708            .child_offset(1, 1);
709        let nested = Tree::<&str>::new().child(
710            TreeNode::new("parent")
711                .children(["skip", "kept", "drop"])
712                .child_offset(1, 1),
713        );
714
715        assert_eq!(plain(&styles().tree().compose(&tree)), "├── one\n└── two");
716        assert_eq!(
717            plain(&styles().tree().compose(&nested)),
718            "└── parent\n    └── kept"
719        );
720    }
721
722    #[test]
723    fn aligns_multiline_values_at_the_node_body() {
724        let tree = Tree::<&str>::new().child(
725            TreeNode::new("親")
726                .child("日本語\nsecond")
727                .child("終端\n続き"),
728        );
729
730        assert_eq!(
731            plain(&styles().tree().compose(&tree)),
732            "└── 親\n    ├── 日本語\n    │   second\n    └── 終端\n        続き"
733        );
734        let rows = plain_rows(&styles().tree().compose(&tree));
735        assert!(
736            rows[2].starts_with("    │   "),
737            "a continuation aligns under its node body: {:?}",
738            rows[2]
739        );
740    }
741
742    fn plain_at(view: &View, width: usize) -> String {
743        crate::resolve(view, crate::Available::columns(width))
744            .unwrap()
745            .rows()
746            .iter()
747            .map(|row| {
748                row.iter()
749                    .map(crate::StyledGrapheme::symbol)
750                    .collect::<String>()
751                    .trim_end()
752                    .to_owned()
753            })
754            .collect::<Vec<_>>()
755            .join("\n")
756    }
757
758    #[test]
759    fn selected_width_reflows_content_without_recomposing() {
760        let view = styles().tree().compose(&Tree::new().child("alpha beta"));
761
762        assert_eq!(plain_at(&view, 14), "└── alpha beta");
763        assert_eq!(plain_at(&view, 9), "└── alpha\n    beta");
764    }
765
766    #[test]
767    fn protects_deep_prefixes_and_wraps_cjk_and_emoji_content() {
768        let tree = Tree::<&str>::new().child(
769            TreeNode::new("親").child(TreeNode::new("branch").children(["日本語", "👩‍💻 end"])),
770        );
771        let view = styles().tree().compose(&tree);
772
773        assert_eq!(
774            plain_at(&view, 15),
775            "└── 親\n    └── branch\n        ├── 日\n        │   本\n        │   語\n        └── 👩‍💻\n            end"
776        );
777    }
778
779    #[test]
780    fn preserves_explicit_and_wrapped_continuations() {
781        let tree = Tree::new().children(["ab cd\n日\n", "last"]);
782        let view = styles().tree().compose(&tree);
783
784        assert_eq!(plain_at(&view, 8), "├── ab\n│   cd\n│   日\n│\n└── last");
785    }
786
787    #[test]
788    fn supports_rounded_ascii_and_custom_connector_glyphs() {
789        let tree = Tree::new().children(["one", "two"]);
790        let rounded = styles()
791            .tree()
792            .clone()
793            .line_glyphs(LineGlyphs::ROUNDED)
794            .compose(&tree);
795        let ascii = styles()
796            .tree()
797            .clone()
798            .line_glyphs(LineGlyphs::ASCII)
799            .compose(&tree);
800        let custom = styles()
801            .tree()
802            .clone()
803            .line_glyphs(LineGlyphs {
804                tee_right: 'T',
805                corner_up_right: 'L',
806                horizontal: '=',
807                end_left: '=',
808                ..LineGlyphs::ASCII
809            })
810            .compose(&tree);
811
812        assert_eq!(plain(&rounded), "├── one\n╰── two");
813        assert_eq!(plain(&ascii), "+-- one\n+-- two");
814        assert_eq!(plain(&custom), "T== one\nL== two");
815    }
816
817    #[test]
818    fn applies_root_item_and_connector_styles() {
819        let root = TextStyle::new().foreground(Color::RED);
820        let item = TextStyle::new().foreground(Color::GREEN);
821        let connector = TextStyle::new().foreground(Color::BLUE);
822        let tree = Tree::new()
823            .root("root")
824            .child(TreeNode::new("parent").children(["one", "two"]))
825            .child("last");
826        let view = styles()
827            .tree()
828            .clone()
829            .root_style(root.clone())
830            .item_style(item.clone())
831            .connector_style(connector.clone())
832            .compose(&tree);
833
834        assert_eq!(style_at(&view, 0, 0), root);
835        assert_eq!(style_at(&view, 1, 0), connector);
836        assert_eq!(style_at(&view, 1, 4), item);
837        assert_eq!(style_at(&view, 2, 0), connector);
838        assert_eq!(style_at(&view, 2, 4), connector);
839        assert_eq!(style_at(&view, 2, 8), item);
840    }
841
842    #[test]
843    fn presentation_and_composed_view_equality_include_policy() {
844        let base = styles().tree().clone();
845        let tree = Tree::new().child("item");
846
847        assert_eq!(base, base.clone());
848        assert_eq!(base.compose(&tree), base.clone().compose(&tree));
849        assert_ne!(base, base.clone().line_glyphs(LineGlyphs::ASCII));
850        assert_ne!(base, base.clone().indent_width(3));
851        assert_ne!(base, base.clone().connector_style(TextStyle::new().bold()));
852    }
853
854    #[test]
855    fn width_requirements_include_content_prefix_and_widest_grapheme() {
856        let tree = Tree::new()
857            .root("root")
858            .child(TreeNode::new("a").child(TreeNode::new("b").child("日本語")));
859        let children = visible_children(&tree.children, tree.child_offset);
860        let mut nodes = Vec::new();
861        let mut groups = Vec::new();
862        let presentation =
863            TreePresentation::new(TextStyle::new(), TextStyle::new(), TextStyle::new());
864        bind_group(
865            &mut nodes,
866            &mut groups,
867            &children,
868            0,
869            &presentation,
870            &display_node::<&str>,
871            &no_node_style::<&str>,
872        );
873        let requirements = TreeFrame {
874            root: tree.root.map(|value| BoundTreeRoot {
875                value: value.to_owned(),
876                style: TextStyle::new(),
877            }),
878            nodes,
879            groups,
880            connector_style: TextStyle::new(),
881            line_glyphs: LineGlyphs::NORMAL,
882            indent_width: 4,
883        }
884        .width_requirements();
885
886        assert_eq!(requirements.demand(), 18);
887        assert_eq!(requirements.floor(), 14);
888    }
889
890    #[test]
891    #[should_panic(expected = "tree indentation must be at least three cells")]
892    fn rejects_indentation_without_junction_continuation_and_gap_cells() {
893        let _ = styles().tree().clone().indent_width(2);
894    }
895
896    #[test]
897    fn tree_nodes_remain_independent_semantic_values() {
898        let node = TreeNode::new("parent").children(["one", "two"]);
899        let tree = Tree::new().root("root").child(node.clone());
900
901        assert_eq!(*node.value(), "parent");
902        assert_eq!(node.child_nodes().len(), 2);
903        assert_eq!(tree.root_value().copied(), Some("root"));
904        assert_eq!(tree.child_nodes(), &[node]);
905    }
906}