Skip to main content

rich/
align.rs

1//! Horizontal alignment.
2//!
3//! Port of upstream `rich/align.py` (horizontal axis). [`Align`] pads a child
4//! renderable to fill the available width, positioning it left, center, or right.
5//!
6//! Slice scope: horizontal alignment. Vertical alignment (`VerticalAlign`) and
7//! explicit `width`/`pad` options are deferred with the rest of `align.py`.
8
9use crate::console::{Console, ConsoleOptions};
10use crate::protocol::Renderable;
11use crate::segment::Segment;
12use crate::style::Style;
13
14/// Where to position content within an available width. Shared by [`Align`],
15/// `Rule` titles, and `Panel` titles.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17pub enum HorizontalAlign {
18    Left,
19    #[default]
20    Center,
21    Right,
22}
23
24/// Aligns a child renderable within the available width. Mirrors `rich.align.Align`.
25pub struct Align {
26    child: Box<dyn Renderable>,
27    align: HorizontalAlign,
28}
29
30impl Align {
31    /// Left-align the child (pads on the right).
32    pub fn left(child: Box<dyn Renderable>) -> Self {
33        Align {
34            child,
35            align: HorizontalAlign::Left,
36        }
37    }
38
39    /// Center the child (pads both sides, extra cell on the right).
40    pub fn center(child: Box<dyn Renderable>) -> Self {
41        Align {
42            child,
43            align: HorizontalAlign::Center,
44        }
45    }
46
47    /// Right-align the child (pads on the left).
48    pub fn right(child: Box<dyn Renderable>) -> Self {
49        Align {
50            child,
51            align: HorizontalAlign::Right,
52        }
53    }
54}
55
56impl Renderable for Align {
57    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
58        let width = options.max_width;
59        let lines = console.render_lines(self.child.as_ref(), options, false);
60        let style = Some(Style::new());
61
62        let mut rows: Vec<Vec<Segment>> = Vec::with_capacity(lines.len());
63        for line in lines {
64            let line_width: usize = line.iter().map(Segment::cell_length).sum();
65            let excess = width.saturating_sub(line_width);
66            let (left_pad, right_pad) = match self.align {
67                HorizontalAlign::Left => (0, excess),
68                HorizontalAlign::Right => (excess, 0),
69                HorizontalAlign::Center => (excess / 2, excess - excess / 2),
70            };
71            let mut row = Vec::new();
72            if left_pad > 0 {
73                row.push(Segment::new(" ".repeat(left_pad), style.clone()));
74            }
75            row.extend(line);
76            if right_pad > 0 {
77                row.push(Segment::new(" ".repeat(right_pad), style.clone()));
78            }
79            rows.push(row);
80        }
81
82        let mut segments = Vec::new();
83        let last = rows.len().saturating_sub(1);
84        for (index, row) in rows.into_iter().enumerate() {
85            segments.extend(row);
86            if index != last {
87                segments.push(Segment::line());
88            }
89        }
90        segments
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::color::ColorSystem;
98    use crate::text::Text;
99
100    fn console(width: usize) -> Console {
101        Console::builder()
102            .force_terminal(true)
103            .color_system(Some(ColorSystem::Truecolor))
104            .width(width)
105            .build()
106    }
107
108    #[test]
109    fn center_pads_both_sides() {
110        let out = console(20).render_export(&Align::center(Box::new(Text::new("hi"))));
111        assert_eq!(out, "         hi         \n");
112    }
113
114    #[test]
115    fn right_pads_left() {
116        let out = console(20).render_export(&Align::right(Box::new(Text::new("hi"))));
117        assert_eq!(out, "                  hi\n");
118    }
119
120    #[test]
121    fn center_odd_remainder_floors_left() {
122        let out = console(21).render_export(&Align::center(Box::new(Text::new("hi"))));
123        assert_eq!(out, "         hi          \n");
124    }
125}