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 contract_pre_render!();
445 let mut sorted: Vec<_> = self.blocks.iter().filter(|b| b.visible).collect();
446 sorted.sort_by_key(|b| b.z_index);
447 sorted
448 }
449
450 #[must_use]
452 pub fn owner_at(&self, row: usize, col: usize) -> Option<&ComputeBlock> {
453 self.ownership
454 .get(row)
455 .and_then(|r| r.get(col))
456 .and_then(|&idx| idx)
457 .map(|idx| &self.blocks[idx])
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Eq)]
463pub enum CompositorError {
464 OutOfBounds { block: String, reason: String },
466 CellConflict {
468 cell: (usize, usize),
469 existing: String,
470 new: String,
471 },
472 BlockNotFound(String),
474}
475
476impl fmt::Display for CompositorError {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 match self {
479 Self::OutOfBounds { block, reason } => {
480 write!(f, "block '{}' out of bounds: {}", block, reason)
481 }
482 Self::CellConflict {
483 cell,
484 existing,
485 new,
486 } => {
487 write!(
488 f,
489 "cell ({}, {}) already owned by '{}', cannot assign to '{}'",
490 cell.0, cell.1, existing, new
491 )
492 }
493 Self::BlockNotFound(name) => {
494 write!(f, "block '{}' not found", name)
495 }
496 }
497 }
498}
499
500impl std::error::Error for CompositorError {}
501
502#[inline]
509fn compute_constraint_width(
510 hint: &SizeHint,
511 constraint: FlexConstraint,
512 available_width: u16,
513) -> u16 {
514 match constraint {
515 FlexConstraint::Fixed(size) => size,
516 FlexConstraint::Min(size) => size.max(hint.min.width),
517 FlexConstraint::Max(size) => size.min(hint.preferred.width),
518 FlexConstraint::Percentage(pct) => (available_width as u32 * pct as u32 / 100) as u16,
519 FlexConstraint::Ratio(num, den) if den > 0 => {
520 (available_width as u32 * num as u32 / den as u32) as u16
521 }
522 FlexConstraint::Ratio(_, _) => 0,
523 FlexConstraint::Content => hint.preferred.width,
524 FlexConstraint::Fill(_) => 0, }
526}
527
528fn distribute_fill_space(
530 allocated: &mut [Size],
531 hints: &[SizeHint],
532 constraints: &[FlexConstraint],
533 remaining_width: u16,
534 count: usize,
535) {
536 let fill_total: u16 = constraints
537 .iter()
538 .take(count)
539 .filter_map(|c| {
540 if let FlexConstraint::Fill(w) = c {
541 Some(*w)
542 } else {
543 None
544 }
545 })
546 .sum();
547
548 if fill_total == 0 || remaining_width == 0 {
549 return;
550 }
551
552 for (i, constraint) in constraints.iter().enumerate().take(count) {
553 if let FlexConstraint::Fill(weight) = constraint {
554 let share = (remaining_width as u32 * *weight as u32 / fill_total as u32) as u16;
555 allocated[i].width = hints[i].max.map_or(share, |max| share.min(max.width));
556 }
557 }
558}
559
560#[must_use]
567pub fn compute_intrinsic_layout(
568 hints: &[SizeHint],
569 constraints: &[FlexConstraint],
570 available: Size,
571) -> Vec<Rect> {
572 if hints.is_empty() || constraints.is_empty() {
573 return Vec::new();
574 }
575
576 let count = hints.len().min(constraints.len());
577 let mut allocated = vec![Size::ZERO; count];
578 let mut remaining_width = available.width;
579
580 for (i, (hint, constraint)) in hints.iter().zip(constraints).enumerate().take(count) {
582 let width = compute_constraint_width(hint, *constraint, available.width);
583 if !matches!(constraint, FlexConstraint::Fill(_)) {
584 allocated[i].width = width;
585 if matches!(constraint, FlexConstraint::Content) {
586 allocated[i].height = hint.preferred.height;
587 }
588 remaining_width = remaining_width.saturating_sub(width);
589 }
590 }
591
592 distribute_fill_space(&mut allocated, hints, constraints, remaining_width, count);
594
595 let mut x = 0u16;
597 allocated
598 .iter()
599 .map(|size| {
600 let rect = Rect::new(x, 0, size.width, available.height);
601 x = x.saturating_add(size.width);
602 rect
603 })
604 .collect()
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::grid::TrackSize;
611
612 #[test]
617 fn test_size_new() {
618 let size = Size::new(80, 24);
619 assert_eq!(size.width, 80);
620 assert_eq!(size.height, 24);
621 }
622
623 #[test]
624 fn test_size_zero() {
625 assert_eq!(Size::ZERO, Size::new(0, 0));
626 }
627
628 #[test]
633 fn test_rect_intersection() {
634 let r1 = Rect::new(0, 0, 10, 10);
635 let r2 = Rect::new(5, 5, 10, 10);
636 let intersection = r1.intersection(r2);
637
638 assert_eq!(intersection.x, 5);
639 assert_eq!(intersection.y, 5);
640 assert_eq!(intersection.width, 5);
641 assert_eq!(intersection.height, 5);
642 }
643
644 #[test]
645 fn test_rect_no_intersection() {
646 let r1 = Rect::new(0, 0, 5, 5);
647 let r2 = Rect::new(10, 10, 5, 5);
648 let intersection = r1.intersection(r2);
649
650 assert_eq!(intersection.area(), 0);
651 }
652
653 #[test]
654 fn test_rect_contains() {
655 let rect = Rect::new(10, 10, 20, 20);
656
657 assert!(rect.contains(10, 10));
658 assert!(rect.contains(15, 15));
659 assert!(rect.contains(29, 29));
660 assert!(!rect.contains(30, 30));
661 assert!(!rect.contains(9, 10));
662 }
663
664 #[test]
669 fn test_size_hint_fixed() {
670 let hint = SizeHint::fixed(Size::new(40, 10));
671 assert_eq!(hint.min, hint.preferred);
672 assert_eq!(hint.preferred, hint.max.unwrap());
673 }
674
675 #[test]
676 fn test_size_hint_flexible() {
677 let hint = SizeHint::flexible(Size::new(10, 3));
678 assert_eq!(hint.min, Size::new(10, 3));
679 assert!(hint.max.is_none());
680 }
681
682 #[test]
687 fn test_flex_constraint_default() {
688 assert_eq!(FlexConstraint::default(), FlexConstraint::Fill(1));
689 }
690
691 #[test]
696 fn test_compute_block_new() {
697 let block = ComputeBlock::new("test", GridArea::cell(0, 0));
698 assert_eq!(block.name, "test");
699 assert_eq!(block.z_index, 0);
700 assert!(block.visible);
701 assert_eq!(block.clip, ClipMode::Strict);
702 }
703
704 #[test]
705 fn test_compute_block_builder() {
706 let block = ComputeBlock::new("overlay", GridArea::cell(1, 1))
707 .with_z_index(10)
708 .with_visible(true)
709 .with_clip(ClipMode::Overflow);
710
711 assert_eq!(block.z_index, 10);
712 assert_eq!(block.clip, ClipMode::Overflow);
713 }
714
715 #[test]
720 fn test_compositor_register() {
721 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)])
722 .with_rows([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
723 let mut compositor = GridCompositor::new(template);
724
725 let idx = compositor
726 .register(ComputeBlock::new("header", GridArea::row_span(0, 0, 2)))
727 .unwrap();
728 assert_eq!(idx, 0);
729
730 let idx = compositor
731 .register(ComputeBlock::new("main", GridArea::cell(1, 0)))
732 .unwrap();
733 assert_eq!(idx, 1);
734 }
735
736 #[test]
737 fn test_compositor_cell_conflict() {
738 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
739 let mut compositor = GridCompositor::new(template);
740
741 compositor
742 .register(ComputeBlock::new("first", GridArea::cell(0, 0)))
743 .unwrap();
744
745 let result = compositor.register(ComputeBlock::new("second", GridArea::cell(0, 0)));
746 assert!(matches!(result, Err(CompositorError::CellConflict { .. })));
747 }
748
749 #[test]
750 fn test_compositor_out_of_bounds() {
751 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
752 let mut compositor = GridCompositor::new(template);
753
754 let result = compositor.register(ComputeBlock::new("bad", GridArea::cell(0, 5)));
755 assert!(matches!(result, Err(CompositorError::OutOfBounds { .. })));
756 }
757
758 #[test]
759 fn test_compositor_bounds() {
760 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)])
761 .with_rows([TrackSize::Fr(1.0)]);
762 let mut compositor = GridCompositor::new(template);
763
764 compositor
765 .register(ComputeBlock::new("left", GridArea::cell(0, 0)))
766 .unwrap();
767 compositor
768 .register(ComputeBlock::new("right", GridArea::cell(0, 1)))
769 .unwrap();
770
771 let total = Rect::new(0, 0, 100, 50);
772 let left_bounds = compositor.bounds("left", total).unwrap();
773 let right_bounds = compositor.bounds("right", total).unwrap();
774
775 assert_eq!(left_bounds.x, 0);
776 assert_eq!(left_bounds.width, 50);
777 assert_eq!(right_bounds.x, 50);
778 assert_eq!(right_bounds.width, 50);
779 }
780
781 #[test]
782 fn test_compositor_render_order() {
783 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
784 let mut compositor = GridCompositor::new(template);
785
786 compositor
787 .register(ComputeBlock::new("back", GridArea::cell(0, 0)).with_z_index(0))
788 .unwrap();
789 compositor
790 .register(ComputeBlock::new("front", GridArea::cell(0, 1)).with_z_index(10))
791 .unwrap();
792
793 let order = compositor.render_order();
794 assert_eq!(order[0].name, "back");
795 assert_eq!(order[1].name, "front");
796 }
797
798 #[test]
799 fn test_compositor_hidden_blocks() {
800 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
801 let mut compositor = GridCompositor::new(template);
802
803 compositor
804 .register(ComputeBlock::new("visible", GridArea::cell(0, 0)))
805 .unwrap();
806
807 let template2 = GridTemplate::columns([TrackSize::Fr(1.0)])
809 .with_rows([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
810 let mut compositor2 = GridCompositor::new(template2);
811
812 compositor2
813 .register(ComputeBlock::new("visible", GridArea::cell(0, 0)))
814 .unwrap();
815 compositor2
816 .register(ComputeBlock::new("hidden", GridArea::cell(1, 0)).with_visible(false))
817 .unwrap();
818
819 let order = compositor2.render_order();
820 assert_eq!(order.len(), 1);
821 assert_eq!(order[0].name, "visible");
822 }
823
824 #[test]
825 fn test_compositor_unregister() {
826 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
827 let mut compositor = GridCompositor::new(template);
828
829 compositor
830 .register(ComputeBlock::new("block", GridArea::cell(0, 0)))
831 .unwrap();
832
833 let block = compositor.unregister("block").unwrap();
834 assert_eq!(block.name, "block");
835
836 compositor
838 .register(ComputeBlock::new("new", GridArea::cell(0, 0)))
839 .unwrap();
840 }
841
842 #[test]
843 fn test_compositor_dirty_tracking() {
844 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
845 let mut compositor = GridCompositor::new(template);
846
847 assert!(!compositor.is_dirty());
848
849 compositor.mark_dirty(Rect::new(0, 0, 10, 10));
850 assert!(compositor.is_dirty());
851
852 let dirty = compositor.take_dirty();
853 assert_eq!(dirty.len(), 1);
854 assert!(!compositor.is_dirty());
855 }
856
857 #[test]
858 fn test_compositor_owner_at() {
859 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
860 let mut compositor = GridCompositor::new(template);
861
862 compositor
863 .register(ComputeBlock::new("left", GridArea::cell(0, 0)))
864 .unwrap();
865
866 assert_eq!(compositor.owner_at(0, 0).unwrap().name, "left");
867 assert!(compositor.owner_at(0, 1).is_none());
868 }
869
870 #[test]
875 fn test_gc001_fill_distributes_space() {
876 let hints = vec![
877 SizeHint::flexible(Size::new(10, 5)),
878 SizeHint::flexible(Size::new(10, 5)),
879 SizeHint::flexible(Size::new(10, 5)),
880 ];
881 let constraints = vec![
882 FlexConstraint::Fill(1),
883 FlexConstraint::Fill(1),
884 FlexConstraint::Fill(1),
885 ];
886
887 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(120, 24));
888
889 assert_eq!(rects.len(), 3);
890 assert_eq!(rects[0].width, 40);
891 assert_eq!(rects[1].width, 40);
892 assert_eq!(rects[2].width, 40);
893 }
894
895 #[test]
896 fn test_gc002_content_uses_size_hint() {
897 let hints = vec![SizeHint::new(
898 Size::new(10, 3),
899 Size::new(40, 8),
900 Some(Size::new(80, 16)),
901 )];
902 let constraints = vec![FlexConstraint::Content];
903
904 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(200, 50));
905
906 assert_eq!(rects[0].width, 40); }
908
909 #[test]
910 fn test_fill_with_weights() {
911 let hints = vec![
912 SizeHint::flexible(Size::new(0, 5)),
913 SizeHint::flexible(Size::new(0, 5)),
914 ];
915 let constraints = vec![FlexConstraint::Fill(2), FlexConstraint::Fill(1)];
916
917 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(90, 24));
918
919 assert_eq!(rects[0].width, 60); assert_eq!(rects[1].width, 30); }
922
923 #[test]
924 fn test_mixed_constraints() {
925 let hints = vec![
926 SizeHint::fixed(Size::new(20, 5)),
927 SizeHint::flexible(Size::new(10, 5)),
928 SizeHint::fixed(Size::new(20, 5)),
929 ];
930 let constraints = vec![
931 FlexConstraint::Fixed(20),
932 FlexConstraint::Fill(1),
933 FlexConstraint::Fixed(20),
934 ];
935
936 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(100, 24));
937
938 assert_eq!(rects[0].width, 20);
939 assert_eq!(rects[1].width, 60); assert_eq!(rects[2].width, 20);
941 }
942
943 #[test]
944 fn test_fill_respects_max() {
945 let hints = vec![SizeHint::new(
946 Size::new(10, 5),
947 Size::new(30, 5),
948 Some(Size::new(50, 5)),
949 )];
950 let constraints = vec![FlexConstraint::Fill(1)];
951
952 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(200, 24));
953
954 assert_eq!(rects[0].width, 50); }
956
957 #[test]
962 fn test_compositor_error_display() {
963 let err = CompositorError::CellConflict {
964 cell: (1, 2),
965 existing: "first".to_string(),
966 new: "second".to_string(),
967 };
968 let msg = format!("{}", err);
969 assert!(msg.contains("first"));
970 assert!(msg.contains("second"));
971 }
972}