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