Skip to main content

presentar_layout/
compute_block.rs

1#![allow(clippy::cast_lossless)] // u16 to u32/f32 casts are intentional and always safe
2//! ComputeBlock Grid Compositor
3//!
4//! Solves two critical TUI layout issues:
5//! - **Issue A**: Automatic space utilization via intrinsic sizing
6//! - **Issue B**: Artifact prevention via cell ownership and clipping
7//!
8//! # Architecture
9//!
10//! ```text
11//! ┌────────────────────────────────────────────────────────────────┐
12//! │                      Frame Compositor                          │
13//! ├────────────────────────────────────────────────────────────────┤
14//! │  ┌──────────────┐    ┌──────────────┐    ┌──────────────────┐ │
15//! │  │ GridLayout   │───▶│ ComputeBlock │───▶│ ClippedRenderer  │ │
16//! │  │              │    │              │    │                  │ │
17//! │  │ - Define NxM │    │ - claim(r,c) │    │ - clip to bounds │ │
18//! │  │ - gutters    │    │ - bounds()   │    │ - z-order        │ │
19//! │  │ - flex sizes │    │ - clear()    │    │ - no overflow    │ │
20//! │  └──────────────┘    └──────────────┘    └──────────────────┘ │
21//! └────────────────────────────────────────────────────────────────┘
22//! ```
23
24use crate::grid::{compute_grid_layout, GridArea, GridTemplate};
25use serde::{Deserialize, Serialize};
26use std::fmt;
27
28// ============================================================================
29// INTRINSIC SIZING (Issue A)
30// ============================================================================
31
32/// Size in terminal cells (u16 for compatibility with ratatui).
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Size {
35    /// Width in terminal columns.
36    pub width: u16,
37    /// Height in terminal rows.
38    pub height: u16,
39}
40
41impl Size {
42    /// Create a new size.
43    #[must_use]
44    pub const fn new(width: u16, height: u16) -> Self {
45        Self { width, height }
46    }
47
48    /// Zero size.
49    pub const ZERO: Self = Self {
50        width: 0,
51        height: 0,
52    };
53}
54
55/// Rectangle in terminal coordinates.
56#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
57pub struct Rect {
58    /// X position (column).
59    pub x: u16,
60    /// Y position (row).
61    pub y: u16,
62    /// Width in columns.
63    pub width: u16,
64    /// Height in rows.
65    pub height: u16,
66}
67
68impl Rect {
69    /// Create a new rectangle.
70    #[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    /// Calculate the intersection of two rectangles.
81    #[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    /// Check if a point is within this rectangle.
101    #[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    /// Get the area of this rectangle.
107    #[must_use]
108    pub const fn area(&self) -> u32 {
109        self.width as u32 * self.height as u32
110    }
111}
112
113/// Size hints for content-aware layout.
114///
115/// Widgets report their sizing requirements through this struct,
116/// allowing the layout engine to make intelligent decisions about
117/// space allocation.
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub struct SizeHint {
120    /// Minimum size needed to render at all.
121    pub min: Size,
122    /// Preferred size for comfortable rendering.
123    pub preferred: Size,
124    /// Maximum useful size (content won't expand beyond).
125    pub max: Option<Size>,
126}
127
128impl SizeHint {
129    /// Create a new size hint.
130    #[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    /// Create a fixed-size hint (all sizes equal).
140    #[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    /// Create a flexible hint with only minimum.
150    #[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/// Extended constraint with Fill support.
161///
162/// This extends the standard constraint system with:
163/// - `Fill`: Distributes remaining space proportionally
164/// - `Content`: Uses widget's `SizeHint` for sizing
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166pub enum FlexConstraint {
167    /// Fixed size in terminal cells.
168    Fixed(u16),
169    /// Minimum size (can grow).
170    Min(u16),
171    /// Maximum size (can shrink).
172    Max(u16),
173    /// Percentage of parent (0-100).
174    Percentage(u16),
175    /// Ratio of remaining space (numerator, denominator).
176    Ratio(u16, u16),
177    /// Fill remaining space with weight.
178    ///
179    /// Multiple Fill constraints share remaining space
180    /// proportionally to their weights.
181    Fill(u16),
182    /// Content-based: use widget's SizeHint.
183    Content,
184}
185
186impl Default for FlexConstraint {
187    fn default() -> Self {
188        Self::Fill(1)
189    }
190}
191
192/// Trait for widgets with intrinsic sizing.
193pub trait IntrinsicSize {
194    /// Report size requirements given available space.
195    fn size_hint(&self, available: Size) -> SizeHint;
196}
197
198// ============================================================================
199// GRID COMPOSITOR (Issue B)
200// ============================================================================
201
202/// A named region in the grid with ownership semantics.
203///
204/// ComputeBlocks prevent rendering conflicts by:
205/// 1. Claiming exclusive ownership of grid cells
206/// 2. Enforcing clipping at render time
207/// 3. Supporting z-ordering for overlays
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
209pub struct ComputeBlock {
210    /// Unique name for this block.
211    pub name: String,
212    /// Grid area this block occupies.
213    pub area: GridArea,
214    /// Z-order for overlapping blocks (higher = on top).
215    pub z_index: i16,
216    /// Whether this block is visible.
217    pub visible: bool,
218    /// Clipping mode.
219    pub clip: ClipMode,
220}
221
222impl ComputeBlock {
223    /// Create a new compute block.
224    #[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    /// Set z-index.
236    #[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    /// Set visibility.
243    #[must_use]
244    pub const fn with_visible(mut self, visible: bool) -> Self {
245        self.visible = visible;
246        self
247    }
248
249    /// Set clip mode.
250    #[must_use]
251    pub const fn with_clip(mut self, clip: ClipMode) -> Self {
252        self.clip = clip;
253        self
254    }
255}
256
257/// Clipping behavior for blocks.
258#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
259pub enum ClipMode {
260    /// Render only within bounds (default, prevents artifacts).
261    #[default]
262    Strict,
263    /// Allow overflow (for tooltips, dropdowns).
264    Overflow,
265    /// Scroll if content exceeds bounds.
266    Scroll,
267}
268
269/// Grid compositor managing block ownership.
270///
271/// The compositor ensures:
272/// - No two blocks claim the same cell
273/// - Blocks are rendered in z-order
274/// - Dirty regions are tracked for efficient redraw
275#[derive(Debug, Clone)]
276pub struct GridCompositor {
277    /// Grid template definition.
278    template: GridTemplate,
279    /// Registered blocks.
280    blocks: Vec<ComputeBlock>,
281    /// Cell ownership map: (row, col) -> block index.
282    ownership: Vec<Vec<Option<usize>>>,
283    /// Dirty rectangles for incremental redraw.
284    dirty: Vec<Rect>,
285}
286
287impl GridCompositor {
288    /// Create a new compositor with the given template.
289    #[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    /// Get the grid template.
302    #[must_use]
303    pub fn template(&self) -> &GridTemplate {
304        &self.template
305    }
306
307    /// Register a block, claiming grid cells.
308    ///
309    /// Returns the block index on success, or an error if:
310    /// - The block area is out of grid bounds
311    /// - The block overlaps with an existing block
312    pub fn register(&mut self, block: ComputeBlock) -> Result<usize, CompositorError> {
313        // Validate area is within grid bounds
314        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        // Check for ownership conflicts
336        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        // Claim cells
349        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    /// Unregister a block by name, freeing its cells.
361    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        // Free cells
371        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        // Update indices in ownership map (shift down after removal)
378        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    /// Get a block by name.
390    #[must_use]
391    pub fn get(&self, name: &str) -> Option<&ComputeBlock> {
392        self.blocks.iter().find(|b| b.name == name)
393    }
394
395    /// Get a mutable block by name.
396    pub fn get_mut(&mut self, name: &str) -> Option<&mut ComputeBlock> {
397        self.blocks.iter_mut().find(|b| b.name == name)
398    }
399
400    /// Get computed bounds for a block.
401    #[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    /// Get all registered blocks.
420    #[must_use]
421    pub fn blocks(&self) -> &[ComputeBlock] {
422        &self.blocks
423    }
424
425    /// Mark a region as dirty (needs redraw).
426    pub fn mark_dirty(&mut self, rect: Rect) {
427        self.dirty.push(rect);
428    }
429
430    /// Clear dirty rectangles and return them.
431    pub fn take_dirty(&mut self) -> Vec<Rect> {
432        std::mem::take(&mut self.dirty)
433    }
434
435    /// Check if any regions are dirty.
436    #[must_use]
437    pub fn is_dirty(&self) -> bool {
438        !self.dirty.is_empty()
439    }
440
441    /// Get blocks sorted by z-index for rendering.
442    #[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    /// Get the block that owns a specific cell.
450    #[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/// Errors from compositor operations.
461#[derive(Debug, Clone, PartialEq, Eq)]
462pub enum CompositorError {
463    /// Block area extends beyond grid bounds.
464    OutOfBounds { block: String, reason: String },
465    /// Two blocks claim the same cell.
466    CellConflict {
467        cell: (usize, usize),
468        existing: String,
469        new: String,
470    },
471    /// Block not found by name.
472    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// ============================================================================
502// INTRINSIC LAYOUT COMPUTATION
503// ============================================================================
504
505/// Compute width allocation for a single constraint.
506/// Returns the allocated width (0 for Fill which is handled in phase 2).
507#[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, // Handle in phase 2
518    }
519}
520
521/// Distribute remaining space among Fill constraints.
522fn 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/// Compute layout respecting intrinsic sizes.
544///
545/// This implements a flexbox-like algorithm:
546/// 1. Allocate fixed and min sizes
547/// 2. Distribute remaining space to Fill constraints
548/// 3. Respect max sizes
549#[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    // Phase 1: Allocate fixed and min sizes
564    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    // Phase 2: Distribute Fill constraints
576    distribute_fill_space(&mut allocated, hints, constraints, remaining_width, count);
577
578    // Phase 3: Convert to Rects
579    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    // =========================================================================
596    // Size Tests
597    // =========================================================================
598
599    #[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    // =========================================================================
612    // Rect Tests
613    // =========================================================================
614
615    #[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    // =========================================================================
648    // SizeHint Tests
649    // =========================================================================
650
651    #[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    // =========================================================================
666    // FlexConstraint Tests
667    // =========================================================================
668
669    #[test]
670    fn test_flex_constraint_default() {
671        assert_eq!(FlexConstraint::default(), FlexConstraint::Fill(1));
672    }
673
674    // =========================================================================
675    // ComputeBlock Tests
676    // =========================================================================
677
678    #[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    // =========================================================================
699    // GridCompositor Tests
700    // =========================================================================
701
702    #[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        // Need a second row for the hidden block
791        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        // Can register same area again
820        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    // =========================================================================
854    // Intrinsic Layout Tests
855    // =========================================================================
856
857    #[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); // Uses preferred
890    }
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); // 2/3
903        assert_eq!(rects[1].width, 30); // 1/3
904    }
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); // Fills remaining
923        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); // Capped at max
938    }
939
940    // =========================================================================
941    // Error Display Tests
942    // =========================================================================
943
944    #[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}