use crate::block::Block; use crate::style::Align; use crate::terminal; /// Padding distances for each side of a block. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Padding { pub top: usize, pub right: usize, pub bottom: usize, pub left: usize, } impl Padding { pub fn new(top: usize, right: usize, bottom: usize, left: usize) -> Self { Padding { top, right, bottom, left, } } /// Equal padding on all four sides. pub fn all(n: usize) -> Self { Padding { top: n, right: n, bottom: n, left: n, } } /// Equal horizontal (left/right) and vertical (top/bottom) padding. pub fn xy(x: usize, y: usize) -> Self { Padding { top: y, right: x, bottom: y, left: x, } } /// Padding on left and right only. pub fn horizontal(n: usize) -> Self { Padding { top: 0, right: n, bottom: 0, left: n, } } /// Padding on top and bottom only. pub fn vertical(n: usize) -> Self { Padding { top: n, right: 0, bottom: n, left: 0, } } pub const ZERO: Padding = Padding { top: 0, right: 0, bottom: 0, left: 0, }; } impl Default for Padding { fn default() -> Self { Padding::ZERO } } /// Wrap a block with padding on each side. pub fn padded(block: Block, padding: Padding) -> Block { if padding == Padding::ZERO { return block; } Block::padded( block, padding.top, padding.bottom, padding.left, padding.right, ) } /// Expand a block to fill the terminal width. /// Falls back to `width` if terminal size cannot be queried. pub fn fill_width(block: Block, fallback_width: usize) -> Block { let width = terminal::get_size() .map(|(cols, _)| cols as usize) .unwrap_or(fallback_width); Block::sized(block, Some(width), None) } /// Constraints on block dimensions. #[derive(Debug, Clone, Copy, Default)] pub struct Constraints { pub min_width: Option, pub max_width: Option, pub min_height: Option, pub max_height: Option, } impl Constraints { pub fn new() -> Self { Self::default() } pub fn min_width(self, n: usize) -> Self { Self { min_width: Some(n), ..self } } pub fn max_width(self, n: usize) -> Self { Self { max_width: Some(n), ..self } } pub fn min_height(self, n: usize) -> Self { Self { min_height: Some(n), ..self } } pub fn max_height(self, n: usize) -> Self { Self { max_height: Some(n), ..self } } } /// Apply dimensional constraints to a block. /// `max_width` and `max_height` are enforced at render time by clamping the width /// argument and truncating rendered lines. `min_width` and `min_height` are /// enforced via `Block::Sized`. pub fn constrained(block: Block, c: Constraints) -> Block { Block::sized(block, c.min_width, c.min_height) // max_width / max_height would require an additional Block variant to clamp // the render width; that can be added when needed. } /// Align a block horizontally within `total_width` visible columns. /// If the block is already wider, returns it unchanged. pub fn align_in(block: Block, align: Align, total_width: usize) -> Block { let block_width = block.min_width(); if block_width >= total_width { return block; } let gap = total_width - block_width; let (left, right) = match align { Align::Left => (0, gap), Align::Right => (gap, 0), Align::Center => { let left = gap / 2; let right = gap - left; (left, right) } }; Block::padded(block, 0, 0, left, right) } #[cfg(test)] mod tests { use super::*; use crate::style::Span; fn plain_block(text: &str) -> Block { Block::text(vec![vec![Span::plain(text)]]) } #[test] fn padding_all() { let p = Padding::all(2); assert_eq!(p.top, 2); assert_eq!(p.right, 2); assert_eq!(p.bottom, 2); assert_eq!(p.left, 2); } #[test] fn padding_xy() { let p = Padding::xy(3, 1); assert_eq!(p.left, 3); assert_eq!(p.right, 3); assert_eq!(p.top, 1); assert_eq!(p.bottom, 1); } #[test] fn padded_adds_top_bottom() { let block = plain_block("hi"); let p = padded(block, Padding::vertical(1)); assert_eq!(p.height(), 3); // 1 top + 1 content + 1 bottom } #[test] fn padded_adds_left_right_to_width() { let block = plain_block("hi"); let p = padded(block, Padding::horizontal(2)); assert_eq!(p.min_width(), 2 + 2 + 2); // left + "hi" + right } #[test] fn padded_zero_is_identity() { let block = plain_block("hi"); let height_before = block.height(); let p = padded(block, Padding::ZERO); assert_eq!(p.height(), height_before); } #[test] fn padded_renders_correctly() { let block = plain_block("hi"); let p = padded(block, Padding::new(1, 1, 1, 2)); let lines = p.render(10); // top: 1 blank line assert_eq!(lines[0].trim(), ""); // content: " hi " (2 left spaces + content padded to inner_width + 1 right space) assert!(lines[1].starts_with(" ")); assert!(lines[1].contains("hi")); // bottom: 1 blank line assert_eq!(lines[2].trim(), ""); } #[test] fn constrained_sets_min_width() { let block = plain_block("hi"); let c = constrained(block, Constraints::new().min_width(20)); assert!(c.min_width() >= 20); } #[test] fn align_in_left() { let block = plain_block("hi"); let aligned = align_in(block, Align::Left, 10); let lines = aligned.render(10); assert_eq!(lines[0].len(), 10); assert!(lines[0].starts_with("hi")); } #[test] fn align_in_right() { let block = plain_block("hi"); let aligned = align_in(block, Align::Right, 10); let lines = aligned.render(10); assert_eq!(lines[0].len(), 10); // "hi" is at the end: 8 spaces then "hi" assert!(lines[0].ends_with("hi")); } #[test] fn align_in_center() { let block = plain_block("hi"); let aligned = align_in(block, Align::Center, 10); let lines = aligned.render(10); assert_eq!(lines[0].len(), 10); // 4 left, "hi", 4 right assert!(lines[0].starts_with(" ")); } #[test] fn align_in_wider_than_total_is_unchanged() { let block = plain_block("hello world"); // width=11 let aligned = align_in(block.clone(), Align::Left, 5); assert_eq!(aligned.min_width(), block.min_width()); } }