use crate::graphics::{GraphicsContext, Rectangle};
use crate::ui::Component;
use anyhow::Result;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LayoutType {
#[allow(dead_code)]
Fixed,
HorizontalFlow,
VerticalFlow,
Grid { columns: usize },
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Start,
Center,
End,
}
#[derive(Debug, Clone, Copy)]
pub struct Spacing {
pub top: i16,
pub right: i16,
pub bottom: i16,
pub left: i16,
}
impl Spacing {
pub fn new(top: i16, right: i16, bottom: i16, left: i16) -> Self {
Self {
top,
right,
bottom,
left,
}
}
pub fn uniform(spacing: i16) -> Self {
Self::new(spacing, spacing, spacing, spacing)
}
#[allow(dead_code)]
pub fn horizontal_vertical(horizontal: i16, vertical: i16) -> Self {
Self::new(vertical, horizontal, vertical, horizontal)
}
}
impl Default for Spacing {
fn default() -> Self {
Self::uniform(0)
}
}
pub struct LayoutItem {
pub component: Box<dyn Component>,
pub computed_bounds: Rectangle,
pub margin: Spacing,
pub visible: bool,
}
impl LayoutItem {
pub fn new(component: Box<dyn Component>) -> Self {
let bounds = component.bounds();
Self {
component,
computed_bounds: bounds,
margin: Spacing::default(),
visible: true,
}
}
#[allow(dead_code)]
pub fn with_margin(mut self, margin: Spacing) -> Self {
self.margin = margin;
self
}
#[allow(dead_code)]
pub fn set_visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
}
pub struct LayoutContainer {
layout_type: LayoutType,
bounds: Rectangle,
items: Vec<LayoutItem>,
gap: i16,
alignment: Alignment,
padding: Spacing,
needs_layout: bool,
}
impl LayoutContainer {
pub fn new(layout_type: LayoutType, bounds: Rectangle) -> Self {
Self {
layout_type,
bounds,
items: Vec::new(),
gap: 0,
alignment: Alignment::Start,
padding: Spacing::default(),
needs_layout: true,
}
}
pub fn with_gap(mut self, gap: i16) -> Self {
self.gap = gap;
self.needs_layout = true;
self
}
pub fn with_alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self.needs_layout = true;
self
}
pub fn with_padding(mut self, padding: Spacing) -> Self {
self.padding = padding;
self.needs_layout = true;
self
}
pub fn add_item(&mut self, item: LayoutItem) {
self.items.push(item);
self.needs_layout = true;
}
pub fn add_component(&mut self, component: Box<dyn Component>) {
self.add_item(LayoutItem::new(component));
}
#[allow(dead_code)]
pub fn set_bounds(&mut self, bounds: Rectangle) {
self.bounds = bounds;
self.needs_layout = true;
}
pub fn layout(&mut self) -> Result<()> {
if !self.needs_layout {
return Ok(());
}
let content_bounds = self.calculate_content_bounds();
match self.layout_type {
LayoutType::Fixed => self.layout_fixed(),
LayoutType::HorizontalFlow => self.layout_horizontal_flow(content_bounds),
LayoutType::VerticalFlow => self.layout_vertical_flow(content_bounds),
LayoutType::Grid { columns } => self.layout_grid(content_bounds, columns),
}?;
self.needs_layout = false;
Ok(())
}
fn calculate_content_bounds(&self) -> Rectangle {
let x = self.bounds.x + self.padding.left;
let y = self.bounds.y + self.padding.top;
let width = self
.bounds
.width
.saturating_sub((self.padding.left + self.padding.right) as u16);
let height = self
.bounds
.height
.saturating_sub((self.padding.top + self.padding.bottom) as u16);
Rectangle {
x,
y,
width,
height,
}
}
fn layout_fixed(&mut self) -> Result<()> {
for item in &mut self.items {
item.computed_bounds = item.component.bounds();
}
Ok(())
}
fn layout_horizontal_flow(&mut self, content_bounds: Rectangle) -> Result<()> {
let mut current_x = content_bounds.x;
let alignment = self.alignment;
for item in &mut self.items {
if !item.visible {
continue;
}
let original_bounds = item.component.bounds();
let y = match alignment {
Alignment::Start => content_bounds.y + item.margin.top,
Alignment::Center => {
content_bounds.y
+ (content_bounds.height as i16 - original_bounds.height as i16) / 2
+ item.margin.top
}
Alignment::End => {
content_bounds.y + content_bounds.height as i16 - original_bounds.height as i16
+ item.margin.top
}
};
item.computed_bounds = Rectangle {
x: current_x + item.margin.left,
y,
width: original_bounds.width,
height: original_bounds.height,
};
current_x +=
original_bounds.width as i16 + item.margin.left + item.margin.right + self.gap;
}
Ok(())
}
fn layout_vertical_flow(&mut self, content_bounds: Rectangle) -> Result<()> {
let mut current_y = content_bounds.y;
let alignment = self.alignment;
for item in &mut self.items {
if !item.visible {
continue;
}
let original_bounds = item.component.bounds();
let x = match alignment {
Alignment::Start => content_bounds.x + item.margin.left,
Alignment::Center => {
content_bounds.x
+ (content_bounds.width as i16 - original_bounds.width as i16) / 2
+ item.margin.left
}
Alignment::End => {
content_bounds.x + content_bounds.width as i16 - original_bounds.width as i16
+ item.margin.left
}
};
item.computed_bounds = Rectangle {
x,
y: current_y + item.margin.top,
width: original_bounds.width,
height: original_bounds.height,
};
current_y +=
original_bounds.height as i16 + item.margin.top + item.margin.bottom + self.gap;
}
Ok(())
}
fn layout_grid(&mut self, content_bounds: Rectangle, columns: usize) -> Result<()> {
if columns == 0 {
return Ok(());
}
let visible_items: Vec<&mut LayoutItem> =
self.items.iter_mut().filter(|item| item.visible).collect();
if visible_items.is_empty() {
return Ok(());
}
let rows_needed = visible_items.len().div_ceil(columns);
let cell_width = content_bounds.width / columns as u16;
let cell_height = if rows_needed > 0 {
content_bounds.height / rows_needed as u16
} else {
content_bounds.height
};
for (index, item) in visible_items.into_iter().enumerate() {
let col = index % columns;
let row = index / columns;
let cell_x = content_bounds.x + (col as u16 * cell_width) as i16;
let cell_y = content_bounds.y + (row as u16 * cell_height) as i16;
let original_bounds = item.component.bounds();
item.computed_bounds = Rectangle {
x: cell_x + item.margin.left,
y: cell_y + item.margin.top,
width: cell_width.min(original_bounds.width),
height: cell_height.min(original_bounds.height),
};
}
Ok(())
}
}
impl Component for LayoutContainer {
fn render(&self, graphics: &mut GraphicsContext) -> Result<()> {
for item in &self.items {
if item.visible && item.component.is_visible() {
let component_bounds = item.component.bounds();
let offset_x = item.computed_bounds.x - component_bounds.x;
let offset_y = item.computed_bounds.y - component_bounds.y;
if let Ok(Some(cairo_ctx)) = graphics.get_cairo_context() {
cairo_ctx.save().unwrap();
cairo_ctx.translate(offset_x as f64, offset_y as f64);
item.component.render(graphics)?;
cairo_ctx.restore().unwrap();
} else {
item.component.render(graphics)?;
}
}
}
Ok(())
}
fn bounds(&self) -> Rectangle {
self.bounds
}
fn update(&mut self, delta_time: f64) -> bool {
let mut needs_redraw = false;
for item in &mut self.items {
if item.component.update(delta_time) {
needs_redraw = true;
self.needs_layout = true; }
}
if self.needs_layout {
let _ = self.layout(); needs_redraw = true;
}
needs_redraw
}
fn is_visible(&self) -> bool {
self.items
.iter()
.any(|item| item.visible && item.component.is_visible())
}
fn should_remove(&self) -> bool {
self.items.iter().all(|item| item.component.should_remove())
}
}
pub struct LayoutBuilder;
impl LayoutBuilder {
pub fn horizontal(bounds: Rectangle) -> LayoutContainer {
LayoutContainer::new(LayoutType::HorizontalFlow, bounds)
}
pub fn vertical(bounds: Rectangle) -> LayoutContainer {
LayoutContainer::new(LayoutType::VerticalFlow, bounds)
}
pub fn grid(bounds: Rectangle, columns: usize) -> LayoutContainer {
LayoutContainer::new(LayoutType::Grid { columns }, bounds)
}
#[allow(dead_code)]
pub fn fixed(bounds: Rectangle) -> LayoutContainer {
LayoutContainer::new(LayoutType::Fixed, bounds)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockComponent {
bounds: Rectangle,
updated: bool,
}
impl MockComponent {
fn new(x: i16, y: i16, width: u16, height: u16) -> Self {
Self {
bounds: Rectangle::new(x, y, width, height),
updated: false,
}
}
}
impl Component for MockComponent {
fn render(&self, _graphics: &mut crate::graphics::GraphicsContext) -> anyhow::Result<()> {
Ok(())
}
fn bounds(&self) -> Rectangle {
self.bounds
}
fn update(&mut self, _delta_time: f64) -> bool {
self.updated = !self.updated;
self.updated
}
}
#[test]
fn test_spacing_creation() {
let spacing = Spacing::new(10, 20, 30, 40);
assert_eq!(spacing.top, 10);
assert_eq!(spacing.right, 20);
assert_eq!(spacing.bottom, 30);
assert_eq!(spacing.left, 40);
let uniform = Spacing::uniform(15);
assert_eq!(uniform.top, 15);
assert_eq!(uniform.right, 15);
assert_eq!(uniform.bottom, 15);
assert_eq!(uniform.left, 15);
}
#[test]
fn test_layout_item_creation() {
let component = MockComponent::new(10, 20, 100, 50);
let item = LayoutItem::new(Box::new(component));
assert_eq!(item.computed_bounds.x, 10);
assert_eq!(item.computed_bounds.y, 20);
assert_eq!(item.computed_bounds.width, 100);
assert_eq!(item.computed_bounds.height, 50);
assert!(item.visible);
}
#[test]
fn test_layout_container_creation() {
let bounds = Rectangle::new(0, 0, 800, 600);
let container = LayoutContainer::new(LayoutType::HorizontalFlow, bounds);
assert_eq!(container.bounds().x, 0);
assert_eq!(container.bounds().y, 0);
assert_eq!(container.bounds().width, 800);
assert_eq!(container.bounds().height, 600);
}
#[test]
fn test_layout_builder() {
let bounds = Rectangle::new(0, 0, 400, 300);
let horizontal = LayoutBuilder::horizontal(bounds);
matches!(horizontal.layout_type, LayoutType::HorizontalFlow);
let vertical = LayoutBuilder::vertical(bounds);
matches!(vertical.layout_type, LayoutType::VerticalFlow);
let grid = LayoutBuilder::grid(bounds, 3);
matches!(grid.layout_type, LayoutType::Grid { columns: 3 });
}
#[test]
fn test_horizontal_layout() {
let bounds = Rectangle::new(0, 0, 400, 100);
let mut container = LayoutBuilder::horizontal(bounds)
.with_gap(10)
.with_alignment(Alignment::Start);
let comp1 = MockComponent::new(0, 0, 50, 30);
let comp2 = MockComponent::new(0, 0, 80, 40);
container.add_component(Box::new(comp1));
container.add_component(Box::new(comp2));
container.layout().unwrap();
assert_eq!(container.items[0].computed_bounds.x, 0);
assert_eq!(container.items[0].computed_bounds.y, 0);
assert_eq!(container.items[1].computed_bounds.x, 50 + 10); assert_eq!(container.items[1].computed_bounds.y, 0);
}
#[test]
fn test_vertical_layout() {
let bounds = Rectangle::new(0, 0, 100, 400);
let mut container = LayoutBuilder::vertical(bounds)
.with_gap(5)
.with_alignment(Alignment::Start);
let comp1 = MockComponent::new(0, 0, 50, 30);
let comp2 = MockComponent::new(0, 0, 60, 40);
container.add_component(Box::new(comp1));
container.add_component(Box::new(comp2));
container.layout().unwrap();
assert_eq!(container.items[0].computed_bounds.x, 0);
assert_eq!(container.items[0].computed_bounds.y, 0);
assert_eq!(container.items[1].computed_bounds.x, 0);
assert_eq!(container.items[1].computed_bounds.y, 30 + 5); }
#[test]
fn test_grid_layout() {
let bounds = Rectangle::new(0, 0, 200, 200);
let mut container = LayoutBuilder::grid(bounds, 2);
for _i in 0..4 {
let comp = MockComponent::new(0, 0, 40, 30);
container.add_component(Box::new(comp));
}
container.layout().unwrap();
let cell_width = 200 / 2;
let cell_height = 200 / 2;
assert_eq!(container.items[0].computed_bounds.x, 0);
assert_eq!(container.items[0].computed_bounds.y, 0);
assert_eq!(container.items[1].computed_bounds.x, cell_width as i16);
assert_eq!(container.items[1].computed_bounds.y, 0);
assert_eq!(container.items[2].computed_bounds.x, 0);
assert_eq!(container.items[2].computed_bounds.y, cell_height as i16);
assert_eq!(container.items[3].computed_bounds.x, cell_width as i16);
assert_eq!(container.items[3].computed_bounds.y, cell_height as i16);
}
#[test]
fn test_alignment_center() {
let bounds = Rectangle::new(0, 0, 200, 100);
let mut container = LayoutBuilder::horizontal(bounds).with_alignment(Alignment::Center);
let comp = MockComponent::new(0, 0, 50, 30);
container.add_component(Box::new(comp));
container.layout().unwrap();
let expected_y = (100 - 30) / 2; assert_eq!(container.items[0].computed_bounds.y, expected_y as i16);
}
#[test]
fn test_padding() {
let bounds = Rectangle::new(0, 0, 200, 100);
let padding = Spacing::uniform(10);
let mut container = LayoutBuilder::horizontal(bounds).with_padding(padding);
let comp = MockComponent::new(0, 0, 50, 30);
container.add_component(Box::new(comp));
container.layout().unwrap();
assert_eq!(container.items[0].computed_bounds.x, 10); assert_eq!(container.items[0].computed_bounds.y, 10); }
#[test]
fn test_container_visibility() {
let bounds = Rectangle::new(0, 0, 100, 100);
let mut container = LayoutContainer::new(LayoutType::Fixed, bounds);
assert!(!container.is_visible());
let comp = MockComponent::new(0, 0, 50, 30);
container.add_component(Box::new(comp));
assert!(container.is_visible());
}
#[test]
fn test_container_update() {
let bounds = Rectangle::new(0, 0, 100, 100);
let mut container = LayoutContainer::new(LayoutType::Fixed, bounds);
let comp = MockComponent::new(0, 0, 50, 30);
container.add_component(Box::new(comp));
let needs_redraw = container.update(0.016);
assert!(needs_redraw); }
}