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::measure::Measurement;
11use crate::protocol::Renderable;
12use crate::segment::Segment;
13use crate::style::Style;
14
15/// Where to position content within an available width. Shared by [`Align`],
16/// `Rule` titles, and `Panel` titles.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum HorizontalAlign {
19    Left,
20    #[default]
21    Center,
22    Right,
23}
24
25/// Aligns a child renderable within the available width. Mirrors `rich.align.Align`.
26pub struct Align {
27    child: Box<dyn Renderable>,
28    align: HorizontalAlign,
29}
30
31impl Align {
32    /// Left-align the child (pads on the right).
33    pub fn left(child: Box<dyn Renderable>) -> Self {
34        Align {
35            child,
36            align: HorizontalAlign::Left,
37        }
38    }
39
40    /// Center the child (pads both sides, extra cell on the right).
41    pub fn center(child: Box<dyn Renderable>) -> Self {
42        Align {
43            child,
44            align: HorizontalAlign::Center,
45        }
46    }
47
48    /// Right-align the child (pads on the left).
49    pub fn right(child: Box<dyn Renderable>) -> Self {
50        Align {
51            child,
52            align: HorizontalAlign::Right,
53        }
54    }
55}
56
57impl Renderable for Align {
58    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
59        // Upstream measures the child, renders it through `Constrain` at that
60        // width, and squares the lines off with `Segment.set_shape`, so the
61        // rendered *block* is aligned as a whole (#443).
62        let block_width = Measurement::get(console, options, self.child.as_ref()).maximum;
63        let mut child_options = options.update_width(block_width);
64        child_options.height = None;
65        let lines = console.render_lines(self.child.as_ref(), &child_options, false);
66        let width = lines
67            .iter()
68            .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
69            .max()
70            .unwrap_or(0);
71        let lines: Vec<Vec<Segment>> = lines
72            .iter()
73            .map(|line| Segment::adjust_line_length(line, width, None))
74            .collect();
75
76        let excess = options.max_width.saturating_sub(width);
77        let style = Some(Style::new());
78        let (left_pad, right_pad) = match self.align {
79            HorizontalAlign::Left => (0, excess),
80            HorizontalAlign::Right => (excess, 0),
81            HorizontalAlign::Center => (excess / 2, excess - excess / 2),
82        };
83
84        let mut rows: Vec<Vec<Segment>> = Vec::with_capacity(lines.len());
85        for line in lines {
86            let mut row = Vec::new();
87            if left_pad > 0 {
88                row.push(Segment::new(" ".repeat(left_pad), style.clone()));
89            }
90            row.extend(line);
91            if right_pad > 0 {
92                row.push(Segment::new(" ".repeat(right_pad), style.clone()));
93            }
94            rows.push(row);
95        }
96
97        let mut segments = Vec::new();
98        let last = rows.len().saturating_sub(1);
99        for (index, row) in rows.into_iter().enumerate() {
100            segments.extend(row);
101            if index != last {
102                segments.push(Segment::line());
103            }
104        }
105        segments
106    }
107
108    /// Port of `Align.__rich_measure__`: the child's measurement.
109    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
110        Measurement::get(console, options, self.child.as_ref())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::color::ColorSystem;
118    use crate::text::Text;
119
120    fn console(width: usize) -> Console {
121        Console::builder()
122            .force_terminal(true)
123            .color_system(Some(ColorSystem::Truecolor))
124            .width(width)
125            .build()
126    }
127
128    #[test]
129    fn center_pads_both_sides() {
130        let out = console(20).render_export(&Align::center(Box::new(Text::new("hi"))));
131        assert_eq!(out, "         hi         \n");
132    }
133
134    #[test]
135    fn right_pads_left() {
136        let out = console(20).render_export(&Align::right(Box::new(Text::new("hi"))));
137        assert_eq!(out, "                  hi\n");
138    }
139
140    #[test]
141    fn center_odd_remainder_floors_left() {
142        let out = console(21).render_export(&Align::center(Box::new(Text::new("hi"))));
143        assert_eq!(out, "         hi          \n");
144    }
145
146    #[test]
147    fn aligns_the_wrapped_block_not_each_line() {
148        // Captured from real rich 15.0.0 (#443): the block is 4 cells wide, so
149        // the shorter wrapped line keeps its place inside it.
150        let out = console(4).render_export(&Align::right(Box::new(Text::new("abcd ef"))));
151        assert_eq!(out, "abcd\nef  \n");
152        let out = console(8).render_export(&Align::right(Box::new(Text::new("abc de"))));
153        assert_eq!(out, "  abc de\n");
154    }
155}