1use super::shapes::{Point, Rect, Size};
6use std::collections::HashMap;
7
8pub const GRID_SIZE: f32 = 8.0;
10
11#[derive(Debug, Clone, Copy)]
13pub struct Viewport {
14 pub width: f32,
16 pub height: f32,
18 pub padding: f32,
20}
21
22impl Viewport {
23 pub fn new(width: f32, height: f32) -> Self {
25 Self {
26 width,
27 height,
28 padding: GRID_SIZE * 3.0, }
30 }
31
32 pub fn presentation() -> Self {
34 Self::new(1920.0, 1080.0)
35 }
36
37 pub fn document() -> Self {
39 Self::new(800.0, 600.0)
40 }
41
42 pub fn square(size: f32) -> Self {
44 Self::new(size, size)
45 }
46
47 pub fn with_padding(mut self, padding: f32) -> Self {
49 self.padding = padding;
50 self
51 }
52
53 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 pub fn center(&self) -> Point {
65 Point::new(self.width / 2.0, self.height / 2.0)
66 }
67
68 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#[derive(Debug, Clone)]
82pub struct LayoutRect {
83 pub id: String,
85 pub rect: Rect,
87 pub layer: i32,
89}
90
91impl LayoutRect {
92 pub fn new(id: &str, rect: Rect) -> Self {
94 Self { id: id.to_string(), rect, layer: 0 }
95 }
96
97 pub fn with_layer(mut self, layer: i32) -> Self {
99 self.layer = layer;
100 self
101 }
102
103 pub fn bounds(&self) -> &Rect {
105 &self.rect
106 }
107
108 pub fn overlaps(&self, other: &LayoutRect) -> bool {
110 self.rect.intersects(&other.rect)
111 }
112}
113
114#[derive(Debug)]
116pub struct LayoutEngine {
117 pub elements: HashMap<String, LayoutRect>,
119 viewport: Viewport,
121 grid_size: f32,
123}
124
125impl LayoutEngine {
126 pub fn new(viewport: Viewport) -> Self {
128 Self { elements: HashMap::new(), viewport, grid_size: GRID_SIZE }
129 }
130
131 pub fn with_grid_size(mut self, size: f32) -> Self {
133 self.grid_size = size;
134 self
135 }
136
137 pub fn snap_to_grid(&self, value: f32) -> f32 {
139 (value / self.grid_size).round() * self.grid_size
140 }
141
142 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 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 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 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 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 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 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 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 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 pub fn remove(&mut self, id: &str) -> Option<LayoutRect> {
215 self.elements.remove(id)
216 }
217
218 pub fn get(&self, id: &str) -> Option<&LayoutRect> {
220 self.elements.get(id)
221 }
222
223 pub fn all_elements(&self) -> impl Iterator<Item = &LayoutRect> {
225 self.elements.values()
226 }
227
228 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 pub fn element_at(&self, point: &Point) -> Option<&LayoutRect> {
237 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 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 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 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 pub fn validate(&self) -> Vec<LayoutError> {
283 let mut errors = Vec::new();
284
285 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 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 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 pub fn viewport(&self) -> &Viewport {
318 &self.viewport
319 }
320
321 pub fn clear(&mut self) {
323 self.elements.clear();
324 }
325
326 pub fn len(&self) -> usize {
328 self.elements.len()
329 }
330
331 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#[derive(Debug, Clone, PartialEq)]
345pub enum LayoutError {
346 Overlap { id1: String, id2: String },
348 OutOfBounds { id: String },
350 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
364pub mod auto_layout {
366 use super::*;
367
368 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 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 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 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 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 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 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 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 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 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 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)), );
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); assert_eq!(layout[2].1.position.x, 130.0); }
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); }
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); 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); }
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 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 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 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 let rect_in = Rect::new(10.0, 10.0, 50.0, 50.0);
790 assert!(engine.is_within_bounds(&rect_in));
791
792 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 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 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); }
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 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}