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