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#[must_use]
512pub fn compute_intrinsic_layout(
513 hints: &[SizeHint],
514 constraints: &[FlexConstraint],
515 available: Size,
516) -> Vec<Rect> {
517 if hints.is_empty() || constraints.is_empty() {
518 return Vec::new();
519 }
520
521 let count = hints.len().min(constraints.len());
522 let mut allocated = vec![Size::ZERO; count];
523 let mut remaining_width = available.width;
524
525 for (i, (hint, constraint)) in hints.iter().zip(constraints).enumerate().take(count) {
527 match constraint {
528 FlexConstraint::Fixed(size) => {
529 allocated[i].width = *size;
530 remaining_width = remaining_width.saturating_sub(*size);
531 }
532 FlexConstraint::Min(size) => {
533 let width = (*size).max(hint.min.width);
534 allocated[i].width = width;
535 remaining_width = remaining_width.saturating_sub(width);
536 }
537 FlexConstraint::Max(size) => {
538 let width = (*size).min(hint.preferred.width);
539 allocated[i].width = width;
540 remaining_width = remaining_width.saturating_sub(width);
541 }
542 FlexConstraint::Percentage(pct) => {
543 let width = (available.width as u32 * *pct as u32 / 100) as u16;
544 allocated[i].width = width;
545 remaining_width = remaining_width.saturating_sub(width);
546 }
547 FlexConstraint::Ratio(num, den) => {
548 if *den > 0 {
549 let width = (available.width as u32 * *num as u32 / *den as u32) as u16;
550 allocated[i].width = width;
551 remaining_width = remaining_width.saturating_sub(width);
552 }
553 }
554 FlexConstraint::Content => {
555 allocated[i] = hint.preferred;
556 remaining_width = remaining_width.saturating_sub(hint.preferred.width);
557 }
558 FlexConstraint::Fill(_) => {
559 }
561 }
562 }
563
564 let fill_total: u16 = constraints
566 .iter()
567 .take(count)
568 .filter_map(|c| match c {
569 FlexConstraint::Fill(weight) => Some(*weight),
570 _ => None,
571 })
572 .sum();
573
574 if fill_total > 0 && remaining_width > 0 {
575 for (i, constraint) in constraints.iter().enumerate().take(count) {
576 if let FlexConstraint::Fill(weight) = constraint {
577 let share = (remaining_width as u32 * *weight as u32 / fill_total as u32) as u16;
578 allocated[i].width = match hints[i].max {
580 Some(max) => share.min(max.width),
581 None => share,
582 };
583 }
584 }
585 }
586
587 let mut x = 0u16;
589 allocated
590 .iter()
591 .map(|size| {
592 let rect = Rect::new(x, 0, size.width, available.height);
593 x = x.saturating_add(size.width);
594 rect
595 })
596 .collect()
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602 use crate::grid::TrackSize;
603
604 #[test]
609 fn test_size_new() {
610 let size = Size::new(80, 24);
611 assert_eq!(size.width, 80);
612 assert_eq!(size.height, 24);
613 }
614
615 #[test]
616 fn test_size_zero() {
617 assert_eq!(Size::ZERO, Size::new(0, 0));
618 }
619
620 #[test]
625 fn test_rect_intersection() {
626 let r1 = Rect::new(0, 0, 10, 10);
627 let r2 = Rect::new(5, 5, 10, 10);
628 let intersection = r1.intersection(r2);
629
630 assert_eq!(intersection.x, 5);
631 assert_eq!(intersection.y, 5);
632 assert_eq!(intersection.width, 5);
633 assert_eq!(intersection.height, 5);
634 }
635
636 #[test]
637 fn test_rect_no_intersection() {
638 let r1 = Rect::new(0, 0, 5, 5);
639 let r2 = Rect::new(10, 10, 5, 5);
640 let intersection = r1.intersection(r2);
641
642 assert_eq!(intersection.area(), 0);
643 }
644
645 #[test]
646 fn test_rect_contains() {
647 let rect = Rect::new(10, 10, 20, 20);
648
649 assert!(rect.contains(10, 10));
650 assert!(rect.contains(15, 15));
651 assert!(rect.contains(29, 29));
652 assert!(!rect.contains(30, 30));
653 assert!(!rect.contains(9, 10));
654 }
655
656 #[test]
661 fn test_size_hint_fixed() {
662 let hint = SizeHint::fixed(Size::new(40, 10));
663 assert_eq!(hint.min, hint.preferred);
664 assert_eq!(hint.preferred, hint.max.unwrap());
665 }
666
667 #[test]
668 fn test_size_hint_flexible() {
669 let hint = SizeHint::flexible(Size::new(10, 3));
670 assert_eq!(hint.min, Size::new(10, 3));
671 assert!(hint.max.is_none());
672 }
673
674 #[test]
679 fn test_flex_constraint_default() {
680 assert_eq!(FlexConstraint::default(), FlexConstraint::Fill(1));
681 }
682
683 #[test]
688 fn test_compute_block_new() {
689 let block = ComputeBlock::new("test", GridArea::cell(0, 0));
690 assert_eq!(block.name, "test");
691 assert_eq!(block.z_index, 0);
692 assert!(block.visible);
693 assert_eq!(block.clip, ClipMode::Strict);
694 }
695
696 #[test]
697 fn test_compute_block_builder() {
698 let block = ComputeBlock::new("overlay", GridArea::cell(1, 1))
699 .with_z_index(10)
700 .with_visible(true)
701 .with_clip(ClipMode::Overflow);
702
703 assert_eq!(block.z_index, 10);
704 assert_eq!(block.clip, ClipMode::Overflow);
705 }
706
707 #[test]
712 fn test_compositor_register() {
713 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)])
714 .with_rows([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
715 let mut compositor = GridCompositor::new(template);
716
717 let idx = compositor
718 .register(ComputeBlock::new("header", GridArea::row_span(0, 0, 2)))
719 .unwrap();
720 assert_eq!(idx, 0);
721
722 let idx = compositor
723 .register(ComputeBlock::new("main", GridArea::cell(1, 0)))
724 .unwrap();
725 assert_eq!(idx, 1);
726 }
727
728 #[test]
729 fn test_compositor_cell_conflict() {
730 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
731 let mut compositor = GridCompositor::new(template);
732
733 compositor
734 .register(ComputeBlock::new("first", GridArea::cell(0, 0)))
735 .unwrap();
736
737 let result = compositor.register(ComputeBlock::new("second", GridArea::cell(0, 0)));
738 assert!(matches!(result, Err(CompositorError::CellConflict { .. })));
739 }
740
741 #[test]
742 fn test_compositor_out_of_bounds() {
743 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
744 let mut compositor = GridCompositor::new(template);
745
746 let result = compositor.register(ComputeBlock::new("bad", GridArea::cell(0, 5)));
747 assert!(matches!(result, Err(CompositorError::OutOfBounds { .. })));
748 }
749
750 #[test]
751 fn test_compositor_bounds() {
752 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)])
753 .with_rows([TrackSize::Fr(1.0)]);
754 let mut compositor = GridCompositor::new(template);
755
756 compositor
757 .register(ComputeBlock::new("left", GridArea::cell(0, 0)))
758 .unwrap();
759 compositor
760 .register(ComputeBlock::new("right", GridArea::cell(0, 1)))
761 .unwrap();
762
763 let total = Rect::new(0, 0, 100, 50);
764 let left_bounds = compositor.bounds("left", total).unwrap();
765 let right_bounds = compositor.bounds("right", total).unwrap();
766
767 assert_eq!(left_bounds.x, 0);
768 assert_eq!(left_bounds.width, 50);
769 assert_eq!(right_bounds.x, 50);
770 assert_eq!(right_bounds.width, 50);
771 }
772
773 #[test]
774 fn test_compositor_render_order() {
775 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
776 let mut compositor = GridCompositor::new(template);
777
778 compositor
779 .register(ComputeBlock::new("back", GridArea::cell(0, 0)).with_z_index(0))
780 .unwrap();
781 compositor
782 .register(ComputeBlock::new("front", GridArea::cell(0, 1)).with_z_index(10))
783 .unwrap();
784
785 let order = compositor.render_order();
786 assert_eq!(order[0].name, "back");
787 assert_eq!(order[1].name, "front");
788 }
789
790 #[test]
791 fn test_compositor_hidden_blocks() {
792 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
793 let mut compositor = GridCompositor::new(template);
794
795 compositor
796 .register(ComputeBlock::new("visible", GridArea::cell(0, 0)))
797 .unwrap();
798
799 let template2 = GridTemplate::columns([TrackSize::Fr(1.0)])
801 .with_rows([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
802 let mut compositor2 = GridCompositor::new(template2);
803
804 compositor2
805 .register(ComputeBlock::new("visible", GridArea::cell(0, 0)))
806 .unwrap();
807 compositor2
808 .register(ComputeBlock::new("hidden", GridArea::cell(1, 0)).with_visible(false))
809 .unwrap();
810
811 let order = compositor2.render_order();
812 assert_eq!(order.len(), 1);
813 assert_eq!(order[0].name, "visible");
814 }
815
816 #[test]
817 fn test_compositor_unregister() {
818 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
819 let mut compositor = GridCompositor::new(template);
820
821 compositor
822 .register(ComputeBlock::new("block", GridArea::cell(0, 0)))
823 .unwrap();
824
825 let block = compositor.unregister("block").unwrap();
826 assert_eq!(block.name, "block");
827
828 compositor
830 .register(ComputeBlock::new("new", GridArea::cell(0, 0)))
831 .unwrap();
832 }
833
834 #[test]
835 fn test_compositor_dirty_tracking() {
836 let template = GridTemplate::columns([TrackSize::Fr(1.0)]);
837 let mut compositor = GridCompositor::new(template);
838
839 assert!(!compositor.is_dirty());
840
841 compositor.mark_dirty(Rect::new(0, 0, 10, 10));
842 assert!(compositor.is_dirty());
843
844 let dirty = compositor.take_dirty();
845 assert_eq!(dirty.len(), 1);
846 assert!(!compositor.is_dirty());
847 }
848
849 #[test]
850 fn test_compositor_owner_at() {
851 let template = GridTemplate::columns([TrackSize::Fr(1.0), TrackSize::Fr(1.0)]);
852 let mut compositor = GridCompositor::new(template);
853
854 compositor
855 .register(ComputeBlock::new("left", GridArea::cell(0, 0)))
856 .unwrap();
857
858 assert_eq!(compositor.owner_at(0, 0).unwrap().name, "left");
859 assert!(compositor.owner_at(0, 1).is_none());
860 }
861
862 #[test]
867 fn test_gc001_fill_distributes_space() {
868 let hints = vec![
869 SizeHint::flexible(Size::new(10, 5)),
870 SizeHint::flexible(Size::new(10, 5)),
871 SizeHint::flexible(Size::new(10, 5)),
872 ];
873 let constraints = vec![
874 FlexConstraint::Fill(1),
875 FlexConstraint::Fill(1),
876 FlexConstraint::Fill(1),
877 ];
878
879 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(120, 24));
880
881 assert_eq!(rects.len(), 3);
882 assert_eq!(rects[0].width, 40);
883 assert_eq!(rects[1].width, 40);
884 assert_eq!(rects[2].width, 40);
885 }
886
887 #[test]
888 fn test_gc002_content_uses_size_hint() {
889 let hints = vec![SizeHint::new(
890 Size::new(10, 3),
891 Size::new(40, 8),
892 Some(Size::new(80, 16)),
893 )];
894 let constraints = vec![FlexConstraint::Content];
895
896 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(200, 50));
897
898 assert_eq!(rects[0].width, 40); }
900
901 #[test]
902 fn test_fill_with_weights() {
903 let hints = vec![
904 SizeHint::flexible(Size::new(0, 5)),
905 SizeHint::flexible(Size::new(0, 5)),
906 ];
907 let constraints = vec![FlexConstraint::Fill(2), FlexConstraint::Fill(1)];
908
909 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(90, 24));
910
911 assert_eq!(rects[0].width, 60); assert_eq!(rects[1].width, 30); }
914
915 #[test]
916 fn test_mixed_constraints() {
917 let hints = vec![
918 SizeHint::fixed(Size::new(20, 5)),
919 SizeHint::flexible(Size::new(10, 5)),
920 SizeHint::fixed(Size::new(20, 5)),
921 ];
922 let constraints = vec![
923 FlexConstraint::Fixed(20),
924 FlexConstraint::Fill(1),
925 FlexConstraint::Fixed(20),
926 ];
927
928 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(100, 24));
929
930 assert_eq!(rects[0].width, 20);
931 assert_eq!(rects[1].width, 60); assert_eq!(rects[2].width, 20);
933 }
934
935 #[test]
936 fn test_fill_respects_max() {
937 let hints = vec![SizeHint::new(
938 Size::new(10, 5),
939 Size::new(30, 5),
940 Some(Size::new(50, 5)),
941 )];
942 let constraints = vec![FlexConstraint::Fill(1)];
943
944 let rects = compute_intrinsic_layout(&hints, &constraints, Size::new(200, 24));
945
946 assert_eq!(rects[0].width, 50); }
948
949 #[test]
954 fn test_compositor_error_display() {
955 let err = CompositorError::CellConflict {
956 cell: (1, 2),
957 existing: "first".to_string(),
958 new: "second".to_string(),
959 };
960 let msg = format!("{}", err);
961 assert!(msg.contains("first"));
962 assert!(msg.contains("second"));
963 }
964}