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