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}
20
21impl Padding {
22    /// Pad `child` by an explicit `(top, right, bottom, left)`.
23    pub fn new(child: Box<dyn Renderable>, pad: (usize, usize, usize, usize)) -> Self {
24        Padding {
25            child,
26            pad,
27            style: Style::new(),
28        }
29    }
30
31    /// Equal padding on all four sides.
32    pub fn uniform(child: Box<dyn Renderable>, amount: usize) -> Self {
33        Padding::new(child, (amount, amount, amount, amount))
34    }
35
36    /// `(vertical, horizontal)` padding (top==bottom, left==right).
37    pub fn symmetric(child: Box<dyn Renderable>, vertical: usize, horizontal: usize) -> Self {
38        Padding::new(child, (vertical, horizontal, vertical, horizontal))
39    }
40
41    /// Set the style applied to the padding (and blank lines).
42    pub fn style(mut self, style: Style) -> Self {
43        self.style = style;
44        self
45    }
46}
47
48impl Renderable for Padding {
49    fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
50        let (top, right, bottom, left) = self.pad;
51        let width = options.max_width;
52        let child_width = width.saturating_sub(left).saturating_sub(right);
53
54        let child_options = options.update_width(child_width);
55        // Upstream renders the child with `style=style`, so the padding style
56        // also sits under the content and its fill (#442).
57        let lines = console.render_lines_styled(
58            self.child.as_ref(),
59            &child_options,
60            Some(&self.style),
61            true,
62        );
63
64        let style = Some(self.style.clone());
65        let blank = || Segment::new(" ".repeat(width), style.clone());
66        let left_pad = |row: &mut Vec<Segment>| {
67            if left > 0 {
68                row.push(Segment::new(" ".repeat(left), style.clone()));
69            }
70        };
71        let right_pad = |row: &mut Vec<Segment>| {
72            if right > 0 {
73                row.push(Segment::new(" ".repeat(right), style.clone()));
74            }
75        };
76
77        let mut rows: Vec<Vec<Segment>> = Vec::new();
78        for _ in 0..top {
79            rows.push(vec![blank()]);
80        }
81        for line in lines {
82            let mut row = Vec::new();
83            left_pad(&mut row);
84            row.extend(line);
85            right_pad(&mut row);
86            rows.push(row);
87        }
88        for _ in 0..bottom {
89            rows.push(vec![blank()]);
90        }
91
92        join_rows(rows)
93    }
94
95    /// Port of `Padding.__rich_measure__`: the child's measurement plus the
96    /// horizontal padding, or the whole width when the padding leaves no room.
97    fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
98        let (_, right, _, left) = self.pad;
99        let max_width = options.max_width;
100        let extra_width = left + right;
101        if max_width < extra_width + 1 {
102            return Measurement::new(max_width, max_width);
103        }
104        let child = Measurement::get(console, options, self.child.as_ref());
105        Measurement::new(child.minimum + extra_width, child.maximum + extra_width)
106            .with_maximum(max_width)
107    }
108}
109
110/// Flatten rows into a segment stream separated by newline segments (no trailing
111/// newline — the console adds one on export/print).
112pub(crate) fn join_rows(rows: Vec<Vec<Segment>>) -> Vec<Segment> {
113    let mut segments = Vec::new();
114    let last = rows.len().saturating_sub(1);
115    for (index, row) in rows.into_iter().enumerate() {
116        segments.extend(row);
117        if index != last {
118            segments.push(Segment::line());
119        }
120    }
121    segments
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::text::Text;
128
129    fn console() -> Console {
130        Console::builder()
131            .force_terminal(true)
132            .color_system(Some(crate::color::ColorSystem::Truecolor))
133            .width(10)
134            .build()
135    }
136
137    #[test]
138    fn pads_all_sides() {
139        let padding = Padding::new(Box::new(Text::new("hi")), (1, 2, 1, 2));
140        let out = console().render_export(&padding);
141        assert_eq!(out, "          \n  hi      \n          \n");
142    }
143
144    #[test]
145    fn horizontal_only() {
146        let padding = Padding::new(Box::new(Text::new("hi")), (0, 1, 0, 1));
147        let out = console().render_export(&padding);
148        assert_eq!(out, " hi       \n");
149    }
150
151    #[test]
152    fn empty_content_keeps_its_line_and_the_style_reaches_the_content() {
153        // Captured from real rich 15.0.0 (#442).
154        let console = Console::builder()
155            .force_terminal(true)
156            .color_system(Some(crate::color::ColorSystem::Truecolor))
157            .width(5)
158            .highlight(false)
159            .build();
160        let empty = Padding::new(Box::new(Text::new("")), (1, 0, 0, 0));
161        assert_eq!(console.render_export(&empty), "     \n     \n");
162        let styled = Padding::new(Box::new(Text::new("ab")), (0, 0, 0, 1))
163            .style(Style::parse("on blue").unwrap());
164        assert_eq!(
165            console.render_export(&styled),
166            "\x1b[44m \x1b[0m\x1b[44mab\x1b[0m\x1b[44m  \x1b[0m\n"
167        );
168    }
169}