1use std::io::Stdout;
16
17use crate::console::ConsoleOptions;
18use crate::measure::Measurement;
19use crate::segment::{Segment, Segments};
20use crate::style::Style;
21use crate::{Console, Renderable};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum PaddingDimensions {
31 All(usize),
33 TwoWay(usize, usize),
35 FourWay(usize, usize, usize, usize),
37}
38
39impl PaddingDimensions {
40 pub fn unpack(&self) -> (usize, usize, usize, usize) {
44 match *self {
45 PaddingDimensions::All(v) => (v, v, v, v),
46 PaddingDimensions::TwoWay(vert, horiz) => (vert, horiz, vert, horiz),
47 PaddingDimensions::FourWay(top, right, bottom, left) => (top, right, bottom, left),
48 }
49 }
50}
51
52impl From<usize> for PaddingDimensions {
53 fn from(v: usize) -> Self {
54 PaddingDimensions::All(v)
55 }
56}
57
58impl From<(usize,)> for PaddingDimensions {
59 fn from(v: (usize,)) -> Self {
60 PaddingDimensions::All(v.0)
61 }
62}
63
64impl From<(usize, usize)> for PaddingDimensions {
65 fn from(v: (usize, usize)) -> Self {
66 PaddingDimensions::TwoWay(v.0, v.1)
67 }
68}
69
70impl From<(usize, usize, usize, usize)> for PaddingDimensions {
71 fn from(v: (usize, usize, usize, usize)) -> Self {
72 PaddingDimensions::FourWay(v.0, v.1, v.2, v.3)
73 }
74}
75
76pub struct Padding {
91 renderable: Box<dyn Renderable + Send + Sync>,
93 top: usize,
95 right: usize,
97 bottom: usize,
99 left: usize,
101 style: Style,
103 expand: bool,
105}
106
107impl std::fmt::Debug for Padding {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct("Padding")
110 .field("top", &self.top)
111 .field("right", &self.right)
112 .field("bottom", &self.bottom)
113 .field("left", &self.left)
114 .field("style", &self.style)
115 .field("expand", &self.expand)
116 .finish_non_exhaustive()
117 }
118}
119
120impl Padding {
121 pub fn new(
142 renderable: Box<dyn Renderable + Send + Sync>,
143 pad: impl Into<PaddingDimensions>,
144 ) -> Self {
145 let (top, right, bottom, left) = pad.into().unpack();
146 Padding {
147 renderable,
148 top,
149 right,
150 bottom,
151 left,
152 style: Style::default(),
153 expand: true,
154 }
155 }
156
157 pub fn indent(renderable: Box<dyn Renderable + Send + Sync>, level: usize) -> Self {
176 Padding {
177 renderable,
178 top: 0,
179 right: 0,
180 bottom: 0,
181 left: level,
182 style: Style::default(),
183 expand: false,
184 }
185 }
186
187 pub fn unpack(pad: impl Into<PaddingDimensions>) -> (usize, usize, usize, usize) {
205 pad.into().unpack()
206 }
207
208 pub fn with_style(mut self, style: Style) -> Self {
214 self.style = style;
215 self
216 }
217
218 pub fn with_expand(mut self, expand: bool) -> Self {
227 self.expand = expand;
228 self
229 }
230
231 pub fn top(&self) -> usize {
233 self.top
234 }
235
236 pub fn right(&self) -> usize {
238 self.right
239 }
240
241 pub fn bottom(&self) -> usize {
243 self.bottom
244 }
245
246 pub fn left(&self) -> usize {
248 self.left
249 }
250
251 pub fn style(&self) -> Style {
253 self.style
254 }
255
256 pub fn expand(&self) -> bool {
258 self.expand
259 }
260}
261
262impl Renderable for Padding {
263 fn render(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Segments {
264 let mut result = Segments::new();
265
266 let width = if self.expand {
268 options.max_width
269 } else {
270 let inner_measurement = self.renderable.measure(console, options);
272 (inner_measurement.maximum + self.left + self.right).min(options.max_width)
273 };
274
275 let total_padding = self.left + self.right;
278 let (effective_left, effective_right, inner_width) = if total_padding >= width {
279 if total_padding == 0 {
281 (0, 0, width)
282 } else {
283 let ratio_left = self.left as f64 / total_padding as f64;
284 let scaled_left = (width as f64 * ratio_left).round() as usize;
285 let scaled_right = width.saturating_sub(scaled_left);
286 (scaled_left, scaled_right, 0)
287 }
288 } else {
289 (self.left, self.right, width - total_padding)
290 };
291
292 let render_options = options.update_width(inner_width);
294
295 let render_options = if let Some(h) = render_options.height {
297 let new_height = h.saturating_sub(self.top + self.bottom);
298 render_options.update_height(new_height)
299 } else {
300 render_options
301 };
302
303 let style_arg = if self.style.is_null() {
308 None
309 } else {
310 Some(self.style)
311 };
312 let lines = console.render_lines(
313 self.renderable.as_ref(),
314 Some(&render_options),
315 style_arg, true, false, );
319
320 let left_padding = if effective_left > 0 {
322 Some(Segment::styled(" ".repeat(effective_left), self.style))
323 } else {
324 None
325 };
326
327 let right_padding_and_newline = if effective_right > 0 {
328 vec![
329 Segment::styled(" ".repeat(effective_right), self.style),
330 Segment::line(),
331 ]
332 } else {
333 vec![Segment::line()]
334 };
335
336 let blank_line = Segment::styled(format!("{}\n", " ".repeat(width)), self.style);
338
339 for _ in 0..self.top {
341 result.push(blank_line.clone());
342 }
343
344 for line in lines {
346 if let Some(ref left) = left_padding {
347 result.push(left.clone());
348 }
349 for segment in line {
350 result.push(segment);
351 }
352 for segment in &right_padding_and_newline {
353 result.push(segment.clone());
354 }
355 }
356
357 for _ in 0..self.bottom {
359 result.push(blank_line.clone());
360 }
361
362 result
363 }
364
365 fn measure(&self, console: &Console<Stdout>, options: &ConsoleOptions) -> Measurement {
366 let max_width = options.max_width;
367 let extra_width = self.left + self.right;
368
369 if max_width < extra_width + 1 {
371 return Measurement::new(max_width, max_width);
372 }
373
374 let inner_measurement = self.renderable.measure(console, options);
376
377 let measurement = Measurement::new(
379 inner_measurement.minimum + extra_width,
380 inner_measurement.maximum + extra_width,
381 );
382
383 measurement.with_maximum(max_width)
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::text::Text;
392
393 #[test]
396 fn test_unpack_single_value() {
397 assert_eq!(Padding::unpack(5), (5, 5, 5, 5));
398 }
399
400 #[test]
401 fn test_unpack_single_tuple() {
402 assert_eq!(Padding::unpack((5,)), (5, 5, 5, 5));
403 }
404
405 #[test]
406 fn test_unpack_two_values() {
407 assert_eq!(Padding::unpack((2, 4)), (2, 4, 2, 4));
409 }
410
411 #[test]
412 fn test_unpack_four_values() {
413 assert_eq!(Padding::unpack((1, 2, 3, 4)), (1, 2, 3, 4));
415 }
416
417 #[test]
418 fn test_unpack_zero() {
419 assert_eq!(Padding::unpack(0), (0, 0, 0, 0));
420 }
421
422 #[test]
425 fn test_padding_new() {
426 let text = Text::plain("Hello");
427 let padding = Padding::new(Box::new(text), (1, 2, 3, 4));
428 assert_eq!(padding.top(), 1);
429 assert_eq!(padding.right(), 2);
430 assert_eq!(padding.bottom(), 3);
431 assert_eq!(padding.left(), 4);
432 assert!(padding.expand());
433 }
434
435 #[test]
436 fn test_padding_indent() {
437 let text = Text::plain("Hello");
438 let padding = Padding::indent(Box::new(text), 4);
439 assert_eq!(padding.top(), 0);
440 assert_eq!(padding.right(), 0);
441 assert_eq!(padding.bottom(), 0);
442 assert_eq!(padding.left(), 4);
443 assert!(!padding.expand());
444 }
445
446 #[test]
447 fn test_padding_with_style() {
448 let text = Text::plain("Hello");
449 let style = Style::new().with_bold(true);
450 let padding = Padding::new(Box::new(text), 1).with_style(style);
451 assert_eq!(padding.style().bold, Some(true));
452 }
453
454 #[test]
455 fn test_padding_with_expand() {
456 let text = Text::plain("Hello");
457 let padding = Padding::new(Box::new(text), 1).with_expand(false);
458 assert!(!padding.expand());
459 }
460
461 #[test]
464 fn test_padding_render_basic() {
465 let text = Text::plain("Hello");
466 let padding = Padding::new(Box::new(text), (0, 2, 0, 2));
467 let console = Console::with_options(ConsoleOptions {
468 max_width: 20,
469 ..Default::default()
470 });
471 let options = console.options().clone();
472
473 let segments = padding.render(&console, &options);
474 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
475
476 assert!(output.contains(" Hello")); assert!(output.ends_with('\n'));
479 }
480
481 #[test]
482 fn test_padding_render_with_top_bottom() {
483 let text = Text::plain("X");
484 let padding = Padding::new(Box::new(text), (1, 0, 1, 0));
485 let console = Console::with_options(ConsoleOptions {
486 max_width: 10,
487 ..Default::default()
488 });
489 let options = console.options().clone();
490
491 let segments = padding.render(&console, &options);
492 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
493 let lines: Vec<&str> = output.lines().collect();
494
495 assert!(lines.len() >= 2); }
499
500 #[test]
501 fn test_padding_render_expand_true() {
502 let text = Text::plain("Hi");
503 let padding = Padding::new(Box::new(text), (0, 0, 0, 0)).with_expand(true);
504 let console = Console::with_options(ConsoleOptions {
505 max_width: 10,
506 ..Default::default()
507 });
508 let options = console.options().clone();
509
510 let segments = padding.render(&console, &options);
511
512 let total_text: String = segments
514 .iter()
515 .filter(|s| !s.text.contains('\n'))
516 .map(|s| s.text.to_string())
517 .collect();
518
519 assert_eq!(crate::cells::cell_len(&total_text), 10);
521 }
522
523 #[test]
524 fn test_padding_render_expand_false() {
525 let text = Text::plain("Hi");
526 let padding = Padding::new(Box::new(text), (0, 1, 0, 1)).with_expand(false);
527 let console = Console::with_options(ConsoleOptions {
528 max_width: 20,
529 ..Default::default()
530 });
531 let options = console.options().clone();
532
533 let segments = padding.render(&console, &options);
534 let output: String = segments.iter().map(|s| s.text.to_string()).collect();
535
536 assert!(output.contains(" Hi ")); }
540
541 #[test]
544 fn test_padding_measure_basic() {
545 let text = Text::plain("Hello"); let padding = Padding::new(Box::new(text), (0, 2, 0, 2)); let console = Console::with_options(ConsoleOptions {
548 max_width: 80,
549 ..Default::default()
550 });
551 let options = console.options().clone();
552
553 let measurement = padding.measure(&console, &options);
554 assert_eq!(measurement.minimum, 9);
557 assert_eq!(measurement.maximum, 9);
558 }
559
560 #[test]
561 fn test_padding_measure_with_words() {
562 let text = Text::plain("Hello World"); let padding = Padding::new(Box::new(text), (0, 1, 0, 1)); let console = Console::with_options(ConsoleOptions {
565 max_width: 80,
566 ..Default::default()
567 });
568 let options = console.options().clone();
569
570 let measurement = padding.measure(&console, &options);
571 assert_eq!(measurement.minimum, 7); assert_eq!(measurement.maximum, 13); }
574
575 #[test]
576 fn test_padding_measure_clamped() {
577 let text = Text::plain("Hello World");
578 let padding = Padding::new(Box::new(text), (0, 2, 0, 2)); let console = Console::with_options(ConsoleOptions {
580 max_width: 10,
581 ..Default::default()
582 });
583 let options = console.options().clone();
584
585 let measurement = padding.measure(&console, &options);
586 assert!(measurement.maximum <= 10);
588 }
589
590 #[test]
591 fn test_padding_measure_insufficient_width() {
592 let text = Text::plain("Hi");
593 let padding = Padding::new(Box::new(text), (0, 5, 0, 5)); let console = Console::with_options(ConsoleOptions {
595 max_width: 8,
596 ..Default::default()
597 });
598 let options = console.options().clone();
599
600 let measurement = padding.measure(&console, &options);
601 assert_eq!(measurement.minimum, 8);
603 assert_eq!(measurement.maximum, 8);
604 }
605
606 #[test]
607 fn test_padding_style_applies_to_content() {
608 use crate::color::SimpleColor;
609
610 let text = Text::plain("Hi");
611 let style = Style::new().with_bgcolor(SimpleColor::Standard(4)); let padding = Padding::new(Box::new(text), (0, 0, 0, 0)).with_style(style);
613 let console = Console::with_options(ConsoleOptions {
614 max_width: 10,
615 ..Default::default()
616 });
617 let options = console.options().clone();
618
619 let segments = padding.render(&console, &options);
620
621 let content_seg = segments.iter().find(|s| s.text.contains("Hi"));
623 assert!(content_seg.is_some(), "Should find 'Hi' segment");
624 let seg = content_seg.unwrap();
625 let seg_style = seg.style.unwrap_or_default();
626 assert!(
627 seg_style.bgcolor.is_some(),
628 "Content should have background color from padding style"
629 );
630 }
631
632 #[test]
635 fn test_padding_is_send_sync() {
636 fn assert_send<T: Send>() {}
637 fn assert_sync<T: Sync>() {}
638 assert_send::<Padding>();
639 assert_sync::<Padding>();
640 }
641
642 #[test]
643 fn test_padding_dimensions_is_send_sync() {
644 fn assert_send<T: Send>() {}
645 fn assert_sync<T: Sync>() {}
646 assert_send::<PaddingDimensions>();
647 assert_sync::<PaddingDimensions>();
648 }
649
650 #[test]
653 fn test_padding_debug() {
654 let text = Text::plain("Hello");
655 let padding = Padding::new(Box::new(text), (1, 2, 3, 4));
656 let debug_str = format!("{:?}", padding);
657 assert!(debug_str.contains("Padding"));
658 assert!(debug_str.contains("top: 1"));
659 assert!(debug_str.contains("right: 2"));
660 assert!(debug_str.contains("bottom: 3"));
661 assert!(debug_str.contains("left: 4"));
662 }
663}