Skip to main content

rusty_rich/
align.rs

1//! Text alignment — equivalent to Rich's `align.py`.
2//!
3//! Provides horizontal and vertical alignment for renderables.
4
5use std::fmt;
6
7// ---------------------------------------------------------------------------
8// AlignMethod
9// ---------------------------------------------------------------------------
10
11/// Horizontal alignment method.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
13pub enum AlignMethod {
14    /// Left-align (the default).
15    #[default]
16    Left,
17    /// Center-align.
18    Center,
19    /// Right-align.
20    Right,
21    /// Full justification (spaces distributed between words).
22    Full,
23}
24
25impl AlignMethod {
26    /// Align text within the given width, returning a padded string.
27    pub fn align_text(&self, text: &str, width: usize) -> String {
28        let text_width = unicode_width::UnicodeWidthStr::width(text);
29        if text_width >= width {
30            return text.to_string();
31        }
32        let padding = width - text_width;
33        match self {
34            Self::Left => format!("{}{}", text, " ".repeat(padding)),
35            Self::Right => format!("{}{}", " ".repeat(padding), text),
36            Self::Center => {
37                let left = padding / 2;
38                let right = padding - left;
39                format!("{}{}{}", " ".repeat(left), text, " ".repeat(right))
40            }
41            Self::Full => {
42                // Full justification: distribute spaces between words
43                let words: Vec<&str> = text.split_whitespace().collect();
44                if words.len() <= 1 {
45                    return format!("{}{}", text, " ".repeat(padding));
46                }
47                let word_chars: usize = words.iter().map(|w| w.chars().count()).sum();
48                let total_gaps = width - word_chars;
49                let gap_count = words.len() - 1;
50                let gap_size = total_gaps / gap_count;
51                let extra = total_gaps % gap_count;
52                let mut out = String::new();
53                for (i, word) in words.iter().enumerate() {
54                    out.push_str(word);
55                    if i < gap_count {
56                        let spaces = gap_size + if i < extra { 1 } else { 0 };
57                        out.push_str(&" ".repeat(spaces));
58                    }
59                }
60                out
61            }
62        }
63    }
64
65    /// Parse an alignment method from its string name (`"left"`, `"center"`, `"right"`, or `"full"`).
66    #[allow(clippy::should_implement_trait)]
67    pub fn from_str(s: &str) -> Self {
68        match s {
69            "left" | "default" => Self::Left,
70            "center" => Self::Center,
71            "right" => Self::Right,
72            "full" => Self::Full,
73            _ => Self::Left,
74        }
75    }
76}
77
78impl fmt::Display for AlignMethod {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        match self {
81            Self::Left => write!(f, "left"),
82            Self::Center => write!(f, "center"),
83            Self::Right => write!(f, "right"),
84            Self::Full => write!(f, "full"),
85        }
86    }
87}
88
89// ---------------------------------------------------------------------------
90// VerticalAlignMethod
91// ---------------------------------------------------------------------------
92
93/// Vertical alignment method.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
95pub enum VerticalAlignMethod {
96    /// Align to the top edge.
97    #[default]
98    Top,
99    /// Align to the vertical center.
100    Middle,
101    /// Align to the bottom edge.
102    Bottom,
103}
104
105impl VerticalAlignMethod {
106    /// Parse a vertical alignment method from its string name (`"top"`, `"middle"`, or `"bottom"`).
107    #[allow(clippy::should_implement_trait)]
108    pub fn from_str(s: &str) -> Self {
109        match s {
110            "top" => Self::Top,
111            "middle" => Self::Middle,
112            "bottom" => Self::Bottom,
113            _ => Self::Top,
114        }
115    }
116}
117
118impl fmt::Display for VerticalAlignMethod {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            Self::Top => write!(f, "top"),
122            Self::Middle => write!(f, "middle"),
123            Self::Bottom => write!(f, "bottom"),
124        }
125    }
126}
127
128// ---------------------------------------------------------------------------
129// Align — Wraps a renderable with alignment
130// ---------------------------------------------------------------------------
131
132use crate::console::{ConsoleOptions, RenderResult};
133use crate::segment::Segment;
134
135/// Wraps a renderable to apply horizontal and/or vertical alignment.
136#[derive(Debug, Clone)]
137pub struct Align<T: crate::console::Renderable> {
138    pub renderable: T,
139    pub align: AlignMethod,
140    pub vertical: VerticalAlignMethod,
141    pub width: Option<usize>,
142    pub height: Option<usize>,
143}
144
145impl<T: crate::console::Renderable> Align<T> {
146    /// Wrap a renderable with default left/top alignment.
147    pub fn new(renderable: T) -> Self {
148        Self {
149            renderable,
150            align: AlignMethod::Left,
151            vertical: VerticalAlignMethod::Top,
152            width: None,
153            height: None,
154        }
155    }
156
157    /// Set the horizontal alignment.
158    pub fn align(mut self, align: AlignMethod) -> Self {
159        self.align = align;
160        self
161    }
162
163    pub fn vertical(mut self, vertical: VerticalAlignMethod) -> Self {
164        self.vertical = vertical;
165        self
166    }
167
168    pub fn center(renderable: T) -> Self {
169        Self::new(renderable).align(AlignMethod::Center)
170    }
171
172    pub fn middle(renderable: T) -> Self {
173        Self::new(renderable).vertical(VerticalAlignMethod::Middle)
174    }
175}
176
177impl<T: crate::console::Renderable + Clone> crate::console::Renderable for Align<T> {
178    fn render(&self, options: &ConsoleOptions) -> RenderResult {
179        let inner_result = self.renderable.render(options);
180        let width = self.width.unwrap_or(options.max_width);
181
182        let mut lines: Vec<Vec<Segment>> = Vec::new();
183
184        for line_segs in inner_result.lines {
185            // Measure the line width
186            let line_text: String = line_segs.iter().map(|s| s.text.as_str()).collect();
187            let line_width = unicode_width::UnicodeWidthStr::width(line_text.as_str());
188
189            if line_width >= width {
190                lines.push(line_segs);
191            } else {
192                let padding = width - line_width;
193                let (left_pad, _right_pad) = match self.align {
194                    AlignMethod::Left => (0, padding),
195                    AlignMethod::Right => (padding, 0),
196                    AlignMethod::Center => (padding / 2, padding - padding / 2),
197                    AlignMethod::Full => (0, padding),
198                };
199
200                let mut aligned = Vec::new();
201                if left_pad > 0 {
202                    aligned.push(Segment::new(" ".repeat(left_pad)));
203                }
204                aligned.extend(line_segs);
205                if padding - left_pad > 0 {
206                    aligned.push(Segment::new(" ".repeat(padding - left_pad)));
207                }
208                aligned.push(Segment::line());
209                lines.push(aligned);
210            }
211        }
212
213        // Vertical alignment
214        if let Some(h) = self.height {
215            if lines.len() < h {
216                let empty_lines = h - lines.len();
217                match self.vertical {
218                    VerticalAlignMethod::Bottom => {
219                        let mut top: Vec<Vec<Segment>> = (0..empty_lines)
220                            .map(|_| vec![Segment::new(" ".repeat(width)), Segment::line()])
221                            .collect();
222                        top.extend(lines);
223                        lines = top;
224                    }
225                    VerticalAlignMethod::Middle => {
226                        let top_h = empty_lines / 2;
227                        let bottom_h = empty_lines - top_h;
228                        let mut result: Vec<Vec<Segment>> = (0..top_h)
229                            .map(|_| vec![Segment::new(" ".repeat(width)), Segment::line()])
230                            .collect();
231                        result.extend(lines);
232                        result.extend(
233                            (0..bottom_h)
234                                .map(|_| vec![Segment::new(" ".repeat(width)), Segment::line()]),
235                        );
236                        lines = result;
237                    }
238                    VerticalAlignMethod::Top => {
239                        lines.extend(
240                            (0..empty_lines)
241                                .map(|_| vec![Segment::new(" ".repeat(width)), Segment::line()]),
242                        );
243                    }
244                }
245            }
246        }
247
248        RenderResult {
249            lines,
250            items: Vec::new(),
251        }
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_align_center() {
261        let result = AlignMethod::Center.align_text("Hi", 10);
262        assert_eq!(result.len(), 10);
263        assert!(result.starts_with("    "));
264    }
265}