1use crate::console::{Console, ConsoleOptions};
7use crate::measure::Measurement;
8use crate::protocol::Renderable;
9use crate::segment::Segment;
10use crate::style::{Style, StyleType};
11
12pub struct Padding {
16 child: Box<dyn Renderable>,
17 pad: (usize, usize, usize, usize),
18 style: StyleType,
19 expand: bool,
20}
21
22impl Padding {
23 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 pub fn indent(child: Box<dyn Renderable>, level: usize) -> Self {
36 Padding::new(child, (0, 0, 0, level)).expand(false)
37 }
38
39 pub fn expand(mut self, expand: bool) -> Self {
42 self.expand = expand;
43 self
44 }
45
46 pub fn uniform(child: Box<dyn Renderable>, amount: usize) -> Self {
48 Padding::new(child, (amount, amount, amount, amount))
49 }
50
51 pub fn symmetric(child: Box<dyn Renderable>, vertical: usize, horizontal: usize) -> Self {
53 Padding::new(child, (vertical, horizontal, vertical, horizontal))
54 }
55
56 pub fn style(mut self, style: impl Into<StyleType>) -> Self {
60 self.style = style.into();
61 self
62 }
63
64 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 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 if let Some(height) = options.height {
94 child_options = child_options.update_height(height.saturating_sub(top + bottom));
95 }
96 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 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
147pub(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 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}