A simple TUI Library written in Rust
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}