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 layout respecting intrinsic sizes.
506///
507/// This implements a flexbox-like algorithm:
508/// 1. Allocate fixed and min sizes
509/// 2. Distribute remaining space to Fill constraints
510/// 3. Respect max sizes
511#[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    // Phase 1: Allocate fixed and min sizes
526    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                // Handle in phase 2
560            }
561        }
562    }
563
564    // Phase 2: Distribute Fill constraints
565    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                // Respect max size if specified
579                allocated[i].width = match hints[i].max {
580                    Some(max) => share.min(max.width),
581                    None => share,
582                };
583            }
584        }
585    }
586
587    // Phase 3: Convert to Rects
588    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    // =========================================================================
605    // Size Tests
606    // =========================================================================
607
608    #[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    // =========================================================================
621    // Rect Tests
622    // =========================================================================
623
624    #[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    // =========================================================================
657    // SizeHint Tests
658    // =========================================================================
659
660    #[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    // =========================================================================
675    // FlexConstraint Tests
676    // =========================================================================
677
678    #[test]
679    fn test_flex_constraint_default() {
680        assert_eq!(FlexConstraint::default(), FlexConstraint::Fill(1));
681    }
682
683    // =========================================================================
684    // ComputeBlock Tests
685    // =========================================================================
686
687    #[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    // =========================================================================
708    // GridCompositor Tests
709    // =========================================================================
710
711    #[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        // Need a second row for the hidden block
800        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        // Can register same area again
829        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    // =========================================================================
863    // Intrinsic Layout Tests
864    // =========================================================================
865
866    #[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); // Uses preferred
899    }
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); // 2/3
912        assert_eq!(rects[1].width, 30); // 1/3
913    }
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); // Fills remaining
932        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); // Capped at max
947    }
948
949    // =========================================================================
950    // Error Display Tests
951    // =========================================================================
952
953    #[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}