1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum TreePosition {
25 Root,
27 Child {
29 index: usize,
31 len: usize,
33 depth: usize,
35 },
36}
37
38#[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 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 #[must_use]
60 pub fn child(mut self, child: impl Into<Self>) -> Self {
61 self.children.push(child.into());
62 self
63 }
64
65 #[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 #[must_use]
78 pub const fn hidden(mut self, hidden: bool) -> Self {
79 self.hidden = hidden;
80 self
81 }
82
83 #[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 pub const fn value(&self) -> &T {
92 &self.value
93 }
94
95 pub fn child_nodes(&self) -> &[Self] {
97 &self.children
98 }
99
100 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#[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 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 #[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 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#[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 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 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 #[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 #[must_use]
228 pub fn root_style(self, style: TextStyle) -> Self {
229 self.style(TreeRole::Root, style)
230 }
231
232 #[must_use]
234 pub fn item_style(self, style: TextStyle) -> Self {
235 self.style(TreeRole::Item, style)
236 }
237
238 #[must_use]
240 pub fn connector_style(self, style: TextStyle) -> Self {
241 self.style(TreeRole::Connector, style)
242 }
243
244 #[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 #[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 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 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#[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 pub fn new() -> Self {
575 Self::default()
576 }
577
578 #[must_use]
580 pub fn root(mut self, root: T) -> Self {
581 self.root = Some(root);
582 self
583 }
584
585 #[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 #[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 #[must_use]
605 pub const fn hidden(mut self, hidden: bool) -> Self {
606 self.hidden = hidden;
607 self
608 }
609
610 #[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 pub const fn root_value(&self) -> Option<&T> {
619 self.root.as_ref()
620 }
621
622 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}