use crate::console::{Console, ConsoleOptions, Renderable, RenderableArc};
use crate::measure::Measurement;
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaddingDimensions {
Uniform(usize),
Pair(usize, usize),
Full(usize, usize, usize, usize),
}
impl PaddingDimensions {
pub fn unpack(&self) -> (usize, usize, usize, usize) {
match *self {
PaddingDimensions::Uniform(v) => (v, v, v, v),
PaddingDimensions::Pair(vert, horiz) => (vert, horiz, vert, horiz),
PaddingDimensions::Full(t, r, b, l) => (t, r, b, l),
}
}
}
impl From<usize> for PaddingDimensions {
fn from(n: usize) -> Self {
PaddingDimensions::Uniform(n)
}
}
impl From<(usize, usize)> for PaddingDimensions {
fn from((v, h): (usize, usize)) -> Self {
PaddingDimensions::Pair(v, h)
}
}
impl From<(usize, usize, usize, usize)> for PaddingDimensions {
fn from((t, r, b, l): (usize, usize, usize, usize)) -> Self {
PaddingDimensions::Full(t, r, b, l)
}
}
impl From<[usize; 1]> for PaddingDimensions {
fn from([n]: [usize; 1]) -> Self {
PaddingDimensions::Uniform(n)
}
}
#[derive(Clone)]
pub struct Padding {
pub content: RenderableArc,
pub top: usize,
pub right: usize,
pub bottom: usize,
pub left: usize,
pub style: Style,
pub expand: bool,
}
impl std::fmt::Debug for Padding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Padding")
.field("content", &"<renderable>")
.field("top", &self.top)
.field("right", &self.right)
.field("bottom", &self.bottom)
.field("left", &self.left)
.field("style", &self.style)
.field("expand", &self.expand)
.finish()
}
}
impl Padding {
pub fn wrap(
content: impl Renderable + Send + Sync + 'static,
pad: impl Into<PaddingDimensions>,
) -> Self {
Self::new(content, pad.into(), Style::null(), true)
}
pub fn new(
content: impl Renderable + Send + Sync + 'static,
pad: PaddingDimensions,
style: Style,
expand: bool,
) -> Self {
let (top, right, bottom, left) = pad.unpack();
Padding {
content: std::sync::Arc::new(content),
top,
right,
bottom,
left,
style,
expand,
}
}
pub fn indent(content: impl Renderable + Send + Sync + 'static, level: usize) -> Self {
Padding::new(
content,
PaddingDimensions::Full(0, 0, 0, level),
Style::null(),
true,
)
}
pub fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
let max_width = options.max_width.saturating_sub(self.left + self.right);
let inner_opts = options.update_width(max_width.max(1));
let content_width = self.content.gilt_measure(console, &inner_opts).maximum;
let min_w = content_width + self.left + self.right;
let max_w = if self.expand {
options.max_width
} else {
min_w.min(options.max_width)
};
Measurement::new(
min_w.min(inner_opts.max_width + self.left + self.right),
max_w,
)
}
}
impl Renderable for Padding {
fn gilt_measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.measure(console, options)
}
fn gilt_console(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let mut segments = Vec::new();
let inner_width_for_measure = options
.max_width
.saturating_sub(self.left + self.right)
.max(1);
let width = if self.expand {
options.max_width
} else {
let measure_opts = options.update_width(inner_width_for_measure);
let content_width = self.content.gilt_measure(console, &measure_opts).maximum;
(content_width + self.left + self.right).min(options.max_width)
};
let inner_width = width.saturating_sub(self.left + self.right).max(1);
let inner_opts = options.update_width(inner_width);
let lines =
console.render_lines(self.content.as_ref(), Some(&inner_opts), None, true, false);
let left_pad = " ".repeat(self.left);
let right_pad_base = self.right;
let blank_line = " ".repeat(width);
for _ in 0..self.top {
segments.push(Segment::styled(&blank_line, self.style.clone()));
segments.push(Segment::line());
}
for line in &lines {
if self.left > 0 {
segments.push(Segment::styled(&left_pad, self.style.clone()));
}
segments.extend(line.iter().cloned());
let line_len = self.left + Segment::get_line_length(line);
let remaining = width.saturating_sub(line_len);
if remaining > 0 {
segments.push(Segment::styled(&" ".repeat(remaining), self.style.clone()));
} else if right_pad_base > 0 && remaining == 0 {
}
segments.push(Segment::line());
}
for _ in 0..self.bottom {
segments.push(Segment::styled(&blank_line, self.style.clone()));
segments.push(Segment::line());
}
segments
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::Text;
use crate::utils::cells::cell_len;
#[test]
fn test_unpack_uniform() {
let pd = PaddingDimensions::Uniform(2);
assert_eq!(pd.unpack(), (2, 2, 2, 2));
}
#[test]
fn test_unpack_pair() {
let pd = PaddingDimensions::Pair(1, 3);
assert_eq!(pd.unpack(), (1, 3, 1, 3));
}
#[test]
fn test_unpack_full() {
let pd = PaddingDimensions::Full(1, 2, 3, 4);
assert_eq!(pd.unpack(), (1, 2, 3, 4));
}
#[test]
fn test_unpack_uniform_zero() {
let pd = PaddingDimensions::Uniform(0);
assert_eq!(pd.unpack(), (0, 0, 0, 0));
}
#[test]
fn test_from_array1_uniform_padding() {
let pd: PaddingDimensions = [3].into();
assert_eq!(
pd,
PaddingDimensions::Uniform(3),
"[3].into() must equal Uniform(3) (matches From<usize> of 3)"
);
assert_eq!(pd.unpack(), (3, 3, 3, 3));
let pd_uniform: PaddingDimensions = 3usize.into();
assert_eq!(pd, pd_uniform);
let pd_full: PaddingDimensions = (3, 3, 3, 3).into();
assert_eq!(pd.unpack(), pd_full.unpack());
}
#[test]
fn test_from_array1_zero() {
let pd: PaddingDimensions = [0].into();
assert_eq!(pd, PaddingDimensions::Uniform(0));
assert_eq!(pd.unpack(), (0, 0, 0, 0));
}
#[test]
fn test_padding_new() {
let text = Text::new("Hello", Style::null());
let padding = Padding::new(
text,
PaddingDimensions::Full(1, 2, 3, 4),
Style::null(),
true,
);
assert_eq!(padding.top, 1);
assert_eq!(padding.right, 2);
assert_eq!(padding.bottom, 3);
assert_eq!(padding.left, 4);
assert!(padding.expand);
}
#[test]
fn test_indent() {
let text = Text::new("Hello", Style::null());
let padding = Padding::indent(text, 4);
assert_eq!(padding.top, 0);
assert_eq!(padding.right, 0);
assert_eq!(padding.bottom, 0);
assert_eq!(padding.left, 4);
assert!(padding.expand);
}
fn make_console(width: usize) -> Console {
Console::builder()
.width(width)
.force_terminal(true)
.no_color(true)
.markup(false)
.build()
}
fn segments_to_text(segments: &[Segment]) -> String {
segments.iter().map(|s| s.text.as_str()).collect()
}
#[test]
fn test_render_no_padding() {
let console = make_console(20);
let text = Text::new("Hello", Style::null());
let padding = Padding::new(text, PaddingDimensions::Uniform(0), Style::null(), false);
let opts = console.options();
let segments = padding.gilt_console(&console, &opts);
let output = segments_to_text(&segments);
assert!(output.contains("Hello"));
}
#[test]
fn test_render_with_left_padding() {
let console = make_console(20);
let text = Text::new("Hi", Style::null());
let padding = Padding::new(
text,
PaddingDimensions::Full(0, 0, 0, 4),
Style::null(),
true,
);
let opts = console.options();
let segments = padding.gilt_console(&console, &opts);
let output = segments_to_text(&segments);
assert!(output.contains(" Hi"));
}
#[test]
fn test_render_top_bottom_padding() {
let console = make_console(20);
let text = Text::new("X", Style::null());
let padding = Padding::new(
text,
PaddingDimensions::Full(2, 0, 3, 0),
Style::null(),
true,
);
let opts = console.options();
let segments = padding.gilt_console(&console, &opts);
let output = segments_to_text(&segments);
let lines: Vec<&str> = output.split('\n').collect();
let non_empty_lines: Vec<&&str> = lines.iter().filter(|l| !l.is_empty()).collect();
assert_eq!(non_empty_lines.len(), 6);
}
#[test]
fn test_render_expand_fills_width() {
let console = make_console(30);
let text = Text::new("Hi", Style::null());
let padding = Padding::new(text, PaddingDimensions::Uniform(1), Style::null(), true);
let opts = console.options();
let segments = padding.gilt_console(&console, &opts);
let output = segments_to_text(&segments);
let lines: Vec<&str> = output.split('\n').collect();
let top_line = lines[0];
assert_eq!(cell_len(top_line), 30);
}
#[test]
fn test_render_no_expand_minimal_width() {
let console = make_console(80);
let text = Text::new("AB", Style::null());
let padding = Padding::new(
text,
PaddingDimensions::Full(0, 1, 0, 1),
Style::null(),
false,
);
let opts = console.options();
let segments = padding.gilt_console(&console, &opts);
let output = segments_to_text(&segments);
let first_line: &str = output.split('\n').next().unwrap();
assert_eq!(cell_len(first_line), 4);
}
#[test]
fn test_indent_rendering() {
let console = make_console(40);
let text = Text::new("indented", Style::null());
let padding = Padding::indent(text, 8);
let opts = console.options();
let segments = padding.gilt_console(&console, &opts);
let output = segments_to_text(&segments);
assert!(output.contains(" indented"));
}
#[test]
fn test_measure() {
let console = make_console(40);
let text = Text::new("Hello", Style::null());
let padding = Padding::new(
text,
PaddingDimensions::Full(0, 2, 0, 2),
Style::null(),
true,
);
let opts = console.options();
let m = padding.measure(&console, &opts);
assert_eq!(m.minimum, 9);
assert_eq!(m.maximum, 40);
}
#[test]
fn test_measure_no_expand() {
let console = make_console(40);
let text = Text::new("Hello", Style::null());
let padding = Padding::new(
text,
PaddingDimensions::Full(0, 2, 0, 2),
Style::null(),
false,
);
let opts = console.options();
let m = padding.measure(&console, &opts);
assert_eq!(m.maximum, 9);
}
#[test]
fn test_padding_with_styled_content() {
let console = make_console(20);
let text = Text::styled("Bold", "bold");
let padding = Padding::new(text, PaddingDimensions::Uniform(1), Style::null(), true);
let opts = console.options();
let segments = padding.gilt_console(&console, &opts);
let plain: String = segments.iter().map(|s| s.text.as_str()).collect();
assert!(plain.contains("Bold"));
}
#[test]
fn test_padding_dimensions_equality() {
assert_eq!(PaddingDimensions::Uniform(1), PaddingDimensions::Uniform(1));
assert_ne!(PaddingDimensions::Uniform(1), PaddingDimensions::Uniform(2));
assert_eq!(PaddingDimensions::Pair(1, 2), PaddingDimensions::Pair(1, 2));
assert_ne!(PaddingDimensions::Pair(1, 2), PaddingDimensions::Pair(2, 1));
}
#[test]
fn padding_gilt_measure_matches_standalone() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello", Style::null());
let padding = Padding::new(
text,
PaddingDimensions::Full(0, 2, 0, 2),
Style::null(),
false,
);
let m_standalone = padding.measure(&console, &opts);
let m_trait = padding.gilt_measure(&console, &opts);
assert_eq!(
m_trait, m_standalone,
"Padding::gilt_measure must delegate to Padding::measure"
);
}
#[test]
fn padding_gilt_measure_expand_matches_standalone() {
let console = make_console(80);
let opts = console.options();
let text = Text::new("Hello World", Style::null());
let padding = Padding::new(text, PaddingDimensions::Uniform(1), Style::null(), true);
let m_standalone = padding.measure(&console, &opts);
let m_trait = padding.gilt_measure(&console, &opts);
assert_eq!(
m_trait, m_standalone,
"Padding::gilt_measure expand must delegate to Padding::measure"
);
}
#[test]
fn padding_new_text_still_compiles() {
let _ = Padding::new(
Text::new("x", Style::null()),
PaddingDimensions::Uniform(0),
Style::null(),
false,
);
}
#[test]
fn padding_wrap_panel_compiles() {
let p = crate::panel::Panel::new(Text::new("x", Style::null()));
let _ = Padding::wrap(p, 1usize);
}
}