1#![allow(clippy::cast_lossless)] use crate::grid::{compute_grid_layout, GridArea, GridTemplate};
25use serde::{Deserialize, Serialize};
26use std::fmt;
27
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Size {
35 pub width: u16,
37 pub height: u16,
39}
40
41impl Size {
42 #[must_use]
44 pub const fn new(width: u16, height: u16) -> Self {
45 Self { width, height }
46 }
47
48 pub const ZERO: Self = Self {
50 width: 0,
51 height: 0,
52 };
53}
54
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub struct Rect {
58 pub x: u16,
60 pub y: u16,
62 pub width: u16,
64 pub height: u16,
66}
67
68impl Rect {
69 #[must_use]
71 pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
72 Self {
73 x,
74 y,
75 width,
76 height,
77 }
78 }
79
80 #[must_use]
82 pub fn intersection(&self, other: Self) -> Self {
83 let x1 = self.x.max(other.x);
84 let y1 = self.y.max(other.y);
85 let x2 = (self.x + self.width).min(other.x + other.width);
86 let y2 = (self.y + self.height).min(other.y + other.height);
87
88 if x2 > x1 && y2 > y1 {
89 Self {
90 x: x1,
91 y: y1,
92 width: x2 - x1,
93 height: y2 - y1,
94 }
95 } else {
96 Self::default()
97 }
98 }
99
100 #[must_use]
102 pub const fn contains(&self, x: u16, y: u16) -> bool {
103 x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
104 }
105
106 #[must_use]
108 pub const fn area(&self) -> u32 {
109 self.width as u32 * self.height as u32
110 }
111}
112
113#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub struct SizeHint {
120 pub min: Size,
122 pub preferred: Size,
124 pub max: Option<Size>,
126}
127
128impl SizeHint {
129 #[must_use]
131 pub const fn new(min: Size, preferred: Size, max: Option<Size>) -> Self {
132 Self {
133 min,
134 preferred,
135 max,
136 }
137 }
138
139 #[must_use]
141 pub const fn fixed(size: Size) -> Self {
142 Self {
143 min: size,
144 preferred: size,
145 max: Some(size),
146 }
147 }
148
149 #[must_use]
151 pub const fn flexible(min: Size) -> Self {
152 Self {
153 min,
154 preferred: min,
155 max: None,
156 }
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166pub enum FlexConstraint {
167 Fixed(u16),
169 Min(u16),
171 Max(u16),
173 Percentage(u16),
175 Ratio(u16, u16),
177 Fill(u16),
182 Content,
184}
185
186impl Default for FlexConstraint {
187 fn default() -> Self {
188 Self::Fill(1)
189 }
190}
191
192pub trait IntrinsicSize {
194 fn size_hint(&self, available: Size) -> SizeHint;
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209pub struct ComputeBlock {
210 pub name: String,
212 pub area: GridArea,
214 pub z_index: i16,
216 pub visible: bool,
218 pub clip: ClipMode,
220}
221
222impl ComputeBlock {
223 #[must_use]
225 pub fn new(name: impl Into<String>, area: GridArea) -> Self {
226 Self {
227 name: name.into(),
228 area,
229 z_index: 0,
230 visible: true,
231 clip: ClipMode::default(),
232 }
233 }
234
235 #[must_use]
237 pub const fn with_z_index(mut self, z_index: i16) -> Self {
238 self.z_index = z_index;
239 self
240 }
241
242 #[must_use]
244 pub const fn with_visible(mut self, visible: bool) -> Self {
245 self.visible = visible;
246 self
247 }
248
249 #[must_use]
251 pub const fn with_clip(mut self, clip: ClipMode) -> Self {
252 self.clip = clip;
253 self
254 }
255}
256
257#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
259pub enum ClipMode {
260 #[default]
262 Strict,
263 Overflow,
265 Scroll,
267}
268
269#[derive(Debug, Clone)]
276pub struct GridCompositor {
277 template: GridTemplate,
279 blocks: Vec<ComputeBlock>,
281 ownership: Vec<Vec<Option<usize>>>,
283 dirty: Vec<Rect>,
285}
286
287impl GridCompositor {
288 #[must_use]
290 pub fn new(template: GridTemplate) -> Self {
291 let rows = template.row_count().max(1);
292 let cols = template.column_count().max(1);
293 Self {
294 template,
295 blocks: Vec::new(),
296 ownership: vec![vec![None; cols]; rows],
297 dirty: Vec::new(),
298 }
299 }
300
301 #[must_use]
303 pub fn template(&self) -> &GridTemplate {
304 &self.template
305 }
306
307 pub fn register(&mut self, block: ComputeBlock) -> Result<usize, CompositorError> {
313 if block.area.col_end > self.template.column_count() {
315 return Err(CompositorError::OutOfBounds {
316 block: block.name.clone(),
317 reason: format!(
318 "column {} exceeds grid width {}",
319 block.area.col_end,
320 self.template.column_count()
321 ),
322 });
323 }
324 if block.area.row_end > self.ownership.len() {
325 return Err(CompositorError::OutOfBounds {
326 block: block.name.clone(),
327 reason: format!(
328 "row {} exceeds grid height {}",
329 block.area.row_end,
330 self.ownership.len()
331 ),
332 });
333 }
334
335 for row in block.area.row_start..block.area.row_end {
337 for col in block.area.col_start..block.area.col_end {
338 if let Some(existing_idx) = self.ownership[row][col] {
339 return Err(CompositorError::CellConflict {
340 cell: (row, col),
341 existing: self.blocks[existing_idx].name.clone(),
342 new: block.name,
343 });
344 }
345 }
346 }
347
348 let idx = self.blocks.len();
350 for row in block.area.row_start..block.area.row_end {
351 for col in block.area.col_start..block.area.col_end {
352 self.ownership[row][col] = Some(idx);
353 }
354 }
355
356 self.blocks.push(block);
357 Ok(idx)
358 }
359
360 pub fn unregister(&mut self, name: &str) -> Result<ComputeBlock, CompositorError> {
362 let idx = self
363 .blocks
364 .iter()
365 .position(|b| b.name == name)
366 .ok_or_else(|| CompositorError::BlockNotFound(name.to_string()))?;
367
368 let block = self.blocks.remove(idx);
369
370 for row in block.area.row_start..block.area.row_end {
372 for col in block.area.col_start..block.area.col_end {
373 self.ownership[row][col] = None;
374 }
375 }
376
377 for row in &mut self.ownership {
379 for i in row.iter_mut().flatten() {
380 if *i > idx {
381 *i -= 1;
382 }
383 }
384 }
385
386 Ok(block)
387 }
388
389 #[must_use]
391 pub fn get(&self, name: &str) -> Option<&ComputeBlock> {
392 self.blocks.iter().find(|b| b.name == name)
393 }
394
395 pub fn get_mut(&mut self, name: &str) -> Option<&mut ComputeBlock> {
397 self.blocks.iter_mut().find(|b| b.name == name)
398 }
399
400 #[must_use]
402 pub fn bounds(&self, name: &str, total_area: Rect) -> Option<Rect> {
403 let block = self.blocks.iter().find(|b| b.name == name)?;
404 let layout = compute_grid_layout(
405 &self.template,
406 total_area.width as f32,
407 total_area.height as f32,
408 &[],
409 );
410 let (x, y, w, h) = layout.area_bounds(&block.area)?;
411 Some(Rect::new(
412 total_area.x + x as u16,
413 total_area.y + y as u16,
414 w as u16,
415 h as u16,
416 ))
417 }
418
419 #[must_use]
421 pub fn blocks(&self) -> &[ComputeBlock] {
422 &self.blocks
423 }
424
425 pub fn mark_dirty(&mut self, rect: Rect) {
427 self.dirty.push(rect);
428 }
429
430 pub fn take_dirty(&mut self) -> Vec<Rect> {
432 std::mem::take(&mut self.dirty)
433 }
434
435 #[must_use]
437 pub fn is_dirty(&self) -> bool {
438 !self.dirty.is_empty()
439 }
440
441 #[must_use]
443 pub fn render_order(&self) -> Vec<&ComputeBlock> {
444 let mut sorted: Vec<_> = self.blocks.iter().filter(|b| b.visible).collect();
445 sorted.sort_by_key(|b| b.z_index);
446 sorted
447 }
448
449 #[must_use]
451 pub fn owner_at(&self, row: usize, col: usize) -> Option<&ComputeBlock> {
452 self.ownership
453 .get(row)
454 .and_then(|r| r.get(col))
455 .and_then(|&idx| idx)
456 .map(|idx| &self.blocks[idx])
457 }
458}
459
460#[derive(Debug, Clone, PartialEq, Eq)]
462pub enum CompositorError {
463 OutOfBounds { block: String, reason: String },
465 CellConflict {
467 cell: (usize, usize),
468 existing: String,
469 new: String,
470 },
471 BlockNotFound(String),
473}
474
475impl fmt::Display for CompositorError {
476 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477 match self {
478 Self::OutOfBounds { block, reason } => {
479 write!(f, "block '{}' out of bounds: {}", block, reason)
480 }
481 Self::CellConflict {
482 cell,
483 existing,
484 new,
485 } => {
486 write!(
487 f,
488 "cell ({}, {}) already owned by '{}', cannot assign to '{}'",
489 cell.0, cell.1, existing, new
490 )
491 }
492 Self::BlockNotFound(name) => {
493 write!(f, "block '{}' not found", name)
494 }
495 }
496 }
497}
498
499impl std::error::Error for CompositorError {}
500
501#[inline]
508fn compute_constraint_width(hint: &SizeHint, constraint: FlexConstraint, available_width: u16) -> u16 {
509 match constraint {
510 FlexConstraint::Fixed(size) => size,
511 FlexConstraint::Min(size) => size.max(hint.min.width),
512 FlexConstraint::Max(size) => size.min(hint.preferred.width),
513 FlexConstraint::Percentage(pct) => (available_width as u32 * pct as u32 / 100) as u16,
514 FlexConstraint::Ratio(num, den) if den > 0 => (available_width as u32 * num as u32 / den as u32) as u16,
515 FlexConstraint::Ratio(_, _) => 0,
516 FlexConstraint::Content => hint.preferred.width,
517 FlexConstraint::Fill(_) => 0, }
519}
520
521fn distribute_fill_space(
523 allocated: &mut [Size],
524 hints: &[SizeHint],
525 constraints: &[FlexConstraint],
526 remaining_width: u16,
527 count: usize,
528) {
529 let fill_total: u16 = constraints.iter().take(count)
530 .filter_map(|c| if let FlexConstraint::Fill(w) = c { Some(*w) } else { None })
531 .sum();
532
533 if fill_total == 0 || remaining_width == 0 { return; }
534
535 for (i, constraint) in constraints.iter().enumerate().take(count) {
536 if let FlexConstraint::Fill(weight) = constraint {
537 let share = (remaining_width as u32 * *weight as u32 / fill_total as u32) as u16;
538 allocated[i].width = hints[i].max.map_or(share, |max| share.min(max.width));
539 }
540 }
541}
542
543#[must_use]
550pub fn compute_intrinsic_layout(
551 hints: &[SizeHint],
552 constraints: &[FlexConstraint],
553 available: Size,
554) -> Vec<Rect> {
555 if hints.is_empty() || constraints.is_empty() {
556 return Vec::new();
557 }
558
559 let count = hints.len().min(constraints.len());
560 let mut allocated = vec![Size::ZERO; count];
561 let mut remaining_width = available.width;
562
563 for (i, (hint, constraint)) in hints.iter().zip(constraints).enumerate().take(count) {
565 let width = compute_constraint_width(hint, *constraint, available.width);
566 if !matches!(constraint, FlexConstraint::Fill(_)) {
567 allocated[i].width = width;
568 if matches!(constraint, FlexConstraint::Content) {
569 allocated[i].height = hint.preferred.height;
570 }
571 remaining_width = remaining_width.saturating_sub(width);
572 }
573 }
574
575 distribute_fill_space(&mut allocated, hints, constraints, remaining_width, count);
577
578 let mut x = 0u16;
580 allocated
581 .iter()
582 .map(|size| {
583 let rect = Rect::new(x, 0, size.width, available.height);
584 x = x.saturating_add(size.width);
585 rect
586 })
587 .collect()
588}
589
590#[cfg(test)]
591mod tests {
592 use super::*;
593 use crate::grid::TrackSize;
594
595 #[test]
600 fn test_size_new() {
601 let size = Size::new(80, 24);
602 assert_eq!(size.width, 80);
603 assert_eq!(size.height, 24);
604 }
605
606 #[test]
607 fn test_size_zero() {
608 assert_eq!(Size::ZERO, Size::new(0, 0));
609 }
610
611 #[test]
616 fn test_rect_intersection() {
617 let r1 = Rect::new(0, 0, 10, 10);
618 let r2 = Rect::new(5, 5, 10, 10);
619 let intersection = r1.intersection(r2);
620
621 assert_eq!(intersection.x, 5);
622 assert_eq!(intersection.y, 5);
623 assert_eq!(intersection.width, 5);
624 assert_eq!(intersection.height, 5);
625 }
626
627 #[test]
628 fn test_rect_no_intersection() {
629 let r1 = Rect::new(0, 0, 5, 5);
630 let r2 = Rect::new(10, 10, 5, 5);
631 let intersection = r1.intersection(r2);
632
633 assert_eq!(intersection.area(), 0);
634 }
635
636 #[test]
637 fn test_rect_contains() {
638 let rect = Rect::new(10, 10, 20, 20);
639
640 assert!(rect.contains(10, 10));
641 assert!(rect.contains(15, 15));
642 assert!(rect.contains(29, 29));
643 assert!(!rect.contains(30, 30));
644 assert!(!rect.contains(9, 10));
645 }
646
647 #[test]
652 fn test_size_hint_fixed() {
653 let hint = SizeHint::fixed(Size::new(40, 10));
654 assert_eq!(hint.min, hint.preferred);
655 assert_eq!(hint.preferred, hint.max.unwrap());
656 }
657
658 #[test]
659 fn test_size_hint_flexible() {
660 let hint = SizeHint::flexible(Size::new(10, 3));
661 assert_eq!(hint.min, Size::new(10, 3));
662 assert!(hint.max.is_none());
663 }
664
665 #[test]
670 fn test_flex_constraint_default() {
671 assert_eq!(FlexConstraint::default(), FlexConstraint::Fill(1));
672 }
673
674 #[test]
679 fn test_compute_block_new() {
680 let block = ComputeBlock::new("test", GridArea::cell(0, 0));
681 assert_eq!(block.name, "test");
682 assert_eq!(block.z_index, 0);
683 assert!(block.visible);
684 assert_eq!(block.clip, ClipMode::Strict);
685 }
686
687 #[test]
688 fn test_compute_block_builder() {
689 let block = ComputeBlock::new("overlay", GridArea::cell(1, 1))
690 .with_z_index(10)
691 .with_visible(true)
692 .with_clip(ClipMode::Overflow);
693
694 assert_eq!(block.z_index, 10);
695 assert_eq!(block.clip, ClipMode::Overflow);
696 }
697
698 #[test]
703 fn test_compositor_register() {
704 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)])
705 .with_rows([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
706 let mut compositor = GridCompositor::new(template);
707
708 let idx = compositor
709 .register(ComputeBlock::new("header", GridArea::row_span(0, 0, 2)))
710 .unwrap();
711 assert_eq!(idx, 0);
712
713 let idx = compositor
714 .register(ComputeBlock::new("main", GridArea::cell(1, 0)))
715 .unwrap();
716 assert_eq!(idx, 1);
717 }
718
719 #[test]
720 fn test_compositor_cell_conflict() {
721 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
722 let mut compositor = GridCompositor::new(template);
723
724 compositor
725 .register(ComputeBlock::new("first", GridArea::cell(0, 0)))
726 .unwrap();
727
728 let result = compositor.register(ComputeBlock::new("second", GridArea::cell(0, 0)));
729 assert!(matches!(result, Err(CompositorError::CellConflict { .. })));
730 }
731
732 #[test]
733 fn test_compositor_out_of_bounds() {
734 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
735 let mut compositor = GridCompositor::new(template);
736
737 let result = compositor.register(ComputeBlock::new("bad", GridArea::cell(0, 5)));
738 assert!(matches!(result, Err(CompositorError::OutOfBounds { .. })));
739 }
740
741 #[test]
742 fn test_compositor_bounds() {
743 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)])
744 .with_rows([TrackSize::Fr(1.0)]);
745 let mut compositor = GridCompositor::new(template);
746
747 compositor
748 .register(ComputeBlock::new("left", GridArea::cell(0, 0)))
749 .unwrap();
750 compositor
751 .register(ComputeBlock::new("right", GridArea::cell(0, 1)))
752 .unwrap();
753
754 let total = Rect::new(0, 0, 100, 50);
755 let left_bounds = compositor.bounds("left", total).unwrap();
756 let right_bounds = compositor.bounds("right", total).unwrap();
757
758 assert_eq!(left_bounds.x, 0);
759 assert_eq!(left_bounds.width, 50);
760 assert_eq!(right_bounds.x, 50);
761 assert_eq!(right_bounds.width, 50);
762 }
763
764 #[test]
765 fn test_compositor_render_order() {
766 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
767 let mut compositor = GridCompositor::new(template);
768
769 compositor
770 .register(ComputeBlock::new("back", GridArea::cell(0, 0)).with_z_index(0))
771 .unwrap();
772 compositor
773 .register(ComputeBlock::new("front", GridArea::cell(0, 1)).with_z_index(10))
774 .unwrap();
775
776 let order = compositor.render_order();
777 assert_eq!(order[0].name, "back");
778 assert_eq!(order[1].name, "front");
779 }
780
781 #[test]
782 fn test_compositor_hidden_blocks() {
783 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
784 let mut compositor = GridCompositor::new(template);
785
786 compositor
787 .register(ComputeBlock::new("visible", GridArea::cell(0, 0)))
788 .unwrap();
789
790 let template2 = GridTemplate::columns([TrackSize::Fr(1.0)])
792 .with_rows([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
793 let mut compositor2 = GridCompositor::new(template2);
794
795 compositor2
796 .register(ComputeBlock::new("visible", GridArea::cell(0, 0)))
797 .unwrap();
798 compositor2
799 .register(ComputeBlock::new("hidden", GridArea::cell(1, 0)).with_visible(false))
800 .unwrap();
801
802 let order = compositor2.render_order();
803 assert_eq!(order.len(), 1);
804 assert_eq!(order[0].name, "visible");
805 }
806
807 #[test]
808 fn test_compositor_unregister() {
809 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
810 let mut compositor = GridCompositor::new(template);
811
812 compositor
813 .register(ComputeBlock::new("block", GridArea::cell(0, 0)))
814 .unwrap();
815
816 let block = compositor.unregister("block").unwrap();
817 assert_eq!(block.name, "block");
818
819 compositor
821 .register(ComputeBlock::new("new", GridArea::cell(0, 0)))
822 .unwrap();
823 }
824
825 #[test]
826 fn test_compositor_dirty_tracking() {
827 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
828 let mut compositor = GridCompositor::new(template);
829
830 assert!(!compositor.is_dirty());
831
832 compositor.mark_dirty(Rect::new(0, 0, 10, 10));
833 assert!(compositor.is_dirty());
834
835 let dirty = compositor.take_dirty();
836 assert_eq!(dirty.len(), 1);
837 assert!(!compositor.is_dirty());
838 }
839
840 #[test]
841 fn test_compositor_owner_at() {
842 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
843 let mut compositor = GridCompositor::new(template);
844
845 compositor
846 .register(ComputeBlock::new("left", GridArea::cell(0, 0)))
847 .unwrap();
848
849 assert_eq!(compositor.owner_at(0, 0).unwrap().name, "left");
850 assert!(compositor.owner_at(0, 1).is_none());
851 }
852
853 #[test]
858 fn test_gc001_fill_distributes_space() {
859 let hints = vec![
860 SizeHint::flexible(Size::new(10, 5)),
861 SizeHint::flexible(Size::new(10, 5)),
862 SizeHint::flexible(Size::new(10, 5)),
863 ];
864 let constraints = vec![
865 FlexConstraint::Fill(1),
866 FlexConstraint::Fill(1),
867 FlexConstraint::Fill(1),
868 ];
869
870 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(120, 24));
871
872 assert_eq!(rects.len(), 3);
873 assert_eq!(rects[0].width, 40);
874 assert_eq!(rects[1].width, 40);
875 assert_eq!(rects[2].width, 40);
876 }
877
878 #[test]
879 fn test_gc002_content_uses_size_hint() {
880 let hints = vec![SizeHint::new(
881 Size::new(10, 3),
882 Size::new(40, 8),
883 Some(Size::new(80, 16)),
884 )];
885 let constraints = vec![FlexConstraint::Content];
886
887 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(200, 50));
888
889 assert_eq!(rects[0].width, 40); }
891
892 #[test]
893 fn test_fill_with_weights() {
894 let hints = vec![
895 SizeHint::flexible(Size::new(0, 5)),
896 SizeHint::flexible(Size::new(0, 5)),
897 ];
898 let constraints = vec![FlexConstraint::Fill(2), FlexConstraint::Fill(1)];
899
900 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(90, 24));
901
902 assert_eq!(rects[0].width, 60); assert_eq!(rects[1].width, 30); }
905
906 #[test]
907 fn test_mixed_constraints() {
908 let hints = vec![
909 SizeHint::fixed(Size::new(20, 5)),
910 SizeHint::flexible(Size::new(10, 5)),
911 SizeHint::fixed(Size::new(20, 5)),
912 ];
913 let constraints = vec![
914 FlexConstraint::Fixed(20),
915 FlexConstraint::Fill(1),
916 FlexConstraint::Fixed(20),
917 ];
918
919 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(100, 24));
920
921 assert_eq!(rects[0].width, 20);
922 assert_eq!(rects[1].width, 60); assert_eq!(rects[2].width, 20);
924 }
925
926 #[test]
927 fn test_fill_respects_max() {
928 let hints = vec![SizeHint::new(
929 Size::new(10, 5),
930 Size::new(30, 5),
931 Some(Size::new(50, 5)),
932 )];
933 let constraints = vec![FlexConstraint::Fill(1)];
934
935 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(200, 24));
936
937 assert_eq!(rects[0].width, 50); }
939
940 #[test]
945 fn test_compositor_error_display() {
946 let err = CompositorError::CellConflict {
947 cell: (1, 2),
948 existing: "first".to_string(),
949 new: "second".to_string(),
950 };
951 let msg = format!("{}", err);
952 assert!(msg.contains("first"));
953 assert!(msg.contains("second"));
954 }
955}