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