Skip to main content

saorsa_core/compositor/
layer.rs

1//! Layer and compositor types for resolving overlapping widgets.
2
3use crate::geometry::Rect;
4use crate::segment::Segment;
5
6/// A single widget layer in the compositor stack.
7///
8/// Contains the widget's rendered output (as lines of segments),
9/// its on-screen bounding box, and its z-index for stacking order.
10#[derive(Debug, Clone)]
11pub struct Layer {
12    /// The widget ID that owns this layer.
13    pub widget_id: u64,
14    /// Bounding box on screen.
15    pub region: Rect,
16    /// Stacking order (higher = on top).
17    pub z_index: i32,
18    /// Per-line styled segment output.
19    pub lines: Vec<Vec<Segment>>,
20}
21
22impl Layer {
23    /// Creates a new layer.
24    pub fn new(widget_id: u64, region: Rect, z_index: i32, lines: Vec<Vec<Segment>>) -> Self {
25        Self {
26            widget_id,
27            region,
28            z_index,
29            lines,
30        }
31    }
32
33    /// Returns true if the given row falls within this layer's region.
34    pub fn contains_row(&self, row: u16) -> bool {
35        row >= self.region.position.y && row < self.region.position.y + self.region.size.height
36    }
37
38    /// Returns the segments for the given screen row, if the row is within this layer's region.
39    ///
40    /// Maps the screen row to a local line index and returns the corresponding segments.
41    pub fn line_for_row(&self, row: u16) -> Option<&Vec<Segment>> {
42        if !self.contains_row(row) {
43            return None;
44        }
45        let local_idx = (row - self.region.position.y) as usize;
46        self.lines.get(local_idx)
47    }
48}
49
50/// A horizontal region on a single screen row, owned by a specific layer.
51///
52/// The compositor cuts each row into non-overlapping regions based on
53/// layer boundaries, then selects the topmost visible layer for each region.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct CompositorRegion {
56    /// Start column.
57    pub x: u16,
58    /// Width in columns.
59    pub width: u16,
60    /// Which layer owns this region (None = background).
61    pub source_layer_idx: Option<usize>,
62}
63
64impl CompositorRegion {
65    /// Creates a new compositor region.
66    pub fn new(x: u16, width: u16, source_layer_idx: Option<usize>) -> Self {
67        Self {
68            x,
69            width,
70            source_layer_idx,
71        }
72    }
73}
74
75/// Errors that can occur during compositing.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum CompositorError {
78    /// Layer validation error.
79    InvalidLayer(String),
80    /// Compositor output doesn't fit in the buffer.
81    BufferTooSmall,
82}
83
84impl std::fmt::Display for CompositorError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            CompositorError::InvalidLayer(msg) => write!(f, "Invalid layer: {}", msg),
88            CompositorError::BufferTooSmall => write!(f, "Compositor buffer too small"),
89        }
90    }
91}
92
93impl std::error::Error for CompositorError {}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn layer_construction() {
101        let region = Rect::new(10, 20, 30, 40);
102        let lines = vec![vec![Segment::new("hello")], vec![Segment::new("world")]];
103        let layer = Layer::new(123, region, 5, lines.clone());
104
105        assert!(layer.widget_id == 123);
106        assert!(layer.region == region);
107        assert!(layer.z_index == 5);
108        assert!(layer.lines.len() == 2);
109    }
110
111    #[test]
112    fn layer_empty_lines() {
113        let region = Rect::new(0, 0, 10, 5);
114        let layer = Layer::new(1, region, 0, vec![]);
115
116        assert!(layer.lines.is_empty());
117    }
118
119    #[test]
120    fn layer_contains_row() {
121        let region = Rect::new(0, 10, 20, 5); // y=10, height=5 -> rows 10..15
122        let layer = Layer::new(1, region, 0, vec![]);
123
124        assert!(layer.contains_row(10)); // start
125        assert!(layer.contains_row(14)); // end-1
126        assert!(!layer.contains_row(9)); // before
127        assert!(!layer.contains_row(15)); // after
128    }
129
130    #[test]
131    fn layer_line_for_row() {
132        let region = Rect::new(0, 10, 20, 3);
133        let lines = vec![
134            vec![Segment::new("line0")],
135            vec![Segment::new("line1")],
136            vec![Segment::new("line2")],
137        ];
138        let layer = Layer::new(1, region, 0, lines);
139
140        let result0 = layer.line_for_row(10);
141        assert!(result0.is_some());
142        let segs0 = match result0 {
143            Some(s) => s,
144            None => unreachable!(),
145        };
146        assert!(segs0.len() == 1);
147        assert!(segs0[0].text == "line0");
148
149        let result1 = layer.line_for_row(11);
150        assert!(result1.is_some());
151        let segs1 = match result1 {
152            Some(s) => s,
153            None => unreachable!(),
154        };
155        assert!(segs1[0].text == "line1");
156
157        let result2 = layer.line_for_row(12);
158        assert!(result2.is_some());
159        let segs2 = match result2 {
160            Some(s) => s,
161            None => unreachable!(),
162        };
163        assert!(segs2[0].text == "line2");
164    }
165
166    #[test]
167    fn layer_line_for_row_outside() {
168        let region = Rect::new(0, 10, 20, 2);
169        let lines = vec![vec![Segment::new("a")], vec![Segment::new("b")]];
170        let layer = Layer::new(1, region, 0, lines);
171
172        assert!(layer.line_for_row(9).is_none()); // before
173        assert!(layer.line_for_row(12).is_none()); // after
174    }
175
176    #[test]
177    fn region_construction() {
178        let region = CompositorRegion::new(5, 10, Some(3));
179
180        assert!(region.x == 5);
181        assert!(region.width == 10);
182        assert!(region.source_layer_idx == Some(3));
183    }
184
185    #[test]
186    fn region_with_no_source() {
187        let region = CompositorRegion::new(0, 20, None);
188
189        assert!(region.x == 0);
190        assert!(region.width == 20);
191        assert!(region.source_layer_idx.is_none());
192    }
193
194    #[test]
195    fn compositor_error_display() {
196        let err1 = CompositorError::InvalidLayer("bad widget".to_string());
197        let display1 = format!("{}", err1);
198        assert!(display1.contains("Invalid layer"));
199        assert!(display1.contains("bad widget"));
200
201        let err2 = CompositorError::BufferTooSmall;
202        let display2 = format!("{}", err2);
203        assert!(display2.contains("Compositor buffer too small"));
204    }
205}