Skip to main content

rich/
align.rs

1//! Horizontal alignment.
2//!
3//! Port of upstream `rich/align.py`. [`Align`] pads a child renderable to fill
4//! the available width, positioning it left, center, or right, and optionally
5//! within a height (top, middle, bottom); [`VerticalCenter`] is upstream's
6//! deprecated vertical-only form.
7
8use crate::console::{Console, ConsoleOptions};
9use crate::measure::Measurement;
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/// Where to position content within an available height. Mirrors
25/// `rich.align.VerticalAlignMethod`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum VerticalAlign {
28    Top,
29    Middle,
30    Bottom,
31}
32
33/// Aligns a child renderable within the available width (and, with a
34/// vertical alignment, height). Mirrors `rich.align.Align`.
35pub struct Align {
36    child: Box<dyn Renderable>,
37    align: HorizontalAlign,
38    style: Option<Style>,
39    vertical: Option<VerticalAlign>,
40    pad: bool,
41    width: Option<usize>,
42    height: Option<usize>,
43}
44
45impl Align {
46    /// Align `child` horizontally. Port of `Align(renderable, align)`; the
47    /// other keywords are builder methods.
48    pub fn new(child: Box<dyn Renderable>, align: HorizontalAlign) -> Self {
49        Align {
50            child,
51            align,
52            style: None,
53            vertical: None,
54            pad: true,
55            width: None,
56            height: None,
57        }
58    }
59
60    /// Left-align the child (pads on the right).
61    pub fn left(child: Box<dyn Renderable>) -> Self {
62        Align::new(child, HorizontalAlign::Left)
63    }
64
65    /// Center the child (pads both sides, extra cell on the right).
66    pub fn center(child: Box<dyn Renderable>) -> Self {
67        Align::new(child, HorizontalAlign::Center)
68    }
69
70    /// Right-align the child (pads on the left).
71    pub fn right(child: Box<dyn Renderable>) -> Self {
72        Align::new(child, HorizontalAlign::Right)
73    }
74
75    /// The background style of the padding (upstream `style=`), applied under
76    /// the whole output.
77    pub fn style(mut self, style: Style) -> Self {
78        self.style = Some(style);
79        self
80    }
81
82    /// Align vertically within `height` (or the options' height). Upstream
83    /// `vertical=`.
84    pub fn vertical(mut self, vertical: VerticalAlign) -> Self {
85        self.vertical = Some(vertical);
86        self
87    }
88
89    /// Pad the right-hand side and blank lines (upstream `pad=`, default on).
90    pub fn pad(mut self, pad: bool) -> Self {
91        self.pad = pad;
92        self
93    }
94
95    /// Constrain the child's width (upstream `width=`).
96    pub fn width(mut self, width: usize) -> Self {
97        self.width = Some(width);
98        self
99    }
100
101    /// The height to align within (upstream `height=`), else the options'.
102    pub fn height(mut self, height: usize) -> Self {
103        self.height = Some(height);
104        self
105    }
106}
107
108impl Align {
109    /// `Align.__rich_console__` over a borrowed child: what
110    /// [`Console::print_with`] wraps a non-`Text` renderable in for
111    /// `justify="left"|"center"|"right"` (upstream's `_collect_renderables`).
112    pub fn render_child(
113        child: &dyn Renderable,
114        align: HorizontalAlign,
115        console: &Console,
116        options: &ConsoleOptions,
117    ) -> Vec<Segment> {
118        AlignSpec {
119            align,
120            style: None,
121            vertical: None,
122            pad: true,
123            width: None,
124            height: None,
125        }
126        .render(child, console, options)
127    }
128
129    fn spec(&self) -> AlignSpec {
130        AlignSpec {
131            align: self.align,
132            style: self.style.clone(),
133            vertical: self.vertical,
134            pad: self.pad,
135            width: self.width,
136            height: self.height,
137        }
138    }
139}
140
141/// Every `Align` option but the child.
142struct AlignSpec {
143    align: HorizontalAlign,
144    style: Option<Style>,
145    vertical: Option<VerticalAlign>,
146    pad: bool,
147    width: Option<usize>,
148    height: Option<usize>,
149}
150
151impl AlignSpec {
152    fn render(
153        &self,
154        child: &dyn Renderable,
155        console: &Console,
156        options: &ConsoleOptions,
157    ) -> Vec<Segment> {
158        // Upstream measures the child, renders it through `Constrain` at that
159        // width, and squares the lines off with `Segment.set_shape`, so the
160        // rendered *block* is aligned as a whole (#443).
161        let measured = Measurement::get(console, options, child).maximum;
162        let block_width = match self.width {
163            Some(width) => measured.min(width),
164            None => measured,
165        };
166        let mut child_options = options.update_width(block_width.min(options.max_width));
167        child_options.height = None;
168        let lines = console.render_lines(child, &child_options, false);
169        let width = lines
170            .iter()
171            .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
172            .max()
173            .unwrap_or(0);
174        let height = lines.len();
175        let lines: Vec<Vec<Segment>> = lines
176            .iter()
177            .map(|line| Segment::adjust_line_length(line, width, None))
178            .collect();
179
180        let excess = options.max_width.saturating_sub(width);
181        let pad_style = Some(self.style.clone().unwrap_or_default());
182        let (left_pad, right_pad) = match self.align {
183            HorizontalAlign::Left => (0, if self.pad { excess } else { 0 }),
184            HorizontalAlign::Right => (excess, 0),
185            HorizontalAlign::Center => (excess / 2, if self.pad { excess - excess / 2 } else { 0 }),
186        };
187
188        let mut rows: Vec<Vec<Segment>> = Vec::with_capacity(lines.len());
189        for line in lines {
190            let mut row = Vec::new();
191            if left_pad > 0 {
192                row.push(Segment::new(" ".repeat(left_pad), pad_style.clone()));
193            }
194            row.extend(line);
195            if right_pad > 0 {
196                row.push(Segment::new(" ".repeat(right_pad), pad_style.clone()));
197            }
198            rows.push(row);
199        }
200
201        // `blank_line`: a full-width row of the padding style, or bare.
202        let blank = || {
203            if self.pad {
204                vec![Segment::new(
205                    " ".repeat(self.width.unwrap_or(options.max_width)),
206                    pad_style.clone(),
207                )]
208            } else {
209                Vec::new()
210            }
211        };
212        if let (Some(vertical), Some(total)) = (self.vertical, self.height.or(options.height)) {
213            let (top, bottom) = match vertical {
214                VerticalAlign::Top => (0, total.saturating_sub(height)),
215                VerticalAlign::Middle => {
216                    let top = total.saturating_sub(height) / 2;
217                    (top, total.saturating_sub(top + height))
218                }
219                VerticalAlign::Bottom => (total.saturating_sub(height), 0),
220            };
221            let mut shaped = Vec::with_capacity(top + rows.len() + bottom);
222            shaped.extend(std::iter::repeat_with(blank).take(top));
223            shaped.append(&mut rows);
224            shaped.extend(std::iter::repeat_with(blank).take(bottom));
225            rows = shaped;
226        }
227
228        let mut segments = Vec::new();
229        let last = rows.len().saturating_sub(1);
230        for (index, row) in rows.into_iter().enumerate() {
231            segments.extend(row);
232            if index != last {
233                segments.push(Segment::line());
234            }
235        }
236        match self.style.as_ref().filter(|style| !style.is_null()) {
237            Some(style) => Segment::apply_style(&segments, style),
238            None => segments,
239        }
240    }
241}
242
243impl Renderable for Align {
244    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
245        self.spec().render(self.child.as_ref(), console, options)
246    }
247
248    /// Port of `Align.__rich_measure__`: the child's measurement.
249    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
250        Measurement::get(console, options, self.child.as_ref())
251    }
252
253    /// Upstream's `Align.vertical`, which a `Table` cell aligns by.
254    fn vertical(&self) -> Option<VerticalAlign> {
255        self.vertical
256    }
257}
258
259/// Vertically centres a renderable in the options' height (else the console
260/// height). Mirrors the deprecated `rich.align.VerticalCenter`.
261pub struct VerticalCenter {
262    child: Box<dyn Renderable>,
263    style: Option<Style>,
264}
265
266impl VerticalCenter {
267    pub fn new(child: Box<dyn Renderable>) -> Self {
268        VerticalCenter { child, style: None }
269    }
270
271    /// The style of the blank lines above and below (upstream `style=`).
272    pub fn style(mut self, style: Style) -> Self {
273        self.style = Some(style);
274        self
275    }
276}
277
278impl Renderable for VerticalCenter {
279    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
280        let mut child_options = options.clone();
281        child_options.height = None;
282        let lines = console.render_lines(self.child.as_ref(), &child_options, false);
283        let width = lines
284            .iter()
285            .map(|line| line.iter().map(Segment::cell_length).sum::<usize>())
286            .max()
287            .unwrap_or(0);
288        let height = options.height.unwrap_or(options.size.height);
289        let top = height.saturating_sub(lines.len()) / 2;
290        let bottom = height.saturating_sub(top + lines.len());
291        let blank = || vec![Segment::new(" ".repeat(width), self.style.clone())];
292        let mut rows: Vec<Vec<Segment>> = Vec::new();
293        rows.extend(std::iter::repeat_with(blank).take(top));
294        rows.extend(lines);
295        rows.extend(std::iter::repeat_with(blank).take(bottom));
296        let mut segments = Vec::new();
297        let last = rows.len().saturating_sub(1);
298        for (index, row) in rows.into_iter().enumerate() {
299            segments.extend(row);
300            if index != last {
301                segments.push(Segment::line());
302            }
303        }
304        segments
305    }
306
307    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
308        Measurement::get(console, options, self.child.as_ref())
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::color::ColorSystem;
316    use crate::text::Text;
317
318    fn console(width: usize) -> Console {
319        Console::builder()
320            .force_terminal(true)
321            .color_system(Some(ColorSystem::Truecolor))
322            .width(width)
323            .build()
324    }
325
326    #[test]
327    fn center_pads_both_sides() {
328        let out = console(20).render_export(&Align::center(Box::new(Text::new("hi"))));
329        assert_eq!(out, "         hi         \n");
330    }
331
332    #[test]
333    fn right_pads_left() {
334        let out = console(20).render_export(&Align::right(Box::new(Text::new("hi"))));
335        assert_eq!(out, "                  hi\n");
336    }
337
338    #[test]
339    fn center_odd_remainder_floors_left() {
340        let out = console(21).render_export(&Align::center(Box::new(Text::new("hi"))));
341        assert_eq!(out, "         hi          \n");
342    }
343
344    #[test]
345    fn aligns_the_wrapped_block_not_each_line() {
346        // Captured from real rich 15.0.0 (#443): the block is 4 cells wide, so
347        // the shorter wrapped line keeps its place inside it.
348        let out = console(4).render_export(&Align::right(Box::new(Text::new("abcd ef"))));
349        assert_eq!(out, "abcd\nef  \n");
350        let out = console(8).render_export(&Align::right(Box::new(Text::new("abc de"))));
351        assert_eq!(out, "  abc de\n");
352    }
353}