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 for y in area.y..area.bottom() {
139 for x in area.x..area.right() {
140 if let Some(cell) = frame.buffer.get_mut(x, y) {
141 cell.content = ftui_render::cell::CellContent::EMPTY;
142 }
143 }
144 }
145
146 let child_area = self.aligned_area(area);
147 if child_area.is_empty() {
148 return;
149 }
150
151 self.inner.render(child_area, frame);
152 }
153
154 fn is_essential(&self) -> bool {
155 self.inner.is_essential()
156 }
157}
158
159impl<W: StatefulWidget> StatefulWidget for Align<W> {
160 type State = W::State;
161
162 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
163 if area.is_empty() {
164 return;
165 }
166
167 for y in area.y..area.bottom() {
169 for x in area.x..area.right() {
170 if let Some(cell) = frame.buffer.get_mut(x, y) {
171 cell.content = ftui_render::cell::CellContent::EMPTY;
172 }
173 }
174 }
175
176 let child_area = self.aligned_area(area);
177 if child_area.is_empty() {
178 return;
179 }
180
181 self.inner.render(child_area, frame, state);
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use ftui_render::cell::Cell;
189 use ftui_render::grapheme_pool::GraphemePool;
190
191 fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
192 let mut lines = Vec::new();
193 for y in 0..buf.height() {
194 let mut row = String::with_capacity(buf.width() as usize);
195 for x in 0..buf.width() {
196 let ch = buf
197 .get(x, y)
198 .and_then(|c| c.content.as_char())
199 .unwrap_or(' ');
200 row.push(ch);
201 }
202 lines.push(row);
203 }
204 lines
205 }
206
207 #[derive(Debug, Clone, Copy)]
209 struct Fill(char);
210
211 impl Widget for Fill {
212 fn render(&self, area: Rect, frame: &mut Frame) {
213 for y in area.y..area.bottom() {
214 for x in area.x..area.right() {
215 frame.buffer.set(x, y, Cell::from_char(self.0));
216 }
217 }
218 }
219 }
220
221 #[test]
222 fn default_alignment_uses_full_area() {
223 let align = Align::new(Fill('X'));
224 let area = Rect::new(0, 0, 5, 3);
225 let mut pool = GraphemePool::new();
226 let mut frame = Frame::new(5, 3, &mut pool);
227 align.render(area, &mut frame);
228
229 for line in buf_to_lines(&frame.buffer) {
230 assert_eq!(line, "XXXXX");
231 }
232 }
233
234 #[test]
235 fn center_horizontal() {
236 let align = Align::new(Fill('X'))
237 .horizontal(Alignment::Center)
238 .child_width(3);
239 let area = Rect::new(0, 0, 7, 1);
240 let mut pool = GraphemePool::new();
241 let mut frame = Frame::new(7, 1, &mut pool);
242 align.render(area, &mut frame);
243
244 assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX "]);
245 }
246
247 #[test]
248 fn right_horizontal() {
249 let align = Align::new(Fill('X'))
250 .horizontal(Alignment::Right)
251 .child_width(3);
252 let area = Rect::new(0, 0, 7, 1);
253 let mut pool = GraphemePool::new();
254 let mut frame = Frame::new(7, 1, &mut pool);
255 align.render(area, &mut frame);
256
257 assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX"]);
258 }
259
260 #[test]
261 fn left_horizontal() {
262 let align = Align::new(Fill('X'))
263 .horizontal(Alignment::Left)
264 .child_width(3);
265 let area = Rect::new(0, 0, 7, 1);
266 let mut pool = GraphemePool::new();
267 let mut frame = Frame::new(7, 1, &mut pool);
268 align.render(area, &mut frame);
269
270 assert_eq!(buf_to_lines(&frame.buffer), vec!["XXX "]);
271 }
272
273 #[test]
274 fn center_vertical() {
275 let align = Align::new(Fill('X'))
276 .vertical(VerticalAlignment::Middle)
277 .child_height(1);
278 let area = Rect::new(0, 0, 3, 5);
279 let mut pool = GraphemePool::new();
280 let mut frame = Frame::new(3, 5, &mut pool);
281 align.render(area, &mut frame);
282
283 assert_eq!(
284 buf_to_lines(&frame.buffer),
285 vec![" ", " ", "XXX", " ", " "]
286 );
287 }
288
289 #[test]
290 fn bottom_vertical() {
291 let align = Align::new(Fill('X'))
292 .vertical(VerticalAlignment::Bottom)
293 .child_height(2);
294 let area = Rect::new(0, 0, 3, 4);
295 let mut pool = GraphemePool::new();
296 let mut frame = Frame::new(3, 4, &mut pool);
297 align.render(area, &mut frame);
298
299 assert_eq!(
300 buf_to_lines(&frame.buffer),
301 vec![" ", " ", "XXX", "XXX"]
302 );
303 }
304
305 #[test]
306 fn center_both_axes() {
307 let align = Align::new(Fill('O'))
308 .horizontal(Alignment::Center)
309 .vertical(VerticalAlignment::Middle)
310 .child_width(1)
311 .child_height(1);
312 let area = Rect::new(0, 0, 5, 5);
313 let mut pool = GraphemePool::new();
314 let mut frame = Frame::new(5, 5, &mut pool);
315 align.render(area, &mut frame);
316
317 assert_eq!(
318 buf_to_lines(&frame.buffer),
319 vec![" ", " ", " O ", " ", " "]
320 );
321 }
322
323 #[test]
324 fn child_larger_than_area_is_clamped() {
325 let align = Align::new(Fill('X'))
326 .horizontal(Alignment::Center)
327 .child_width(20)
328 .child_height(10);
329 let area = Rect::new(0, 0, 5, 3);
330
331 let child_area = align.aligned_area(area);
332 assert_eq!(child_area.width, 5);
333 assert_eq!(child_area.height, 3);
334 }
335
336 #[test]
337 fn zero_size_area_is_noop() {
338 let align = Align::new(Fill('X'))
339 .horizontal(Alignment::Center)
340 .child_width(3);
341 let area = Rect::new(0, 0, 0, 0);
342 let mut pool = GraphemePool::new();
343 let mut frame = Frame::new(5, 5, &mut pool);
344 align.render(area, &mut frame);
345
346 for y in 0..5 {
348 for x in 0..5u16 {
349 assert!(frame.buffer.get(x, y).unwrap().is_empty());
350 }
351 }
352 }
353
354 #[test]
355 fn zero_child_size_is_noop() {
356 let align = Align::new(Fill('X')).child_width(0).child_height(0);
357 let area = Rect::new(0, 0, 5, 5);
358 let mut pool = GraphemePool::new();
359 let mut frame = Frame::new(5, 5, &mut pool);
360 align.render(area, &mut frame);
361
362 for y in 0..5 {
363 for x in 0..5u16 {
364 assert!(frame.buffer.get(x, y).unwrap().is_empty());
365 }
366 }
367 }
368
369 #[test]
370 fn smaller_second_render_clears_old_child_region() {
371 let mut pool = GraphemePool::new();
372 let mut frame = Frame::new(5, 1, &mut pool);
373 let area = Rect::new(0, 0, 5, 1);
374
375 Align::new(Fill('X')).render(area, &mut frame);
376 Align::new(Fill('O'))
377 .horizontal(Alignment::Center)
378 .child_width(1)
379 .render(area, &mut frame);
380
381 assert_eq!(buf_to_lines(&frame.buffer), vec![" O "]);
382 }
383
384 #[test]
385 fn zero_size_child_clears_previous_content() {
386 let mut pool = GraphemePool::new();
387 let mut frame = Frame::new(5, 1, &mut pool);
388 let area = Rect::new(0, 0, 5, 1);
389
390 Align::new(Fill('X')).render(area, &mut frame);
391 Align::new(Fill('O'))
392 .child_width(0)
393 .child_height(0)
394 .render(area, &mut frame);
395
396 for x in 0..5u16 {
397 assert!(frame.buffer.get(x, 0).unwrap().is_empty());
398 }
399 }
400
401 #[test]
402 fn area_with_offset() {
403 let align = Align::new(Fill('X'))
404 .horizontal(Alignment::Center)
405 .child_width(2);
406 let area = Rect::new(10, 5, 6, 1);
407
408 let child = align.aligned_area(area);
409 assert_eq!(child.x, 12);
410 assert_eq!(child.y, 5);
411 assert_eq!(child.width, 2);
412 }
413
414 #[test]
415 fn aligned_area_right_bottom() {
416 let align = Align::new(Fill('X'))
417 .horizontal(Alignment::Right)
418 .vertical(VerticalAlignment::Bottom)
419 .child_width(2)
420 .child_height(1);
421 let area = Rect::new(0, 0, 10, 5);
422
423 let child = align.aligned_area(area);
424 assert_eq!(child.x, 8);
425 assert_eq!(child.y, 4);
426 assert_eq!(child.width, 2);
427 assert_eq!(child.height, 1);
428 }
429
430 #[test]
431 fn vertical_alignment_default_is_top() {
432 assert_eq!(VerticalAlignment::default(), VerticalAlignment::Top);
433 }
434
435 #[test]
436 fn inner_accessors() {
437 let mut align = Align::new(Fill('A'));
438 assert_eq!(align.inner().0, 'A');
439 align.inner_mut().0 = 'B';
440 assert_eq!(align.inner().0, 'B');
441 let inner = align.into_inner();
442 assert_eq!(inner.0, 'B');
443 }
444
445 #[test]
446 fn stateful_widget_render() {
447 use std::cell::RefCell;
448 use std::rc::Rc;
449
450 #[derive(Debug, Clone)]
451 struct StatefulFill {
452 ch: char,
453 }
454
455 impl StatefulWidget for StatefulFill {
456 type State = Rc<RefCell<Rect>>;
457
458 fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
459 *state.borrow_mut() = area;
460 for y in area.y..area.bottom() {
461 for x in area.x..area.right() {
462 frame.buffer.set(x, y, Cell::from_char(self.ch));
463 }
464 }
465 }
466 }
467
468 let align = Align::new(StatefulFill { ch: 'S' })
469 .horizontal(Alignment::Center)
470 .child_width(2)
471 .child_height(1);
472 let area = Rect::new(0, 0, 6, 3);
473 let mut pool = GraphemePool::new();
474 let mut frame = Frame::new(6, 3, &mut pool);
475 let mut state = Rc::new(RefCell::new(Rect::default()));
476 StatefulWidget::render(&align, area, &mut frame, &mut state);
477
478 let rendered_area = *state.borrow();
479 assert_eq!(rendered_area.x, 2);
480 assert_eq!(rendered_area.width, 2);
481 }
482
483 #[test]
484 fn stateful_smaller_second_render_clears_old_child_region() {
485 #[derive(Debug, Clone, Copy)]
486 struct StatefulFill(char);
487
488 impl StatefulWidget for StatefulFill {
489 type State = ();
490
491 fn render(&self, area: Rect, frame: &mut Frame, _state: &mut Self::State) {
492 for y in area.y..area.bottom() {
493 for x in area.x..area.right() {
494 frame.buffer.set(x, y, Cell::from_char(self.0));
495 }
496 }
497 }
498 }
499
500 let mut pool = GraphemePool::new();
501 let mut frame = Frame::new(5, 1, &mut pool);
502 let area = Rect::new(0, 0, 5, 1);
503 let mut state = ();
504
505 StatefulWidget::render(&Align::new(StatefulFill('X')), area, &mut frame, &mut state);
506 StatefulWidget::render(
507 &Align::new(StatefulFill('O'))
508 .horizontal(Alignment::Center)
509 .child_width(1),
510 area,
511 &mut frame,
512 &mut state,
513 );
514
515 assert_eq!(buf_to_lines(&frame.buffer), vec![" O "]);
516 }
517
518 #[test]
521 fn center_odd_remainder_floors_left() {
522 let align = Align::new(Fill('X'))
524 .horizontal(Alignment::Center)
525 .child_width(3);
526 let area = Rect::new(0, 0, 6, 1);
527 let child = align.aligned_area(area);
528 assert_eq!(child.x, 1);
529 assert_eq!(child.width, 3);
530 }
531
532 #[test]
533 fn center_vertical_odd_remainder_floors_top() {
534 let align = Align::new(Fill('X'))
536 .vertical(VerticalAlignment::Middle)
537 .child_height(3);
538 let area = Rect::new(0, 0, 1, 6);
539 let child = align.aligned_area(area);
540 assert_eq!(child.y, 1);
541 assert_eq!(child.height, 3);
542 }
543
544 #[test]
545 fn child_width_only_height_fills() {
546 let align = Align::new(Fill('X'))
547 .horizontal(Alignment::Center)
548 .child_width(2);
549 let area = Rect::new(0, 0, 8, 5);
550 let child = align.aligned_area(area);
551 assert_eq!(child.width, 2);
552 assert_eq!(child.height, 5, "height should be full parent height");
553 }
554
555 #[test]
556 fn child_height_only_width_fills() {
557 let align = Align::new(Fill('X'))
558 .vertical(VerticalAlignment::Bottom)
559 .child_height(2);
560 let area = Rect::new(0, 0, 8, 5);
561 let child = align.aligned_area(area);
562 assert_eq!(child.width, 8, "width should be full parent width");
563 assert_eq!(child.height, 2);
564 assert_eq!(child.y, 3);
565 }
566
567 #[test]
568 fn right_alignment_exact_fit() {
569 let align = Align::new(Fill('X'))
571 .horizontal(Alignment::Right)
572 .child_width(10);
573 let area = Rect::new(5, 0, 10, 1);
574 let child = align.aligned_area(area);
575 assert_eq!(child.x, 5, "exact fit should not shift");
576 assert_eq!(child.width, 10);
577 }
578
579 #[test]
580 fn bottom_alignment_exact_fit() {
581 let align = Align::new(Fill('X'))
582 .vertical(VerticalAlignment::Bottom)
583 .child_height(5);
584 let area = Rect::new(0, 10, 1, 5);
585 let child = align.aligned_area(area);
586 assert_eq!(child.y, 10, "exact fit should not shift");
587 }
588
589 #[test]
590 fn center_1x1_in_large_area() {
591 let align = Align::new(Fill('O'))
592 .horizontal(Alignment::Center)
593 .vertical(VerticalAlignment::Middle)
594 .child_width(1)
595 .child_height(1);
596 let area = Rect::new(0, 0, 100, 100);
597 let child = align.aligned_area(area);
598 assert_eq!(child.x, 49); assert_eq!(child.y, 49);
600 assert_eq!(child.width, 1);
601 assert_eq!(child.height, 1);
602 }
603
604 #[test]
605 fn vertical_alignment_copy_and_eq() {
606 let a = VerticalAlignment::Middle;
607 let b = a; assert_eq!(a, b);
609 assert_ne!(a, VerticalAlignment::Top);
610 assert_ne!(a, VerticalAlignment::Bottom);
611 }
612
613 #[test]
614 fn align_clone_preserves_settings() {
615 let align = Align::new(Fill('X'))
616 .horizontal(Alignment::Right)
617 .vertical(VerticalAlignment::Bottom)
618 .child_width(5)
619 .child_height(3);
620 let cloned = align.clone();
621 let area = Rect::new(0, 0, 20, 20);
622 assert_eq!(align.aligned_area(area), cloned.aligned_area(area));
623 }
624
625 #[test]
626 fn debug_format() {
627 let align = Align::new(Fill('X'))
628 .horizontal(Alignment::Center)
629 .vertical(VerticalAlignment::Middle);
630 let dbg = format!("{align:?}");
631 assert!(dbg.contains("Align"));
632 assert!(dbg.contains("Center"));
633 assert!(dbg.contains("Middle"));
634 }
635
636 #[test]
637 fn stateful_zero_area_is_noop() {
638 use std::cell::RefCell;
639 use std::rc::Rc;
640
641 #[derive(Debug, Clone)]
642 struct StatefulFill;
643 impl StatefulWidget for StatefulFill {
644 type State = Rc<RefCell<bool>>;
645 fn render(&self, _: Rect, _: &mut Frame, state: &mut Self::State) {
646 *state.borrow_mut() = true;
647 }
648 }
649
650 let align = Align::new(StatefulFill)
651 .horizontal(Alignment::Center)
652 .child_width(3);
653 let mut pool = GraphemePool::new();
654 let mut frame = Frame::new(10, 10, &mut pool);
655 let mut rendered = Rc::new(RefCell::new(false));
656 StatefulWidget::render(&align, Rect::new(0, 0, 0, 0), &mut frame, &mut rendered);
657 assert!(!*rendered.borrow(), "should not render in zero area");
658 }
659
660 #[test]
661 fn stateful_zero_child_is_noop() {
662 use std::cell::RefCell;
663 use std::rc::Rc;
664
665 #[derive(Debug, Clone)]
666 struct StatefulFill;
667 impl StatefulWidget for StatefulFill {
668 type State = Rc<RefCell<bool>>;
669 fn render(&self, _: Rect, _: &mut Frame, state: &mut Self::State) {
670 *state.borrow_mut() = true;
671 }
672 }
673
674 let align = Align::new(StatefulFill).child_width(0).child_height(0);
675 let mut pool = GraphemePool::new();
676 let mut frame = Frame::new(10, 10, &mut pool);
677 let mut rendered = Rc::new(RefCell::new(false));
678 StatefulWidget::render(&align, Rect::new(0, 0, 10, 10), &mut frame, &mut rendered);
679 assert!(!*rendered.borrow(), "should not render zero-size child");
680 }
681
682 #[test]
685 fn is_essential_delegates() {
686 struct Essential;
687 impl Widget for Essential {
688 fn render(&self, _: Rect, _: &mut Frame) {}
689 fn is_essential(&self) -> bool {
690 true
691 }
692 }
693
694 assert!(Align::new(Essential).is_essential());
695 assert!(!Align::new(Fill('X')).is_essential());
696 }
697}