Skip to main content

rusty_rich/
padding.rs

1//! Padding — draw space around content. Equivalent to Rich's `padding.py`.
2
3use crate::console::{ConsoleOptions, DynRenderable, RenderResult, Renderable};
4use crate::segment::Segment;
5use crate::style::Style;
6
7// ---------------------------------------------------------------------------
8// PaddingDimensions
9// ---------------------------------------------------------------------------
10
11/// Padding specification (CSS-style: 1, 2, or 4 values).
12#[derive(Debug, Clone, Copy)]
13pub struct PaddingDimensions {
14    pub top: usize,
15    pub right: usize,
16    pub bottom: usize,
17    pub left: usize,
18}
19
20impl PaddingDimensions {
21    /// Create from a single value (all sides equal).
22    pub fn all(pad: usize) -> Self {
23        Self {
24            top: pad,
25            right: pad,
26            bottom: pad,
27            left: pad,
28        }
29    }
30
31    /// Create from (vertical, horizontal).
32    pub fn symmetric(vertical: usize, horizontal: usize) -> Self {
33        Self {
34            top: vertical,
35            right: horizontal,
36            bottom: vertical,
37            left: horizontal,
38        }
39    }
40
41    /// Create from (top, right, bottom, left).
42    pub fn new(top: usize, right: usize, bottom: usize, left: usize) -> Self {
43        Self {
44            top,
45            right,
46            bottom,
47            left,
48        }
49    }
50}
51
52// ---------------------------------------------------------------------------
53// Padding
54// ---------------------------------------------------------------------------
55
56/// A renderable that adds padding around its content.
57#[derive(Clone)]
58pub struct Padding {
59    /// The inner renderable.
60    pub renderable: DynRenderable,
61    /// Padding dimensions.
62    pub pad: PaddingDimensions,
63    /// Style for the padding (space) characters.
64    pub style: Style,
65    /// If true, expand padding to fill available width.
66    pub expand: bool,
67}
68
69impl Padding {
70    /// Create a new Padding wrapper.
71    pub fn new(renderable: impl Renderable + Send + Sync + 'static) -> Self {
72        Self {
73            renderable: DynRenderable::new(renderable),
74            pad: PaddingDimensions::all(0),
75            style: Style::new(),
76            expand: true,
77        }
78    }
79
80    /// Builder: set padding as (top, right, bottom, left).
81    pub fn pad(mut self, top: usize, right: usize, bottom: usize, left: usize) -> Self {
82        self.pad = PaddingDimensions::new(top, right, bottom, left);
83        self
84    }
85
86    /// Builder: set uniform padding on all sides.
87    pub fn pad_all(mut self, pad: usize) -> Self {
88        self.pad = PaddingDimensions::all(pad);
89        self
90    }
91
92    /// Builder: indent by `level` spaces on the left.
93    pub fn indent(mut self, level: usize) -> Self {
94        self.pad = PaddingDimensions::new(0, 0, 0, level);
95        self.expand = false;
96        self
97    }
98
99    /// Builder: set the style.
100    pub fn style(mut self, style: Style) -> Self {
101        self.style = style;
102        self
103    }
104}
105
106impl std::fmt::Debug for Padding {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.debug_struct("Padding").field("pad", &self.pad).finish()
109    }
110}
111
112impl Renderable for Padding {
113    fn render(&self, options: &ConsoleOptions) -> RenderResult {
114        let pad = &self.pad;
115        let width = if self.expand {
116            options.max_width
117        } else {
118            (crate::measure::Measurement::fixed(0).maximum + pad.left + pad.right)
119                .min(options.max_width)
120        };
121
122        let inner_width = width.saturating_sub(pad.left + pad.right);
123        let inner_opts = options.update_width(inner_width.max(1));
124
125        let inner_height = options
126            .height
127            .map(|h| h.saturating_sub(pad.top + pad.bottom));
128        let inner_opts = if let Some(h) = inner_height {
129            inner_opts.update_height(h)
130        } else {
131            inner_opts
132        };
133
134        let content = self.renderable.render(&inner_opts);
135        let mut lines: Vec<Vec<Segment>> = Vec::new();
136
137        // Top padding
138        for _ in 0..pad.top {
139            lines.push(vec![Segment::new(" ".repeat(width)), Segment::line()]);
140        }
141
142        // Content lines with left/right padding
143        for content_line in &content.lines {
144            let mut line = Vec::new();
145            if pad.left > 0 {
146                line.push(Segment::new(" ".repeat(pad.left)));
147            }
148            line.extend(content_line.iter().cloned());
149            if pad.right > 0 {
150                line.push(Segment::new(" ".repeat(pad.right)));
151                line.push(Segment::line());
152            }
153            lines.push(line);
154        }
155
156        // Bottom padding
157        for _ in 0..pad.bottom {
158            lines.push(vec![Segment::new(" ".repeat(width)), Segment::line()]);
159        }
160
161        RenderResult {
162            lines,
163            items: Vec::new(),
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::console::ConsoleOptions;
172
173    #[test]
174    fn test_padding() {
175        let p = Padding::new("Hello").pad_all(2);
176        let opts = ConsoleOptions::default();
177        let result = p.render(&opts);
178        let ansi = result.to_ansi();
179        assert!(ansi.contains("Hello"));
180    }
181}