1#![forbid(unsafe_code)]
2
3use crate::block::Alignment;
10use crate::{StatefulWidget, Widget};
11use ftui_core::geometry::Rect;
12use ftui_render::frame::Frame;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum VerticalAlignment {
17 #[default]
19 Top,
20 Middle,
22 Bottom,
24}
25
26#[derive(Debug, Clone)]
45pub struct Align<W> {
46 inner: W,
47 horizontal: Alignment,
48 vertical: VerticalAlignment,
49 child_width: Option<u16>,
50 child_height: Option<u16>,
51}
52
53impl<W> Align<W> {
54 pub fn new(inner: W) -> Self {
56 Self {
57 inner,
58 horizontal: Alignment::Left,
59 vertical: VerticalAlignment::Top,
60 child_width: None,
61 child_height: None,
62 }
63 }
64
65 #[must_use]
67 pub fn horizontal(mut self, alignment: Alignment) -> Self {
68 self.horizontal = alignment;
69 self
70 }
71
72 #[must_use]
74 pub fn vertical(mut self, alignment: VerticalAlignment) -> Self {
75 self.vertical = alignment;
76 self
77 }
78
79 #[must_use]
81 pub fn child_width(mut self, width: u16) -> Self {
82 self.child_width = Some(width);
83 self
84 }
85
86 #[must_use]
88 pub fn child_height(mut self, height: u16) -> Self {
89 self.child_height = Some(height);
90 self
91 }
92
93 pub fn aligned_area(&self, area: Rect) -> Rect {
95 let w = self.child_width.unwrap_or(area.width).min(area.width);
96 let h = self.child_height.unwrap_or(area.height).min(area.height);
97
98 let x = match self.horizontal {
99 Alignment::Left => area.x,
100 Alignment::Center => area.x.saturating_add((area.width.saturating_sub(w)) / 2),
101 Alignment::Right => area.x.saturating_add(area.width.saturating_sub(w)),
102 };
103
104 let y = match self.vertical {
105 VerticalAlignment::Top => area.y,
106 VerticalAlignment::Middle => area.y.saturating_add((area.height.saturating_sub(h)) / 2),
107 VerticalAlignment::Bottom => area.y.saturating_add(area.height.saturating_sub(h)),
108 };
109
110 Rect::new(x, y, w, h)
111 }
112
113 pub const fn inner(&self) -> &W {
115 &self.inner
116 }
117
118 pub fn inner_mut(&mut self) -> &mut W {
120 &mut self.inner
121 }
122
123 pub fn into_inner(self) -> W {
125 self.inner
126 }
127}
128
129impl<W: Widget> Widget for Align<W> {
130 fn render(&self, area: Rect, frame: &mut Frame) {
131 if area.is_empty() {
132 return;
133 }
134
135 let child_area = self.aligned_area(area);
136 if child_area.is_empty() {
137 return;
138 }
139
140 self.inner.render(child_area, frame);
141 }
142
143 fn is_essential(&self) -> bool {
144 self.inner.is_essential()
145 }
146}
147
148impl<W: StatefulWidget> StatefulWidget for Align<W> {
149 type State = W::State;
150
151 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
152 if area.is_empty() {
153 return;
154 }
155
156 let child_area = self.aligned_area(area);
157 if child_area.is_empty() {
158 return;
159 }
160
161 self.inner.render(child_area, frame, state);
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use ftui_render::cell::Cell;
169 use ftui_render::grapheme_pool::GraphemePool;
170
171 fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
172 let mut lines = Vec::new();
173 for y in 0..buf.height() {
174 let mut row = String::with_capacity(buf.width() as usize);
175 for x in 0..buf.width() {
176 let ch = buf
177 .get(x, y)
178 .and_then(|c| c.content.as_char())
179 .unwrap_or(' ');
180 row.push(ch);
181 }
182 lines.push(row);
183 }
184 lines
185 }
186
187 #[derive(Debug, Clone, Copy)]
189 struct Fill(char);
190
191 impl Widget for Fill {
192 fn render(&self, area: Rect, frame: &mut Frame) {
193 for y in area.y..area.bottom() {
194 for x in area.x..area.right() {
195 frame.buffer.set(x, y, Cell::from_char(self.0));
196 }
197 }
198 }
199 }
200
201 #[test]
202 fn default_alignment_uses_full_area() {
203 let align = Align::new(Fill('X'));
204 let area = Rect::new(0, 0, 5, 3);
205 let mut pool = GraphemePool::new();
206 let mut frame = Frame::new(5, 3, &mut pool);
207 align.render(area, &mut frame);
208
209 for line in buf_to_lines(&frame.buffer) {
210 assert_eq!(line, "XXXXX");
211 }
212 }
213
214 #[test]
215 fn center_horizontal() {
216 let align = Align::new(Fill('X'))
217 .horizontal(Alignment::Center)
218 .child_width(3);
219 let area = Rect::new(0, 0, 7, 1);
220 let mut pool = GraphemePool::new();
221 let mut frame = Frame::new(7, 1, &mut pool);
222 align.render(area, &mut frame);
223
224 assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX "]);
225 }
226
227 #[test]
228 fn right_horizontal() {
229 let align = Align::new(Fill('X'))
230 .horizontal(Alignment::Right)
231 .child_width(3);
232 let area = Rect::new(0, 0, 7, 1);
233 let mut pool = GraphemePool::new();
234 let mut frame = Frame::new(7, 1, &mut pool);
235 align.render(area, &mut frame);
236
237 assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX"]);
238 }
239
240 #[test]
241 fn left_horizontal() {
242 let align = Align::new(Fill('X'))
243 .horizontal(Alignment::Left)
244 .child_width(3);
245 let area = Rect::new(0, 0, 7, 1);
246 let mut pool = GraphemePool::new();
247 let mut frame = Frame::new(7, 1, &mut pool);
248 align.render(area, &mut frame);
249
250 assert_eq!(buf_to_lines(&frame.buffer), vec!["XXX "]);
251 }
252
253 #[test]
254 fn center_vertical() {
255 let align = Align::new(Fill('X'))
256 .vertical(VerticalAlignment::Middle)
257 .child_height(1);
258 let area = Rect::new(0, 0, 3, 5);
259 let mut pool = GraphemePool::new();
260 let mut frame = Frame::new(3, 5, &mut pool);
261 align.render(area, &mut frame);
262
263 assert_eq!(
264 buf_to_lines(&frame.buffer),
265 vec![" ", " ", "XXX", " ", " "]
266 );
267 }
268
269 #[test]
270 fn bottom_vertical() {
271 let align = Align::new(Fill('X'))
272 .vertical(VerticalAlignment::Bottom)
273 .child_height(2);
274 let area = Rect::new(0, 0, 3, 4);
275 let mut pool = GraphemePool::new();
276 let mut frame = Frame::new(3, 4, &mut pool);
277 align.render(area, &mut frame);
278
279 assert_eq!(
280 buf_to_lines(&frame.buffer),
281 vec![" ", " ", "XXX", "XXX"]
282 );
283 }
284
285 #[test]
286 fn center_both_axes() {
287 let align = Align::new(Fill('O'))
288 .horizontal(Alignment::Center)
289 .vertical(VerticalAlignment::Middle)
290 .child_width(1)
291 .child_height(1);
292 let area = Rect::new(0, 0, 5, 5);
293 let mut pool = GraphemePool::new();
294 let mut frame = Frame::new(5, 5, &mut pool);
295 align.render(area, &mut frame);
296
297 assert_eq!(
298 buf_to_lines(&frame.buffer),
299 vec![" ", " ", " O ", " ", " "]
300 );
301 }
302
303 #[test]
304 fn child_larger_than_area_is_clamped() {
305 let align = Align::new(Fill('X'))
306 .horizontal(Alignment::Center)
307 .child_width(20)
308 .child_height(10);
309 let area = Rect::new(0, 0, 5, 3);
310
311 let child_area = align.aligned_area(area);
312 assert_eq!(child_area.width, 5);
313 assert_eq!(child_area.height, 3);
314 }
315
316 #[test]
317 fn zero_size_area_is_noop() {
318 let align = Align::new(Fill('X'))
319 .horizontal(Alignment::Center)
320 .child_width(3);
321 let area = Rect::new(0, 0, 0, 0);
322 let mut pool = GraphemePool::new();
323 let mut frame = Frame::new(5, 5, &mut pool);
324 align.render(area, &mut frame);
325
326 for y in 0..5 {
328 for x in 0..5u16 {
329 assert!(frame.buffer.get(x, y).unwrap().is_empty());
330 }
331 }
332 }
333
334 #[test]
335 fn zero_child_size_is_noop() {
336 let align = Align::new(Fill('X')).child_width(0).child_height(0);
337 let area = Rect::new(0, 0, 5, 5);
338 let mut pool = GraphemePool::new();
339 let mut frame = Frame::new(5, 5, &mut pool);
340 align.render(area, &mut frame);
341
342 for y in 0..5 {
343 for x in 0..5u16 {
344 assert!(frame.buffer.get(x, y).unwrap().is_empty());
345 }
346 }
347 }
348
349 #[test]
350 fn area_with_offset() {
351 let align = Align::new(Fill('X'))
352 .horizontal(Alignment::Center)
353 .child_width(2);
354 let area = Rect::new(10, 5, 6, 1);
355
356 let child = align.aligned_area(area);
357 assert_eq!(child.x, 12);
358 assert_eq!(child.y, 5);
359 assert_eq!(child.width, 2);
360 }
361
362 #[test]
363 fn aligned_area_right_bottom() {
364 let align = Align::new(Fill('X'))
365 .horizontal(Alignment::Right)
366 .vertical(VerticalAlignment::Bottom)
367 .child_width(2)
368 .child_height(1);
369 let area = Rect::new(0, 0, 10, 5);
370
371 let child = align.aligned_area(area);
372 assert_eq!(child.x, 8);
373 assert_eq!(child.y, 4);
374 assert_eq!(child.width, 2);
375 assert_eq!(child.height, 1);
376 }
377
378 #[test]
379 fn vertical_alignment_default_is_top() {
380 assert_eq!(VerticalAlignment::default(), VerticalAlignment::Top);
381 }
382
383 #[test]
384 fn inner_accessors() {
385 let mut align = Align::new(Fill('A'));
386 assert_eq!(align.inner().0, 'A');
387 align.inner_mut().0 = 'B';
388 assert_eq!(align.inner().0, 'B');
389 let inner = align.into_inner();
390 assert_eq!(inner.0, 'B');
391 }
392
393 #[test]
394 fn stateful_widget_render() {
395 use std::cell::RefCell;
396 use std::rc::Rc;
397
398 #[derive(Debug, Clone)]
399 struct StatefulFill {
400 ch: char,
401 }
402
403 impl StatefulWidget for StatefulFill {
404 type State = Rc<RefCell<Rect>>;
405
406 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
407 *state.borrow_mut() = area;
408 for y in area.y..area.bottom() {
409 for x in area.x..area.right() {
410 frame.buffer.set(x, y, Cell::from_char(self.ch));
411 }
412 }
413 }
414 }
415
416 let align = Align::new(StatefulFill { ch: 'S' })
417 .horizontal(Alignment::Center)
418 .child_width(2)
419 .child_height(1);
420 let area = Rect::new(0, 0, 6, 3);
421 let mut pool = GraphemePool::new();
422 let mut frame = Frame::new(6, 3, &mut pool);
423 let mut state = Rc::new(RefCell::new(Rect::default()));
424 StatefulWidget::render(&align, area, &mut frame, &mut state);
425
426 let rendered_area = *state.borrow();
427 assert_eq!(rendered_area.x, 2);
428 assert_eq!(rendered_area.width, 2);
429 }
430
431 #[test]
434 fn center_odd_remainder_floors_left() {
435 let align = Align::new(Fill('X'))
437 .horizontal(Alignment::Center)
438 .child_width(3);
439 let area = Rect::new(0, 0, 6, 1);
440 let child = align.aligned_area(area);
441 assert_eq!(child.x, 1);
442 assert_eq!(child.width, 3);
443 }
444
445 #[test]
446 fn center_vertical_odd_remainder_floors_top() {
447 let align = Align::new(Fill('X'))
449 .vertical(VerticalAlignment::Middle)
450 .child_height(3);
451 let area = Rect::new(0, 0, 1, 6);
452 let child = align.aligned_area(area);
453 assert_eq!(child.y, 1);
454 assert_eq!(child.height, 3);
455 }
456
457 #[test]
458 fn child_width_only_height_fills() {
459 let align = Align::new(Fill('X'))
460 .horizontal(Alignment::Center)
461 .child_width(2);
462 let area = Rect::new(0, 0, 8, 5);
463 let child = align.aligned_area(area);
464 assert_eq!(child.width, 2);
465 assert_eq!(child.height, 5, "height should be full parent height");
466 }
467
468 #[test]
469 fn child_height_only_width_fills() {
470 let align = Align::new(Fill('X'))
471 .vertical(VerticalAlignment::Bottom)
472 .child_height(2);
473 let area = Rect::new(0, 0, 8, 5);
474 let child = align.aligned_area(area);
475 assert_eq!(child.width, 8, "width should be full parent width");
476 assert_eq!(child.height, 2);
477 assert_eq!(child.y, 3);
478 }
479
480 #[test]
481 fn right_alignment_exact_fit() {
482 let align = Align::new(Fill('X'))
484 .horizontal(Alignment::Right)
485 .child_width(10);
486 let area = Rect::new(5, 0, 10, 1);
487 let child = align.aligned_area(area);
488 assert_eq!(child.x, 5, "exact fit should not shift");
489 assert_eq!(child.width, 10);
490 }
491
492 #[test]
493 fn bottom_alignment_exact_fit() {
494 let align = Align::new(Fill('X'))
495 .vertical(VerticalAlignment::Bottom)
496 .child_height(5);
497 let area = Rect::new(0, 10, 1, 5);
498 let child = align.aligned_area(area);
499 assert_eq!(child.y, 10, "exact fit should not shift");
500 }
501
502 #[test]
503 fn center_1x1_in_large_area() {
504 let align = Align::new(Fill('O'))
505 .horizontal(Alignment::Center)
506 .vertical(VerticalAlignment::Middle)
507 .child_width(1)
508 .child_height(1);
509 let area = Rect::new(0, 0, 100, 100);
510 let child = align.aligned_area(area);
511 assert_eq!(child.x, 49); assert_eq!(child.y, 49);
513 assert_eq!(child.width, 1);
514 assert_eq!(child.height, 1);
515 }
516
517 #[test]
518 fn vertical_alignment_copy_and_eq() {
519 let a = VerticalAlignment::Middle;
520 let b = a; assert_eq!(a, b);
522 assert_ne!(a, VerticalAlignment::Top);
523 assert_ne!(a, VerticalAlignment::Bottom);
524 }
525
526 #[test]
527 fn align_clone_preserves_settings() {
528 let align = Align::new(Fill('X'))
529 .horizontal(Alignment::Right)
530 .vertical(VerticalAlignment::Bottom)
531 .child_width(5)
532 .child_height(3);
533 let cloned = align.clone();
534 let area = Rect::new(0, 0, 20, 20);
535 assert_eq!(align.aligned_area(area), cloned.aligned_area(area));
536 }
537
538 #[test]
539 fn debug_format() {
540 let align = Align::new(Fill('X'))
541 .horizontal(Alignment::Center)
542 .vertical(VerticalAlignment::Middle);
543 let dbg = format!("{align:?}");
544 assert!(dbg.contains("Align"));
545 assert!(dbg.contains("Center"));
546 assert!(dbg.contains("Middle"));
547 }
548
549 #[test]
550 fn stateful_zero_area_is_noop() {
551 use std::cell::RefCell;
552 use std::rc::Rc;
553
554 #[derive(Debug, Clone)]
555 struct StatefulFill;
556 impl StatefulWidget for StatefulFill {
557 type State = Rc<RefCell<bool>>;
558 fn render(&self, _: Rect, _: &mut Frame, state: &mut Self::State) {
559 *state.borrow_mut() = true;
560 }
561 }
562
563 let align = Align::new(StatefulFill)
564 .horizontal(Alignment::Center)
565 .child_width(3);
566 let mut pool = GraphemePool::new();
567 let mut frame = Frame::new(10, 10, &mut pool);
568 let mut rendered = Rc::new(RefCell::new(false));
569 StatefulWidget::render(&align, Rect::new(0, 0, 0, 0), &mut frame, &mut rendered);
570 assert!(!*rendered.borrow(), "should not render in zero area");
571 }
572
573 #[test]
574 fn stateful_zero_child_is_noop() {
575 use std::cell::RefCell;
576 use std::rc::Rc;
577
578 #[derive(Debug, Clone)]
579 struct StatefulFill;
580 impl StatefulWidget for StatefulFill {
581 type State = Rc<RefCell<bool>>;
582 fn render(&self, _: Rect, _: &mut Frame, state: &mut Self::State) {
583 *state.borrow_mut() = true;
584 }
585 }
586
587 let align = Align::new(StatefulFill).child_width(0).child_height(0);
588 let mut pool = GraphemePool::new();
589 let mut frame = Frame::new(10, 10, &mut pool);
590 let mut rendered = Rc::new(RefCell::new(false));
591 StatefulWidget::render(&align, Rect::new(0, 0, 10, 10), &mut frame, &mut rendered);
592 assert!(!*rendered.borrow(), "should not render zero-size child");
593 }
594
595 #[test]
598 fn is_essential_delegates() {
599 struct Essential;
600 impl Widget for Essential {
601 fn render(&self, _: Rect, _: &mut Frame) {}
602 fn is_essential(&self) -> bool {
603 true
604 }
605 }
606
607 assert!(Align::new(Essential).is_essential());
608 assert!(!Align::new(Fill('X')).is_essential());
609 }
610}