Skip to main content

tree_display/
format.rs

1//! Tree rendering and formatting.
2//!
3//! This module handles the conversion of `Tree` structures into formatted
4//! string output. It manages indentation, line drawing, coloring, and
5//! alignment of tree nodes.
6
7use super::{color::Colored, context::Context, theme::Theme, Tree, TreeDisplay};
8use std::any::Any;
9
10// ──── API ───────────────────────────────────────────────────────────────────────────────────────
11
12/// Configurable formatter for tree display.
13///
14/// A formatter holds a value that implements `TreeDisplay` along with
15/// optional theme and context settings. It produces a formatted string
16/// representation of the tree.
17///
18/// ## Example
19/// ```no_run
20/// use tree_display::{Formatter, Theme};
21///
22/// let value = 0;
23/// let output = Formatter::of(&value)
24///     .theme(Theme::default())
25///     .format();
26/// ```
27pub struct Formatter<'value, 'context, T: TreeDisplay> {
28  value: &'value T,
29  theme: Option<Theme>,
30  context: Option<&'context Context>,
31}
32
33/// A value that can be displayed as content in a tree node.
34///
35/// This trait is implemented for all types that can appear as leaf
36/// content or labels in a tree. It provides a way to convert a value
37/// to a colored string representation.
38pub trait Content: Any {
39  fn to_string(&self, theme: &Theme) -> String;
40}
41
42// ──── Utility ───────────────────────────────────────────────────────────────────────────────────
43
44impl<'value, 'context, T: TreeDisplay> Formatter<'value, 'context, T> {
45  /// Creates a new formatter for the given value.
46  pub fn of(value: &'value T) -> Self {
47    Self {
48      value,
49      theme: None,
50      context: None,
51    }
52  }
53
54  /// Sets the theme for this formatter.
55  pub fn theme(&mut self, theme: Theme) -> &mut Self {
56    self.theme = Some(theme);
57    self
58  }
59
60  /// Sets the context for this formatter.
61  ///
62  /// The context provides custom mappers for transforming values during tree display.
63  pub fn context(&mut self, context: &'context Context) -> &mut Self {
64    self.context = Some(context);
65    self
66  }
67
68  /// Formats the tree and returns a string.
69  pub fn format(&self) -> String {
70    let mut result = String::new();
71    let theme = self.theme.unwrap_or(Theme::default());
72    let empty_context = Context::new();
73    let context = self.context.unwrap_or(&empty_context);
74    self.value.tree(&context).write_root(&mut result, &theme);
75    result
76  }
77}
78
79/// A type name for tree nodes.
80///
81/// Wraps a string that represents a type name, displayed with the type color from the theme.
82pub struct TypeName(pub String);
83
84/// A keyword for tree nodes.
85///
86/// Wraps a string that represents a keyword-like value, displayed with the keyword color from the theme.
87pub struct Keyword(pub String);
88
89/// A member name for tree nodes.
90///
91/// Wraps a string that represents a member/field name, displayed with the member color from the theme.
92pub struct Member(pub String);
93
94/// An index value for tree nodes.
95///
96/// Wraps a value that is displayed as an index, typically used for array or tuple indexing.
97pub struct Index(pub Box<dyn Content>);
98
99impl TypeName {
100  /// Creates a new type name from a string.
101  pub fn new(name: impl Into<String>) -> Self {
102    Self(name.into())
103  }
104}
105
106impl Member {
107  /// Creates a new member name from a string.
108  pub fn new(name: impl Into<String>) -> Self {
109    Self(name.into())
110  }
111}
112
113impl Keyword {
114  /// Creates a new keyword from a string.
115  pub fn new(name: impl Into<String>) -> Self {
116    Self(name.into())
117  }
118}
119
120impl Index {
121  /// Creates a new index from a content value.
122  pub fn new(value: impl Content) -> Self {
123    Self(Box::new(value))
124  }
125}
126
127// ──── Impl ──────────────────────────────────────────────────────────────────────────────────────
128
129impl Content for TypeName {
130  fn to_string(&self, theme: &Theme) -> String {
131    self.0.clone().fg(theme.colors.types)
132  }
133}
134
135impl Content for Keyword {
136  fn to_string(&self, theme: &Theme) -> String {
137    self.0.clone().fg(theme.colors.keywords)
138  }
139}
140
141impl Content for Member {
142  fn to_string(&self, theme: &Theme) -> String {
143    self.0.clone().fg(theme.colors.members)
144  }
145}
146
147impl Content for Index {
148  fn to_string(&self, theme: &Theme) -> String {
149    format!("[{}]", self.0.to_string(theme))
150  }
151}
152
153impl Content for Box<dyn Content> {
154  fn to_string(&self, theme: &Theme) -> String {
155    (**self).to_string(theme)
156  }
157}
158
159/// Blanket implementation of `Content` for any `Debug + Any` type.
160///
161/// This provides automatic content conversion for most Rust types:
162/// - Strings and `&str` are displayed with string coloring
163/// - All other types are displayed with value coloring
164impl<T: std::fmt::Debug + Any> Content for T {
165  fn to_string(&self, theme: &Theme) -> String {
166    if let Some(s) = (self as &dyn Any).downcast_ref::<String>() {
167      format!("\"{}\"", s).fg(theme.colors.strings)
168    } else if let Some(s) = (self as &dyn Any).downcast_ref::<&str>() {
169      format!("\"{}\"", s).fg(theme.colors.strings)
170    } else {
171      format!("{:?}", self).fg(theme.colors.values)
172    }
173  }
174}
175
176// ──── Rendering ─────────────────────────────────────────────────────────────────────────────────
177
178impl Theme {
179  /// Returns the connector string for a tree node.
180  ///
181  /// The connector is the line segment that connects a node to its parent:
182  fn connector(&self, is_leaf: bool, is_last: bool) -> String {
183    let vertical = if is_last {
184      self.lines.end
185    } else {
186      self.lines.junction
187    };
188    let horizontal = if is_leaf {
189      self.lines.horizontal
190    } else {
191      self.lines.tip
192    };
193    format!("{}{} ", vertical, horizontal).fg(self.colors.branches)
194  }
195
196  /// Returns the indentation string for the next level of the tree.
197  ///
198  /// This determines what prefix is added to child nodes when rendering
199  /// the tree. It creates the visual "spine" that connects siblings.
200  fn continuation(&self, is_last: bool) -> String {
201    if is_last {
202      "   ".to_string()
203    } else {
204      format!("{}  ", self.lines.vertical).fg(self.colors.vertical)
205    }
206  }
207}
208
209impl Tree {
210  /// Writes the root of the tree to the output string.
211  ///
212  /// This is the entry point for tree rendering. It writes the root
213  /// content and then recursively renders all children.
214  fn write_root(&self, out: &mut String, theme: &Theme) {
215    out.push_str(&self.content.to_string(theme));
216
217    let mut index = 0;
218    for child in &self.subtrees {
219      index += 1;
220      out.push('\n');
221      child.write(out, "", index == self.subtrees.len(), theme);
222    }
223  }
224
225  /// Recursively writes a tree node and its descendants.
226  ///
227  /// This handles indentation, connector drawing, labels, and proper alignment of child nodes.
228  fn write(&self, out: &mut String, prefix: &str, is_last: bool, theme: &Theme) {
229    out.push_str(prefix);
230    let connector = theme.connector(self.is_leaf(), is_last);
231    out.push_str(&connector);
232
233    let mut offset = 0;
234    if let Some(label) = &self.label {
235      let label_string = label.to_string(theme);
236      if theme.align_to_values {
237        offset = label_string.stripped().len() + 2;
238      }
239      out.push_str(&label_string);
240      out.push_str(": ");
241    }
242    out.push_str(&self.content.to_string(theme));
243
244    let padding = theme.continuation(is_last);
245    let next_prefix = format!("{}{}{}", prefix, padding, " ".repeat(offset),);
246
247    let mut index = 0;
248    for child in &self.subtrees {
249      index += 1;
250      out.push('\n');
251      child.write(out, &next_prefix, index == self.subtrees.len(), theme);
252    }
253  }
254}