1#![forbid(unsafe_code)]
2
3use crate::Widget;
6use ftui_core::geometry::{Rect, Sides};
7use ftui_layout::{Constraint, Flex};
8use ftui_render::frame::Frame;
9
10pub struct Column<'a> {
12 widget: Box<dyn Widget + 'a>,
13 constraint: Constraint,
14 padding: Sides,
15}
16
17impl std::fmt::Debug for Column<'_> {
18 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19 f.debug_struct("Column")
20 .field("widget", &"<dyn Widget>")
21 .field("constraint", &self.constraint)
22 .field("padding", &self.padding)
23 .finish()
24 }
25}
26
27impl<'a> Column<'a> {
28 pub fn new(widget: impl Widget + 'a, constraint: Constraint) -> Self {
30 Self {
31 widget: Box::new(widget),
32 constraint,
33 padding: Sides::default(),
34 }
35 }
36
37 #[must_use]
39 pub fn padding(mut self, padding: Sides) -> Self {
40 self.padding = padding;
41 self
42 }
43
44 #[must_use]
46 pub fn constraint(mut self, constraint: Constraint) -> Self {
47 self.constraint = constraint;
48 self
49 }
50}
51
52#[derive(Debug, Default)]
54pub struct Columns<'a> {
55 columns: Vec<Column<'a>>,
56 gap: u16,
57}
58
59impl<'a> Columns<'a> {
60 pub fn new() -> Self {
62 Self::default()
63 }
64
65 #[must_use]
67 pub fn gap(mut self, gap: u16) -> Self {
68 self.gap = gap;
69 self
70 }
71
72 #[must_use]
74 pub fn push(mut self, column: Column<'a>) -> Self {
75 self.columns.push(column);
76 self
77 }
78
79 #[must_use]
81 pub fn column(mut self, widget: impl Widget + 'a, constraint: Constraint) -> Self {
82 self.columns.push(Column::new(widget, constraint));
83 self
84 }
85
86 #[must_use]
88 #[allow(clippy::should_implement_trait)] pub fn add(mut self, widget: impl Widget + 'a) -> Self {
90 self.columns.push(Column::new(widget, Constraint::Fill));
91 self
92 }
93}
94
95struct ScissorGuard<'a, 'pool> {
96 frame: &'a mut Frame<'pool>,
97}
98
99impl<'a, 'pool> ScissorGuard<'a, 'pool> {
100 fn new(frame: &'a mut Frame<'pool>, rect: Rect) -> Self {
101 frame.buffer.push_scissor(rect);
102 Self { frame }
103 }
104
105 fn frame_mut(&mut self) -> &mut Frame<'pool> {
106 self.frame
107 }
108}
109
110impl Drop for ScissorGuard<'_, '_> {
111 fn drop(&mut self) {
112 self.frame.buffer.pop_scissor();
113 }
114}
115
116impl Widget for Columns<'_> {
117 fn render(&self, area: Rect, frame: &mut Frame) {
118 #[cfg(feature = "tracing")]
119 let _span = tracing::debug_span!(
120 "widget_render",
121 widget = "Columns",
122 x = area.x,
123 y = area.y,
124 w = area.width,
125 h = area.height
126 )
127 .entered();
128
129 if area.is_empty() {
130 return;
131 }
132
133 for y in area.y..area.bottom() {
137 for x in area.x..area.right() {
138 if let Some(cell) = frame.buffer.get_mut(x, y) {
139 cell.content = ftui_render::cell::CellContent::EMPTY;
140 }
141 }
142 }
143
144 if self.columns.is_empty() {
145 return;
146 }
147
148 let flex = Flex::horizontal()
149 .gap(self.gap)
150 .constraints(self.columns.iter().map(|c| c.constraint));
151 let rects = flex.split(area);
152
153 for (col, rect) in self.columns.iter().zip(rects) {
154 if rect.is_empty() {
155 continue;
156 }
157 let inner = rect.inner(col.padding);
158 if inner.is_empty() {
159 continue;
160 }
161
162 let mut guard = ScissorGuard::new(frame, inner);
163 col.widget.render(inner, guard.frame_mut());
164 }
165 }
166
167 fn is_essential(&self) -> bool {
168 self.columns.iter().any(|c| c.widget.is_essential())
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use ftui_render::budget::DegradationLevel;
176 use ftui_render::cell::Cell;
177 use ftui_render::grapheme_pool::GraphemePool;
178 use std::cell::RefCell;
179 use std::rc::Rc;
180
181 #[derive(Clone, Debug)]
182 struct Record {
183 rects: Rc<RefCell<Vec<Rect>>>,
184 }
185
186 impl Record {
187 fn new() -> (Self, Rc<RefCell<Vec<Rect>>>) {
188 let rects = Rc::new(RefCell::new(Vec::new()));
189 (
190 Self {
191 rects: rects.clone(),
192 },
193 rects,
194 )
195 }
196 }
197
198 impl Widget for Record {
199 fn render(&self, area: Rect, _frame: &mut Frame) {
200 self.rects.borrow_mut().push(area);
201 }
202 }
203
204 #[test]
205 fn equal_columns_split_evenly() {
206 let (a, a_rects) = Record::new();
207 let (b, b_rects) = Record::new();
208 let (c, c_rects) = Record::new();
209
210 let columns = Columns::new().add(a).add(b).add(c).gap(0);
211
212 let mut pool = GraphemePool::new();
213 let mut frame = Frame::new(12, 2, &mut pool);
214 columns.render(Rect::new(0, 0, 12, 2), &mut frame);
215
216 let a = a_rects.borrow()[0];
217 let b = b_rects.borrow()[0];
218 let c = c_rects.borrow()[0];
219
220 assert_eq!(a, Rect::new(0, 0, 4, 2));
221 assert_eq!(b, Rect::new(4, 0, 4, 2));
222 assert_eq!(c, Rect::new(8, 0, 4, 2));
223 }
224
225 #[test]
226 fn fixed_columns_with_gap() {
227 let (a, a_rects) = Record::new();
228 let (b, b_rects) = Record::new();
229
230 let columns = Columns::new()
231 .column(a, Constraint::Fixed(4))
232 .column(b, Constraint::Fixed(4))
233 .gap(2);
234
235 let mut pool = GraphemePool::new();
236 let mut frame = Frame::new(20, 1, &mut pool);
237 columns.render(Rect::new(0, 0, 20, 1), &mut frame);
238
239 let a = a_rects.borrow()[0];
240 let b = b_rects.borrow()[0];
241
242 assert_eq!(a, Rect::new(0, 0, 4, 1));
243 assert_eq!(b, Rect::new(6, 0, 4, 1));
244 }
245
246 #[test]
247 fn ratio_columns_split_proportionally() {
248 let (a, a_rects) = Record::new();
249 let (b, b_rects) = Record::new();
250
251 let columns = Columns::new()
252 .column(a, Constraint::Ratio(1, 3))
253 .column(b, Constraint::Ratio(2, 3));
254
255 let mut pool = GraphemePool::new();
256 let mut frame = Frame::new(30, 1, &mut pool);
257 columns.render(Rect::new(0, 0, 30, 1), &mut frame);
258
259 let a = a_rects.borrow()[0];
260 let b = b_rects.borrow()[0];
261
262 assert_eq!(a.width + b.width, 30);
263 assert_eq!(a.width, 10);
264 assert_eq!(b.width, 20);
265 }
266
267 #[test]
268 fn column_padding_applies_to_child_area() {
269 let (a, a_rects) = Record::new();
270 let columns =
271 Columns::new().push(Column::new(a, Constraint::Fixed(6)).padding(Sides::all(1)));
272
273 let mut pool = GraphemePool::new();
274 let mut frame = Frame::new(6, 3, &mut pool);
275 columns.render(Rect::new(0, 0, 6, 3), &mut frame);
276
277 let rect = a_rects.borrow()[0];
278 assert_eq!(rect, Rect::new(1, 1, 4, 1));
279 }
280
281 #[test]
282 fn empty_columns_does_not_panic() {
283 let columns = Columns::new();
284 let mut pool = GraphemePool::new();
285 let mut frame = Frame::new(10, 5, &mut pool);
286 columns.render(Rect::new(0, 0, 10, 5), &mut frame);
287 }
288
289 #[test]
290 fn zero_area_does_not_panic() {
291 let (a, a_rects) = Record::new();
292 let columns = Columns::new().add(a);
293 let mut pool = GraphemePool::new();
294 let mut frame = Frame::new(1, 1, &mut pool);
295 columns.render(Rect::new(0, 0, 0, 0), &mut frame);
296 assert!(a_rects.borrow().is_empty());
297 }
298
299 #[test]
300 fn single_column_gets_full_width() {
301 let (a, a_rects) = Record::new();
302 let columns = Columns::new().column(a, Constraint::Min(0));
303
304 let mut pool = GraphemePool::new();
305 let mut frame = Frame::new(20, 3, &mut pool);
306 columns.render(Rect::new(0, 0, 20, 3), &mut frame);
307
308 let rect = a_rects.borrow()[0];
309 assert_eq!(rect.width, 20);
310 assert_eq!(rect.height, 3);
311 }
312
313 #[test]
314 fn fixed_and_fill_columns() {
315 let (a, a_rects) = Record::new();
316 let (b, b_rects) = Record::new();
317
318 let columns = Columns::new()
319 .column(a, Constraint::Fixed(5))
320 .column(b, Constraint::Min(0));
321
322 let mut pool = GraphemePool::new();
323 let mut frame = Frame::new(20, 1, &mut pool);
324 columns.render(Rect::new(0, 0, 20, 1), &mut frame);
325
326 let a = a_rects.borrow()[0];
327 let b = b_rects.borrow()[0];
328 assert_eq!(a.width, 5);
329 assert_eq!(b.width, 15);
330 }
331
332 #[test]
333 fn is_essential_delegates_to_children() {
334 struct Essential;
335 impl Widget for Essential {
336 fn render(&self, _area: Rect, _frame: &mut Frame) {}
337 fn is_essential(&self) -> bool {
338 true
339 }
340 }
341
342 let columns = Columns::new().add(Essential);
343 assert!(columns.is_essential());
344
345 let (non_essential, _) = Record::new();
346 let columns2 = Columns::new().add(non_essential);
347 assert!(!columns2.is_essential());
348 }
349
350 #[test]
351 fn column_constraint_setter() {
352 let (a, _) = Record::new();
353 let col = Column::new(a, Constraint::Fixed(5)).constraint(Constraint::Fixed(10));
354 assert_eq!(col.constraint, Constraint::Fixed(10));
355 }
356
357 #[test]
358 fn all_columns_receive_same_height() {
359 let (a, a_rects) = Record::new();
360 let (b, b_rects) = Record::new();
361 let (c, c_rects) = Record::new();
362
363 let columns = Columns::new().add(a).add(b).add(c);
364
365 let mut pool = GraphemePool::new();
366 let mut frame = Frame::new(12, 5, &mut pool);
367 columns.render(Rect::new(0, 0, 12, 5), &mut frame);
368
369 let a = a_rects.borrow()[0];
370 let b = b_rects.borrow()[0];
371 let c = c_rects.borrow()[0];
372
373 assert_eq!(a.height, 5);
374 assert_eq!(b.height, 5);
375 assert_eq!(c.height, 5);
376 }
377
378 #[test]
379 fn column_debug_format() {
380 let (a, _) = Record::new();
381 let col = Column::new(a, Constraint::Fixed(5));
382 let dbg = format!("{:?}", col);
383 assert!(dbg.contains("Column"));
384 assert!(dbg.contains("<dyn Widget>"));
385 }
386
387 #[test]
388 fn columns_default_is_empty() {
389 let cols = Columns::default();
390 assert!(cols.columns.is_empty());
391 assert_eq!(cols.gap, 0);
392 }
393
394 #[test]
395 fn column_builder_chain() {
396 let (a, _) = Record::new();
397 let col = Column::new(a, Constraint::Fixed(5))
398 .padding(Sides::all(2))
399 .constraint(Constraint::Ratio(1, 3));
400 assert_eq!(col.constraint, Constraint::Ratio(1, 3));
401 assert_eq!(col.padding, Sides::all(2));
402 }
403
404 #[test]
405 fn many_columns_with_gap() {
406 let mut rects_all = Vec::new();
407 let mut cols = Columns::new().gap(1);
408 for _ in 0..5 {
409 let (rec, rects) = Record::new();
410 rects_all.push(rects);
411 cols = cols.column(rec, Constraint::Fixed(2));
412 }
413
414 let mut pool = GraphemePool::new();
415 let mut frame = Frame::new(20, 1, &mut pool);
416 cols.render(Rect::new(0, 0, 20, 1), &mut frame);
417
418 for (i, rects) in rects_all.iter().enumerate() {
420 let r = rects.borrow()[0];
421 assert_eq!(r.width, 2, "column {i} should be width 2");
422 }
423
424 for i in 0..4 {
426 let a = rects_all[i].borrow()[0];
427 let b = rects_all[i + 1].borrow()[0];
428 assert!(
429 b.x >= a.right(),
430 "column {} (right={}) overlaps column {} (x={})",
431 i,
432 a.right(),
433 i + 1,
434 b.x
435 );
436 }
437 }
438
439 #[test]
440 fn skeleton_still_renders_essential_child() {
441 struct Essential;
442
443 impl Widget for Essential {
444 fn render(&self, area: Rect, frame: &mut Frame) {
445 frame.buffer.set(area.x, area.y, Cell::from_char('E'));
446 }
447
448 fn is_essential(&self) -> bool {
449 true
450 }
451 }
452
453 let columns = Columns::new().add(Essential);
454 let mut pool = GraphemePool::new();
455 let mut frame = Frame::new(4, 1, &mut pool);
456 frame.buffer.degradation = DegradationLevel::Skeleton;
457
458 columns.render(Rect::new(0, 0, 4, 1), &mut frame);
459
460 assert_eq!(
461 frame
462 .buffer
463 .get(0, 0)
464 .and_then(|cell| cell.content.as_char()),
465 Some('E')
466 );
467 }
468
469 #[derive(Debug)]
470 struct Marker(char);
471
472 impl Widget for Marker {
473 fn render(&self, area: Rect, frame: &mut Frame) {
474 frame.buffer.set(area.x, area.y, Cell::from_char(self.0));
475 }
476 }
477
478 #[test]
479 fn render_fewer_columns_clears_removed_column_output() {
480 let two = Columns::new().add(Marker('A')).add(Marker('B'));
481 let one = Columns::new().add(Marker('A'));
482 let area = Rect::new(0, 0, 8, 1);
483 let mut pool = GraphemePool::new();
484 let mut frame = Frame::new(8, 1, &mut pool);
485
486 two.render(area, &mut frame);
487 one.render(area, &mut frame);
488
489 assert_eq!(frame.buffer.get(0, 0).unwrap().content.as_char(), Some('A'));
490 assert!(frame.buffer.get(4, 0).unwrap().is_empty());
491 }
492
493 #[test]
494 fn empty_columns_clears_previous_content() {
495 let filled = Columns::new().add(Marker('A'));
496 let empty = Columns::new();
497 let area = Rect::new(0, 0, 4, 1);
498 let mut pool = GraphemePool::new();
499 let mut frame = Frame::new(4, 1, &mut pool);
500
501 filled.render(area, &mut frame);
502 empty.render(area, &mut frame);
503
504 assert!(frame.buffer.get(0, 0).unwrap().is_empty());
505 assert!(frame.buffer.get(3, 0).unwrap().is_empty());
506 }
507}