use super::shapes::{Point, Rect, Size};
use std::collections::HashMap;
pub const GRID_SIZE: f32 = 8.0;
#[derive(Debug, Clone, Copy)]
pub struct Viewport {
pub width: f32,
pub height: f32,
pub padding: f32,
}
impl Viewport {
pub fn new(width: f32, height: f32) -> Self {
Self {
width,
height,
padding: GRID_SIZE * 3.0, }
}
pub fn presentation() -> Self {
Self::new(1920.0, 1080.0)
}
pub fn document() -> Self {
Self::new(800.0, 600.0)
}
pub fn square(size: f32) -> Self {
Self::new(size, size)
}
pub fn with_padding(mut self, padding: f32) -> Self {
self.padding = padding;
self
}
pub fn content_area(&self) -> Rect {
Rect::new(
self.padding,
self.padding,
self.width - 2.0 * self.padding,
self.height - 2.0 * self.padding,
)
}
pub fn center(&self) -> Point {
Point::new(self.width / 2.0, self.height / 2.0)
}
pub fn view_box(&self) -> String {
format!("0 0 {} {}", self.width, self.height)
}
}
impl Default for Viewport {
fn default() -> Self {
Self::presentation()
}
}
#[derive(Debug, Clone)]
pub struct LayoutRect {
pub id: String,
pub rect: Rect,
pub layer: i32,
}
impl LayoutRect {
pub fn new(id: &str, rect: Rect) -> Self {
Self { id: id.to_string(), rect, layer: 0 }
}
pub fn with_layer(mut self, layer: i32) -> Self {
self.layer = layer;
self
}
pub fn bounds(&self) -> &Rect {
&self.rect
}
pub fn overlaps(&self, other: &LayoutRect) -> bool {
self.rect.intersects(&other.rect)
}
}
#[derive(Debug)]
pub struct LayoutEngine {
pub elements: HashMap<String, LayoutRect>,
viewport: Viewport,
grid_size: f32,
}
impl LayoutEngine {
pub fn new(viewport: Viewport) -> Self {
Self { elements: HashMap::new(), viewport, grid_size: GRID_SIZE }
}
pub fn with_grid_size(mut self, size: f32) -> Self {
self.grid_size = size;
self
}
pub fn snap_to_grid(&self, value: f32) -> f32 {
(value / self.grid_size).round() * self.grid_size
}
pub fn snap_point(&self, point: Point) -> Point {
Point::new(self.snap_to_grid(point.x), self.snap_to_grid(point.y))
}
pub fn snap_rect(&self, rect: &Rect) -> Rect {
Rect::new(
self.snap_to_grid(rect.position.x),
self.snap_to_grid(rect.position.y),
self.snap_to_grid(rect.size.width),
self.snap_to_grid(rect.size.height),
)
.with_radius(rect.corner_radius)
}
pub fn add(&mut self, id: &str, rect: Rect) -> bool {
let snapped = self.snap_rect(&rect);
let layout_rect = LayoutRect::new(id, snapped);
if self.has_collision(&layout_rect) {
return false;
}
self.elements.insert(id.to_string(), layout_rect);
true
}
pub fn add_with_layer(&mut self, id: &str, rect: Rect, layer: i32) -> bool {
let snapped = self.snap_rect(&rect);
let layout_rect = LayoutRect::new(id, snapped).with_layer(layer);
if self.has_collision_on_layer(&layout_rect, layer) {
return false;
}
self.elements.insert(id.to_string(), layout_rect);
true
}
pub fn has_collision(&self, new_rect: &LayoutRect) -> bool {
for existing in self.elements.values() {
if existing.id != new_rect.id && existing.overlaps(new_rect) {
return true;
}
}
false
}
pub fn has_collision_on_layer(&self, new_rect: &LayoutRect, layer: i32) -> bool {
for existing in self.elements.values() {
if existing.id != new_rect.id && existing.layer == layer && existing.overlaps(new_rect)
{
return true;
}
}
false
}
pub fn find_collisions(&self, rect: &Rect) -> Vec<&LayoutRect> {
let test_rect = LayoutRect::new("_test", rect.clone());
self.elements.values().filter(|e| e.overlaps(&test_rect)).collect()
}
pub fn remove(&mut self, id: &str) -> Option<LayoutRect> {
self.elements.remove(id)
}
pub fn get(&self, id: &str) -> Option<&LayoutRect> {
self.elements.get(id)
}
pub fn all_elements(&self) -> impl Iterator<Item = &LayoutRect> {
self.elements.values()
}
pub fn elements_by_layer(&self) -> Vec<&LayoutRect> {
let mut elements: Vec<_> = self.elements.values().collect();
elements.sort_by_key(|e| e.layer);
elements
}
pub fn element_at(&self, point: &Point) -> Option<&LayoutRect> {
let mut candidates: Vec<_> =
self.elements.values().filter(|e| e.rect.contains(point)).collect();
candidates.sort_by_key(|e| -e.layer);
candidates.first().copied()
}
pub fn find_free_position(&self, size: Size, start: Point) -> Option<Point> {
let content = self.viewport.content_area();
let max_x = content.right() - size.width;
let max_y = content.bottom() - size.height;
let mut x = self.snap_to_grid(start.x.max(content.position.x));
let mut y = self.snap_to_grid(start.y.max(content.position.y));
while y <= max_y {
while x <= max_x {
let test_rect = Rect::new(x, y, size.width, size.height);
let layout_rect = LayoutRect::new("_test", test_rect);
if !self.has_collision(&layout_rect) {
return Some(Point::new(x, y));
}
x += self.grid_size;
}
x = self.snap_to_grid(content.position.x);
y += self.grid_size;
}
None
}
pub fn is_within_bounds(&self, rect: &Rect) -> bool {
let content = self.viewport.content_area();
rect.position.x >= content.position.x
&& rect.position.y >= content.position.y
&& rect.right() <= content.right()
&& rect.bottom() <= content.bottom()
}
pub fn validate(&self) -> Vec<LayoutError> {
let mut errors = Vec::new();
let elements: Vec<_> = self.elements.values().collect();
for i in 0..elements.len() {
for j in (i + 1)..elements.len() {
if elements[i].layer == elements[j].layer && elements[i].overlaps(elements[j]) {
errors.push(LayoutError::Overlap {
id1: elements[i].id.clone(),
id2: elements[j].id.clone(),
});
}
}
}
for element in &elements {
if !self.is_within_bounds(&element.rect) {
errors.push(LayoutError::OutOfBounds { id: element.id.clone() });
}
}
for element in &elements {
let rect = &element.rect;
if rect.position.x % self.grid_size != 0.0 || rect.position.y % self.grid_size != 0.0 {
errors.push(LayoutError::NotAligned { id: element.id.clone() });
}
}
errors
}
pub fn viewport(&self) -> &Viewport {
&self.viewport
}
pub fn clear(&mut self) {
self.elements.clear();
}
pub fn len(&self) -> usize {
self.elements.len()
}
pub fn is_empty(&self) -> bool {
self.elements.is_empty()
}
}
impl Default for LayoutEngine {
fn default() -> Self {
Self::new(Viewport::default())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum LayoutError {
Overlap { id1: String, id2: String },
OutOfBounds { id: String },
NotAligned { id: String },
}
impl std::fmt::Display for LayoutError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Overlap { id1, id2 } => write!(f, "Elements '{}' and '{}' overlap", id1, id2),
Self::OutOfBounds { id } => write!(f, "Element '{}' is outside viewport", id),
Self::NotAligned { id } => write!(f, "Element '{}' is not grid-aligned", id),
}
}
}
pub mod auto_layout {
use super::*;
pub fn row(elements: &[(&str, Size)], start: Point, spacing: f32) -> Vec<(String, Rect)> {
let mut x = start.x;
let mut result = Vec::new();
for (id, size) in elements {
result.push(((*id).to_string(), Rect::new(x, start.y, size.width, size.height)));
x += size.width + spacing;
}
result
}
pub fn column(elements: &[(&str, Size)], start: Point, spacing: f32) -> Vec<(String, Rect)> {
let mut y = start.y;
let mut result = Vec::new();
for (id, size) in elements {
result.push(((*id).to_string(), Rect::new(start.x, y, size.width, size.height)));
y += size.height + spacing;
}
result
}
pub fn grid(
elements: &[(&str, Size)],
start: Point,
columns: usize,
h_spacing: f32,
v_spacing: f32,
) -> Vec<(String, Rect)> {
let mut result = Vec::new();
let mut x = start.x;
let mut y = start.y;
let mut row_height: f32 = 0.0;
for (i, (id, size)) in elements.iter().enumerate() {
if i > 0 && i % columns == 0 {
x = start.x;
y += row_height + v_spacing;
row_height = 0.0;
}
result.push(((*id).to_string(), Rect::new(x, y, size.width, size.height)));
x += size.width + h_spacing;
row_height = row_height.max(size.height);
}
result
}
pub fn center_horizontal(
elements: &[(String, Rect)],
viewport: &Viewport,
) -> Vec<(String, Rect)> {
if elements.is_empty() {
return vec![];
}
let min_x = elements.iter().map(|(_, r)| r.position.x).fold(f32::INFINITY, f32::min);
let max_x = elements.iter().map(|(_, r)| r.right()).fold(f32::NEG_INFINITY, f32::max);
let total_width = max_x - min_x;
let center_offset = (viewport.width - total_width) / 2.0 - min_x;
elements
.iter()
.map(|(id, r)| {
(
id.clone(),
Rect::new(
r.position.x + center_offset,
r.position.y,
r.size.width,
r.size.height,
),
)
})
.collect()
}
pub fn center_vertical(
elements: &[(String, Rect)],
viewport: &Viewport,
) -> Vec<(String, Rect)> {
if elements.is_empty() {
return vec![];
}
let min_y = elements.iter().map(|(_, r)| r.position.y).fold(f32::INFINITY, f32::min);
let max_y = elements.iter().map(|(_, r)| r.bottom()).fold(f32::NEG_INFINITY, f32::max);
let total_height = max_y - min_y;
let center_offset = (viewport.height - total_height) / 2.0 - min_y;
elements
.iter()
.map(|(id, r)| {
(
id.clone(),
Rect::new(
r.position.x,
r.position.y + center_offset,
r.size.width,
r.size.height,
),
)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_viewport_creation() {
let vp = Viewport::new(800.0, 600.0);
assert_eq!(vp.width, 800.0);
assert_eq!(vp.height, 600.0);
}
#[test]
fn test_viewport_center() {
let vp = Viewport::new(100.0, 100.0);
let center = vp.center();
assert_eq!(center.x, 50.0);
assert_eq!(center.y, 50.0);
}
#[test]
fn test_viewport_content_area() {
let vp = Viewport::new(100.0, 100.0).with_padding(10.0);
let content = vp.content_area();
assert_eq!(content.position.x, 10.0);
assert_eq!(content.position.y, 10.0);
assert_eq!(content.size.width, 80.0);
assert_eq!(content.size.height, 80.0);
}
#[test]
fn test_layout_engine_add() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
assert!(engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0)));
assert!(engine.add("rect2", Rect::new(60.0, 0.0, 50.0, 50.0)));
assert!(!engine.add("rect3", Rect::new(25.0, 25.0, 50.0, 50.0)));
}
#[test]
fn test_layout_engine_snap() {
let engine = LayoutEngine::new(Viewport::default());
assert_eq!(engine.snap_to_grid(13.0), 16.0);
assert_eq!(engine.snap_to_grid(12.0), 16.0);
assert_eq!(engine.snap_to_grid(11.0), 8.0);
}
#[test]
fn test_layout_engine_collision() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
let collisions = engine.find_collisions(&Rect::new(25.0, 25.0, 50.0, 50.0));
assert_eq!(collisions.len(), 1);
assert_eq!(collisions[0].id, "rect1");
}
#[test]
fn test_layout_engine_layers() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
assert!(engine.add_with_layer("rect1", Rect::new(0.0, 0.0, 50.0, 50.0), 0));
assert!(engine.add_with_layer("rect2", Rect::new(0.0, 0.0, 50.0, 50.0), 1));
assert!(!engine.add_with_layer("rect3", Rect::new(0.0, 0.0, 50.0, 50.0), 0));
}
#[test]
fn test_layout_engine_validate() {
let mut engine = LayoutEngine::new(Viewport::new(100.0, 100.0).with_padding(0.0));
engine
.elements
.insert("rect1".to_string(), LayoutRect::new("rect1", Rect::new(0.0, 0.0, 50.0, 50.0)));
engine.elements.insert(
"rect2".to_string(),
LayoutRect::new("rect2", Rect::new(200.0, 0.0, 50.0, 50.0)), );
let errors = engine.validate();
assert!(errors
.iter()
.any(|e| matches!(e, LayoutError::OutOfBounds { id } if id == "rect2")));
}
#[test]
fn test_auto_layout_row() {
let elements = vec![
("a", Size::new(50.0, 30.0)),
("b", Size::new(60.0, 30.0)),
("c", Size::new(40.0, 30.0)),
];
let layout = auto_layout::row(&elements, Point::new(10.0, 10.0), 5.0);
assert_eq!(layout[0].1.position.x, 10.0);
assert_eq!(layout[1].1.position.x, 65.0); assert_eq!(layout[2].1.position.x, 130.0); }
#[test]
fn test_auto_layout_column() {
let elements = vec![("a", Size::new(50.0, 30.0)), ("b", Size::new(50.0, 40.0))];
let layout = auto_layout::column(&elements, Point::new(10.0, 10.0), 5.0);
assert_eq!(layout[0].1.position.y, 10.0);
assert_eq!(layout[1].1.position.y, 45.0); }
#[test]
fn test_auto_layout_grid() {
let elements = vec![
("a", Size::new(50.0, 30.0)),
("b", Size::new(50.0, 30.0)),
("c", Size::new(50.0, 30.0)),
("d", Size::new(50.0, 30.0)),
];
let layout = auto_layout::grid(&elements, Point::new(0.0, 0.0), 2, 10.0, 10.0);
assert_eq!(layout[0].1.position.x, 0.0);
assert_eq!(layout[0].1.position.y, 0.0);
assert_eq!(layout[1].1.position.x, 60.0); assert_eq!(layout[1].1.position.y, 0.0);
assert_eq!(layout[2].1.position.x, 0.0);
assert_eq!(layout[2].1.position.y, 40.0); }
#[test]
fn test_viewport_presentation() {
let vp = Viewport::presentation();
assert_eq!(vp.width, 1920.0);
assert_eq!(vp.height, 1080.0);
}
#[test]
fn test_viewport_document() {
let vp = Viewport::document();
assert_eq!(vp.width, 800.0);
assert_eq!(vp.height, 600.0);
}
#[test]
fn test_viewport_square() {
let vp = Viewport::square(500.0);
assert_eq!(vp.width, 500.0);
assert_eq!(vp.height, 500.0);
}
#[test]
fn test_viewport_view_box() {
let vp = Viewport::new(100.0, 200.0);
assert_eq!(vp.view_box(), "0 0 100 200");
}
#[test]
fn test_viewport_default() {
let vp = Viewport::default();
assert_eq!(vp.width, 1920.0);
assert_eq!(vp.height, 1080.0);
}
#[test]
fn test_layout_rect_new() {
let rect = LayoutRect::new("test", Rect::new(10.0, 20.0, 30.0, 40.0));
assert_eq!(rect.id, "test");
assert_eq!(rect.layer, 0);
}
#[test]
fn test_layout_rect_with_layer() {
let rect = LayoutRect::new("test", Rect::new(0.0, 0.0, 10.0, 10.0)).with_layer(5);
assert_eq!(rect.layer, 5);
}
#[test]
fn test_layout_rect_bounds() {
let rect = LayoutRect::new("test", Rect::new(10.0, 20.0, 30.0, 40.0));
let bounds = rect.bounds();
assert_eq!(bounds.position.x, 10.0);
assert_eq!(bounds.position.y, 20.0);
}
#[test]
fn test_layout_rect_overlaps() {
let rect1 = LayoutRect::new("r1", Rect::new(0.0, 0.0, 50.0, 50.0));
let rect2 = LayoutRect::new("r2", Rect::new(25.0, 25.0, 50.0, 50.0));
let rect3 = LayoutRect::new("r3", Rect::new(100.0, 100.0, 50.0, 50.0));
assert!(rect1.overlaps(&rect2));
assert!(!rect1.overlaps(&rect3));
}
#[test]
fn test_layout_engine_get() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
assert!(engine.get("rect1").is_some());
assert!(engine.get("nonexistent").is_none());
}
#[test]
fn test_layout_engine_remove() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
let removed = engine.remove("rect1");
assert!(removed.is_some());
assert!(engine.get("rect1").is_none());
}
#[test]
fn test_layout_engine_clear() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
engine.add("rect2", Rect::new(60.0, 0.0, 50.0, 50.0));
engine.clear();
assert!(engine.is_empty());
assert_eq!(engine.len(), 0);
}
#[test]
fn test_layout_engine_len_is_empty() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
assert!(engine.is_empty());
assert_eq!(engine.len(), 0);
engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
assert!(!engine.is_empty());
assert_eq!(engine.len(), 1);
}
#[test]
fn test_layout_engine_viewport() {
let vp = Viewport::new(123.0, 456.0);
let engine = LayoutEngine::new(vp);
assert_eq!(engine.viewport().width, 123.0);
assert_eq!(engine.viewport().height, 456.0);
}
#[test]
fn test_layout_engine_snap_point() {
let engine = LayoutEngine::new(Viewport::default());
let point = engine.snap_point(Point::new(13.0, 27.0));
assert_eq!(point.x, 16.0);
assert_eq!(point.y, 24.0);
}
#[test]
fn test_layout_engine_elements_by_layer() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
engine.add_with_layer("back", Rect::new(0.0, 0.0, 50.0, 50.0), 0);
engine.add_with_layer("front", Rect::new(60.0, 0.0, 50.0, 50.0), 2);
engine.add_with_layer("middle", Rect::new(120.0, 0.0, 50.0, 50.0), 1);
let elements = engine.elements_by_layer();
assert_eq!(elements[0].layer, 0);
assert_eq!(elements[1].layer, 1);
assert_eq!(elements[2].layer, 2);
}
#[test]
fn test_layout_engine_element_at() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
engine.add_with_layer("back", Rect::new(0.0, 0.0, 100.0, 100.0), 0);
engine.add_with_layer("front", Rect::new(0.0, 0.0, 50.0, 50.0), 1);
let element = engine.element_at(&Point::new(25.0, 25.0));
assert!(element.is_some());
assert_eq!(element.expect("unexpected failure").id, "front");
let outside = engine.element_at(&Point::new(150.0, 150.0));
assert!(outside.is_none());
}
#[test]
fn test_layout_engine_find_free_position() {
let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
engine.add("block", Rect::new(0.0, 0.0, 80.0, 80.0));
let free_pos = engine.find_free_position(Size::new(50.0, 50.0), Point::new(0.0, 0.0));
assert!(free_pos.is_some());
let pos = free_pos.expect("unexpected failure");
assert!(pos.x >= 80.0 || pos.y >= 80.0);
}
#[test]
fn test_layout_engine_is_within_bounds() {
let engine = LayoutEngine::new(Viewport::new(100.0, 100.0).with_padding(10.0));
let rect_in = Rect::new(10.0, 10.0, 50.0, 50.0);
assert!(engine.is_within_bounds(&rect_in));
let rect_out = Rect::new(95.0, 95.0, 50.0, 50.0);
assert!(!engine.is_within_bounds(&rect_out));
}
#[test]
fn test_layout_engine_default() {
let engine = LayoutEngine::default();
assert_eq!(engine.viewport().width, 1920.0);
assert!(engine.is_empty());
}
#[test]
fn test_layout_error_display() {
let overlap = LayoutError::Overlap { id1: "a".to_string(), id2: "b".to_string() };
assert!(overlap.to_string().contains("overlap"));
let oob = LayoutError::OutOfBounds { id: "c".to_string() };
assert!(oob.to_string().contains("outside viewport"));
let aligned = LayoutError::NotAligned { id: "d".to_string() };
assert!(aligned.to_string().contains("not grid-aligned"));
}
#[test]
fn test_auto_layout_center_horizontal_empty() {
let result = auto_layout::center_horizontal(&[], &Viewport::new(100.0, 100.0));
assert!(result.is_empty());
}
#[test]
fn test_auto_layout_center_horizontal() {
let elements = vec![
("a".to_string(), Rect::new(0.0, 10.0, 20.0, 20.0)),
("b".to_string(), Rect::new(30.0, 10.0, 20.0, 20.0)),
];
let vp = Viewport::new(100.0, 100.0);
let centered = auto_layout::center_horizontal(&elements, &vp);
assert_eq!(centered[0].1.position.x, 25.0);
assert_eq!(centered[1].1.position.x, 55.0);
}
#[test]
fn test_auto_layout_center_vertical_empty() {
let result = auto_layout::center_vertical(&[], &Viewport::new(100.0, 100.0));
assert!(result.is_empty());
}
#[test]
fn test_auto_layout_center_vertical() {
let elements = vec![
("a".to_string(), Rect::new(10.0, 0.0, 20.0, 20.0)),
("b".to_string(), Rect::new(10.0, 30.0, 20.0, 20.0)),
];
let vp = Viewport::new(100.0, 100.0);
let centered = auto_layout::center_vertical(&elements, &vp);
assert_eq!(centered[0].1.position.y, 25.0);
assert_eq!(centered[1].1.position.y, 55.0);
}
#[test]
fn test_layout_engine_with_grid_size() {
let engine = LayoutEngine::new(Viewport::default()).with_grid_size(16.0);
assert_eq!(engine.snap_to_grid(10.0), 16.0);
assert_eq!(engine.snap_to_grid(24.0), 32.0);
}
#[test]
fn test_layout_engine_snap_rect() {
let engine = LayoutEngine::new(Viewport::default());
let rect = Rect::new(13.0, 27.0, 45.0, 67.0).with_radius(5.0);
let snapped = engine.snap_rect(&rect);
assert_eq!(snapped.position.x, 16.0);
assert_eq!(snapped.position.y, 24.0);
assert_eq!(snapped.size.width, 48.0);
assert_eq!(snapped.size.height, 64.0);
assert_eq!(snapped.corner_radius, 5.0); }
#[test]
fn test_layout_validate_not_aligned() {
let mut engine = LayoutEngine::new(Viewport::new(100.0, 100.0).with_padding(0.0));
engine.elements.insert(
"unaligned".to_string(),
LayoutRect::new("unaligned", Rect::new(3.0, 5.0, 10.0, 10.0)),
);
let errors = engine.validate();
assert!(errors.iter().any(|e| matches!(e, LayoutError::NotAligned { .. })));
}
}