Skip to main content

batuta/oracle/svg/
layout.rs

1//! Layout Engine
2//!
3//! Grid-based layout with collision detection for diagram elements.
4
5use super::shapes::{Point, Rect, Size};
6use std::collections::HashMap;
7
8/// Material Design 3 grid size (8px)
9pub const GRID_SIZE: f32 = 8.0;
10
11/// Standard viewport for diagrams
12#[derive(Debug, Clone, Copy)]
13pub struct Viewport {
14    /// Width in pixels
15    pub width: f32,
16    /// Height in pixels
17    pub height: f32,
18    /// Padding from edges
19    pub padding: f32,
20}
21
22impl Viewport {
23    /// Create a new viewport
24    pub fn new(width: f32, height: f32) -> Self {
25        Self {
26            width,
27            height,
28            padding: GRID_SIZE * 3.0, // 24px default padding
29        }
30    }
31
32    /// Standard 16:9 presentation viewport (1920x1080)
33    pub fn presentation() -> Self {
34        Self::new(1920.0, 1080.0)
35    }
36
37    /// Standard 4:3 document viewport (800x600)
38    pub fn document() -> Self {
39        Self::new(800.0, 600.0)
40    }
41
42    /// Square viewport
43    pub fn square(size: f32) -> Self {
44        Self::new(size, size)
45    }
46
47    /// Set padding
48    pub fn with_padding(mut self, padding: f32) -> Self {
49        self.padding = padding;
50        self
51    }
52
53    /// Get the usable content area
54    pub fn content_area(&self) -> Rect {
55        Rect::new(
56            self.padding,
57            self.padding,
58            self.width - 2.0 * self.padding,
59            self.height - 2.0 * self.padding,
60        )
61    }
62
63    /// Get the center point
64    pub fn center(&self) -> Point {
65        Point::new(self.width / 2.0, self.height / 2.0)
66    }
67
68    /// Generate SVG viewBox attribute
69    pub fn view_box(&self) -> String {
70        format!("0 0 {} {}", self.width, self.height)
71    }
72}
73
74impl Default for Viewport {
75    fn default() -> Self {
76        Self::presentation()
77    }
78}
79
80/// Layout rectangle with ID for tracking
81#[derive(Debug, Clone)]
82pub struct LayoutRect {
83    /// Unique ID
84    pub id: String,
85    /// Rectangle bounds
86    pub rect: Rect,
87    /// Layer (higher = on top)
88    pub layer: i32,
89}
90
91impl LayoutRect {
92    /// Create a new layout rect
93    pub fn new(id: &str, rect: Rect) -> Self {
94        Self { id: id.to_string(), rect, layer: 0 }
95    }
96
97    /// Set the layer
98    pub fn with_layer(mut self, layer: i32) -> Self {
99        self.layer = layer;
100        self
101    }
102
103    /// Get the bounding box
104    pub fn bounds(&self) -> &Rect {
105        &self.rect
106    }
107
108    /// Check if this overlaps with another layout rect
109    pub fn overlaps(&self, other: &LayoutRect) -> bool {
110        self.rect.intersects(&other.rect)
111    }
112}
113
114/// Simple layout engine with collision detection
115#[derive(Debug)]
116pub struct LayoutEngine {
117    /// All placed elements
118    pub elements: HashMap<String, LayoutRect>,
119    /// Viewport
120    viewport: Viewport,
121    /// Grid size for snapping
122    grid_size: f32,
123}
124
125impl LayoutEngine {
126    /// Create a new layout engine
127    pub fn new(viewport: Viewport) -> Self {
128        Self { elements: HashMap::new(), viewport, grid_size: GRID_SIZE }
129    }
130
131    /// Set custom grid size
132    pub fn with_grid_size(mut self, size: f32) -> Self {
133        self.grid_size = size;
134        self
135    }
136
137    /// Snap a value to the grid
138    pub fn snap_to_grid(&self, value: f32) -> f32 {
139        (value / self.grid_size).round() * self.grid_size
140    }
141
142    /// Snap a point to the grid
143    pub fn snap_point(&self, point: Point) -> Point {
144        Point::new(self.snap_to_grid(point.x), self.snap_to_grid(point.y))
145    }
146
147    /// Snap a rectangle to the grid
148    pub fn snap_rect(&self, rect: &Rect) -> Rect {
149        Rect::new(
150            self.snap_to_grid(rect.position.x),
151            self.snap_to_grid(rect.position.y),
152            self.snap_to_grid(rect.size.width),
153            self.snap_to_grid(rect.size.height),
154        )
155        .with_radius(rect.corner_radius)
156    }
157
158    /// Add an element to the layout
159    pub fn add(&mut self, id: &str, rect: Rect) -> bool {
160        let snapped = self.snap_rect(&rect);
161        let layout_rect = LayoutRect::new(id, snapped);
162
163        // Check for collisions
164        if self.has_collision(&layout_rect) {
165            return false;
166        }
167
168        self.elements.insert(id.to_string(), layout_rect);
169        true
170    }
171
172    /// Add element with layer
173    pub fn add_with_layer(&mut self, id: &str, rect: Rect, layer: i32) -> bool {
174        let snapped = self.snap_rect(&rect);
175        let layout_rect = LayoutRect::new(id, snapped).with_layer(layer);
176
177        // Check for collisions on the same layer
178        if self.has_collision_on_layer(&layout_rect, layer) {
179            return false;
180        }
181
182        self.elements.insert(id.to_string(), layout_rect);
183        true
184    }
185
186    /// Check if a rect would collide with existing elements
187    pub fn has_collision(&self, new_rect: &LayoutRect) -> bool {
188        for existing in self.elements.values() {
189            if existing.id != new_rect.id && existing.overlaps(new_rect) {
190                return true;
191            }
192        }
193        false
194    }
195
196    /// Check for collision on a specific layer
197    pub fn has_collision_on_layer(&self, new_rect: &LayoutRect, layer: i32) -> bool {
198        for existing in self.elements.values() {
199            if existing.id != new_rect.id && existing.layer == layer && existing.overlaps(new_rect)
200            {
201                return true;
202            }
203        }
204        false
205    }
206
207    /// Get all elements that would overlap with a rect
208    pub fn find_collisions(&self, rect: &Rect) -> Vec<&LayoutRect> {
209        let test_rect = LayoutRect::new("_test", rect.clone());
210        self.elements.values().filter(|e| e.overlaps(&test_rect)).collect()
211    }
212
213    /// Remove an element
214    pub fn remove(&mut self, id: &str) -> Option<LayoutRect> {
215        self.elements.remove(id)
216    }
217
218    /// Get an element by ID
219    pub fn get(&self, id: &str) -> Option<&LayoutRect> {
220        self.elements.get(id)
221    }
222
223    /// Get all elements
224    pub fn all_elements(&self) -> impl Iterator<Item = &LayoutRect> {
225        self.elements.values()
226    }
227
228    /// Get elements sorted by layer (back to front)
229    pub fn elements_by_layer(&self) -> Vec<&LayoutRect> {
230        let mut elements: Vec<_> = self.elements.values().collect();
231        elements.sort_by_key(|e| e.layer);
232        elements
233    }
234
235    /// Check if a point is inside any element
236    pub fn element_at(&self, point: &Point) -> Option<&LayoutRect> {
237        // Return topmost element (highest layer)
238        let mut candidates: Vec<_> =
239            self.elements.values().filter(|e| e.rect.contains(point)).collect();
240        candidates.sort_by_key(|e| -e.layer);
241        candidates.first().copied()
242    }
243
244    /// Find a free position for a rect with given size
245    pub fn find_free_position(&self, size: Size, start: Point) -> Option<Point> {
246        let content = self.viewport.content_area();
247        let max_x = content.right() - size.width;
248        let max_y = content.bottom() - size.height;
249
250        // Try positions in a spiral pattern from start
251        let mut x = self.snap_to_grid(start.x.max(content.position.x));
252        let mut y = self.snap_to_grid(start.y.max(content.position.y));
253
254        while y <= max_y {
255            while x <= max_x {
256                let test_rect = Rect::new(x, y, size.width, size.height);
257                let layout_rect = LayoutRect::new("_test", test_rect);
258
259                if !self.has_collision(&layout_rect) {
260                    return Some(Point::new(x, y));
261                }
262
263                x += self.grid_size;
264            }
265            x = self.snap_to_grid(content.position.x);
266            y += self.grid_size;
267        }
268
269        None
270    }
271
272    /// Check if a rect is within the viewport content area
273    pub fn is_within_bounds(&self, rect: &Rect) -> bool {
274        let content = self.viewport.content_area();
275        rect.position.x >= content.position.x
276            && rect.position.y >= content.position.y
277            && rect.right() <= content.right()
278            && rect.bottom() <= content.bottom()
279    }
280
281    /// Get layout validation errors
282    pub fn validate(&self) -> Vec<LayoutError> {
283        let mut errors = Vec::new();
284
285        // Check for overlaps
286        let elements: Vec<_> = self.elements.values().collect();
287        for i in 0..elements.len() {
288            for j in (i + 1)..elements.len() {
289                if elements[i].layer == elements[j].layer && elements[i].overlaps(elements[j]) {
290                    errors.push(LayoutError::Overlap {
291                        id1: elements[i].id.clone(),
292                        id2: elements[j].id.clone(),
293                    });
294                }
295            }
296        }
297
298        // Check for out-of-bounds
299        for element in &elements {
300            if !self.is_within_bounds(&element.rect) {
301                errors.push(LayoutError::OutOfBounds { id: element.id.clone() });
302            }
303        }
304
305        // Check grid alignment
306        for element in &elements {
307            let rect = &element.rect;
308            if rect.position.x % self.grid_size != 0.0 || rect.position.y % self.grid_size != 0.0 {
309                errors.push(LayoutError::NotAligned { id: element.id.clone() });
310            }
311        }
312
313        errors
314    }
315
316    /// Get the viewport
317    pub fn viewport(&self) -> &Viewport {
318        &self.viewport
319    }
320
321    /// Clear all elements
322    pub fn clear(&mut self) {
323        self.elements.clear();
324    }
325
326    /// Get element count
327    pub fn len(&self) -> usize {
328        self.elements.len()
329    }
330
331    /// Check if empty
332    pub fn is_empty(&self) -> bool {
333        self.elements.is_empty()
334    }
335}
336
337impl Default for LayoutEngine {
338    fn default() -> Self {
339        Self::new(Viewport::default())
340    }
341}
342
343/// Layout validation error
344#[derive(Debug, Clone, PartialEq)]
345pub enum LayoutError {
346    /// Two elements overlap
347    Overlap { id1: String, id2: String },
348    /// Element is outside viewport
349    OutOfBounds { id: String },
350    /// Element is not grid-aligned
351    NotAligned { id: String },
352}
353
354impl std::fmt::Display for LayoutError {
355    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356        match self {
357            Self::Overlap { id1, id2 } => write!(f, "Elements '{}' and '{}' overlap", id1, id2),
358            Self::OutOfBounds { id } => write!(f, "Element '{}' is outside viewport", id),
359            Self::NotAligned { id } => write!(f, "Element '{}' is not grid-aligned", id),
360        }
361    }
362}
363
364/// Auto-layout algorithms
365pub mod auto_layout {
366    use super::*;
367
368    /// Arrange elements in a horizontal row
369    pub fn row(elements: &[(&str, Size)], start: Point, spacing: f32) -> Vec<(String, Rect)> {
370        let mut x = start.x;
371        let mut result = Vec::new();
372
373        for (id, size) in elements {
374            result.push(((*id).to_string(), Rect::new(x, start.y, size.width, size.height)));
375            x += size.width + spacing;
376        }
377
378        result
379    }
380
381    /// Arrange elements in a vertical column
382    pub fn column(elements: &[(&str, Size)], start: Point, spacing: f32) -> Vec<(String, Rect)> {
383        let mut y = start.y;
384        let mut result = Vec::new();
385
386        for (id, size) in elements {
387            result.push(((*id).to_string(), Rect::new(start.x, y, size.width, size.height)));
388            y += size.height + spacing;
389        }
390
391        result
392    }
393
394    /// Arrange elements in a grid
395    pub fn grid(
396        elements: &[(&str, Size)],
397        start: Point,
398        columns: usize,
399        h_spacing: f32,
400        v_spacing: f32,
401    ) -> Vec<(String, Rect)> {
402        let mut result = Vec::new();
403        let mut x = start.x;
404        let mut y = start.y;
405        let mut row_height: f32 = 0.0;
406
407        for (i, (id, size)) in elements.iter().enumerate() {
408            if i > 0 && i % columns == 0 {
409                // New row
410                x = start.x;
411                y += row_height + v_spacing;
412                row_height = 0.0;
413            }
414
415            result.push(((*id).to_string(), Rect::new(x, y, size.width, size.height)));
416            x += size.width + h_spacing;
417            row_height = row_height.max(size.height);
418        }
419
420        result
421    }
422
423    /// Center elements horizontally within a viewport
424    pub fn center_horizontal(
425        elements: &[(String, Rect)],
426        viewport: &Viewport,
427    ) -> Vec<(String, Rect)> {
428        if elements.is_empty() {
429            return vec![];
430        }
431
432        // Calculate total width
433        let min_x = elements.iter().map(|(_, r)| r.position.x).fold(f32::INFINITY, f32::min);
434        let max_x = elements.iter().map(|(_, r)| r.right()).fold(f32::NEG_INFINITY, f32::max);
435        let total_width = max_x - min_x;
436
437        let center_offset = (viewport.width - total_width) / 2.0 - min_x;
438
439        elements
440            .iter()
441            .map(|(id, r)| {
442                (
443                    id.clone(),
444                    Rect::new(
445                        r.position.x + center_offset,
446                        r.position.y,
447                        r.size.width,
448                        r.size.height,
449                    ),
450                )
451            })
452            .collect()
453    }
454
455    /// Center elements vertically within a viewport
456    pub fn center_vertical(
457        elements: &[(String, Rect)],
458        viewport: &Viewport,
459    ) -> Vec<(String, Rect)> {
460        if elements.is_empty() {
461            return vec![];
462        }
463
464        // Calculate total height
465        let min_y = elements.iter().map(|(_, r)| r.position.y).fold(f32::INFINITY, f32::min);
466        let max_y = elements.iter().map(|(_, r)| r.bottom()).fold(f32::NEG_INFINITY, f32::max);
467        let total_height = max_y - min_y;
468
469        let center_offset = (viewport.height - total_height) / 2.0 - min_y;
470
471        elements
472            .iter()
473            .map(|(id, r)| {
474                (
475                    id.clone(),
476                    Rect::new(
477                        r.position.x,
478                        r.position.y + center_offset,
479                        r.size.width,
480                        r.size.height,
481                    ),
482                )
483            })
484            .collect()
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491
492    #[test]
493    fn test_viewport_creation() {
494        let vp = Viewport::new(800.0, 600.0);
495        assert_eq!(vp.width, 800.0);
496        assert_eq!(vp.height, 600.0);
497    }
498
499    #[test]
500    fn test_viewport_center() {
501        let vp = Viewport::new(100.0, 100.0);
502        let center = vp.center();
503        assert_eq!(center.x, 50.0);
504        assert_eq!(center.y, 50.0);
505    }
506
507    #[test]
508    fn test_viewport_content_area() {
509        let vp = Viewport::new(100.0, 100.0).with_padding(10.0);
510        let content = vp.content_area();
511        assert_eq!(content.position.x, 10.0);
512        assert_eq!(content.position.y, 10.0);
513        assert_eq!(content.size.width, 80.0);
514        assert_eq!(content.size.height, 80.0);
515    }
516
517    #[test]
518    fn test_layout_engine_add() {
519        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
520
521        assert!(engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0)));
522        assert!(engine.add("rect2", Rect::new(60.0, 0.0, 50.0, 50.0)));
523
524        // Overlapping rect should fail
525        assert!(!engine.add("rect3", Rect::new(25.0, 25.0, 50.0, 50.0)));
526    }
527
528    #[test]
529    fn test_layout_engine_snap() {
530        let engine = LayoutEngine::new(Viewport::default());
531
532        assert_eq!(engine.snap_to_grid(13.0), 16.0);
533        assert_eq!(engine.snap_to_grid(12.0), 16.0);
534        assert_eq!(engine.snap_to_grid(11.0), 8.0);
535    }
536
537    #[test]
538    fn test_layout_engine_collision() {
539        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
540
541        engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
542
543        let collisions = engine.find_collisions(&Rect::new(25.0, 25.0, 50.0, 50.0));
544        assert_eq!(collisions.len(), 1);
545        assert_eq!(collisions[0].id, "rect1");
546    }
547
548    #[test]
549    fn test_layout_engine_layers() {
550        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
551
552        // Same position but different layers should work
553        assert!(engine.add_with_layer("rect1", Rect::new(0.0, 0.0, 50.0, 50.0), 0));
554        assert!(engine.add_with_layer("rect2", Rect::new(0.0, 0.0, 50.0, 50.0), 1));
555
556        // Same layer should fail
557        assert!(!engine.add_with_layer("rect3", Rect::new(0.0, 0.0, 50.0, 50.0), 0));
558    }
559
560    #[test]
561    fn test_layout_engine_validate() {
562        let mut engine = LayoutEngine::new(Viewport::new(100.0, 100.0).with_padding(0.0));
563
564        engine
565            .elements
566            .insert("rect1".to_string(), LayoutRect::new("rect1", Rect::new(0.0, 0.0, 50.0, 50.0)));
567        engine.elements.insert(
568            "rect2".to_string(),
569            LayoutRect::new("rect2", Rect::new(200.0, 0.0, 50.0, 50.0)), // Out of bounds
570        );
571
572        let errors = engine.validate();
573        assert!(errors
574            .iter()
575            .any(|e| matches!(e, LayoutError::OutOfBounds { id } if id == "rect2")));
576    }
577
578    #[test]
579    fn test_auto_layout_row() {
580        let elements = vec![
581            ("a", Size::new(50.0, 30.0)),
582            ("b", Size::new(60.0, 30.0)),
583            ("c", Size::new(40.0, 30.0)),
584        ];
585
586        let layout = auto_layout::row(&elements, Point::new(10.0, 10.0), 5.0);
587
588        assert_eq!(layout[0].1.position.x, 10.0);
589        assert_eq!(layout[1].1.position.x, 65.0); // 10 + 50 + 5
590        assert_eq!(layout[2].1.position.x, 130.0); // 65 + 60 + 5
591    }
592
593    #[test]
594    fn test_auto_layout_column() {
595        let elements = vec![("a", Size::new(50.0, 30.0)), ("b", Size::new(50.0, 40.0))];
596
597        let layout = auto_layout::column(&elements, Point::new(10.0, 10.0), 5.0);
598
599        assert_eq!(layout[0].1.position.y, 10.0);
600        assert_eq!(layout[1].1.position.y, 45.0); // 10 + 30 + 5
601    }
602
603    #[test]
604    fn test_auto_layout_grid() {
605        let elements = vec![
606            ("a", Size::new(50.0, 30.0)),
607            ("b", Size::new(50.0, 30.0)),
608            ("c", Size::new(50.0, 30.0)),
609            ("d", Size::new(50.0, 30.0)),
610        ];
611
612        let layout = auto_layout::grid(&elements, Point::new(0.0, 0.0), 2, 10.0, 10.0);
613
614        assert_eq!(layout[0].1.position.x, 0.0);
615        assert_eq!(layout[0].1.position.y, 0.0);
616        assert_eq!(layout[1].1.position.x, 60.0); // 0 + 50 + 10
617        assert_eq!(layout[1].1.position.y, 0.0);
618        assert_eq!(layout[2].1.position.x, 0.0);
619        assert_eq!(layout[2].1.position.y, 40.0); // 0 + 30 + 10
620    }
621
622    #[test]
623    fn test_viewport_presentation() {
624        let vp = Viewport::presentation();
625        assert_eq!(vp.width, 1920.0);
626        assert_eq!(vp.height, 1080.0);
627    }
628
629    #[test]
630    fn test_viewport_document() {
631        let vp = Viewport::document();
632        assert_eq!(vp.width, 800.0);
633        assert_eq!(vp.height, 600.0);
634    }
635
636    #[test]
637    fn test_viewport_square() {
638        let vp = Viewport::square(500.0);
639        assert_eq!(vp.width, 500.0);
640        assert_eq!(vp.height, 500.0);
641    }
642
643    #[test]
644    fn test_viewport_view_box() {
645        let vp = Viewport::new(100.0, 200.0);
646        assert_eq!(vp.view_box(), "0 0 100 200");
647    }
648
649    #[test]
650    fn test_viewport_default() {
651        let vp = Viewport::default();
652        assert_eq!(vp.width, 1920.0);
653        assert_eq!(vp.height, 1080.0);
654    }
655
656    #[test]
657    fn test_layout_rect_new() {
658        let rect = LayoutRect::new("test", Rect::new(10.0, 20.0, 30.0, 40.0));
659        assert_eq!(rect.id, "test");
660        assert_eq!(rect.layer, 0);
661    }
662
663    #[test]
664    fn test_layout_rect_with_layer() {
665        let rect = LayoutRect::new("test", Rect::new(0.0, 0.0, 10.0, 10.0)).with_layer(5);
666        assert_eq!(rect.layer, 5);
667    }
668
669    #[test]
670    fn test_layout_rect_bounds() {
671        let rect = LayoutRect::new("test", Rect::new(10.0, 20.0, 30.0, 40.0));
672        let bounds = rect.bounds();
673        assert_eq!(bounds.position.x, 10.0);
674        assert_eq!(bounds.position.y, 20.0);
675    }
676
677    #[test]
678    fn test_layout_rect_overlaps() {
679        let rect1 = LayoutRect::new("r1", Rect::new(0.0, 0.0, 50.0, 50.0));
680        let rect2 = LayoutRect::new("r2", Rect::new(25.0, 25.0, 50.0, 50.0));
681        let rect3 = LayoutRect::new("r3", Rect::new(100.0, 100.0, 50.0, 50.0));
682        assert!(rect1.overlaps(&rect2));
683        assert!(!rect1.overlaps(&rect3));
684    }
685
686    #[test]
687    fn test_layout_engine_get() {
688        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
689        engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
690
691        assert!(engine.get("rect1").is_some());
692        assert!(engine.get("nonexistent").is_none());
693    }
694
695    #[test]
696    fn test_layout_engine_remove() {
697        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
698        engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
699
700        let removed = engine.remove("rect1");
701        assert!(removed.is_some());
702        assert!(engine.get("rect1").is_none());
703    }
704
705    #[test]
706    fn test_layout_engine_clear() {
707        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
708        engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
709        engine.add("rect2", Rect::new(60.0, 0.0, 50.0, 50.0));
710
711        engine.clear();
712        assert!(engine.is_empty());
713        assert_eq!(engine.len(), 0);
714    }
715
716    #[test]
717    fn test_layout_engine_len_is_empty() {
718        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
719        assert!(engine.is_empty());
720        assert_eq!(engine.len(), 0);
721
722        engine.add("rect1", Rect::new(0.0, 0.0, 50.0, 50.0));
723        assert!(!engine.is_empty());
724        assert_eq!(engine.len(), 1);
725    }
726
727    #[test]
728    fn test_layout_engine_viewport() {
729        let vp = Viewport::new(123.0, 456.0);
730        let engine = LayoutEngine::new(vp);
731        assert_eq!(engine.viewport().width, 123.0);
732        assert_eq!(engine.viewport().height, 456.0);
733    }
734
735    #[test]
736    fn test_layout_engine_snap_point() {
737        let engine = LayoutEngine::new(Viewport::default());
738        let point = engine.snap_point(Point::new(13.0, 27.0));
739        assert_eq!(point.x, 16.0);
740        assert_eq!(point.y, 24.0);
741    }
742
743    #[test]
744    fn test_layout_engine_elements_by_layer() {
745        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
746        engine.add_with_layer("back", Rect::new(0.0, 0.0, 50.0, 50.0), 0);
747        engine.add_with_layer("front", Rect::new(60.0, 0.0, 50.0, 50.0), 2);
748        engine.add_with_layer("middle", Rect::new(120.0, 0.0, 50.0, 50.0), 1);
749
750        let elements = engine.elements_by_layer();
751        assert_eq!(elements[0].layer, 0);
752        assert_eq!(elements[1].layer, 1);
753        assert_eq!(elements[2].layer, 2);
754    }
755
756    #[test]
757    fn test_layout_engine_element_at() {
758        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
759        engine.add_with_layer("back", Rect::new(0.0, 0.0, 100.0, 100.0), 0);
760        engine.add_with_layer("front", Rect::new(0.0, 0.0, 50.0, 50.0), 1);
761
762        // Should return topmost element
763        let element = engine.element_at(&Point::new(25.0, 25.0));
764        assert!(element.is_some());
765        assert_eq!(element.expect("unexpected failure").id, "front");
766
767        // Point outside all elements
768        let outside = engine.element_at(&Point::new(150.0, 150.0));
769        assert!(outside.is_none());
770    }
771
772    #[test]
773    fn test_layout_engine_find_free_position() {
774        let mut engine = LayoutEngine::new(Viewport::new(200.0, 200.0).with_padding(0.0));
775        engine.add("block", Rect::new(0.0, 0.0, 80.0, 80.0));
776
777        let free_pos = engine.find_free_position(Size::new(50.0, 50.0), Point::new(0.0, 0.0));
778        assert!(free_pos.is_some());
779        let pos = free_pos.expect("unexpected failure");
780        // Should find a position that doesn't overlap
781        assert!(pos.x >= 80.0 || pos.y >= 80.0);
782    }
783
784    #[test]
785    fn test_layout_engine_is_within_bounds() {
786        let engine = LayoutEngine::new(Viewport::new(100.0, 100.0).with_padding(10.0));
787
788        // Within bounds
789        let rect_in = Rect::new(10.0, 10.0, 50.0, 50.0);
790        assert!(engine.is_within_bounds(&rect_in));
791
792        // Outside bounds
793        let rect_out = Rect::new(95.0, 95.0, 50.0, 50.0);
794        assert!(!engine.is_within_bounds(&rect_out));
795    }
796
797    #[test]
798    fn test_layout_engine_default() {
799        let engine = LayoutEngine::default();
800        assert_eq!(engine.viewport().width, 1920.0);
801        assert!(engine.is_empty());
802    }
803
804    #[test]
805    fn test_layout_error_display() {
806        let overlap = LayoutError::Overlap { id1: "a".to_string(), id2: "b".to_string() };
807        assert!(overlap.to_string().contains("overlap"));
808
809        let oob = LayoutError::OutOfBounds { id: "c".to_string() };
810        assert!(oob.to_string().contains("outside viewport"));
811
812        let aligned = LayoutError::NotAligned { id: "d".to_string() };
813        assert!(aligned.to_string().contains("not grid-aligned"));
814    }
815
816    #[test]
817    fn test_auto_layout_center_horizontal_empty() {
818        let result = auto_layout::center_horizontal(&[], &Viewport::new(100.0, 100.0));
819        assert!(result.is_empty());
820    }
821
822    #[test]
823    fn test_auto_layout_center_horizontal() {
824        let elements = vec![
825            ("a".to_string(), Rect::new(0.0, 10.0, 20.0, 20.0)),
826            ("b".to_string(), Rect::new(30.0, 10.0, 20.0, 20.0)),
827        ];
828        let vp = Viewport::new(100.0, 100.0);
829        let centered = auto_layout::center_horizontal(&elements, &vp);
830
831        // Total width is 50 (from 0 to 50), centered in 100 should be at 25
832        assert_eq!(centered[0].1.position.x, 25.0);
833        assert_eq!(centered[1].1.position.x, 55.0);
834    }
835
836    #[test]
837    fn test_auto_layout_center_vertical_empty() {
838        let result = auto_layout::center_vertical(&[], &Viewport::new(100.0, 100.0));
839        assert!(result.is_empty());
840    }
841
842    #[test]
843    fn test_auto_layout_center_vertical() {
844        let elements = vec![
845            ("a".to_string(), Rect::new(10.0, 0.0, 20.0, 20.0)),
846            ("b".to_string(), Rect::new(10.0, 30.0, 20.0, 20.0)),
847        ];
848        let vp = Viewport::new(100.0, 100.0);
849        let centered = auto_layout::center_vertical(&elements, &vp);
850
851        // Total height is 50 (from 0 to 50), centered in 100 should be at 25
852        assert_eq!(centered[0].1.position.y, 25.0);
853        assert_eq!(centered[1].1.position.y, 55.0);
854    }
855
856    #[test]
857    fn test_layout_engine_with_grid_size() {
858        let engine = LayoutEngine::new(Viewport::default()).with_grid_size(16.0);
859        assert_eq!(engine.snap_to_grid(10.0), 16.0);
860        assert_eq!(engine.snap_to_grid(24.0), 32.0);
861    }
862
863    #[test]
864    fn test_layout_engine_snap_rect() {
865        let engine = LayoutEngine::new(Viewport::default());
866        let rect = Rect::new(13.0, 27.0, 45.0, 67.0).with_radius(5.0);
867        let snapped = engine.snap_rect(&rect);
868        assert_eq!(snapped.position.x, 16.0);
869        assert_eq!(snapped.position.y, 24.0);
870        assert_eq!(snapped.size.width, 48.0);
871        assert_eq!(snapped.size.height, 64.0);
872        assert_eq!(snapped.corner_radius, 5.0); // Preserved
873    }
874
875    #[test]
876    fn test_layout_validate_not_aligned() {
877        let mut engine = LayoutEngine::new(Viewport::new(100.0, 100.0).with_padding(0.0));
878        // Force insert unaligned element
879        engine.elements.insert(
880            "unaligned".to_string(),
881            LayoutRect::new("unaligned", Rect::new(3.0, 5.0, 10.0, 10.0)),
882        );
883
884        let errors = engine.validate();
885        assert!(errors.iter().any(|e| matches!(e, LayoutError::NotAligned { .. })));
886    }
887}