#![forbid(unsafe_code)]
use crate::block::Alignment;
use crate::{StatefulWidget, Widget};
use ftui_core::geometry::Rect;
use ftui_render::frame::Frame;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum VerticalAlignment {
#[default]
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone)]
pub struct Align<W> {
inner: W,
horizontal: Alignment,
vertical: VerticalAlignment,
child_width: Option<u16>,
child_height: Option<u16>,
}
impl<W> Align<W> {
pub fn new(inner: W) -> Self {
Self {
inner,
horizontal: Alignment::Left,
vertical: VerticalAlignment::Top,
child_width: None,
child_height: None,
}
}
#[must_use]
pub fn horizontal(mut self, alignment: Alignment) -> Self {
self.horizontal = alignment;
self
}
#[must_use]
pub fn vertical(mut self, alignment: VerticalAlignment) -> Self {
self.vertical = alignment;
self
}
#[must_use]
pub fn child_width(mut self, width: u16) -> Self {
self.child_width = Some(width);
self
}
#[must_use]
pub fn child_height(mut self, height: u16) -> Self {
self.child_height = Some(height);
self
}
pub fn aligned_area(&self, area: Rect) -> Rect {
let w = self.child_width.unwrap_or(area.width).min(area.width);
let h = self.child_height.unwrap_or(area.height).min(area.height);
let x = match self.horizontal {
Alignment::Left => area.x,
Alignment::Center => area.x.saturating_add((area.width.saturating_sub(w)) / 2),
Alignment::Right => area.x.saturating_add(area.width.saturating_sub(w)),
};
let y = match self.vertical {
VerticalAlignment::Top => area.y,
VerticalAlignment::Middle => area.y.saturating_add((area.height.saturating_sub(h)) / 2),
VerticalAlignment::Bottom => area.y.saturating_add(area.height.saturating_sub(h)),
};
Rect::new(x, y, w, h)
}
pub const fn inner(&self) -> &W {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut W {
&mut self.inner
}
pub fn into_inner(self) -> W {
self.inner
}
}
impl<W: Widget> Widget for Align<W> {
fn render(&self, area: Rect, frame: &mut Frame) {
if area.is_empty() {
return;
}
for y in area.y..area.bottom() {
for x in area.x..area.right() {
if let Some(cell) = frame.buffer.get_mut(x, y) {
cell.content = ftui_render::cell::CellContent::EMPTY;
}
}
}
let child_area = self.aligned_area(area);
if child_area.is_empty() {
return;
}
self.inner.render(child_area, frame);
}
fn is_essential(&self) -> bool {
self.inner.is_essential()
}
}
impl<W: StatefulWidget> StatefulWidget for Align<W> {
type State = W::State;
fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
if area.is_empty() {
return;
}
for y in area.y..area.bottom() {
for x in area.x..area.right() {
if let Some(cell) = frame.buffer.get_mut(x, y) {
cell.content = ftui_render::cell::CellContent::EMPTY;
}
}
}
let child_area = self.aligned_area(area);
if child_area.is_empty() {
return;
}
self.inner.render(child_area, frame, state);
}
}
#[cfg(test)]
mod tests {
use super::*;
use ftui_render::cell::Cell;
use ftui_render::grapheme_pool::GraphemePool;
fn buf_to_lines(buf: &ftui_render::buffer::Buffer) -> Vec<String> {
let mut lines = Vec::new();
for y in 0..buf.height() {
let mut row = String::with_capacity(buf.width() as usize);
for x in 0..buf.width() {
let ch = buf
.get(x, y)
.and_then(|c| c.content.as_char())
.unwrap_or(' ');
row.push(ch);
}
lines.push(row);
}
lines
}
#[derive(Debug, Clone, Copy)]
struct Fill(char);
impl Widget for Fill {
fn render(&self, area: Rect, frame: &mut Frame) {
for y in area.y..area.bottom() {
for x in area.x..area.right() {
frame.buffer.set(x, y, Cell::from_char(self.0));
}
}
}
}
#[test]
fn default_alignment_uses_full_area() {
let align = Align::new(Fill('X'));
let area = Rect::new(0, 0, 5, 3);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 3, &mut pool);
align.render(area, &mut frame);
for line in buf_to_lines(&frame.buffer) {
assert_eq!(line, "XXXXX");
}
}
#[test]
fn center_horizontal() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Center)
.child_width(3);
let area = Rect::new(0, 0, 7, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(7, 1, &mut pool);
align.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX "]);
}
#[test]
fn right_horizontal() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Right)
.child_width(3);
let area = Rect::new(0, 0, 7, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(7, 1, &mut pool);
align.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec![" XXX"]);
}
#[test]
fn left_horizontal() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Left)
.child_width(3);
let area = Rect::new(0, 0, 7, 1);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(7, 1, &mut pool);
align.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec!["XXX "]);
}
#[test]
fn center_vertical() {
let align = Align::new(Fill('X'))
.vertical(VerticalAlignment::Middle)
.child_height(1);
let area = Rect::new(0, 0, 3, 5);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 5, &mut pool);
align.render(area, &mut frame);
assert_eq!(
buf_to_lines(&frame.buffer),
vec![" ", " ", "XXX", " ", " "]
);
}
#[test]
fn bottom_vertical() {
let align = Align::new(Fill('X'))
.vertical(VerticalAlignment::Bottom)
.child_height(2);
let area = Rect::new(0, 0, 3, 4);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(3, 4, &mut pool);
align.render(area, &mut frame);
assert_eq!(
buf_to_lines(&frame.buffer),
vec![" ", " ", "XXX", "XXX"]
);
}
#[test]
fn center_both_axes() {
let align = Align::new(Fill('O'))
.horizontal(Alignment::Center)
.vertical(VerticalAlignment::Middle)
.child_width(1)
.child_height(1);
let area = Rect::new(0, 0, 5, 5);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 5, &mut pool);
align.render(area, &mut frame);
assert_eq!(
buf_to_lines(&frame.buffer),
vec![" ", " ", " O ", " ", " "]
);
}
#[test]
fn child_larger_than_area_is_clamped() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Center)
.child_width(20)
.child_height(10);
let area = Rect::new(0, 0, 5, 3);
let child_area = align.aligned_area(area);
assert_eq!(child_area.width, 5);
assert_eq!(child_area.height, 3);
}
#[test]
fn zero_size_area_is_noop() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Center)
.child_width(3);
let area = Rect::new(0, 0, 0, 0);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 5, &mut pool);
align.render(area, &mut frame);
for y in 0..5 {
for x in 0..5u16 {
assert!(frame.buffer.get(x, y).unwrap().is_empty());
}
}
}
#[test]
fn zero_child_size_is_noop() {
let align = Align::new(Fill('X')).child_width(0).child_height(0);
let area = Rect::new(0, 0, 5, 5);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 5, &mut pool);
align.render(area, &mut frame);
for y in 0..5 {
for x in 0..5u16 {
assert!(frame.buffer.get(x, y).unwrap().is_empty());
}
}
}
#[test]
fn smaller_second_render_clears_old_child_region() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let area = Rect::new(0, 0, 5, 1);
Align::new(Fill('X')).render(area, &mut frame);
Align::new(Fill('O'))
.horizontal(Alignment::Center)
.child_width(1)
.render(area, &mut frame);
assert_eq!(buf_to_lines(&frame.buffer), vec![" O "]);
}
#[test]
fn zero_size_child_clears_previous_content() {
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let area = Rect::new(0, 0, 5, 1);
Align::new(Fill('X')).render(area, &mut frame);
Align::new(Fill('O'))
.child_width(0)
.child_height(0)
.render(area, &mut frame);
for x in 0..5u16 {
assert!(frame.buffer.get(x, 0).unwrap().is_empty());
}
}
#[test]
fn area_with_offset() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Center)
.child_width(2);
let area = Rect::new(10, 5, 6, 1);
let child = align.aligned_area(area);
assert_eq!(child.x, 12);
assert_eq!(child.y, 5);
assert_eq!(child.width, 2);
}
#[test]
fn aligned_area_right_bottom() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Right)
.vertical(VerticalAlignment::Bottom)
.child_width(2)
.child_height(1);
let area = Rect::new(0, 0, 10, 5);
let child = align.aligned_area(area);
assert_eq!(child.x, 8);
assert_eq!(child.y, 4);
assert_eq!(child.width, 2);
assert_eq!(child.height, 1);
}
#[test]
fn vertical_alignment_default_is_top() {
assert_eq!(VerticalAlignment::default(), VerticalAlignment::Top);
}
#[test]
fn inner_accessors() {
let mut align = Align::new(Fill('A'));
assert_eq!(align.inner().0, 'A');
align.inner_mut().0 = 'B';
assert_eq!(align.inner().0, 'B');
let inner = align.into_inner();
assert_eq!(inner.0, 'B');
}
#[test]
fn stateful_widget_render() {
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
struct StatefulFill {
ch: char,
}
impl StatefulWidget for StatefulFill {
type State = Rc<RefCell<Rect>>;
fn render(&self, area: Rect, frame: &mut Frame, state: &mut Self::State) {
*state.borrow_mut() = area;
for y in area.y..area.bottom() {
for x in area.x..area.right() {
frame.buffer.set(x, y, Cell::from_char(self.ch));
}
}
}
}
let align = Align::new(StatefulFill { ch: 'S' })
.horizontal(Alignment::Center)
.child_width(2)
.child_height(1);
let area = Rect::new(0, 0, 6, 3);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(6, 3, &mut pool);
let mut state = Rc::new(RefCell::new(Rect::default()));
StatefulWidget::render(&align, area, &mut frame, &mut state);
let rendered_area = *state.borrow();
assert_eq!(rendered_area.x, 2);
assert_eq!(rendered_area.width, 2);
}
#[test]
fn stateful_smaller_second_render_clears_old_child_region() {
#[derive(Debug, Clone, Copy)]
struct StatefulFill(char);
impl StatefulWidget for StatefulFill {
type State = ();
fn render(&self, area: Rect, frame: &mut Frame, _state: &mut Self::State) {
for y in area.y..area.bottom() {
for x in area.x..area.right() {
frame.buffer.set(x, y, Cell::from_char(self.0));
}
}
}
}
let mut pool = GraphemePool::new();
let mut frame = Frame::new(5, 1, &mut pool);
let area = Rect::new(0, 0, 5, 1);
let mut state = ();
StatefulWidget::render(&Align::new(StatefulFill('X')), area, &mut frame, &mut state);
StatefulWidget::render(
&Align::new(StatefulFill('O'))
.horizontal(Alignment::Center)
.child_width(1),
area,
&mut frame,
&mut state,
);
assert_eq!(buf_to_lines(&frame.buffer), vec![" O "]);
}
#[test]
fn center_odd_remainder_floors_left() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Center)
.child_width(3);
let area = Rect::new(0, 0, 6, 1);
let child = align.aligned_area(area);
assert_eq!(child.x, 1);
assert_eq!(child.width, 3);
}
#[test]
fn center_vertical_odd_remainder_floors_top() {
let align = Align::new(Fill('X'))
.vertical(VerticalAlignment::Middle)
.child_height(3);
let area = Rect::new(0, 0, 1, 6);
let child = align.aligned_area(area);
assert_eq!(child.y, 1);
assert_eq!(child.height, 3);
}
#[test]
fn child_width_only_height_fills() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Center)
.child_width(2);
let area = Rect::new(0, 0, 8, 5);
let child = align.aligned_area(area);
assert_eq!(child.width, 2);
assert_eq!(child.height, 5, "height should be full parent height");
}
#[test]
fn child_height_only_width_fills() {
let align = Align::new(Fill('X'))
.vertical(VerticalAlignment::Bottom)
.child_height(2);
let area = Rect::new(0, 0, 8, 5);
let child = align.aligned_area(area);
assert_eq!(child.width, 8, "width should be full parent width");
assert_eq!(child.height, 2);
assert_eq!(child.y, 3);
}
#[test]
fn right_alignment_exact_fit() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Right)
.child_width(10);
let area = Rect::new(5, 0, 10, 1);
let child = align.aligned_area(area);
assert_eq!(child.x, 5, "exact fit should not shift");
assert_eq!(child.width, 10);
}
#[test]
fn bottom_alignment_exact_fit() {
let align = Align::new(Fill('X'))
.vertical(VerticalAlignment::Bottom)
.child_height(5);
let area = Rect::new(0, 10, 1, 5);
let child = align.aligned_area(area);
assert_eq!(child.y, 10, "exact fit should not shift");
}
#[test]
fn center_1x1_in_large_area() {
let align = Align::new(Fill('O'))
.horizontal(Alignment::Center)
.vertical(VerticalAlignment::Middle)
.child_width(1)
.child_height(1);
let area = Rect::new(0, 0, 100, 100);
let child = align.aligned_area(area);
assert_eq!(child.x, 49); assert_eq!(child.y, 49);
assert_eq!(child.width, 1);
assert_eq!(child.height, 1);
}
#[test]
fn vertical_alignment_copy_and_eq() {
let a = VerticalAlignment::Middle;
let b = a; assert_eq!(a, b);
assert_ne!(a, VerticalAlignment::Top);
assert_ne!(a, VerticalAlignment::Bottom);
}
#[test]
fn align_clone_preserves_settings() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Right)
.vertical(VerticalAlignment::Bottom)
.child_width(5)
.child_height(3);
let cloned = align.clone();
let area = Rect::new(0, 0, 20, 20);
assert_eq!(align.aligned_area(area), cloned.aligned_area(area));
}
#[test]
fn debug_format() {
let align = Align::new(Fill('X'))
.horizontal(Alignment::Center)
.vertical(VerticalAlignment::Middle);
let dbg = format!("{align:?}");
assert!(dbg.contains("Align"));
assert!(dbg.contains("Center"));
assert!(dbg.contains("Middle"));
}
#[test]
fn stateful_zero_area_is_noop() {
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
struct StatefulFill;
impl StatefulWidget for StatefulFill {
type State = Rc<RefCell<bool>>;
fn render(&self, _: Rect, _: &mut Frame, state: &mut Self::State) {
*state.borrow_mut() = true;
}
}
let align = Align::new(StatefulFill)
.horizontal(Alignment::Center)
.child_width(3);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
let mut rendered = Rc::new(RefCell::new(false));
StatefulWidget::render(&align, Rect::new(0, 0, 0, 0), &mut frame, &mut rendered);
assert!(!*rendered.borrow(), "should not render in zero area");
}
#[test]
fn stateful_zero_child_is_noop() {
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Clone)]
struct StatefulFill;
impl StatefulWidget for StatefulFill {
type State = Rc<RefCell<bool>>;
fn render(&self, _: Rect, _: &mut Frame, state: &mut Self::State) {
*state.borrow_mut() = true;
}
}
let align = Align::new(StatefulFill).child_width(0).child_height(0);
let mut pool = GraphemePool::new();
let mut frame = Frame::new(10, 10, &mut pool);
let mut rendered = Rc::new(RefCell::new(false));
StatefulWidget::render(&align, Rect::new(0, 0, 10, 10), &mut frame, &mut rendered);
assert!(!*rendered.borrow(), "should not render zero-size child");
}
#[test]
fn is_essential_delegates() {
struct Essential;
impl Widget for Essential {
fn render(&self, _: Rect, _: &mut Frame) {}
fn is_essential(&self) -> bool {
true
}
}
assert!(Align::new(Essential).is_essential());
assert!(!Align::new(Fill('X')).is_essential());
}
}