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;
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: Style,
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: 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).
57    pub fn style(mut self, style: Style) -> Self {
58        self.style = style;
59        self
60    }
61}
62
63impl Renderable for Padding {
64    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
65        let (top, right, bottom, left) = self.pad;
66        let width = if self.expand {
67            options.max_width
68        } else {
69            let child = Measurement::get(console, options, self.child.as_ref()).maximum;
70            (child + left + right).min(options.max_width)
71        };
72        let child_width = width.saturating_sub(left).saturating_sub(right);
73
74        let mut child_options = options.update_width(child_width);
75        // `render_options.update_height(height - top - bottom)`.
76        if let Some(height) = options.height {
77            child_options = child_options.update_height(height.saturating_sub(top + bottom));
78        }
79        // Upstream renders the child with `style=style`, so the padding style
80        // also sits under the content and its fill (#442).
81        let lines = console.render_lines_styled(
82            self.child.as_ref(),
83            &child_options,
84            Some(&self.style),
85            true,
86        );
87
88        let style = Some(self.style.clone());
89        let blank = || Segment::new(" ".repeat(width), style.clone());
90        let left_pad = |row: &mut Vec<Segment>| {
91            if left > 0 {
92                row.push(Segment::new(" ".repeat(left), style.clone()));
93            }
94        };
95        let right_pad = |row: &mut Vec<Segment>| {
96            if right > 0 {
97                row.push(Segment::new(" ".repeat(right), style.clone()));
98            }
99        };
100
101        let mut rows: Vec<Vec<Segment>> = Vec::new();
102        for _ in 0..top {
103            rows.push(vec![blank()]);
104        }
105        for line in lines {
106            let mut row = Vec::new();
107            left_pad(&mut row);
108            row.extend(line);
109            right_pad(&mut row);
110            rows.push(row);
111        }
112        for _ in 0..bottom {
113            rows.push(vec![blank()]);
114        }
115
116        join_rows(rows)
117    }
118
119    /// Port of `Padding.__rich_measure__`: the child's measurement plus the
120    /// horizontal padding, or the whole width when the padding leaves no room.
121    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
122        let (_, right, _, left) = self.pad;
123        let max_width = options.max_width;
124        let extra_width = left + right;
125        if max_width < extra_width + 1 {
126            return Measurement::new(max_width, max_width);
127        }
128        let child = Measurement::get(console, options, self.child.as_ref());
129        Measurement::new(child.minimum + extra_width, child.maximum + extra_width)
130            .with_maximum(max_width)
131    }
132}
133
134/// Flatten rows into a segment stream separated by newline segments (no trailing
135/// newline — the console adds one on export/print).
136pub(crate) fn join_rows(rows: Vec<Vec<Segment>>) -> Vec<Segment> {
137    let mut segments = Vec::new();
138    let last = rows.len().saturating_sub(1);
139    for (index, row) in rows.into_iter().enumerate() {
140        segments.extend(row);
141        if index != last {
142            segments.push(Segment::line());
143        }
144    }
145    segments
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::text::Text;
152
153    fn console() -> Console {
154        Console::builder()
155            .force_terminal(true)
156            .color_system(Some(crate::color::ColorSystem::Truecolor))
157            .width(10)
158            .build()
159    }
160
161    #[test]
162    fn pads_all_sides() {
163        let padding = Padding::new(Box::new(Text::new("hi")), (1, 2, 1, 2));
164        let out = console().render_export(&padding);
165        assert_eq!(out, "          \n  hi      \n          \n");
166    }
167
168    #[test]
169    fn horizontal_only() {
170        let padding = Padding::new(Box::new(Text::new("hi")), (0, 1, 0, 1));
171        let out = console().render_export(&padding);
172        assert_eq!(out, " hi       \n");
173    }
174
175    #[test]
176    fn empty_content_keeps_its_line_and_the_style_reaches_the_content() {
177        // Captured from real rich 15.0.0 (#442).
178        let console = Console::builder()
179            .force_terminal(true)
180            .color_system(Some(crate::color::ColorSystem::Truecolor))
181            .width(5)
182            .highlight(false)
183            .build();
184        let empty = Padding::new(Box::new(Text::new("")), (1, 0, 0, 0));
185        assert_eq!(console.render_export(&empty), "     \n     \n");
186        let styled = Padding::new(Box::new(Text::new("ab")), (0, 0, 0, 1))
187            .style(Style::parse("on blue").unwrap());
188        assert_eq!(
189            console.render_export(&styled),
190            "\x1b[44m \x1b[0m\x1b[44mab\x1b[0m\x1b[44m  \x1b[0m\n"
191        );
192    }
193}