1use crate::layout_engine::elements::border::Border;
18use crate::layout_engine::types::{
19 DataContext, Direction, LayoutConfig, LayoutElement, RenderContext, RenderError,
20};
21use dotzuki_engine::render::{Painter, TilePos, TileRect};
22
23#[derive(Debug, Clone, Copy)]
30pub struct ChildRect {
31 pub rect: TileRect,
33 pub z_index: i32,
35 pub visible: bool,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum GroupLayout {
44 Absolute,
47 Horizontal { gap: u32 },
50 Vertical { gap: u32 },
53}
54
55impl GroupLayout {
56 pub fn from_config(config: &LayoutConfig) -> Self {
58 match config.direction {
59 Some(Direction::Horizontal) => GroupLayout::Horizontal { gap: config.gap },
60 Some(Direction::Vertical) => GroupLayout::Vertical { gap: config.gap },
61 None => GroupLayout::Absolute,
62 }
63 }
64
65 #[inline]
67 pub fn is_auto(&self) -> bool {
68 !matches!(self, GroupLayout::Absolute)
69 }
70}
71
72impl Default for GroupLayout {
73 fn default() -> Self {
74 GroupLayout::Absolute
75 }
76}
77
78#[derive(Debug, Clone)]
93pub struct Group {
94 pub rect: TileRect,
96 pub layout: GroupLayout,
98 pub clip: bool,
100 pub border: Option<Border>,
102}
103
104impl Group {
105 #[inline]
107 pub fn new(rect: TileRect) -> Self {
108 Self {
109 rect,
110 layout: GroupLayout::Absolute,
111 clip: false,
112 border: None,
113 }
114 }
115
116 #[inline]
118 pub fn with_layout(mut self, layout: GroupLayout) -> Self {
119 self.layout = layout;
120 self
121 }
122
123 #[inline]
125 pub fn with_clip(mut self, clip: bool) -> Self {
126 self.clip = clip;
127 self
128 }
129
130 #[inline]
132 pub fn with_border(mut self, border: Border) -> Self {
133 self.border = Some(border);
134 self
135 }
136
137 #[inline]
145 pub fn to_absolute(&self, tx: u32, ty: u32) -> TilePos {
146 TilePos::new(self.rect.tx + tx, self.rect.ty + ty)
147 }
148
149 #[inline]
151 pub fn rect_to_absolute(&self, relative: TileRect) -> TileRect {
152 TileRect::new(
153 self.rect.tx + relative.tx,
154 self.rect.ty + relative.ty,
155 relative.tw,
156 relative.th,
157 )
158 }
159
160 pub fn child_rect(&self, child: &LayoutElement, _index: usize, ctx: &DataContext) -> TileRect {
170 let child_tw = child.rect.tw.unwrap_or(0);
171 let child_th = child.rect.th.unwrap_or(0);
172
173 let (rel_tx, rel_ty) = match self.layout {
174 GroupLayout::Absolute => (child.rect.tx.resolve(ctx), child.rect.ty.resolve(ctx)),
175 GroupLayout::Horizontal { .. } => {
176 (0, 0)
177 }
178 GroupLayout::Vertical { .. } => {
179 (0, 0)
180 }
181 };
182
183 TileRect::new(
184 self.rect.tx + rel_tx,
185 self.rect.ty + rel_ty,
186 child_tw,
187 child_th,
188 )
189 }
190
191 pub fn layout_offset(&self, index: usize, children: &[LayoutElement]) -> (u32, u32) {
203 match self.layout {
204 GroupLayout::Absolute => (0, 0),
205 GroupLayout::Horizontal { gap } => {
206 let mut x = 0u32;
207 for child in children.iter().take(index) {
208 x += child.rect.tw.unwrap_or(0) + gap;
209 }
210 (x, 0)
211 }
212 GroupLayout::Vertical { gap } => {
213 let mut y = 0u32;
214 for child in children.iter().take(index) {
215 y += child.rect.th.unwrap_or(0) + gap;
216 }
217 (0, y)
218 }
219 }
220 }
221
222 pub fn resolve_children(&self, children: &[LayoutElement], ctx: &DataContext) -> Vec<ChildRect> {
227 children
228 .iter()
229 .enumerate()
230 .map(|(i, child)| {
231 let (layout_dx, layout_dy) = self.layout_offset(i, children);
232 let child_tx = match self.layout {
233 GroupLayout::Absolute => child.rect.tx.resolve(ctx),
234 _ => layout_dx,
235 };
236 let child_ty = match self.layout {
237 GroupLayout::Absolute => child.rect.ty.resolve(ctx),
238 _ => layout_dy,
239 };
240
241 ChildRect {
242 rect: TileRect::new(
243 self.rect.tx + child_tx,
244 self.rect.ty + child_ty,
245 child.rect.tw.unwrap_or(0),
246 child.rect.th.unwrap_or(0),
247 ),
248 z_index: child.z_index,
249 visible: child.visible.eval(ctx),
250 }
251 })
252 .collect()
253 }
254
255 pub fn render_border(&self, painter: &mut dyn Painter) {
263 if let Some(ref border) = self.border {
264 border.render(painter);
265 }
266 }
267
268 pub fn render(
274 &self,
275 children: &[LayoutElement],
276 ctx: &DataContext,
277 render_ctx: &RenderContext,
278 painter: &mut dyn Painter,
279 registry: &crate::layout_engine::registry::ElementRegistry,
280 ) -> Result<(), RenderError> {
281 self.render_border(painter);
283
284 let resolved = self.resolve_children(children, ctx);
286
287 let mut sorted: Vec<(usize, &ChildRect)> =
289 resolved.iter().enumerate().collect();
290 sorted.sort_by_key(|(_, cr)| cr.z_index);
291
292 for (child_idx, child_rect) in sorted {
294 if !child_rect.visible {
295 continue;
296 }
297 let child_elem = &children[child_idx];
298
299 if self.clip && !self.overlaps(child_rect.rect) {
301 continue;
302 }
303
304 if let Some(custom_elem) = registry.get(&child_elem.element_type) {
306 custom_elem.render(child_elem, ctx, render_ctx, painter)?;
307 }
308 }
309
310 Ok(())
311 }
312
313 fn overlaps(&self, r: TileRect) -> bool {
315 let gx2 = self.rect.tx + self.rect.tw;
316 let gy2 = self.rect.ty + self.rect.th;
317 let rx2 = r.tx + r.tw;
318 let ry2 = r.ty + r.th;
319
320 r.tx < gx2 && self.rect.tx < rx2 && r.ty < gy2 && self.rect.ty < ry2
321 }
322}
323
324#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::layout_engine::types::{Coord, ElementRect, EdgeInsets};
330 use dotzuki_engine::render::{Rgba, TileRect};
331
332 fn make_element(tx: u32, ty: u32, tw: u32, th: u32) -> LayoutElement {
334 LayoutElement {
335 id: String::new(),
336 element_type: "text".to_string(),
337 rect: ElementRect {
338 tx: Coord::Literal(tx),
339 ty: Coord::Literal(ty),
340 tw: Some(tw),
341 th: Some(th),
342 },
343 visible: crate::layout_engine::types::Visibility::Static(true),
344 z_index: 0,
345 params: crate::layout_engine::types::ElementParams::Custom(
346 serde_json::Value::Null,
347 ),
348 }
349 }
350
351 #[test]
354 fn layout_from_config_horizontal() {
355 let config = LayoutConfig {
356 direction: Some(Direction::Horizontal),
357 gap: 2,
358 padding: EdgeInsets::default(),
359 };
360 let layout = GroupLayout::from_config(&config);
361 assert_eq!(layout, GroupLayout::Horizontal { gap: 2 });
362 }
363
364 #[test]
365 fn layout_from_config_vertical() {
366 let config = LayoutConfig {
367 direction: Some(Direction::Vertical),
368 gap: 1,
369 padding: EdgeInsets::default(),
370 };
371 let layout = GroupLayout::from_config(&config);
372 assert_eq!(layout, GroupLayout::Vertical { gap: 1 });
373 }
374
375 #[test]
376 fn layout_from_config_absolute_when_no_direction() {
377 let config = LayoutConfig {
378 direction: None,
379 gap: 5,
380 padding: EdgeInsets::default(),
381 };
382 let layout = GroupLayout::from_config(&config);
383 assert_eq!(layout, GroupLayout::Absolute);
384 }
385
386 #[test]
387 fn is_auto_returns_false_for_absolute() {
388 assert!(!GroupLayout::Absolute.is_auto());
389 }
390
391 #[test]
392 fn is_auto_returns_true_for_horizontal_and_vertical() {
393 assert!(GroupLayout::Horizontal { gap: 0 }.is_auto());
394 assert!(GroupLayout::Vertical { gap: 0 }.is_auto());
395 }
396
397 #[test]
400 fn group_defaults() {
401 let rect = TileRect::new(5, 5, 10, 8);
402 let g = Group::new(rect);
403 assert_eq!(g.rect, rect);
404 assert_eq!(g.layout, GroupLayout::Absolute);
405 assert!(!g.clip);
406 assert!(g.border.is_none());
407 }
408
409 #[test]
410 fn group_builder_pattern() {
411 let rect = TileRect::new(0, 0, 20, 18);
412 let border = Border::new(rect, Rgba::INK_BLACK);
413 let g = Group::new(rect)
414 .with_layout(GroupLayout::Vertical { gap: 2 })
415 .with_clip(true)
416 .with_border(border);
417
418 assert_eq!(g.layout, GroupLayout::Vertical { gap: 2 });
419 assert!(g.clip);
420 assert!(g.border.is_some());
421 }
422
423 #[test]
426 fn to_absolute_adds_group_offset() {
427 let g = Group::new(TileRect::new(3, 7, 10, 10));
428 assert_eq!(g.to_absolute(0, 0), TilePos::new(3, 7));
429 assert_eq!(g.to_absolute(2, 3), TilePos::new(5, 10));
430 assert_eq!(g.to_absolute(9, 9), TilePos::new(12, 16));
431 }
432
433 #[test]
434 fn rect_to_absolute_preserves_size() {
435 let g = Group::new(TileRect::new(2, 3, 10, 10));
436 let rel = TileRect::new(1, 1, 4, 3);
437 let abs = g.rect_to_absolute(rel);
438 assert_eq!(abs.tx, 3); assert_eq!(abs.ty, 4); assert_eq!(abs.tw, 4);
441 assert_eq!(abs.th, 3);
442 }
443
444 #[test]
447 fn absolute_layout_offset_is_zero() {
448 let g = Group::new(TileRect::new(0, 0, 20, 18));
449 let children = vec![make_element(0, 0, 5, 2), make_element(0, 0, 3, 2)];
450 assert_eq!(g.layout_offset(0, &children), (0, 0));
451 assert_eq!(g.layout_offset(1, &children), (0, 0));
452 }
453
454 #[test]
455 fn horizontal_layout_offset_accumulates_widths() {
456 let g = Group::new(TileRect::new(0, 0, 20, 18))
457 .with_layout(GroupLayout::Horizontal { gap: 1 });
458 let children = vec![
459 make_element(0, 0, 5, 2), make_element(0, 0, 3, 2), make_element(0, 0, 4, 2), ];
463
464 assert_eq!(g.layout_offset(0, &children), (0, 0));
465 assert_eq!(g.layout_offset(1, &children), (6, 0));
466 assert_eq!(g.layout_offset(2, &children), (10, 0));
467 }
468
469 #[test]
470 fn vertical_layout_offset_accumulates_heights() {
471 let g = Group::new(TileRect::new(0, 0, 20, 18))
472 .with_layout(GroupLayout::Vertical { gap: 2 });
473 let children = vec![
474 make_element(0, 0, 10, 3), make_element(0, 0, 10, 5), make_element(0, 0, 10, 2), ];
478
479 assert_eq!(g.layout_offset(0, &children), (0, 0));
480 assert_eq!(g.layout_offset(1, &children), (0, 5));
481 assert_eq!(g.layout_offset(2, &children), (0, 12));
482 }
483
484 #[test]
485 fn horizontal_layout_no_gap() {
486 let g = Group::new(TileRect::new(0, 0, 20, 18))
487 .with_layout(GroupLayout::Horizontal { gap: 0 });
488 let children = vec![make_element(0, 0, 3, 1), make_element(0, 0, 7, 1)];
489 assert_eq!(g.layout_offset(1, &children), (3, 0));
490 }
491
492 #[test]
495 fn resolve_absolute_children_preserves_positions() {
496 let g = Group::new(TileRect::new(2, 3, 10, 10));
497 let children = vec![
498 make_element(0, 0, 4, 2),
499 make_element(1, 1, 6, 3),
500 ];
501 let ctx = DataContext::new();
502
503 let resolved = g.resolve_children(&children, &ctx);
504 assert_eq!(resolved.len(), 2);
505
506 assert_eq!(resolved[0].rect, TileRect::new(2, 3, 4, 2));
508 assert_eq!(resolved[1].rect, TileRect::new(3, 4, 6, 3));
510 }
511
512 #[test]
513 fn resolve_horizontal_children() {
514 let g = Group::new(TileRect::new(1, 1, 20, 10))
515 .with_layout(GroupLayout::Horizontal { gap: 1 });
516 let children = vec![
517 make_element(0, 0, 5, 3),
518 make_element(0, 0, 4, 3),
519 make_element(0, 0, 6, 3),
520 ];
521 let ctx = DataContext::new();
522
523 let resolved = g.resolve_children(&children, &ctx);
524
525 assert_eq!(resolved[0].rect, TileRect::new(1, 1, 5, 3));
527 assert_eq!(resolved[1].rect, TileRect::new(7, 1, 4, 3));
529 assert_eq!(resolved[2].rect, TileRect::new(12, 1, 6, 3));
531 }
532
533 #[test]
534 fn resolve_vertical_children() {
535 let g = Group::new(TileRect::new(0, 2, 10, 15))
536 .with_layout(GroupLayout::Vertical { gap: 0 });
537 let children = vec![
538 make_element(0, 0, 10, 3),
539 make_element(0, 0, 10, 4),
540 ];
541 let ctx = DataContext::new();
542
543 let resolved = g.resolve_children(&children, &ctx);
544 assert_eq!(resolved[0].rect, TileRect::new(0, 2, 10, 3));
546 assert_eq!(resolved[1].rect, TileRect::new(0, 5, 10, 4));
548 }
549
550 #[test]
551 fn resolve_passes_z_index_and_visibility() {
552 let g = Group::new(TileRect::new(0, 0, 10, 10));
553 let mut c0 = make_element(0, 0, 3, 3);
554 c0.z_index = 5;
555 c0.visible = crate::layout_engine::types::Visibility::Static(false);
556 let c1 = make_element(1, 1, 3, 3);
557 let ctx = DataContext::new();
558
559 let resolved = g.resolve_children(&[c0, c1], &ctx);
560 assert_eq!(resolved[0].z_index, 5);
561 assert!(!resolved[0].visible);
562 assert_eq!(resolved[1].z_index, 0);
563 assert!(resolved[1].visible);
564 }
565
566 #[test]
569 fn overlaps_detects_intersection() {
570 let g = Group::new(TileRect::new(0, 0, 10, 10));
571
572 assert!(g.overlaps(TileRect::new(2, 2, 4, 4)));
574
575 assert!(g.overlaps(TileRect::new(8, 8, 6, 6)));
577
578 assert!(g.overlaps(TileRect::new(0, 1, 1, 1)));
580
581 assert!(g.overlaps(TileRect::new(1, 0, 1, 1)));
583 }
584
585 #[test]
586 fn overlaps_detects_non_intersection() {
587 let g = Group::new(TileRect::new(0, 0, 10, 10));
588
589 assert!(!g.overlaps(TileRect::new(10, 0, 1, 1)));
591
592 assert!(!g.overlaps(TileRect::new(0, 10, 1, 1)));
594
595 assert!(!g.overlaps(TileRect::new(20, 20, 1, 1)));
598 }
599
600 #[test]
603 fn resolve_single_child_at_origin() {
604 let g = Group::new(TileRect::new(0, 0, 20, 18));
605 let children = vec![make_element(0, 0, 20, 18)];
606 let ctx = DataContext::new();
607 let resolved = g.resolve_children(&children, &ctx);
608 assert_eq!(resolved.len(), 1);
609 assert_eq!(resolved[0].rect, TileRect::new(0, 0, 20, 18));
610 }
611
612 #[test]
613 fn resolve_empty_children() {
614 let g = Group::new(TileRect::new(0, 0, 10, 10));
615 let resolved = g.resolve_children(&[], &DataContext::new());
616 assert!(resolved.is_empty());
617 }
618
619 #[test]
622 fn child_rect_absolute_mode() {
623 let g = Group::new(TileRect::new(5, 5, 20, 18));
624 let child = make_element(2, 3, 8, 4);
625 let ctx = DataContext::new();
626
627 let cr = g.child_rect(&child, 0, &ctx);
628 assert_eq!(cr.tx, 7); assert_eq!(cr.ty, 8); assert_eq!(cr.tw, 8);
631 assert_eq!(cr.th, 4);
632 }
633
634 #[test]
635 fn child_rect_horizontal_mode() {
636 let g = Group::new(TileRect::new(1, 1, 20, 10))
637 .with_layout(GroupLayout::Horizontal { gap: 1 });
638 let child = make_element(99, 99, 4, 3); let ctx = DataContext::new();
640 let cr = g.child_rect(&child, 0, &ctx);
641 assert_eq!(cr.tx, 1); assert_eq!(cr.ty, 1); assert_eq!(cr.tw, 4);
644 assert_eq!(cr.th, 3);
645 }
646
647 #[test]
648 fn child_rect_vertical_mode() {
649 let g = Group::new(TileRect::new(2, 2, 10, 20))
650 .with_layout(GroupLayout::Vertical { gap: 2 });
651 let child = make_element(42, 42, 6, 5);
652 let ctx = DataContext::new();
653 let cr = g.child_rect(&child, 0, &ctx);
654 assert_eq!(cr.tx, 2); assert_eq!(cr.ty, 2); assert_eq!(cr.tw, 6);
657 assert_eq!(cr.th, 5);
658 }
659}