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