use crate::vdom::VNode;
use ratatui::layout::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum FlexDirection {
#[default]
Row,
Column,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum JustifyContent {
#[default]
Start,
Center,
End,
SpaceBetween,
SpaceAround,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum AlignItems {
#[default]
Start,
Center,
End,
Stretch,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FlexStyle {
pub direction: FlexDirection,
pub justify_content: JustifyContent,
pub align_items: AlignItems,
pub gap: u16,
pub flex_grow: f32,
pub flex_shrink: f32,
}
impl FlexStyle {
pub fn new() -> Self {
Self::default()
}
pub fn row(mut self) -> Self {
self.direction = FlexDirection::Row;
self
}
pub fn column(mut self) -> Self {
self.direction = FlexDirection::Column;
self
}
pub fn justify(mut self, justify: JustifyContent) -> Self {
self.justify_content = justify;
self
}
pub fn align(mut self, align: AlignItems) -> Self {
self.align_items = align;
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
pub fn flex_grow(mut self, grow: f32) -> Self {
self.flex_grow = grow;
self
}
pub fn flex_shrink(mut self, shrink: f32) -> Self {
self.flex_shrink = shrink;
self
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FlexboxLayout;
impl FlexboxLayout {
pub fn new() -> Self {
Self
}
pub fn calculate(node: &VNode, constraints: Rect) -> Vec<Rect> {
let children = match node.children() {
Some(children) if !children.is_empty() => children,
_ => return vec![],
};
let style = Self::extract_style(node);
let base_sizes: Vec<Rect> = children
.iter()
.map(|_| Rect {
x: 0,
y: 0,
width: 1, height: 1, })
.collect();
Self::layout_children(&base_sizes, constraints, &style, children.len())
}
fn extract_style(node: &VNode) -> FlexStyle {
let attrs = node.attrs().unwrap();
FlexStyle {
direction: attrs
.get("direction")
.and_then(|v| match v.as_str() {
"row" => Some(FlexDirection::Row),
"column" => Some(FlexDirection::Column),
_ => None,
})
.unwrap_or(FlexDirection::Row),
justify_content: attrs
.get("justify")
.and_then(|v| match v.as_str() {
"start" => Some(JustifyContent::Start),
"center" => Some(JustifyContent::Center),
"end" => Some(JustifyContent::End),
"space-between" => Some(JustifyContent::SpaceBetween),
"space-around" => Some(JustifyContent::SpaceAround),
_ => None,
})
.unwrap_or(JustifyContent::Start),
align_items: attrs
.get("align")
.and_then(|v| match v.as_str() {
"start" => Some(AlignItems::Start),
"center" => Some(AlignItems::Center),
"end" => Some(AlignItems::End),
"stretch" => Some(AlignItems::Stretch),
_ => None,
})
.unwrap_or(AlignItems::Start),
gap: attrs.get("gap").and_then(|v| v.parse().ok()).unwrap_or(0),
flex_grow: attrs
.get("flex-grow")
.and_then(|v| v.parse().ok())
.unwrap_or(0.0),
flex_shrink: attrs
.get("flex-shrink")
.and_then(|v| v.parse().ok())
.unwrap_or(1.0),
}
}
fn layout_children(
_base_sizes: &[Rect],
constraints: Rect,
style: &FlexStyle,
child_count: usize,
) -> Vec<Rect> {
if child_count == 0 {
return vec![];
}
let main_axis_size = match style.direction {
FlexDirection::Row => constraints.width,
FlexDirection::Column => constraints.height,
};
let cross_axis_size = match style.direction {
FlexDirection::Row => constraints.height,
FlexDirection::Column => constraints.width,
};
let total_gap = style
.gap
.saturating_mul(child_count.saturating_sub(1) as u16);
let available_space = main_axis_size.saturating_sub(total_gap);
let main_positions =
Self::distribute_main_axis(available_space, style, child_count, main_axis_size);
let cross_positions =
Self::distribute_cross_axis(cross_axis_size, &style.align_items, child_count);
let mut results = Vec::with_capacity(child_count);
for i in 0..child_count {
let child_width = match style.direction {
FlexDirection::Row => main_positions[i].size,
FlexDirection::Column => cross_axis_size,
};
let child_height = match style.direction {
FlexDirection::Row => cross_axis_size,
FlexDirection::Column => main_positions[i].size,
};
let x = match style.direction {
FlexDirection::Row => constraints.x + main_positions[i].pos,
FlexDirection::Column => constraints.x + cross_positions[i].pos,
};
let y = match style.direction {
FlexDirection::Row => constraints.y + cross_positions[i].pos,
FlexDirection::Column => constraints.y + main_positions[i].pos,
};
results.push(Rect {
x,
y,
width: child_width,
height: child_height,
});
}
results
}
fn distribute_main_axis(
available_space: u16,
style: &FlexStyle,
child_count: usize,
_container_size: u16,
) -> Vec<PositionInfo> {
let mut positions = Vec::with_capacity(child_count);
if child_count == 0 {
return positions;
}
let gap_space = style
.gap
.saturating_mul(child_count.saturating_sub(1) as u16);
let usable_space = available_space.saturating_sub(gap_space);
let total_flex_grow = if style.flex_grow > 0.0 {
style.flex_grow * child_count as f32
} else {
child_count as f32 };
let base_size = if total_flex_grow > 0.0 {
(usable_space as f32 / total_flex_grow) as u16
} else {
0
};
let total_used = base_size
.saturating_mul(child_count as u16)
.saturating_add(gap_space);
let mut current_pos = match style.justify_content {
JustifyContent::Start => 0,
JustifyContent::Center => available_space.saturating_sub(total_used) / 2,
JustifyContent::End => available_space.saturating_sub(total_used),
JustifyContent::SpaceBetween => {
if child_count > 1 {
let _extra_gap = (usable_space
.saturating_sub(base_size.saturating_mul(child_count as u16)))
.saturating_div(child_count.saturating_sub(1) as u16);
0
} else {
0
}
}
JustifyContent::SpaceAround => {
let extra_gap = (usable_space
.saturating_sub(base_size.saturating_mul(child_count as u16)))
.saturating_div(child_count as u16);
extra_gap / 2
}
};
let mut gap = style.gap;
if style.justify_content == JustifyContent::SpaceBetween && child_count > 1 {
let extra = usable_space.saturating_sub(base_size.saturating_mul(child_count as u16));
gap = gap.saturating_add(extra / (child_count.saturating_sub(1) as u16));
}
if style.justify_content == JustifyContent::SpaceAround {
let extra = usable_space.saturating_sub(base_size.saturating_mul(child_count as u16));
let extra_gap = extra / (child_count as u16);
gap = extra_gap;
}
for _i in 0..child_count {
positions.push(PositionInfo {
pos: current_pos,
size: base_size,
});
current_pos = current_pos.saturating_add(base_size).saturating_add(gap);
}
positions
}
fn distribute_cross_axis(
cross_axis_size: u16,
style: &AlignItems,
child_count: usize,
) -> Vec<PositionInfo> {
let mut positions = Vec::with_capacity(child_count);
if child_count == 0 {
return positions;
}
let child_size = cross_axis_size;
for _i in 0..child_count {
let pos = match style {
AlignItems::Start => 0,
AlignItems::Center => 0, AlignItems::End => 0, AlignItems::Stretch => 0,
};
positions.push(PositionInfo {
pos,
size: child_size,
});
}
positions
}
}
#[derive(Debug, Clone, Copy)]
struct PositionInfo {
pos: u16,
size: u16,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vdom::VNode;
use std::collections::HashMap;
fn create_flex_container(attrs: HashMap<String, String>, children: Vec<VNode>) -> VNode {
VNode::Element {
tag: "flex".to_string(),
attrs,
children,
}
}
fn create_text_node(text: &str) -> VNode {
VNode::Text(text.to_string())
}
#[test]
fn test_layout_simple_row() {
let children = vec![
create_text_node("A"),
create_text_node("B"),
create_text_node("C"),
];
let container = create_flex_container(HashMap::new(), children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 3);
assert!(results[0].x < results[1].x);
assert!(results[1].x < results[2].x);
assert_eq!(results[0].height, 10);
assert_eq!(results[1].height, 10);
assert_eq!(results[2].height, 10);
assert_eq!(results[0].y, 0);
assert_eq!(results[1].y, 0);
assert_eq!(results[2].y, 0);
}
#[test]
fn test_layout_simple_column() {
let children = vec![
create_text_node("A"),
create_text_node("B"),
create_text_node("C"),
];
let mut attrs = HashMap::new();
attrs.insert("direction".to_string(), "column".to_string());
let container = create_flex_container(attrs, children);
let constraints = Rect {
x: 0,
y: 0,
width: 10,
height: 20,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 3);
assert!(results[0].y < results[1].y);
assert!(results[1].y < results[2].y);
assert_eq!(results[0].width, 10);
assert_eq!(results[1].width, 10);
assert_eq!(results[2].width, 10);
assert_eq!(results[0].x, 0);
assert_eq!(results[1].x, 0);
assert_eq!(results[2].x, 0);
}
#[test]
fn test_layout_justify_center() {
let children = vec![
create_text_node("A"),
create_text_node("B"),
create_text_node("C"),
];
let mut attrs = HashMap::new();
attrs.insert("justify".to_string(), "center".to_string());
let container = create_flex_container(attrs, children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 3);
let total_width = results[2].x + results[2].width;
assert!(total_width <= 20);
assert!(results[0].x > 0 || total_width == 20);
}
#[test]
fn test_layout_flex_grow() {
let children = vec![
create_text_node("A"),
create_text_node("B"),
create_text_node("C"),
];
let mut attrs = HashMap::new();
attrs.insert("flex-grow".to_string(), "1.0".to_string());
let container = create_flex_container(attrs, children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 3);
assert!(results[0].width > 0);
assert!(results[1].width > 0);
assert!(results[2].width > 0);
}
#[test]
fn test_layout_gap() {
let children = vec![
create_text_node("A"),
create_text_node("B"),
create_text_node("C"),
];
let mut attrs = HashMap::new();
attrs.insert("gap".to_string(), "2".to_string());
let container = create_flex_container(attrs, children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 3);
let gap_1 = results[1].x - (results[0].x + results[0].width);
let gap_2 = results[2].x - (results[1].x + results[1].width);
assert!(gap_1 >= 2 || gap_2 >= 2);
}
#[test]
fn test_layout_nested() {
let inner_children = vec![create_text_node("A"), create_text_node("B")];
let mut inner_attrs = HashMap::new();
inner_attrs.insert("direction".to_string(), "row".to_string());
let inner_container = create_flex_container(inner_attrs, inner_children);
let outer_children = vec![
create_text_node("X"),
inner_container,
create_text_node("Y"),
];
let mut outer_attrs = HashMap::new();
outer_attrs.insert("direction".to_string(), "column".to_string());
let outer_container = create_flex_container(outer_attrs, outer_children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 30,
};
let results = FlexboxLayout::calculate(&outer_container, constraints);
assert_eq!(results.len(), 3);
assert!(results[0].y < results[1].y);
assert!(results[1].y < results[2].y);
assert_eq!(results[0].width, 20);
assert_eq!(results[1].width, 20);
assert_eq!(results[2].width, 20);
}
#[test]
fn test_flex_style_builder() {
let style = FlexStyle::new()
.row()
.justify(JustifyContent::Center)
.align(AlignItems::Stretch)
.gap(2)
.flex_grow(1.0)
.flex_shrink(0.5);
assert_eq!(style.direction, FlexDirection::Row);
assert_eq!(style.justify_content, JustifyContent::Center);
assert_eq!(style.align_items, AlignItems::Stretch);
assert_eq!(style.gap, 2);
assert_eq!(style.flex_grow, 1.0);
assert_eq!(style.flex_shrink, 0.5);
}
#[test]
fn test_empty_children() {
let container = create_flex_container(HashMap::new(), vec![]);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 0);
}
#[test]
fn test_single_child() {
let children = vec![create_text_node("A")];
let container = create_flex_container(HashMap::new(), children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 1);
assert_eq!(results[0].x, 0);
assert_eq!(results[0].y, 0);
assert_eq!(results[0].height, 10);
}
#[test]
fn test_justify_end() {
let children = vec![create_text_node("A"), create_text_node("B")];
let mut attrs = HashMap::new();
attrs.insert("justify".to_string(), "end".to_string());
let container = create_flex_container(attrs, children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 2);
let total_width = results[1].x + results[1].width;
assert!(total_width <= 20);
}
#[test]
fn test_space_between() {
let children = vec![
create_text_node("A"),
create_text_node("B"),
create_text_node("C"),
];
let mut attrs = HashMap::new();
attrs.insert("justify".to_string(), "space-between".to_string());
let container = create_flex_container(attrs, children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 3);
assert_eq!(results[0].x, 0);
let last_end = results[2].x + results[2].width;
assert!(last_end <= 20);
}
#[test]
fn test_space_around() {
let children = vec![
create_text_node("A"),
create_text_node("B"),
create_text_node("C"),
];
let mut attrs = HashMap::new();
attrs.insert("justify".to_string(), "space-around".to_string());
let container = create_flex_container(attrs, children);
let constraints = Rect {
x: 0,
y: 0,
width: 20,
height: 10,
};
let results = FlexboxLayout::calculate(&container, constraints);
assert_eq!(results.len(), 3);
assert!(!results.is_empty());
assert!(results[0].width > 0);
}
}