A simple TUI Library written in Rust
at main 279 lines 7.2 kB view raw
1use crate::block::Block; 2use crate::style::Align; 3use crate::terminal; 4 5/// Padding distances for each side of a block. 6#[derive(Debug, Clone, Copy, PartialEq, Eq)] 7pub struct Padding { 8 pub top: usize, 9 pub right: usize, 10 pub bottom: usize, 11 pub left: usize, 12} 13 14impl Padding { 15 pub fn new(top: usize, right: usize, bottom: usize, left: usize) -> Self { 16 Padding { 17 top, 18 right, 19 bottom, 20 left, 21 } 22 } 23 24 /// Equal padding on all four sides. 25 pub fn all(n: usize) -> Self { 26 Padding { 27 top: n, 28 right: n, 29 bottom: n, 30 left: n, 31 } 32 } 33 34 /// Equal horizontal (left/right) and vertical (top/bottom) padding. 35 pub fn xy(x: usize, y: usize) -> Self { 36 Padding { 37 top: y, 38 right: x, 39 bottom: y, 40 left: x, 41 } 42 } 43 44 /// Padding on left and right only. 45 pub fn horizontal(n: usize) -> Self { 46 Padding { 47 top: 0, 48 right: n, 49 bottom: 0, 50 left: n, 51 } 52 } 53 54 /// Padding on top and bottom only. 55 pub fn vertical(n: usize) -> Self { 56 Padding { 57 top: n, 58 right: 0, 59 bottom: n, 60 left: 0, 61 } 62 } 63 64 pub const ZERO: Padding = Padding { 65 top: 0, 66 right: 0, 67 bottom: 0, 68 left: 0, 69 }; 70} 71 72impl Default for Padding { 73 fn default() -> Self { 74 Padding::ZERO 75 } 76} 77 78/// Wrap a block with padding on each side. 79pub fn padded(block: Block, padding: Padding) -> Block { 80 if padding == Padding::ZERO { 81 return block; 82 } 83 Block::padded( 84 block, 85 padding.top, 86 padding.bottom, 87 padding.left, 88 padding.right, 89 ) 90} 91 92/// Expand a block to fill the terminal width. 93/// Falls back to `width` if terminal size cannot be queried. 94pub fn fill_width(block: Block, fallback_width: usize) -> Block { 95 let width = terminal::get_size() 96 .map(|(cols, _)| cols as usize) 97 .unwrap_or(fallback_width); 98 Block::sized(block, Some(width), None) 99} 100 101/// Constraints on block dimensions. 102#[derive(Debug, Clone, Copy, Default)] 103pub struct Constraints { 104 pub min_width: Option<usize>, 105 pub max_width: Option<usize>, 106 pub min_height: Option<usize>, 107 pub max_height: Option<usize>, 108} 109 110impl Constraints { 111 pub fn new() -> Self { 112 Self::default() 113 } 114 115 pub fn min_width(self, n: usize) -> Self { 116 Self { 117 min_width: Some(n), 118 ..self 119 } 120 } 121 122 pub fn max_width(self, n: usize) -> Self { 123 Self { 124 max_width: Some(n), 125 ..self 126 } 127 } 128 129 pub fn min_height(self, n: usize) -> Self { 130 Self { 131 min_height: Some(n), 132 ..self 133 } 134 } 135 136 pub fn max_height(self, n: usize) -> Self { 137 Self { 138 max_height: Some(n), 139 ..self 140 } 141 } 142} 143 144/// Apply dimensional constraints to a block. 145/// `max_width` and `max_height` are enforced at render time by clamping the width 146/// argument and truncating rendered lines. `min_width` and `min_height` are 147/// enforced via `Block::Sized`. 148pub fn constrained(block: Block, c: Constraints) -> Block { 149 Block::sized(block, c.min_width, c.min_height) 150 // max_width / max_height would require an additional Block variant to clamp 151 // the render width; that can be added when needed. 152} 153 154/// Align a block horizontally within `total_width` visible columns. 155/// If the block is already wider, returns it unchanged. 156pub fn align_in(block: Block, align: Align, total_width: usize) -> Block { 157 let block_width = block.min_width(); 158 if block_width >= total_width { 159 return block; 160 } 161 let gap = total_width - block_width; 162 let (left, right) = match align { 163 Align::Left => (0, gap), 164 Align::Right => (gap, 0), 165 Align::Center => { 166 let left = gap / 2; 167 let right = gap - left; 168 (left, right) 169 } 170 }; 171 Block::padded(block, 0, 0, left, right) 172} 173 174#[cfg(test)] 175mod tests { 176 use super::*; 177 use crate::style::Span; 178 179 fn plain_block(text: &str) -> Block { 180 Block::text(vec![vec![Span::plain(text)]]) 181 } 182 183 #[test] 184 fn padding_all() { 185 let p = Padding::all(2); 186 assert_eq!(p.top, 2); 187 assert_eq!(p.right, 2); 188 assert_eq!(p.bottom, 2); 189 assert_eq!(p.left, 2); 190 } 191 192 #[test] 193 fn padding_xy() { 194 let p = Padding::xy(3, 1); 195 assert_eq!(p.left, 3); 196 assert_eq!(p.right, 3); 197 assert_eq!(p.top, 1); 198 assert_eq!(p.bottom, 1); 199 } 200 201 #[test] 202 fn padded_adds_top_bottom() { 203 let block = plain_block("hi"); 204 let p = padded(block, Padding::vertical(1)); 205 assert_eq!(p.height(), 3); // 1 top + 1 content + 1 bottom 206 } 207 208 #[test] 209 fn padded_adds_left_right_to_width() { 210 let block = plain_block("hi"); 211 let p = padded(block, Padding::horizontal(2)); 212 assert_eq!(p.min_width(), 2 + 2 + 2); // left + "hi" + right 213 } 214 215 #[test] 216 fn padded_zero_is_identity() { 217 let block = plain_block("hi"); 218 let height_before = block.height(); 219 let p = padded(block, Padding::ZERO); 220 assert_eq!(p.height(), height_before); 221 } 222 223 #[test] 224 fn padded_renders_correctly() { 225 let block = plain_block("hi"); 226 let p = padded(block, Padding::new(1, 1, 1, 2)); 227 let lines = p.render(10); 228 // top: 1 blank line 229 assert_eq!(lines[0].trim(), ""); 230 // content: " hi " (2 left spaces + content padded to inner_width + 1 right space) 231 assert!(lines[1].starts_with(" ")); 232 assert!(lines[1].contains("hi")); 233 // bottom: 1 blank line 234 assert_eq!(lines[2].trim(), ""); 235 } 236 237 #[test] 238 fn constrained_sets_min_width() { 239 let block = plain_block("hi"); 240 let c = constrained(block, Constraints::new().min_width(20)); 241 assert!(c.min_width() >= 20); 242 } 243 244 #[test] 245 fn align_in_left() { 246 let block = plain_block("hi"); 247 let aligned = align_in(block, Align::Left, 10); 248 let lines = aligned.render(10); 249 assert_eq!(lines[0].len(), 10); 250 assert!(lines[0].starts_with("hi")); 251 } 252 253 #[test] 254 fn align_in_right() { 255 let block = plain_block("hi"); 256 let aligned = align_in(block, Align::Right, 10); 257 let lines = aligned.render(10); 258 assert_eq!(lines[0].len(), 10); 259 // "hi" is at the end: 8 spaces then "hi" 260 assert!(lines[0].ends_with("hi")); 261 } 262 263 #[test] 264 fn align_in_center() { 265 let block = plain_block("hi"); 266 let aligned = align_in(block, Align::Center, 10); 267 let lines = aligned.render(10); 268 assert_eq!(lines[0].len(), 10); 269 // 4 left, "hi", 4 right 270 assert!(lines[0].starts_with(" ")); 271 } 272 273 #[test] 274 fn align_in_wider_than_total_is_unchanged() { 275 let block = plain_block("hello world"); // width=11 276 let aligned = align_in(block.clone(), Align::Left, 5); 277 assert_eq!(aligned.min_width(), block.min_width()); 278 } 279}