lipgloss_tree/renderer.rs
1//! Tree rendering engine for lipgloss-tree.
2//!
3//! This module provides the core rendering functionality for tree structures,
4//! handling complex styling, alignment, and multiline content rendering.
5//! It supports both built-in tree glyphs (├──, └──, etc.) and custom
6//! enumerators with proper alignment and styling inheritance.
7
8use crate::children::Children;
9use crate::{default_enumerator, default_indenter, Enumerator, Indenter, Node, StyleFunc};
10use lipgloss::{height, join_horizontal, join_vertical, Style, LEFT, TOP};
11use unicode_width::UnicodeWidthStr;
12
13/// Minimal Children implementation used to synthesize enumerator glyphs with controlled length/index.
14///
15/// This is used internally by the renderer to generate the correct branch characters
16/// (├── vs └──) based on whether a node is the last in its sequence.
17struct DummyChildren {
18 /// The virtual length of this children collection
19 len: usize,
20}
21impl Children for DummyChildren {
22 fn at(&self, _index: usize) -> Option<&dyn Node> {
23 None
24 }
25 fn length(&self) -> usize {
26 self.len
27 }
28}
29
30/// A filtered view of children that exposes only visible, non-empty nodes via an index map.
31///
32/// This wrapper allows the renderer to work with only the nodes that will actually
33/// produce visible output, while maintaining the correct indices for style functions.
34struct VisibleChildren<'a> {
35 /// Reference to the underlying children collection
36 base: &'a dyn Children,
37 /// Mapping from visible index to actual index in the base collection
38 map: Vec<usize>,
39}
40impl<'a> Children for VisibleChildren<'a> {
41 fn at(&self, index: usize) -> Option<&dyn Node> {
42 self.map.get(index).and_then(|&i| self.base.at(i))
43 }
44 fn length(&self) -> usize {
45 self.map.len()
46 }
47}
48
49/// Styling configuration for tree rendering.
50///
51/// `TreeStyle` controls how different parts of the tree are styled, including
52/// enumerators (branch characters like ├──), items (node content), and the root.
53/// It supports both base styles (applied unconditionally) and function styles
54/// (applied per-child based on index and context).
55///
56/// # Examples
57///
58/// ```rust
59/// use lipgloss::Style;
60/// use lipgloss_tree::renderer::TreeStyle;
61///
62/// let style = TreeStyle {
63/// enumerator_func: |_, _| Style::new().foreground("blue"),
64/// item_func: |_, _| Style::new().bold(true),
65/// enumerator_base: Some(Style::new().padding_right(1)),
66/// item_base: None,
67/// root: Style::new().bold(true),
68/// };
69/// ```
70#[derive(Debug, Clone)]
71pub struct TreeStyle {
72 /// Function style applied to enumerators (branch characters) based on child index
73 pub enumerator_func: StyleFunc,
74 /// Function style applied to item content based on child index
75 pub item_func: StyleFunc,
76 /// Base style applied unconditionally to all enumerators before function styles
77 pub enumerator_base: Option<Style>,
78 /// Base style applied unconditionally to all items before function styles
79 pub item_base: Option<Style>,
80 /// Style applied to the root node
81 pub root: Style,
82}
83
84impl Default for TreeStyle {
85 /// Creates a default `TreeStyle` with Go lipgloss-compatible behavior.
86 ///
87 /// The default includes `padding_right(1)` on enumerators to match Go's
88 /// behavior of printing a space after tree branch prefixes. Tests that
89 /// need different behavior can override with no-op functions.
90 fn default() -> Self {
91 Self {
92 // By default, Go prints a space after the prefix. Model this
93 // with padding_right(1). Tests that set nil funcs expect no space
94 // and will override with a no-op function.
95 enumerator_func: |_children, _i| Style::new().padding_right(1),
96 item_func: |_children, _i| Style::new(),
97 enumerator_base: None,
98 item_base: None,
99 root: Style::new(),
100 }
101 }
102}
103
104/// The main tree rendering engine.
105///
106/// `Renderer` is responsible for converting tree node structures into styled
107/// text output. It handles complex features like:
108/// - Multi-line content with proper indentation
109/// - Custom enumerators and indenters
110/// - Style inheritance and override resolution
111/// - Alignment of custom enumerators
112/// - Branch continuation across container nodes
113///
114/// # Examples
115///
116/// ```rust
117/// use lipgloss_tree::{Renderer, Children};
118/// use lipgloss_tree::renderer::TreeStyle;
119/// use lipgloss::Style;
120///
121/// let renderer = Renderer::new()
122/// .style(TreeStyle::default())
123/// .enumerator(|children, i| {
124/// if i == children.length() - 1 { "└──".to_string() }
125/// else { "├──".to_string() }
126/// });
127/// ```
128#[derive(Clone)]
129pub struct Renderer {
130 /// Styling configuration for this renderer
131 style: TreeStyle,
132 /// Function to generate enumerator strings (branch characters)
133 enumerator: Enumerator,
134 /// Function to generate indentation strings for nested content
135 indenter: Indenter,
136}
137
138impl Renderer {
139 /// Creates a new renderer with default configuration.
140 ///
141 /// The default renderer uses:
142 /// - Standard box-drawing characters for branches (├──, └──)
143 /// - Standard indentation (│ for continuing, spaces for last)
144 /// - Default styling with padding after enumerators
145 ///
146 /// # Examples
147 ///
148 /// ```rust
149 /// use lipgloss_tree::Renderer;
150 ///
151 /// let renderer = Renderer::new();
152 /// ```
153 pub fn new() -> Self {
154 Self {
155 style: TreeStyle::default(),
156 enumerator: default_enumerator,
157 indenter: default_indenter,
158 }
159 }
160
161 /// Sets the styling configuration for this renderer.
162 ///
163 /// This replaces the entire `TreeStyle`, affecting how enumerators,
164 /// items, and the root are styled.
165 ///
166 /// # Arguments
167 ///
168 /// * `style` - The new `TreeStyle` configuration to use
169 ///
170 /// # Examples
171 ///
172 /// ```rust
173 /// use lipgloss_tree::Renderer;
174 /// use lipgloss_tree::renderer::TreeStyle;
175 /// use lipgloss::Style;
176 ///
177 /// let custom_style = TreeStyle {
178 /// root: Style::new().bold(true),
179 /// ..TreeStyle::default()
180 /// };
181 ///
182 /// let renderer = Renderer::new().style(custom_style);
183 /// ```
184 pub fn style(mut self, style: TreeStyle) -> Self {
185 self.style = style;
186 self
187 }
188
189 /// Sets the enumerator function for this renderer.
190 ///
191 /// The enumerator function generates the branch characters (like ├──, └──)
192 /// based on the child's position in its parent's children collection.
193 ///
194 /// # Arguments
195 ///
196 /// * `enumerator` - Function that takes `(children, index)` and returns a string
197 ///
198 /// # Examples
199 ///
200 /// ```rust
201 /// use lipgloss_tree::{Renderer, Children};
202 ///
203 /// let renderer = Renderer::new()
204 /// .enumerator(|children, i| {
205 /// if i == children.length() - 1 {
206 /// "╰──".to_string() // Rounded last branch
207 /// } else {
208 /// "├──".to_string() // Standard continuing branch
209 /// }
210 /// });
211 /// ```
212 pub fn enumerator(mut self, enumerator: Enumerator) -> Self {
213 self.enumerator = enumerator;
214 self
215 }
216
217 /// Sets the indenter function for this renderer.
218 ///
219 /// The indenter function generates indentation strings for child content
220 /// that appears below parent nodes, providing the visual connection
221 /// between parent and nested children.
222 ///
223 /// # Arguments
224 ///
225 /// * `indenter` - Function that takes `(children, index)` and returns indentation string
226 ///
227 /// # Examples
228 ///
229 /// ```rust
230 /// use lipgloss_tree::{Renderer, Children};
231 ///
232 /// let renderer = Renderer::new()
233 /// .indenter(|children, i| {
234 /// if i == children.length() - 1 {
235 /// " ".to_string() // Spaces for last child
236 /// } else {
237 /// "│ ".to_string() // Vertical line for continuing
238 /// }
239 /// });
240 /// ```
241 pub fn indenter(mut self, indenter: Indenter) -> Self {
242 self.indenter = indenter;
243 self
244 }
245
246 /// Renders a tree node and its children to a formatted string.
247 ///
248 /// This is the main rendering method that converts a tree structure into
249 /// styled text output. It handles complex scenarios like multi-line content,
250 /// style inheritance, custom enumerators, and proper indentation.
251 ///
252 /// # Arguments
253 ///
254 /// * `node` - The tree node to render
255 /// * `root` - Whether this is the root node (affects root style application)
256 /// * `prefix` - String prefix to prepend to each line (for nested rendering)
257 ///
258 /// # Returns
259 ///
260 /// A formatted string representation of the tree with proper styling and indentation
261 ///
262 /// # Examples
263 ///
264 /// ```rust
265 /// use lipgloss_tree::{Renderer, Tree};
266 ///
267 /// let tree = Tree::new()
268 /// .root("My Tree")
269 /// .child(vec!["Item 1".into(), "Item 2".into()]);
270 ///
271 /// let renderer = Renderer::new();
272 /// let output = renderer.render(&tree, true, "");
273 /// println!("{}", output);
274 /// ```
275 pub fn render(&self, node: &dyn Node, root: bool, prefix: &str) -> String {
276 if node.hidden() {
277 return String::new();
278 }
279
280 // Debug: uncomment for debugging
281 // eprintln!("RENDER_START: root={}, prefix='{}', prefix_len={}", root, prefix.replace('\n', "\\n"), prefix.len());
282
283 let mut strs = Vec::new();
284 let children = node.children();
285 // Prefer per-node overrides for enumerator/indenter when present, otherwise use renderer config
286 let enumerator = node.get_enumerator().copied().unwrap_or(self.enumerator);
287 let indenter = node.get_indenter().copied().unwrap_or(self.indenter);
288
289 // Print the root node name if it's not empty
290 if !node.value().is_empty() && root {
291 strs.push(self.style.root.render(&node.value()));
292 }
293
294 // Build a filtered view of direct children that will render a line (non-hidden, non-empty)
295 let mut visible_nodes: Vec<Box<dyn Node>> = Vec::new();
296 for i in 0..children.length() {
297 if let Some(child) = children.at(i) {
298 if child.hidden() || child.value().is_empty() {
299 continue;
300 }
301 visible_nodes.push(child.clone_node());
302 }
303 }
304 let filtered_children = crate::children::NodeChildren::from_nodes(visible_nodes);
305
306 // Helper: does this node or any of its descendants render a visible line?
307 fn has_visible_line(node: &dyn Node) -> bool {
308 if node.hidden() {
309 return false;
310 }
311 if !node.value().is_empty() {
312 return true;
313 }
314 let ch = node.children();
315 for i in 0..ch.length() {
316 if let Some(n) = ch.at(i) {
317 if has_visible_line(n) {
318 return true;
319 }
320 }
321 }
322 false
323 }
324
325 // Precompute which visible direct child is last in the overall visual sequence.
326 // A visible child is last if there is no later sibling (direct) whose subtree would
327 // render any visible line.
328 let mut is_last_vec: Vec<bool> = vec![true; filtered_children.length()];
329 #[allow(clippy::needless_range_loop)]
330 for vi in 0..filtered_children.length() {
331 let mut last = true;
332 // locate this visible child among all direct children
333 let mut seen = 0usize;
334 for i in 0..children.length() {
335 if let Some(ch) = children.at(i) {
336 if ch.hidden() {
337 continue;
338 }
339 if !ch.value().is_empty() {
340 if seen == vi {
341 // check later siblings
342 for j in (i + 1)..children.length() {
343 if let Some(next) = children.at(j) {
344 if has_visible_line(next) {
345 last = false;
346 break;
347 }
348 }
349 }
350 break;
351 }
352 seen += 1;
353 }
354 }
355 }
356 is_last_vec[vi] = last;
357 }
358
359 // Prepare a visible-children view for style functions
360 let mut vis_map: Vec<usize> = Vec::new();
361 for i in 0..children.length() {
362 if let Some(ch) = children.at(i) {
363 if !ch.hidden() && !ch.value().is_empty() {
364 vis_map.push(i);
365 }
366 }
367 }
368 let vis_children = VisibleChildren {
369 base: &*children,
370 map: vis_map,
371 };
372
373 // Helper to detect built-in branch glyphs
374 let is_branch = |s: &str| s == "├──" || s == "└──" || s == "╰──";
375
376 // Calculate alignment padding for custom enumerators (not built-in branch glyphs)
377 let mut max_enum_width = 0;
378 for i in 0..filtered_children.length() {
379 let user_pref = enumerator(&vis_children, i);
380 if !is_branch(&user_pref) {
381 // Only consider custom enumerators for alignment
382 let width = user_pref.width();
383 if width > max_enum_width {
384 max_enum_width = width;
385 }
386 }
387 }
388
389 // Render children
390 let mut last_display_indent = String::new();
391 for i in 0..children.length() {
392 if let Some(child) = children.at(i) {
393 if child.hidden() {
394 continue;
395 }
396
397 // Determine display index for visible children by counting prior visible siblings
398 let mut display_idx_opt: Option<usize> = None;
399 if !child.value().is_empty() {
400 let mut count = 0usize;
401 for j in 0..i {
402 if let Some(prev) = children.at(j) {
403 if !prev.hidden() && !prev.value().is_empty() {
404 count += 1;
405 }
406 }
407 }
408 display_idx_opt = Some(count);
409 }
410 let idx = display_idx_opt.unwrap_or(0);
411 // Build effective styles for this node, respecting node overrides first
412 let enum_style_func = node
413 .get_enumerator_style_func()
414 .copied()
415 .unwrap_or(self.style.enumerator_func);
416 let item_style_func = node
417 .get_item_style_func()
418 .copied()
419 .unwrap_or(self.style.item_func);
420
421 let enum_base = node
422 .get_enumerator_style()
423 .cloned()
424 .or_else(|| self.style.enumerator_base.clone());
425 let item_base = node
426 .get_item_style()
427 .cloned()
428 .or_else(|| self.style.item_base.clone());
429
430 // Compute indent: for visible children use indenter(filtered, display_idx);
431 // for container (empty value) nodes, reuse the last visible indent so nested
432 // content attaches under the previous item.
433 let raw_indent = if let Some(di) = display_idx_opt {
434 indenter(&filtered_children, di)
435 } else {
436 last_display_indent.clone()
437 };
438 // Apply styling to indent based on the type of indenter
439 let indent = if raw_indent.trim().is_empty() {
440 // Standard whitespace indenter - apply item_base style if present (for padding)
441 if let Some(base) = &item_base {
442 base.render(&raw_indent)
443 } else {
444 raw_indent.clone()
445 }
446 } else {
447 // Custom indenter (like "->") - apply enum_base style if present (for colors/styling)
448 if let Some(base) = &enum_base {
449 base.render(&raw_indent)
450 } else {
451 raw_indent.clone()
452 }
453 };
454
455 // Compute enumerator only for visible children
456 // Base branch according to position in overall visual sequence
457 let user_pref = enumerator(&vis_children, idx);
458 let is_custom_enum = !is_branch(&user_pref);
459 let mut node_prefix = if !is_custom_enum {
460 let dc = DummyChildren { len: 2 };
461 if is_last_vec[idx] {
462 enumerator(&dc, 1)
463 } else {
464 enumerator(&dc, 0)
465 }
466 } else {
467 user_pref.clone()
468 };
469
470 // Apply alignment padding for custom enumerators
471 if !is_custom_enum {
472 // Built-in branch glyph - no alignment needed
473 } else if max_enum_width > 0 {
474 // Custom enumerator - apply alignment padding
475 let current_width = node_prefix.width();
476 let padding_needed = max_enum_width.saturating_sub(current_width);
477 if padding_needed > 0 {
478 node_prefix = format!("{}{}", " ".repeat(padding_needed), node_prefix);
479 }
480 }
481
482 // CRITICAL: Apply either base style OR function style, NEVER both
483 // This prevents double-padding issues where both base and function styles add spacing
484 //
485 // PADDING_RIGHT BEHAVIOR:
486 // - TreeStyle::default() sets enumerator_func to add padding_right(1)
487 // - If enumerator_style (base) is set, it REPLACES the function entirely
488 // - The base style can include its own padding_right(1)
489 // - Go behavior: EnumeratorStyle() method replaces the default function
490 //
491 // SPACING CALCULATION:
492 // - Tree symbols: "├──", "└──" are 3 chars wide
493 // - padding_right(1) adds 1 space → "├── " (4 chars total)
494 // - This aligns with default_indenter: "│ " (4 chars) and " " (4 spaces)
495 if let Some(base) = &enum_base {
496 // Base style set via .enumerator_style() - use ONLY this style
497 // Example: Style::new().foreground(color).padding_right(1)
498 node_prefix = base.render(&node_prefix);
499 } else {
500 // No base style - use the function style (default or custom)
501 let enum_style_result = enum_style_func(&vis_children, idx);
502 let enum_lead = enum_style_result.render("");
503
504 // Check if this is a set_string style vs padding-only style
505 if !enum_lead.is_empty() && !enum_lead.trim().is_empty() {
506 // Set_string style with actual content (e.g., "+" prefix)
507 // Apply default padding to tree structure, then prepend the content
508 let default_styled = Style::new().padding_right(1).render(&node_prefix);
509 if !enum_lead.ends_with(' ') {
510 node_prefix = format!("{} {}", enum_lead, default_styled);
511 } else {
512 node_prefix = format!("{}{}", enum_lead, default_styled);
513 }
514 } else {
515 // Padding-only style or no additional content
516 // Apply style function directly to tree structure
517 node_prefix = enum_style_result.render(&node_prefix);
518 }
519 }
520 // Note: Alignment padding disabled - Go doesn't align prefixes to same width
521 // Debug: uncomment for debugging
522 // eprintln!("RENDER: idx={}, prefix='{}', width={}, max_len={}, final='{}'", idx, node_prefix.replace('\n', "\\n"), prefix_width, max_len, node_prefix.replace('\n', "\\n"));
523 // Apply item styling: base style OR function style, not both
524 let mut item = child.value();
525 if let Some(base) = &item_base {
526 item = base.render(&item);
527 } else {
528 // Only apply function style if no base style is set
529 let item_style_result = item_style_func(&vis_children, idx);
530 let item_lead = item_style_result.render("");
531
532 // Check if this is a true set_string style vs padding-only style
533 // Padding-only styles render to whitespace-only strings (spaces, tabs, newlines)
534 // Set_string styles have non-whitespace content
535 let is_padding_only = item_lead.chars().all(|c| c.is_whitespace());
536
537 if is_padding_only {
538 // Apply the style function to the item directly (padding-only or no style)
539 item = item_style_result.render(&item);
540 } else {
541 // This is a style with a string set via set_string (like Go's SetString)
542 if !item_lead.ends_with(' ') {
543 item = format!("{} {}", item_lead, item);
544 } else {
545 item = format!("{}{}", item_lead, item);
546 }
547 }
548 }
549 let mut multiline_prefix = prefix.to_string();
550
551 // Handle multiline prefixes and items
552 let item_height = height(&item);
553 let mut node_prefix_height = height(&node_prefix);
554
555 // Extend node prefix if item is taller
556 while item_height > node_prefix_height {
557 // Use raw indent for multiline extension - no styling needed
558 let extension_indent = indent.clone();
559 node_prefix = join_vertical(LEFT, &[&node_prefix, &extension_indent]);
560 node_prefix_height = height(&node_prefix);
561 }
562
563 // Extend multiline prefix if node prefix is taller
564 let mut multiline_prefix_height = height(&multiline_prefix);
565 while node_prefix_height > multiline_prefix_height {
566 multiline_prefix = join_vertical(LEFT, &[&multiline_prefix, prefix]);
567 multiline_prefix_height = height(&multiline_prefix);
568 }
569
570 // Only emit a line if the child has a non-empty value; empty-value nodes are containers for sublists
571 if !child.value().is_empty() {
572 // FINAL LINE ASSEMBLY:
573 // multiline_prefix: parent indentation (e.g., 10 spaces from nested list level)
574 // node_prefix: tree symbol with styling (e.g., "[color]├──[reset] " with padding_right)
575 // item: content with any item styling applied
576 //
577 // BACKGROUND COLOR CONSISTENCY:
578 // When tree components have background colors, we need to ensure the multiline_prefix
579 // also gets the same background color to avoid black rectangular gaps.
580
581 let styled_multiline_prefix = if !multiline_prefix.trim().is_empty() {
582 // If there is indentation content, check if we should apply background styling
583 // Priority: enum_base > item_base (prefer the enumerator background)
584 if let Some(base) = &enum_base {
585 // If enumerator has background, apply it to prefix indentation
586 if base.get_background().is_some() {
587 base.render(&multiline_prefix)
588 } else {
589 multiline_prefix.clone()
590 }
591 } else if let Some(base) = &item_base {
592 // If item has background, apply it to prefix indentation
593 if base.get_background().is_some() {
594 base.render(&multiline_prefix)
595 } else {
596 multiline_prefix.clone()
597 }
598 } else {
599 multiline_prefix.clone()
600 }
601 } else {
602 multiline_prefix.clone()
603 };
604
605 // DEBUG: Uncomment to debug spacing issues
606 // eprintln!("DEBUG: multiline_prefix='{}', node_prefix='{}', item='{}'",
607 // styled_multiline_prefix.replace(' ', "·"),
608 // node_prefix.replace(' ', "·"),
609 // item.replace(' ', "·"));
610
611 let line =
612 join_horizontal(TOP, &[&styled_multiline_prefix, &node_prefix, &item]);
613 strs.push(line);
614 // Remember raw indent for subsequent container nodes (before styling)
615 last_display_indent = raw_indent.clone();
616 }
617
618 // Recursively render children
619 if child.children().length() > 0 {
620 // Even if the child has an empty value (container), we still need to
621 // indent its children so they appear nested under the current item.
622 // Use styled indent with enum styling applied to indenter characters
623 // This ensures custom indenters (like "->") get the same styling as enumerators
624 let styled_indent = indent.clone();
625
626 // SMART INDENTATION LOGIC:
627 // Trees nested in lists should not inherit the list's indenter to avoid double indentation.
628 let dummy_children = crate::children::NodeChildren::new();
629 let parent_indent_sample = indenter(&dummy_children, 0);
630 #[allow(unused_variables)]
631 let is_parent_list_indenter =
632 parent_indent_sample.trim() == "" && parent_indent_sample.len() == 2;
633
634 let child_prefix = if let Some(child_indenter) = child.get_indenter() {
635 let child_indent_sample = child_indenter(&dummy_children, 0);
636 #[allow(unused_variables)]
637 let is_child_tree_indenter =
638 child_indent_sample.contains('│') || child_indent_sample.len() == 4;
639
640 // DEBUG: uncomment for debugging tree indentation
641 // if child.get_enumerator_style().is_some() {
642 // eprintln!("DEBUG TREE: parent_indent='{}', child_indent='{}', is_parent_list={}, is_child_tree={}, prefix_len={}",
643 // parent_indent_sample.replace(' ', "·"), child_indent_sample.replace(' ', "·"), is_parent_list_indenter, is_child_tree_indenter, prefix.len());
644 // }
645
646 // Always provide proper nesting prefix - the issue is tree's internal indentation, not prefix
647 format!("{}{}", prefix, styled_indent)
648 } else {
649 // Child has no specific indenter - inherit parent's indentation
650 format!("{}{}", prefix, styled_indent)
651 };
652
653 // Detect if child has style overrides (indicating it's a styled tree vs plain container)
654 let has_style_overrides = child.get_enumerator_style().is_some()
655 || child.get_item_style().is_some()
656 || child.get_enumerator_style_func().is_some()
657 || child.get_item_style_func().is_some();
658
659 // Special case: if this child is a tree with its own indenter, use fresh renderer
660 // to prevent list indenter from affecting tree's internal rendering
661 let child_uses_tree_indenter =
662 if let Some(child_indenter) = child.get_indenter() {
663 let dummy = crate::children::NodeChildren::new();
664 let sample = child_indenter(&dummy, 0);
665 sample.contains('│') || sample.len() == 4
666 } else {
667 false
668 };
669
670 let mut child_renderer = if child_uses_tree_indenter {
671 // Tree child: use fresh renderer to avoid inheriting list behavior
672 Renderer::new()
673 } else if has_style_overrides {
674 // Child has style overrides: use tree defaults for behavior but child styles
675 Renderer::new()
676 } else {
677 // Child has no overrides: inherit parent's behavior
678 Renderer::new()
679 .enumerator(self.enumerator)
680 .indenter(self.indenter)
681 };
682
683 // Apply any explicit functional overrides from the child node itself
684 if let Some(e) = child.get_enumerator() {
685 child_renderer = child_renderer.enumerator(*e);
686 }
687 if let Some(i) = child.get_indenter() {
688 child_renderer = child_renderer.indenter(*i);
689 }
690
691 // For style functions, use tree defaults unless child has specific overrides
692 // Children should inherit parent styles when they don't have their own
693 let style = TreeStyle {
694 enumerator_func: child
695 .get_enumerator_style_func()
696 .copied()
697 .unwrap_or(|_, _| Style::new().padding_right(1)),
698 item_func: child
699 .get_item_style_func()
700 .copied()
701 .unwrap_or(|_, _| Style::new()),
702 root: Style::default(),
703 // Inherit parent's base styles if child doesn't have overrides
704 enumerator_base: child
705 .get_enumerator_style()
706 .cloned()
707 .or_else(|| self.style.enumerator_base.clone()),
708 item_base: child
709 .get_item_style()
710 .cloned()
711 .or_else(|| self.style.item_base.clone()),
712 };
713 child_renderer = child_renderer.style(style);
714
715 let mut child_output = child_renderer.render(child, false, &child_prefix);
716 // If this child is an unnamed container and there are later siblings that
717 // will render visible lines, ensure the container's last visible branch uses
718 // the mid-branch glyph to visually continue the vertical line across
719 // containers (e.g., the 5th Qux before subsequent Quux entries).
720 if child.value().is_empty() {
721 let mut future_exists = false;
722 for j in (i + 1)..children.length() {
723 if let Some(next) = children.at(j) {
724 // Only consider later unnamed containers, which will visually
725 // continue under the same prior item indent.
726 if next.value().is_empty() && has_visible_line(next) {
727 future_exists = true;
728 break;
729 }
730 }
731 }
732 if future_exists {
733 // Replace the deepest last-branch at this level with a mid-branch.
734 let dc = DummyChildren { len: 2 };
735 let last_branch = enumerator(&dc, 1);
736 let mid_branch = enumerator(&dc, 0);
737 let look_for = format!("{}{}", child_prefix, last_branch);
738 if let Some(pos) = child_output.rfind(&look_for) {
739 // Ensure we are at line start; find preceding newline or start
740 let line_start =
741 child_output[..pos].rfind('\n').map(|p| p + 1).unwrap_or(0);
742 if line_start == pos {
743 child_output.replace_range(
744 pos..pos + look_for.len(),
745 &format!("{}{}", child_prefix, mid_branch),
746 );
747 }
748 }
749 }
750 }
751 if !child_output.is_empty() {
752 strs.push(child_output);
753 }
754 }
755 }
756 }
757
758 strs.join("\n")
759 }
760}
761
762impl Default for Renderer {
763 fn default() -> Self {
764 Self::new()
765 }
766}
767
768/// Creates a new renderer with default configuration.
769///
770/// This is a convenience function equivalent to `Renderer::new()`.
771/// Provided for API compatibility with other lipgloss libraries.
772///
773/// # Returns
774///
775/// A new `Renderer` instance with default settings
776///
777/// # Examples
778///
779/// ```rust
780/// use lipgloss_tree::renderer::new_renderer;
781///
782/// let renderer = new_renderer();
783/// ```
784pub fn new_renderer() -> Renderer {
785 Renderer::new()
786}