Skip to main content

rich/
padding.rs

1//! Padding around a renderable.
2//!
3//! Port of upstream `rich/padding.py`. [`Padding`] surrounds a child renderable
4//! with blank space on any of its four sides.
5
6use crate::console::{Console, ConsoleOptions};
7use crate::measure::Measurement;
8use crate::protocol::Renderable;
9use crate::segment::Segment;
10use crate::style::{Style, StyleType};
11
12/// Blank space around a child renderable. Mirrors `rich.padding.Padding`.
13///
14/// The pad is `(top, right, bottom, left)` — the same order as CSS and upstream.
15pub struct Padding {
16    child: Box<dyn Renderable>,
17    pad: (usize, usize, usize, usize),
18    style: StyleType,
19    expand: bool,
20}
21
22impl Padding {
23    /// Pad `child` by an explicit `(top, right, bottom, left)`.
24    pub fn new(child: Box<dyn Renderable>, pad: (usize, usize, usize, usize)) -> Self {
25        Padding {
26            child,
27            pad,
28            style: StyleType::Style(Style::new()),
29            expand: true,
30        }
31    }
32
33    /// Indent `child` by `level` cells without expanding it. Port of
34    /// `Padding.indent` (`pad=(0, 0, 0, level)`, `expand=False`).
35    pub fn indent(child: Box<dyn Renderable>, level: usize) -> Self {
36        Padding::new(child, (0, 0, 0, level)).expand(false)
37    }
38
39    /// Expand to the available width (upstream `expand=`, default on), or fit
40    /// the child's measured width plus the padding.
41    pub fn expand(mut self, expand: bool) -> Self {
42        self.expand = expand;
43        self
44    }
45
46    /// Equal padding on all four sides.
47    pub fn uniform(child: Box<dyn Renderable>, amount: usize) -> Self {
48        Padding::new(child, (amount, amount, amount, amount))
49    }
50
51    /// `(vertical, horizontal)` padding (top==bottom, left==right).
52    pub fn symmetric(child: Box<dyn Renderable>, vertical: usize, horizontal: usize) -> Self {
53        Padding::new(child, (vertical, horizontal, vertical, horizontal))
54    }
55
56    /// Set the style applied to the padding (and blank lines): a [`Style`],
57    /// or a theme name or style definition resolved at render time, as
58    /// upstream's `style=` is.
59    pub fn style(mut self, style: impl Into<StyleType>) -> Self {
60        self.style = style.into();
61        self
62    }
63
64    /// Unpack padding given CSS style, as `(top, right, bottom, left)`. Port
65    /// of `Padding.unpack`: one value pads every side, two are
66    /// `(vertical, horizontal)` and four are explicit. Any other count is
67    /// `None`, where upstream raises `ValueError`.
68    pub fn unpack(pad: &[usize]) -> Option<(usize, usize, usize, usize)> {
69        match *pad {
70            [pad] => Some((pad, pad, pad, pad)),
71            [top, right] => Some((top, right, top, right)),
72            [top, right, bottom, left] => Some((top, right, bottom, left)),
73            _ => None,
74        }
75    }
76}
77
78impl Renderable for Padding {
79    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
80        let (top, right, bottom, left) = self.pad;
81        // `style = console.get_style(self.style)`.
82        let style = console.get_style(&self.style).unwrap_or_default();
83        let width = if self.expand {
84            options.max_width
85        } else {
86            let child = Measurement::get(console, options, self.child.as_ref()).maximum;
87            (child + left + right).min(options.max_width)
88        };
89        let child_width = width.saturating_sub(left).saturating_sub(right);
90
91        let mut child_options = options.update_width(child_width);
92        // `render_options.update_height(height - top - bottom)`.
93        if let Some(height) = options.height {
94            child_options = child_options.update_height(height.saturating_sub(top + bottom));
95        }
96        // Upstream renders the child with `style=style`, so the padding style
97        // also sits under the content and its fill (#442).
98        let lines =
99            console.render_lines_styled(self.child.as_ref(), &child_options, Some(&style), true);
100
101        let style = Some(style);
102        let blank = || Segment::new(" ".repeat(width), style.clone());
103        let left_pad = |row: &mut Vec<Segment>| {
104            if left > 0 {
105                row.push(Segment::new(" ".repeat(left), style.clone()));
106            }
107        };
108        let right_pad = |row: &mut Vec<Segment>| {
109            if right > 0 {
110                row.push(Segment::new(" ".repeat(right), style.clone()));
111            }
112        };
113
114        let mut rows: Vec<Vec<Segment>> = Vec::new();
115        for _ in 0..top {
116            rows.push(vec![blank()]);
117        }
118        for line in lines {
119            let mut row = Vec::new();
120            left_pad(&mut row);
121            row.extend(line);
122            right_pad(&mut row);
123            rows.push(row);
124        }
125        for _ in 0..bottom {
126            rows.push(vec![blank()]);
127        }
128
129        join_rows(rows)
130    }
131
132    /// Port of `Padding.__rich_measure__`: the child's measurement plus the
133    /// horizontal padding, or the whole width when the padding leaves no room.
134    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
135        let (_, right, _, left) = self.pad;
136        let max_width = options.max_width;
137        let extra_width = left + right;
138        if max_width < extra_width + 1 {
139            return Measurement::new(max_width, max_width);
140        }
141        let child = Measurement::get(console, options, self.child.as_ref());
142        Measurement::new(child.minimum + extra_width, child.maximum + extra_width)
143            .with_maximum(max_width)
144    }
145}
146
147/// Flatten rows into a segment stream separated by newline segments (no trailing
148/// newline — the console adds one on export/print).
149pub(crate) fn join_rows(rows: Vec<Vec<Segment>>) -> Vec<Segment> {
150    let mut segments = Vec::new();
151    let last = rows.len().saturating_sub(1);
152    for (index, row) in rows.into_iter().enumerate() {
153        segments.extend(row);
154        if index != last {
155            segments.push(Segment::line());
156        }
157    }
158    segments
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::text::Text;
165
166    fn console() -> Console {
167        Console::builder()
168            .force_terminal(true)
169            .color_system(Some(crate::color::ColorSystem::Truecolor))
170            .width(10)
171            .build()
172    }
173
174    #[test]
175    fn pads_all_sides() {
176        let padding = Padding::new(Box::new(Text::new("hi")), (1, 2, 1, 2));
177        let out = console().render_export(&padding);
178        assert_eq!(out, "          \n  hi      \n          \n");
179    }
180
181    #[test]
182    fn horizontal_only() {
183        let padding = Padding::new(Box::new(Text::new("hi")), (0, 1, 0, 1));
184        let out = console().render_export(&padding);
185        assert_eq!(out, " hi       \n");
186    }
187
188    #[test]
189    fn empty_content_keeps_its_line_and_the_style_reaches_the_content() {
190        // Captured from real rich 15.0.0 (#442).
191        let console = Console::builder()
192            .force_terminal(true)
193            .color_system(Some(crate::color::ColorSystem::Truecolor))
194            .width(5)
195            .highlight(false)
196            .build();
197        let empty = Padding::new(Box::new(Text::new("")), (1, 0, 0, 0));
198        assert_eq!(console.render_export(&empty), "     \n     \n");
199        let styled = Padding::new(Box::new(Text::new("ab")), (0, 0, 0, 1))
200            .style(Style::parse("on blue").unwrap());
201        assert_eq!(
202            console.render_export(&styled),
203            "\x1b[44m \x1b[0m\x1b[44mab\x1b[0m\x1b[44m  \x1b[0m\n"
204        );
205    }
206}