#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl Rect {
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
Rect {
x,
y,
width,
height,
}
}
pub fn fullscreen(cols: u16, rows: u16) -> Self {
Rect::new(0, 0, cols, rows)
}
pub fn right(&self) -> u16 {
self.x.saturating_add(self.width)
}
pub fn bottom(&self) -> u16 {
self.y.saturating_add(self.height)
}
pub fn contains(&self, x: u16, y: u16) -> bool {
x >= self.x && x < self.right() && y >= self.y && y < self.bottom()
}
pub fn inner(&self, padding: u16) -> Self {
let padding2 = padding.saturating_mul(2);
Rect {
x: self.x.saturating_add(padding),
y: self.y.saturating_add(padding),
width: self.width.saturating_sub(padding2),
height: self.height.saturating_sub(padding2),
}
}
pub fn split_horizontal(&self, top_height: u16) -> (Rect, Rect) {
let top = Rect {
x: self.x,
y: self.y,
width: self.width,
height: top_height.min(self.height),
};
let bottom = Rect {
x: self.x,
y: self.y.saturating_add(top_height),
width: self.width,
height: self.height.saturating_sub(top_height),
};
(top, bottom)
}
pub fn split_vertical(&self, left_width: u16) -> (Rect, Rect) {
let left = Rect {
x: self.x,
y: self.y,
width: left_width.min(self.width),
height: self.height,
};
let right = Rect {
x: self.x.saturating_add(left_width),
y: self.y,
width: self.width.saturating_sub(left_width),
height: self.height,
};
(left, right)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlexDirection {
Row,
Column,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Start,
Center,
End,
Stretch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Size {
Fixed(u16),
Flex(u16),
Auto,
}
#[derive(Debug, Clone)]
pub struct FlexLayout {
direction: FlexDirection,
gap: u16,
padding: u16,
align: Alignment,
}
impl FlexLayout {
pub fn new(direction: FlexDirection) -> Self {
FlexLayout {
direction,
gap: 0,
padding: 0,
align: Alignment::Stretch,
}
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
pub fn padding(mut self, padding: u16) -> Self {
self.padding = padding;
self
}
pub fn align(mut self, align: Alignment) -> Self {
self.align = align;
self
}
pub fn layout(&self, container: Rect, sizes: &[Size]) -> Vec<Rect> {
if sizes.is_empty() {
return Vec::new();
}
let inner = container.inner(self.padding);
let flex_unit_size = self.flex_unit_size(&inner, sizes);
let mut rects = Vec::with_capacity(sizes.len());
let mut offset = 0u16;
for size in sizes {
let child_main_size = match size {
Size::Fixed(s) => *s,
Size::Flex(f) => flex_unit_size.saturating_mul(*f),
Size::Auto => flex_unit_size,
};
let rect = self.child_rect(&inner, offset, child_main_size);
rects.push(rect);
offset = offset
.saturating_add(child_main_size)
.saturating_add(self.gap);
}
rects
}
fn flex_unit_size(&self, inner: &Rect, sizes: &[Size]) -> u16 {
let main_size = match self.direction {
FlexDirection::Row => inner.width,
FlexDirection::Column => inner.height,
};
let total_gap = self
.gap
.saturating_mul(sizes.len().saturating_sub(1) as u16);
let available = main_size.saturating_sub(total_gap);
let mut fixed_space = 0u16;
let mut flex_units = 0u16;
for size in sizes {
match size {
Size::Fixed(s) => fixed_space = fixed_space.saturating_add(*s),
Size::Flex(f) => flex_units = flex_units.saturating_add(*f),
Size::Auto => flex_units = flex_units.saturating_add(1),
}
}
let flex_space = available.saturating_sub(fixed_space);
if flex_units > 0 {
flex_space / flex_units
} else {
0
}
}
fn child_rect(&self, inner: &Rect, offset: u16, child_main_size: u16) -> Rect {
match self.direction {
FlexDirection::Row => {
let x = inner.x.saturating_add(offset);
let y = self.calculate_cross_offset(inner.y, inner.height, inner.height);
Rect::new(x, y, child_main_size, inner.height)
}
FlexDirection::Column => {
let x = self.calculate_cross_offset(inner.x, inner.width, inner.width);
let y = inner.y.saturating_add(offset);
Rect::new(x, y, inner.width, child_main_size)
}
}
}
fn calculate_cross_offset(&self, base: u16, container_size: u16, child_size: u16) -> u16 {
match self.align {
Alignment::Start => base,
Alignment::Center => {
base.saturating_add((container_size.saturating_sub(child_size)) / 2)
}
Alignment::End => base.saturating_add(container_size.saturating_sub(child_size)),
Alignment::Stretch => base,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rect_creation() {
let r = Rect::new(10, 20, 30, 40);
assert_eq!(r.x, 10);
assert_eq!(r.y, 20);
assert_eq!(r.width, 30);
assert_eq!(r.height, 40);
assert_eq!(r.right(), 40);
assert_eq!(r.bottom(), 60);
}
#[test]
fn test_rect_contains() {
let r = Rect::new(10, 10, 20, 20);
assert!(r.contains(15, 15));
assert!(r.contains(10, 10)); assert!(!r.contains(30, 30)); assert!(!r.contains(5, 15)); }
#[test]
fn test_rect_split_horizontal() {
let r = Rect::new(0, 0, 80, 24);
let (top, bottom) = r.split_horizontal(3);
assert_eq!(top, Rect::new(0, 0, 80, 3));
assert_eq!(bottom, Rect::new(0, 3, 80, 21));
}
#[test]
fn test_rect_split_vertical() {
let r = Rect::new(0, 0, 80, 24);
let (left, right) = r.split_vertical(20);
assert_eq!(left, Rect::new(0, 0, 20, 24));
assert_eq!(right, Rect::new(20, 0, 60, 24));
}
#[test]
fn test_flex_layout_row() {
let container = Rect::new(0, 0, 100, 10);
let layout = FlexLayout::new(FlexDirection::Row);
let sizes = vec![Size::Fixed(20), Size::Flex(1), Size::Fixed(20)];
let rects = layout.layout(container, &sizes);
assert_eq!(rects.len(), 3);
assert_eq!(rects[0].width, 20);
assert_eq!(rects[1].width, 60); assert_eq!(rects[2].width, 20);
}
#[test]
fn test_flex_layout_column_with_gap() {
let container = Rect::new(0, 0, 80, 24);
let layout = FlexLayout::new(FlexDirection::Column).gap(1);
let sizes = vec![Size::Fixed(3), Size::Flex(1), Size::Fixed(1)];
let rects = layout.layout(container, &sizes);
assert_eq!(rects[0].height, 3);
assert_eq!(rects[1].height, 18);
assert_eq!(rects[2].height, 1);
assert_eq!(rects[0].y, 0);
assert_eq!(rects[1].y, 4); assert_eq!(rects[2].y, 23); }
}