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
91 .push(Column::new(widget, Constraint::Ratio(1, 1)));
92 self
93 }
94}
95
96struct ScissorGuard<'a, 'pool> {
97 frame: &'a mut Frame<'pool>,
98}
99
100impl<'a, 'pool> ScissorGuard<'a, 'pool> {
101 fn new(frame: &'a mut Frame<'pool>, rect: Rect) -> Self {
102 frame.buffer.push_scissor(rect);
103 Self { frame }
104 }
105
106 fn frame_mut(&mut self) -> &mut Frame<'pool> {
107 self.frame
108 }
109}
110
111impl Drop for ScissorGuard<'_, '_> {
112 fn drop(&mut self) {
113 self.frame.buffer.pop_scissor();
114 }
115}
116
117impl Widget for Columns<'_> {
118 fn render(&self, area: Rect, frame: &mut Frame) {
119 #[cfg(feature = "tracing")]
120 let _span = tracing::debug_span!(
121 "widget_render",
122 widget = "Columns",
123 x = area.x,
124 y = area.y,
125 w = area.width,
126 h = area.height
127 )
128 .entered();
129
130 if area.is_empty() || self.columns.is_empty() {
131 return;
132 }
133
134 if !frame.buffer.degradation.render_content() {
135 return;
136 }
137
138 let flex = Flex::horizontal()
139 .gap(self.gap)
140 .constraints(self.columns.iter().map(|c| c.constraint));
141 let rects = flex.split(area);
142
143 for (col, rect) in self.columns.iter().zip(rects) {
144 if rect.is_empty() {
145 continue;
146 }
147 let inner = rect.inner(col.padding);
148 if inner.is_empty() {
149 continue;
150 }
151
152 let mut guard = ScissorGuard::new(frame, inner);
153 col.widget.render(inner, guard.frame_mut());
154 }
155 }
156
157 fn is_essential(&self) -> bool {
158 self.columns.iter().any(|c| c.widget.is_essential())
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use ftui_render::grapheme_pool::GraphemePool;
166 use std::cell::RefCell;
167 use std::rc::Rc;
168
169 #[derive(Clone, Debug)]
170 struct Record {
171 rects: Rc<RefCell<Vec<Rect>>>,
172 }
173
174 impl Record {
175 fn new() -> (Self, Rc<RefCell<Vec<Rect>>>) {
176 let rects = Rc::new(RefCell::new(Vec::new()));
177 (
178 Self {
179 rects: rects.clone(),
180 },
181 rects,
182 )
183 }
184 }
185
186 impl Widget for Record {
187 fn render(&self, area: Rect, _frame: &mut Frame) {
188 self.rects.borrow_mut().push(area);
189 }
190 }
191
192 #[test]
193 fn equal_columns_split_evenly() {
194 let (a, a_rects) = Record::new();
195 let (b, b_rects) = Record::new();
196 let (c, c_rects) = Record::new();
197
198 let columns = Columns::new().add(a).add(b).add(c).gap(0);
199
200 let mut pool = GraphemePool::new();
201 let mut frame = Frame::new(12, 2, &mut pool);
202 columns.render(Rect::new(0, 0, 12, 2), &mut frame);
203
204 let a = a_rects.borrow()[0];
205 let b = b_rects.borrow()[0];
206 let c = c_rects.borrow()[0];
207
208 assert_eq!(a, Rect::new(0, 0, 4, 2));
209 assert_eq!(b, Rect::new(4, 0, 4, 2));
210 assert_eq!(c, Rect::new(8, 0, 4, 2));
211 }
212
213 #[test]
214 fn fixed_columns_with_gap() {
215 let (a, a_rects) = Record::new();
216 let (b, b_rects) = Record::new();
217
218 let columns = Columns::new()
219 .column(a, Constraint::Fixed(4))
220 .column(b, Constraint::Fixed(4))
221 .gap(2);
222
223 let mut pool = GraphemePool::new();
224 let mut frame = Frame::new(20, 1, &mut pool);
225 columns.render(Rect::new(0, 0, 20, 1), &mut frame);
226
227 let a = a_rects.borrow()[0];
228 let b = b_rects.borrow()[0];
229
230 assert_eq!(a, Rect::new(0, 0, 4, 1));
231 assert_eq!(b, Rect::new(6, 0, 4, 1));
232 }
233
234 #[test]
235 fn ratio_columns_split_proportionally() {
236 let (a, a_rects) = Record::new();
237 let (b, b_rects) = Record::new();
238
239 let columns = Columns::new()
240 .column(a, Constraint::Ratio(1, 3))
241 .column(b, Constraint::Ratio(2, 3));
242
243 let mut pool = GraphemePool::new();
244 let mut frame = Frame::new(30, 1, &mut pool);
245 columns.render(Rect::new(0, 0, 30, 1), &mut frame);
246
247 let a = a_rects.borrow()[0];
248 let b = b_rects.borrow()[0];
249
250 assert_eq!(a.width + b.width, 30);
251 assert_eq!(a.width, 10);
252 assert_eq!(b.width, 20);
253 }
254
255 #[test]
256 fn column_padding_applies_to_child_area() {
257 let (a, a_rects) = Record::new();
258 let columns =
259 Columns::new().push(Column::new(a, Constraint::Fixed(6)).padding(Sides::all(1)));
260
261 let mut pool = GraphemePool::new();
262 let mut frame = Frame::new(6, 3, &mut pool);
263 columns.render(Rect::new(0, 0, 6, 3), &mut frame);
264
265 let rect = a_rects.borrow()[0];
266 assert_eq!(rect, Rect::new(1, 1, 4, 1));
267 }
268
269 #[test]
270 fn empty_columns_does_not_panic() {
271 let columns = Columns::new();
272 let mut pool = GraphemePool::new();
273 let mut frame = Frame::new(10, 5, &mut pool);
274 columns.render(Rect::new(0, 0, 10, 5), &mut frame);
275 }
276
277 #[test]
278 fn zero_area_does_not_panic() {
279 let (a, a_rects) = Record::new();
280 let columns = Columns::new().add(a);
281 let mut pool = GraphemePool::new();
282 let mut frame = Frame::new(1, 1, &mut pool);
283 columns.render(Rect::new(0, 0, 0, 0), &mut frame);
284 assert!(a_rects.borrow().is_empty());
285 }
286
287 #[test]
288 fn single_column_gets_full_width() {
289 let (a, a_rects) = Record::new();
290 let columns = Columns::new().column(a, Constraint::Min(0));
291
292 let mut pool = GraphemePool::new();
293 let mut frame = Frame::new(20, 3, &mut pool);
294 columns.render(Rect::new(0, 0, 20, 3), &mut frame);
295
296 let rect = a_rects.borrow()[0];
297 assert_eq!(rect.width, 20);
298 assert_eq!(rect.height, 3);
299 }
300
301 #[test]
302 fn fixed_and_fill_columns() {
303 let (a, a_rects) = Record::new();
304 let (b, b_rects) = Record::new();
305
306 let columns = Columns::new()
307 .column(a, Constraint::Fixed(5))
308 .column(b, Constraint::Min(0));
309
310 let mut pool = GraphemePool::new();
311 let mut frame = Frame::new(20, 1, &mut pool);
312 columns.render(Rect::new(0, 0, 20, 1), &mut frame);
313
314 let a = a_rects.borrow()[0];
315 let b = b_rects.borrow()[0];
316 assert_eq!(a.width, 5);
317 assert_eq!(b.width, 15);
318 }
319
320 #[test]
321 fn is_essential_delegates_to_children() {
322 struct Essential;
323 impl Widget for Essential {
324 fn render(&self, _area: Rect, _frame: &mut Frame) {}
325 fn is_essential(&self) -> bool {
326 true
327 }
328 }
329
330 let columns = Columns::new().add(Essential);
331 assert!(columns.is_essential());
332
333 let (non_essential, _) = Record::new();
334 let columns2 = Columns::new().add(non_essential);
335 assert!(!columns2.is_essential());
336 }
337
338 #[test]
339 fn column_constraint_setter() {
340 let (a, _) = Record::new();
341 let col = Column::new(a, Constraint::Fixed(5)).constraint(Constraint::Fixed(10));
342 assert_eq!(col.constraint, Constraint::Fixed(10));
343 }
344
345 #[test]
346 fn all_columns_receive_same_height() {
347 let (a, a_rects) = Record::new();
348 let (b, b_rects) = Record::new();
349 let (c, c_rects) = Record::new();
350
351 let columns = Columns::new().add(a).add(b).add(c);
352
353 let mut pool = GraphemePool::new();
354 let mut frame = Frame::new(12, 5, &mut pool);
355 columns.render(Rect::new(0, 0, 12, 5), &mut frame);
356
357 let a = a_rects.borrow()[0];
358 let b = b_rects.borrow()[0];
359 let c = c_rects.borrow()[0];
360
361 assert_eq!(a.height, 5);
362 assert_eq!(b.height, 5);
363 assert_eq!(c.height, 5);
364 }
365
366 #[test]
367 fn column_debug_format() {
368 let (a, _) = Record::new();
369 let col = Column::new(a, Constraint::Fixed(5));
370 let dbg = format!("{:?}", col);
371 assert!(dbg.contains("Column"));
372 assert!(dbg.contains("<dyn Widget>"));
373 }
374
375 #[test]
376 fn columns_default_is_empty() {
377 let cols = Columns::default();
378 assert!(cols.columns.is_empty());
379 assert_eq!(cols.gap, 0);
380 }
381
382 #[test]
383 fn column_builder_chain() {
384 let (a, _) = Record::new();
385 let col = Column::new(a, Constraint::Fixed(5))
386 .padding(Sides::all(2))
387 .constraint(Constraint::Ratio(1, 3));
388 assert_eq!(col.constraint, Constraint::Ratio(1, 3));
389 assert_eq!(col.padding, Sides::all(2));
390 }
391
392 #[test]
393 fn many_columns_with_gap() {
394 let mut rects_all = Vec::new();
395 let mut cols = Columns::new().gap(1);
396 for _ in 0..5 {
397 let (rec, rects) = Record::new();
398 rects_all.push(rects);
399 cols = cols.column(rec, Constraint::Fixed(2));
400 }
401
402 let mut pool = GraphemePool::new();
403 let mut frame = Frame::new(20, 1, &mut pool);
404 cols.render(Rect::new(0, 0, 20, 1), &mut frame);
405
406 for (i, rects) in rects_all.iter().enumerate() {
408 let r = rects.borrow()[0];
409 assert_eq!(r.width, 2, "column {i} should be width 2");
410 }
411
412 for i in 0..4 {
414 let a = rects_all[i].borrow()[0];
415 let b = rects_all[i + 1].borrow()[0];
416 assert!(
417 b.x >= a.right(),
418 "column {} (right={}) overlaps column {} (x={})",
419 i,
420 a.right(),
421 i + 1,
422 b.x
423 );
424 }
425 }
426}