1use crate::console::{Console, ConsoleOptions, Renderable};
5use crate::measure::Measurement;
6use crate::segment::Segment;
7use crate::style::Style;
8use crate::text::Text;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum PaddingDimensions {
17 Uniform(usize),
19 Pair(usize, usize),
21 Full(usize, usize, usize, usize),
23}
24
25impl PaddingDimensions {
26 pub fn unpack(&self) -> (usize, usize, usize, usize) {
28 match *self {
29 PaddingDimensions::Uniform(v) => (v, v, v, v),
30 PaddingDimensions::Pair(vert, horiz) => (vert, horiz, vert, horiz),
31 PaddingDimensions::Full(t, r, b, l) => (t, r, b, l),
32 }
33 }
34}
35
36impl From<usize> for PaddingDimensions {
37 fn from(n: usize) -> Self {
38 PaddingDimensions::Uniform(n)
39 }
40}
41
42impl From<(usize, usize)> for PaddingDimensions {
43 fn from((v, h): (usize, usize)) -> Self {
44 PaddingDimensions::Pair(v, h)
45 }
46}
47
48impl From<(usize, usize, usize, usize)> for PaddingDimensions {
49 fn from((t, r, b, l): (usize, usize, usize, usize)) -> Self {
50 PaddingDimensions::Full(t, r, b, l)
51 }
52}
53
54#[derive(Debug, Clone)]
60pub struct Padding {
61 pub content: Text,
63 pub top: usize,
65 pub right: usize,
67 pub bottom: usize,
69 pub left: usize,
71 pub style: Style,
73 pub expand: bool,
75}
76
77impl Padding {
78 pub fn wrap(content: Text, pad: impl Into<PaddingDimensions>) -> Self {
87 Self::new(content, pad.into(), Style::null(), true)
88 }
89
90 pub fn new(content: Text, pad: PaddingDimensions, style: Style, expand: bool) -> Self {
92 let (top, right, bottom, left) = pad.unpack();
93 Padding {
94 content,
95 top,
96 right,
97 bottom,
98 left,
99 style,
100 expand,
101 }
102 }
103
104 pub fn indent(content: Text, level: usize) -> Self {
106 Padding::new(
107 content,
108 PaddingDimensions::Full(0, 0, 0, level),
109 Style::null(),
110 true,
111 )
112 }
113
114 pub fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
116 let max_width = options.max_width.saturating_sub(self.left + self.right);
117 let inner_opts = options.update_width(max_width.max(1));
118 let content_width = self.content.cell_len();
120 let min_w = content_width + self.left + self.right;
121 let max_w = if self.expand {
122 options.max_width
123 } else {
124 min_w.min(options.max_width)
125 };
126 Measurement::new(
127 min_w.min(inner_opts.max_width + self.left + self.right),
128 max_w,
129 )
130 }
131}
132
133impl Renderable for Padding {
134 fn gilt_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
135 let mut segments = Vec::new();
136
137 let width = if self.expand {
139 options.max_width
140 } else {
141 let content_width = self.content.cell_len();
142 (content_width + self.left + self.right).min(options.max_width)
143 };
144
145 let inner_width = width.saturating_sub(self.left + self.right).max(1);
147
148 let inner_opts = options.update_width(inner_width);
150 let lines = console.render_lines(&self.content, Some(&inner_opts), None, true, false);
151
152 let left_pad = " ".repeat(self.left);
154 let right_pad_base = self.right;
155
156 let blank_line = " ".repeat(width);
158 for _ in 0..self.top {
159 segments.push(Segment::styled(&blank_line, self.style.clone()));
160 segments.push(Segment::line());
161 }
162
163 for line in &lines {
165 if self.left > 0 {
167 segments.push(Segment::styled(&left_pad, self.style.clone()));
168 }
169
170 segments.extend(line.iter().cloned());
172
173 let line_len = self.left + Segment::get_line_length(line);
175 let remaining = width.saturating_sub(line_len);
176 if remaining > 0 {
177 segments.push(Segment::styled(&" ".repeat(remaining), self.style.clone()));
178 } else if right_pad_base > 0 && remaining == 0 {
179 }
181
182 segments.push(Segment::line());
183 }
184
185 for _ in 0..self.bottom {
187 segments.push(Segment::styled(&blank_line, self.style.clone()));
188 segments.push(Segment::line());
189 }
190
191 segments
192 }
193}
194
195#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::utils::cells::cell_len;
203
204 #[test]
207 fn test_unpack_uniform() {
208 let pd = PaddingDimensions::Uniform(2);
209 assert_eq!(pd.unpack(), (2, 2, 2, 2));
210 }
211
212 #[test]
213 fn test_unpack_pair() {
214 let pd = PaddingDimensions::Pair(1, 3);
215 assert_eq!(pd.unpack(), (1, 3, 1, 3));
216 }
217
218 #[test]
219 fn test_unpack_full() {
220 let pd = PaddingDimensions::Full(1, 2, 3, 4);
221 assert_eq!(pd.unpack(), (1, 2, 3, 4));
222 }
223
224 #[test]
225 fn test_unpack_uniform_zero() {
226 let pd = PaddingDimensions::Uniform(0);
227 assert_eq!(pd.unpack(), (0, 0, 0, 0));
228 }
229
230 #[test]
233 fn test_padding_new() {
234 let text = Text::new("Hello", Style::null());
235 let padding = Padding::new(
236 text,
237 PaddingDimensions::Full(1, 2, 3, 4),
238 Style::null(),
239 true,
240 );
241 assert_eq!(padding.top, 1);
242 assert_eq!(padding.right, 2);
243 assert_eq!(padding.bottom, 3);
244 assert_eq!(padding.left, 4);
245 assert!(padding.expand);
246 }
247
248 #[test]
249 fn test_indent() {
250 let text = Text::new("Hello", Style::null());
251 let padding = Padding::indent(text, 4);
252 assert_eq!(padding.top, 0);
253 assert_eq!(padding.right, 0);
254 assert_eq!(padding.bottom, 0);
255 assert_eq!(padding.left, 4);
256 assert!(padding.expand);
257 }
258
259 fn make_console(width: usize) -> Console {
262 Console::builder()
263 .width(width)
264 .force_terminal(true)
265 .no_color(true)
266 .markup(false)
267 .build()
268 }
269
270 fn segments_to_text(segments: &[Segment]) -> String {
271 segments.iter().map(|s| s.text.as_str()).collect()
272 }
273
274 #[test]
275 fn test_render_no_padding() {
276 let console = make_console(20);
277 let text = Text::new("Hello", Style::null());
278 let padding = Padding::new(text, PaddingDimensions::Uniform(0), Style::null(), false);
279 let opts = console.options();
280 let segments = padding.gilt_console(&console, &opts);
281 let output = segments_to_text(&segments);
282 assert!(output.contains("Hello"));
283 }
284
285 #[test]
286 fn test_render_with_left_padding() {
287 let console = make_console(20);
288 let text = Text::new("Hi", Style::null());
289 let padding = Padding::new(
290 text,
291 PaddingDimensions::Full(0, 0, 0, 4),
292 Style::null(),
293 true,
294 );
295 let opts = console.options();
296 let segments = padding.gilt_console(&console, &opts);
297 let output = segments_to_text(&segments);
298 assert!(output.contains(" Hi"));
300 }
301
302 #[test]
303 fn test_render_top_bottom_padding() {
304 let console = make_console(20);
305 let text = Text::new("X", Style::null());
306 let padding = Padding::new(
307 text,
308 PaddingDimensions::Full(2, 0, 3, 0),
309 Style::null(),
310 true,
311 );
312 let opts = console.options();
313 let segments = padding.gilt_console(&console, &opts);
314 let output = segments_to_text(&segments);
315 let lines: Vec<&str> = output.split('\n').collect();
316 let non_empty_lines: Vec<&&str> = lines.iter().filter(|l| !l.is_empty()).collect();
319 assert_eq!(non_empty_lines.len(), 6);
320 }
321
322 #[test]
323 fn test_render_expand_fills_width() {
324 let console = make_console(30);
325 let text = Text::new("Hi", Style::null());
326 let padding = Padding::new(text, PaddingDimensions::Uniform(1), Style::null(), true);
327 let opts = console.options();
328 let segments = padding.gilt_console(&console, &opts);
329 let output = segments_to_text(&segments);
330 let lines: Vec<&str> = output.split('\n').collect();
331 let top_line = lines[0];
333 assert_eq!(cell_len(top_line), 30);
334 }
335
336 #[test]
337 fn test_render_no_expand_minimal_width() {
338 let console = make_console(80);
339 let text = Text::new("AB", Style::null());
340 let padding = Padding::new(
341 text,
342 PaddingDimensions::Full(0, 1, 0, 1),
343 Style::null(),
344 false,
345 );
346 let opts = console.options();
347 let segments = padding.gilt_console(&console, &opts);
348 let output = segments_to_text(&segments);
349 let first_line: &str = output.split('\n').next().unwrap();
351 assert_eq!(cell_len(first_line), 4);
352 }
353
354 #[test]
355 fn test_indent_rendering() {
356 let console = make_console(40);
357 let text = Text::new("indented", Style::null());
358 let padding = Padding::indent(text, 8);
359 let opts = console.options();
360 let segments = padding.gilt_console(&console, &opts);
361 let output = segments_to_text(&segments);
362 assert!(output.contains(" indented"));
363 }
364
365 #[test]
366 fn test_measure() {
367 let console = make_console(40);
368 let text = Text::new("Hello", Style::null());
369 let padding = Padding::new(
370 text,
371 PaddingDimensions::Full(0, 2, 0, 2),
372 Style::null(),
373 true,
374 );
375 let opts = console.options();
376 let m = padding.measure(&console, &opts);
377 assert_eq!(m.minimum, 9);
379 assert_eq!(m.maximum, 40);
380 }
381
382 #[test]
383 fn test_measure_no_expand() {
384 let console = make_console(40);
385 let text = Text::new("Hello", Style::null());
386 let padding = Padding::new(
387 text,
388 PaddingDimensions::Full(0, 2, 0, 2),
389 Style::null(),
390 false,
391 );
392 let opts = console.options();
393 let m = padding.measure(&console, &opts);
394 assert_eq!(m.maximum, 9);
396 }
397
398 #[test]
399 fn test_padding_with_styled_content() {
400 let console = make_console(20);
401 let text = Text::styled("Bold", "bold");
402 let padding = Padding::new(text, PaddingDimensions::Uniform(1), Style::null(), true);
403 let opts = console.options();
404 let segments = padding.gilt_console(&console, &opts);
405 let plain: String = segments.iter().map(|s| s.text.as_str()).collect();
406 assert!(plain.contains("Bold"));
407 }
408
409 #[test]
410 fn test_padding_dimensions_equality() {
411 assert_eq!(PaddingDimensions::Uniform(1), PaddingDimensions::Uniform(1));
412 assert_ne!(PaddingDimensions::Uniform(1), PaddingDimensions::Uniform(2));
413 assert_eq!(PaddingDimensions::Pair(1, 2), PaddingDimensions::Pair(1, 2));
414 assert_ne!(PaddingDimensions::Pair(1, 2), PaddingDimensions::Pair(2, 1));
415 }
416}