Skip to main content

saorsa_core/compositor/
mod.rs

1//! Compositor — resolves overlapping widget layers into a flat cell grid.
2//!
3//! The compositor collects styled segment output from each widget,
4//! finds cut boundaries where widget edges meet, selects the topmost
5//! visible widget for each region, and writes the result to a screen buffer.
6
7pub mod chop;
8pub mod compose;
9pub mod cuts;
10pub mod layer;
11pub mod zorder;
12
13pub use layer::{CompositorError, CompositorRegion, Layer};
14
15use crate::buffer::ScreenBuffer;
16use crate::cell::Cell;
17use crate::geometry::{Rect, Size};
18use crate::segment::Segment;
19use unicode_segmentation::UnicodeSegmentation;
20use unicode_width::UnicodeWidthStr;
21
22/// The compositor collects widget layers and resolves overlapping regions.
23pub struct Compositor {
24    layers: Vec<Layer>,
25    screen_width: u16,
26    screen_height: u16,
27}
28
29impl Compositor {
30    /// Creates a new compositor with the given screen dimensions.
31    pub fn new(width: u16, height: u16) -> Self {
32        Self {
33            layers: Vec::new(),
34            screen_width: width,
35            screen_height: height,
36        }
37    }
38
39    /// Removes all layers from the compositor.
40    pub fn clear(&mut self) {
41        self.layers.clear();
42    }
43
44    /// Adds a layer to the compositor stack.
45    pub fn add_layer(&mut self, layer: Layer) {
46        self.layers.push(layer);
47    }
48
49    /// Convenience method that creates and adds a layer.
50    ///
51    /// Creates a new layer from the given parameters and adds it to the stack.
52    pub fn add_widget(
53        &mut self,
54        widget_id: u64,
55        region: Rect,
56        z_index: i32,
57        lines: Vec<Vec<Segment>>,
58    ) {
59        let layer = Layer::new(widget_id, region, z_index, lines);
60        self.add_layer(layer);
61    }
62
63    /// Returns the number of layers in the compositor.
64    pub fn layer_count(&self) -> usize {
65        self.layers.len()
66    }
67
68    /// Returns the screen size.
69    pub fn screen_size(&self) -> Size {
70        Size::new(self.screen_width, self.screen_height)
71    }
72
73    /// Resizes the compositor screen dimensions.
74    ///
75    /// Clears all layers since they may no longer be valid for the new size.
76    pub fn resize(&mut self, width: u16, height: u16) {
77        self.screen_width = width;
78        self.screen_height = height;
79        self.layers.clear();
80    }
81
82    /// Returns a slice of all layers in the compositor.
83    pub fn layers(&self) -> &[Layer] {
84        &self.layers
85    }
86
87    /// Compose all layers and write the result to the screen buffer.
88    ///
89    /// Processes each row by calling `compose_line` to resolve overlapping
90    /// layers, then writes the resulting segments as cells to the buffer.
91    pub fn compose(&self, buf: &mut ScreenBuffer) {
92        for row in 0..self.screen_height {
93            let segments = compose::compose_line(&self.layers, row, self.screen_width);
94            self.write_segments_to_buffer(buf, row, &segments);
95        }
96    }
97
98    /// Write segments to a row of the screen buffer, converting each
99    /// segment's graphemes to cells with proper style and width handling.
100    fn write_segments_to_buffer(&self, buf: &mut ScreenBuffer, row: u16, segments: &[Segment]) {
101        let mut x = 0;
102
103        for segment in segments {
104            // Skip control segments (they don't render)
105            if segment.is_control {
106                continue;
107            }
108
109            // Process each grapheme in the segment
110            for grapheme in segment.text.graphemes(true) {
111                if x >= self.screen_width {
112                    return; // Reached end of screen width
113                }
114
115                let width = UnicodeWidthStr::width(grapheme);
116                let cell = Cell::new(grapheme, segment.style.clone());
117                buf.set(x, row, cell);
118                x += width as u16;
119            }
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::geometry::Rect;
128    use crate::segment::Segment;
129
130    #[test]
131    fn new_compositor_empty() {
132        let compositor = Compositor::new(80, 24);
133        assert!(compositor.layer_count() == 0);
134    }
135
136    #[test]
137    fn add_layer_increases_count() {
138        let mut compositor = Compositor::new(80, 24);
139        let region = Rect::new(0, 0, 10, 5);
140        let layer = Layer::new(1, region, 0, vec![]);
141
142        compositor.add_layer(layer);
143        assert!(compositor.layer_count() == 1);
144    }
145
146    #[test]
147    fn add_multiple_layers() {
148        let mut compositor = Compositor::new(80, 24);
149        let region1 = Rect::new(0, 0, 10, 5);
150        let region2 = Rect::new(10, 10, 20, 10);
151        let region3 = Rect::new(30, 5, 15, 8);
152
153        compositor.add_layer(Layer::new(1, region1, 0, vec![]));
154        compositor.add_layer(Layer::new(2, region2, 1, vec![]));
155        compositor.add_layer(Layer::new(3, region3, 2, vec![]));
156
157        assert!(compositor.layer_count() == 3);
158    }
159
160    #[test]
161    fn add_widget_convenience() {
162        let mut compositor = Compositor::new(80, 24);
163        let region = Rect::new(5, 10, 20, 15);
164        let lines = vec![vec![Segment::new("test")]];
165
166        compositor.add_widget(42, region, 5, lines);
167
168        assert!(compositor.layer_count() == 1);
169        let layer_slice = compositor.layers();
170        assert!(layer_slice.len() == 1);
171        let layer = match layer_slice.first() {
172            Some(l) => l,
173            None => unreachable!(),
174        };
175        assert!(layer.widget_id == 42);
176        assert!(layer.z_index == 5);
177        assert!(layer.region == region);
178    }
179
180    #[test]
181    fn clear_removes_all() {
182        let mut compositor = Compositor::new(80, 24);
183        let region1 = Rect::new(0, 0, 10, 5);
184        let region2 = Rect::new(10, 10, 20, 10);
185
186        compositor.add_layer(Layer::new(1, region1, 0, vec![]));
187        compositor.add_layer(Layer::new(2, region2, 1, vec![]));
188        assert!(compositor.layer_count() == 2);
189
190        compositor.clear();
191        assert!(compositor.layer_count() == 0);
192    }
193
194    #[test]
195    fn screen_size_accessible() {
196        let compositor = Compositor::new(100, 50);
197        let size = compositor.screen_size();
198        assert!(size.width == 100);
199        assert!(size.height == 50);
200    }
201
202    #[test]
203    fn layers_accessible() {
204        let mut compositor = Compositor::new(80, 24);
205        let region1 = Rect::new(0, 0, 10, 5);
206        let region2 = Rect::new(10, 10, 20, 10);
207
208        compositor.add_layer(Layer::new(1, region1, 0, vec![]));
209        compositor.add_layer(Layer::new(2, region2, 1, vec![]));
210
211        let layers = compositor.layers();
212        assert!(layers.len() == 2);
213        assert!(layers[0].widget_id == 1);
214        assert!(layers[1].widget_id == 2);
215    }
216
217    #[test]
218    fn compose_single_layer_to_buffer() {
219        use crate::geometry::Size;
220
221        let mut compositor = Compositor::new(80, 10);
222        let region = Rect::new(0, 0, 80, 10);
223        let lines = vec![vec![Segment::new("Hello, World!")]];
224        compositor.add_layer(Layer::new(1, region, 0, lines));
225
226        let mut buf = ScreenBuffer::new(Size::new(80, 10));
227        compositor.compose(&mut buf);
228
229        // Check that the text appears in the buffer
230        assert!(buf.get(0, 0).is_some());
231        match buf.get(0, 0) {
232            Some(cell) => {
233                assert!(cell.grapheme == "H");
234            }
235            None => unreachable!(),
236        }
237
238        // Check second character
239        match buf.get(1, 0) {
240            Some(cell) => {
241                assert!(cell.grapheme == "e");
242            }
243            None => unreachable!(),
244        }
245    }
246
247    #[test]
248    fn compose_overlapping_layers_to_buffer() {
249        use crate::geometry::Size;
250
251        let mut compositor = Compositor::new(80, 10);
252
253        // Background layer (z=0)
254        let bg_region = Rect::new(0, 0, 80, 10);
255        let bg_lines = vec![vec![Segment::new("Background")]];
256        compositor.add_layer(Layer::new(1, bg_region, 0, bg_lines));
257
258        // Overlay layer (z=10) at position (5, 0)
259        let overlay_region = Rect::new(5, 0, 20, 10);
260        let overlay_lines = vec![vec![Segment::new("Overlay")]];
261        compositor.add_layer(Layer::new(2, overlay_region, 10, overlay_lines));
262
263        let mut buf = ScreenBuffer::new(Size::new(80, 10));
264        compositor.compose(&mut buf);
265
266        // Position 0 should have 'B' from Background
267        match buf.get(0, 0) {
268            Some(cell) => {
269                assert!(cell.grapheme == "B");
270            }
271            None => unreachable!(),
272        }
273
274        // Position 5 should have 'O' from Overlay (topmost)
275        match buf.get(5, 0) {
276            Some(cell) => {
277                assert!(cell.grapheme == "O");
278            }
279            None => unreachable!(),
280        }
281    }
282
283    #[test]
284    fn compose_correct_cell_styles() {
285        use crate::color::{Color, NamedColor};
286        use crate::geometry::Size;
287        use crate::style::Style;
288
289        let mut compositor = Compositor::new(80, 10);
290        let style = Style {
291            fg: Some(Color::Named(NamedColor::Red)),
292            bold: true,
293            ..Default::default()
294        };
295
296        let mut seg = Segment::new("Styled");
297        seg.style = style.clone();
298
299        let region = Rect::new(0, 0, 20, 10);
300        let lines = vec![vec![seg]];
301        compositor.add_layer(Layer::new(1, region, 0, lines));
302
303        let mut buf = ScreenBuffer::new(Size::new(80, 10));
304        compositor.compose(&mut buf);
305
306        // Check that the style is preserved
307        match buf.get(0, 0) {
308            Some(cell) => {
309                assert!(cell.style.bold);
310                assert!(matches!(cell.style.fg, Some(Color::Named(NamedColor::Red))));
311            }
312            None => unreachable!(),
313        }
314    }
315
316    #[test]
317    fn compose_empty_compositor_all_blank() {
318        use crate::geometry::Size;
319
320        let compositor = Compositor::new(80, 10);
321        let mut buf = ScreenBuffer::new(Size::new(80, 10));
322
323        compositor.compose(&mut buf);
324
325        // All cells should be blank (space)
326        for y in 0..10 {
327            for x in 0..80 {
328                match buf.get(x, y) {
329                    Some(cell) => {
330                        assert!(cell.is_blank());
331                    }
332                    None => unreachable!(),
333                }
334            }
335        }
336    }
337
338    #[test]
339    fn compose_wide_characters() {
340        use crate::geometry::Size;
341
342        let mut compositor = Compositor::new(80, 10);
343        let region = Rect::new(0, 0, 20, 10);
344        // 世 is a CJK character with width 2
345        let lines = vec![vec![Segment::new("\u{4e16}界")]]; // 世界
346        compositor.add_layer(Layer::new(1, region, 0, lines));
347
348        let mut buf = ScreenBuffer::new(Size::new(80, 10));
349        compositor.compose(&mut buf);
350
351        // First cell should have the wide character
352        match buf.get(0, 0) {
353            Some(cell) => {
354                assert!(cell.grapheme == "\u{4e16}");
355                assert!(cell.width == 2);
356            }
357            None => unreachable!(),
358        }
359
360        // Second cell should be continuation (width 0)
361        match buf.get(1, 0) {
362            Some(cell) => {
363                assert!(cell.width == 0);
364            }
365            None => unreachable!(),
366        }
367
368        // Third cell should have the second character
369        match buf.get(2, 0) {
370            Some(cell) => {
371                assert!(cell.grapheme == "界");
372                assert!(cell.width == 2);
373            }
374            None => unreachable!(),
375        }
376    }
377}
378
379#[cfg(test)]
380mod integration_tests {
381    use super::*;
382    use crate::color::{Color, NamedColor};
383    use crate::geometry::{Rect, Size};
384    use crate::segment::Segment;
385    use crate::style::Style;
386
387    #[test]
388    fn integration_chat_layout() {
389        let mut compositor = Compositor::new(80, 24);
390
391        // Header at top (z=0)
392        let header_region = Rect::new(0, 0, 80, 1);
393        let header_lines = vec![vec![Segment::new("Chat App Header")]];
394        compositor.add_layer(Layer::new(1, header_region, 0, header_lines));
395
396        // Messages area (z=0)
397        let messages_region = Rect::new(0, 1, 80, 20);
398        let messages_lines = vec![vec![Segment::new("Message 1")]];
399        compositor.add_layer(Layer::new(2, messages_region, 0, messages_lines));
400
401        // Input bar at bottom (z=0)
402        let input_region = Rect::new(0, 21, 80, 3);
403        let input_lines = vec![vec![Segment::new("Type here...")]];
404        compositor.add_layer(Layer::new(3, input_region, 0, input_lines));
405
406        // Modal overlay (z=10) centered
407        let modal_region = Rect::new(20, 8, 40, 8);
408        let modal_lines = vec![vec![Segment::new("Modal Dialog")]];
409        compositor.add_layer(Layer::new(4, modal_region, 10, modal_lines));
410
411        let mut buf = ScreenBuffer::new(Size::new(80, 24));
412        compositor.compose(&mut buf);
413
414        // Header text should be visible at row 0
415        match buf.get(0, 0) {
416            Some(cell) => {
417                assert!(cell.grapheme == "C");
418            }
419            None => unreachable!(),
420        }
421
422        // Modal should overlay messages at row 8, column 20
423        match buf.get(20, 8) {
424            Some(cell) => {
425                assert!(cell.grapheme == "M");
426            }
427            None => unreachable!(),
428        }
429
430        // Input bar should be visible at row 21
431        match buf.get(0, 21) {
432            Some(cell) => {
433                assert!(cell.grapheme == "T");
434            }
435            None => unreachable!(),
436        }
437    }
438
439    #[test]
440    fn integration_three_overlapping_windows() {
441        let mut compositor = Compositor::new(80, 24);
442
443        // Bottom window (z=0)
444        let window1_region = Rect::new(0, 0, 40, 20);
445        let window1_lines = vec![vec![Segment::new("Window 1")]];
446        compositor.add_layer(Layer::new(1, window1_region, 0, window1_lines));
447
448        // Middle window (z=5)
449        let window2_region = Rect::new(20, 5, 40, 15);
450        let window2_lines = vec![vec![Segment::new("Window 2")]];
451        compositor.add_layer(Layer::new(2, window2_region, 5, window2_lines));
452
453        // Top window (z=10)
454        let window3_region = Rect::new(30, 10, 30, 10);
455        let window3_lines = vec![vec![Segment::new("Window 3")]];
456        compositor.add_layer(Layer::new(3, window3_region, 10, window3_lines));
457
458        let mut buf = ScreenBuffer::new(Size::new(80, 24));
459        compositor.compose(&mut buf);
460
461        // Position (0, 0) should have Window 1 (only window here)
462        match buf.get(0, 0) {
463            Some(cell) => {
464                assert!(cell.grapheme == "W");
465            }
466            None => unreachable!(),
467        }
468
469        // Position (20, 5) should have Window 2 (overlaps Window 1)
470        match buf.get(20, 5) {
471            Some(cell) => {
472                assert!(cell.grapheme == "W");
473            }
474            None => unreachable!(),
475        }
476
477        // Position (30, 10) should have Window 3 (highest z-index)
478        match buf.get(30, 10) {
479            Some(cell) => {
480                assert!(cell.grapheme == "W");
481            }
482            None => unreachable!(),
483        }
484    }
485
486    #[test]
487    fn integration_styled_segments_preserved() {
488        let mut compositor = Compositor::new(80, 24);
489
490        let red_style = Style {
491            fg: Some(Color::Named(NamedColor::Red)),
492            bold: true,
493            ..Default::default()
494        };
495
496        let blue_style = Style {
497            fg: Some(Color::Named(NamedColor::Blue)),
498            italic: true,
499            ..Default::default()
500        };
501
502        let mut red_seg = Segment::new("Red ");
503        red_seg.style = red_style.clone();
504
505        let mut blue_seg = Segment::new("Blue");
506        blue_seg.style = blue_style.clone();
507
508        let region = Rect::new(0, 0, 40, 10);
509        let lines = vec![vec![red_seg, blue_seg]];
510        compositor.add_layer(Layer::new(1, region, 0, lines));
511
512        let mut buf = ScreenBuffer::new(Size::new(80, 24));
513        compositor.compose(&mut buf);
514
515        // First character should have red style
516        match buf.get(0, 0) {
517            Some(cell) => {
518                assert!(cell.style.bold);
519                assert!(matches!(cell.style.fg, Some(Color::Named(NamedColor::Red))));
520            }
521            None => unreachable!(),
522        }
523
524        // Fifth character (index 4) should have blue style
525        match buf.get(4, 0) {
526            Some(cell) => {
527                assert!(cell.style.italic);
528                assert!(matches!(
529                    cell.style.fg,
530                    Some(Color::Named(NamedColor::Blue))
531                ));
532            }
533            None => unreachable!(),
534        }
535    }
536
537    #[test]
538    fn integration_resize_recompose() {
539        // Compose at 80x24
540        let mut compositor1 = Compositor::new(80, 24);
541        let region1 = Rect::new(0, 0, 40, 10);
542        let lines1 = vec![vec![Segment::new("Test")]];
543        compositor1.add_layer(Layer::new(1, region1, 0, lines1));
544
545        let mut buf1 = ScreenBuffer::new(Size::new(80, 24));
546        compositor1.compose(&mut buf1);
547
548        match buf1.get(0, 0) {
549            Some(cell) => {
550                assert!(cell.grapheme == "T");
551            }
552            None => unreachable!(),
553        }
554
555        // Now compose at 120x30
556        let mut compositor2 = Compositor::new(120, 30);
557        let region2 = Rect::new(0, 0, 60, 15);
558        let lines2 = vec![vec![Segment::new("Resized")]];
559        compositor2.add_layer(Layer::new(1, region2, 0, lines2));
560
561        let mut buf2 = ScreenBuffer::new(Size::new(120, 30));
562        compositor2.compose(&mut buf2);
563
564        match buf2.get(0, 0) {
565            Some(cell) => {
566                assert!(cell.grapheme == "R");
567            }
568            None => unreachable!(),
569        }
570
571        // Both buffers should work correctly
572        assert!(buf1.width() == 80);
573        assert!(buf2.width() == 120);
574    }
575}
576
577#[cfg(test)]
578mod advanced_integration_tests {
579    use super::*;
580    use crate::color::{Color, NamedColor};
581    use crate::geometry::{Rect, Size};
582    use crate::segment::Segment;
583    use crate::style::Style;
584
585    /// Task 7, Test 1: Syntax-highlighted code with multiple styled segments.
586    #[test]
587    fn syntax_highlighted_code() {
588        let mut compositor = Compositor::new(40, 5);
589
590        let keyword_style = Style::new().fg(Color::Named(NamedColor::Blue)).bold(true);
591        let ident_style = Style::new().fg(Color::Named(NamedColor::White));
592        let paren_style = Style::new().fg(Color::Named(NamedColor::Yellow));
593
594        let segments = vec![
595            Segment::styled("fn", keyword_style.clone()),
596            Segment::styled(" ", Style::default()),
597            Segment::styled("main", ident_style.clone()),
598            Segment::styled("()", paren_style.clone()),
599        ];
600
601        let region = Rect::new(0, 0, 40, 5);
602        compositor.add_layer(Layer::new(1, region, 0, vec![segments]));
603
604        let mut buf = ScreenBuffer::new(Size::new(40, 5));
605        compositor.compose(&mut buf);
606
607        // "fn" at positions 0-1 should have blue, bold
608        match buf.get(0, 0) {
609            Some(cell) => {
610                assert!(cell.grapheme == "f");
611                assert!(cell.style.bold);
612                assert!(matches!(
613                    cell.style.fg,
614                    Some(Color::Named(NamedColor::Blue))
615                ));
616            }
617            None => unreachable!(),
618        }
619        match buf.get(1, 0) {
620            Some(cell) => {
621                assert!(cell.grapheme == "n");
622                assert!(cell.style.bold);
623            }
624            None => unreachable!(),
625        }
626
627        // Space at position 2 should have default style
628        match buf.get(2, 0) {
629            Some(cell) => {
630                assert!(cell.grapheme == " ");
631            }
632            None => unreachable!(),
633        }
634
635        // "main" at positions 3-6 should have white fg
636        match buf.get(3, 0) {
637            Some(cell) => {
638                assert!(cell.grapheme == "m");
639                assert!(matches!(
640                    cell.style.fg,
641                    Some(Color::Named(NamedColor::White))
642                ));
643            }
644            None => unreachable!(),
645        }
646
647        // "()" at positions 7-8 should have yellow fg
648        match buf.get(7, 0) {
649            Some(cell) => {
650                assert!(cell.grapheme == "(");
651                assert!(matches!(
652                    cell.style.fg,
653                    Some(Color::Named(NamedColor::Yellow))
654                ));
655            }
656            None => unreachable!(),
657        }
658    }
659
660    /// Task 7, Test 2: Overlapping styled windows with different background colors.
661    #[test]
662    fn overlapping_styled_windows() {
663        let mut compositor = Compositor::new(20, 5);
664
665        // Bottom window (z=0) with green bg, fills region 0,0 -> 20,5
666        let green_bg = Style::new().bg(Color::Named(NamedColor::Green));
667        let green_line = vec![Segment::styled("GGGGGGGGGGGGGGGGGGG", green_bg.clone())];
668        let bottom_region = Rect::new(0, 0, 20, 5);
669        let bottom_lines = vec![green_line.clone(); 5];
670        compositor.add_layer(Layer::new(1, bottom_region, 0, bottom_lines));
671
672        // Top window (z=5) with blue bg, covers region 5,1 -> 10,3
673        let blue_bg = Style::new().bg(Color::Named(NamedColor::Blue));
674        let blue_line = vec![Segment::styled("BBBBB", blue_bg.clone())];
675        let top_region = Rect::new(5, 1, 10, 3);
676        let top_lines = vec![blue_line.clone(); 3];
677        compositor.add_layer(Layer::new(2, top_region, 5, top_lines));
678
679        let mut buf = ScreenBuffer::new(Size::new(20, 5));
680        compositor.compose(&mut buf);
681
682        // Row 0, col 5: only bottom window here -> green bg
683        match buf.get(5, 0) {
684            Some(cell) => {
685                assert!(matches!(
686                    cell.style.bg,
687                    Some(Color::Named(NamedColor::Green))
688                ));
689            }
690            None => unreachable!(),
691        }
692
693        // Row 1, col 5: top window starts here -> blue bg
694        match buf.get(5, 1) {
695            Some(cell) => {
696                assert!(matches!(
697                    cell.style.bg,
698                    Some(Color::Named(NamedColor::Blue))
699                ));
700            }
701            None => unreachable!(),
702        }
703
704        // Row 1, col 0: still bottom window region -> green bg
705        match buf.get(0, 1) {
706            Some(cell) => {
707                assert!(matches!(
708                    cell.style.bg,
709                    Some(Color::Named(NamedColor::Green))
710                ));
711            }
712            None => unreachable!(),
713        }
714    }
715
716    /// Task 7, Test 3: Full-width CJK text in compositor.
717    #[test]
718    fn cjk_text_in_compositor() {
719        let mut compositor = Compositor::new(20, 3);
720        let region = Rect::new(0, 0, 20, 3);
721        // "世界" = two CJK chars, each width 2, total width 4
722        let lines = vec![vec![Segment::new("\u{4e16}\u{754c}")]];
723        compositor.add_layer(Layer::new(1, region, 0, lines));
724
725        let mut buf = ScreenBuffer::new(Size::new(20, 3));
726        compositor.compose(&mut buf);
727
728        // First CJK char at column 0, width 2
729        match buf.get(0, 0) {
730            Some(cell) => {
731                assert!(cell.grapheme == "\u{4e16}");
732                assert!(cell.width == 2);
733            }
734            None => unreachable!(),
735        }
736
737        // Continuation cell at column 1
738        match buf.get(1, 0) {
739            Some(cell) => {
740                assert!(cell.width == 0);
741            }
742            None => unreachable!(),
743        }
744
745        // Second CJK char at column 2
746        match buf.get(2, 0) {
747            Some(cell) => {
748                assert!(cell.grapheme == "\u{754c}");
749                assert!(cell.width == 2);
750            }
751            None => unreachable!(),
752        }
753
754        // Continuation cell at column 3
755        match buf.get(3, 0) {
756            Some(cell) => {
757                assert!(cell.width == 0);
758            }
759            None => unreachable!(),
760        }
761    }
762
763    /// Task 7, Test 4: Multiple rows in a layer.
764    #[test]
765    fn multiple_rows_in_layer() {
766        let mut compositor = Compositor::new(40, 10);
767        let region = Rect::new(0, 0, 40, 10);
768        let lines = vec![
769            vec![Segment::new("Row Zero")],
770            vec![Segment::new("Row One")],
771            vec![Segment::new("Row Two")],
772        ];
773        compositor.add_layer(Layer::new(1, region, 0, lines));
774
775        let mut buf = ScreenBuffer::new(Size::new(40, 10));
776        compositor.compose(&mut buf);
777
778        // Row 0 starts with 'R' from "Row Zero"
779        match buf.get(0, 0) {
780            Some(cell) => {
781                assert!(cell.grapheme == "R");
782            }
783            None => unreachable!(),
784        }
785        match buf.get(4, 0) {
786            Some(cell) => {
787                assert!(cell.grapheme == "Z");
788            }
789            None => unreachable!(),
790        }
791
792        // Row 1 starts with 'R' from "Row One"
793        match buf.get(0, 1) {
794            Some(cell) => {
795                assert!(cell.grapheme == "R");
796            }
797            None => unreachable!(),
798        }
799        match buf.get(4, 1) {
800            Some(cell) => {
801                assert!(cell.grapheme == "O");
802            }
803            None => unreachable!(),
804        }
805
806        // Row 2 starts with 'R' from "Row Two"
807        match buf.get(0, 2) {
808            Some(cell) => {
809                assert!(cell.grapheme == "R");
810            }
811            None => unreachable!(),
812        }
813        match buf.get(4, 2) {
814            Some(cell) => {
815                assert!(cell.grapheme == "T");
816            }
817            None => unreachable!(),
818        }
819    }
820
821    /// Task 7, Test 5: Layer partially off-screen.
822    #[test]
823    fn layer_partially_off_screen() {
824        // Screen is 10x5
825        let mut compositor = Compositor::new(10, 5);
826
827        // Layer starts at x=7, width=10 — extends to x=17 which is beyond screen width 10
828        let region = Rect::new(7, 0, 10, 3);
829        let lines = vec![vec![Segment::new("ABCDEFGHIJ")]];
830        compositor.add_layer(Layer::new(1, region, 0, lines));
831
832        let mut buf = ScreenBuffer::new(Size::new(10, 5));
833        compositor.compose(&mut buf);
834
835        // Column 7 should have 'A'
836        match buf.get(7, 0) {
837            Some(cell) => {
838                assert!(cell.grapheme == "A");
839            }
840            None => unreachable!(),
841        }
842
843        // Column 9 (last column) should have 'C'
844        match buf.get(9, 0) {
845            Some(cell) => {
846                assert!(cell.grapheme == "C");
847            }
848            None => unreachable!(),
849        }
850
851        // No out-of-bounds access — buffer should be fine
852        assert!(buf.get(10, 0).is_none());
853    }
854
855    /// Task 7, Test 6: Zero-layer compositor produces all blank cells.
856    #[test]
857    fn zero_layer_compositor_all_blank() {
858        let compositor = Compositor::new(20, 10);
859        let mut buf = ScreenBuffer::new(Size::new(20, 10));
860        compositor.compose(&mut buf);
861
862        for y in 0..10 {
863            for x in 0..20 {
864                match buf.get(x, y) {
865                    Some(cell) => {
866                        assert!(cell.is_blank());
867                    }
868                    None => unreachable!(),
869                }
870            }
871        }
872    }
873
874    /// Task 7, Test 7: Background layer with overlay covering middle portion.
875    #[test]
876    fn styled_segments_split_by_overlay() {
877        let mut compositor = Compositor::new(20, 3);
878
879        // Background: "Hello World" at (0,0), z=0
880        let bg_region = Rect::new(0, 0, 20, 3);
881        let bg_lines = vec![vec![Segment::new("Hello World")]];
882        compositor.add_layer(Layer::new(1, bg_region, 0, bg_lines));
883
884        // Overlay: "XXXXX" at x=3, covering positions 3-7, z=10
885        let overlay_region = Rect::new(3, 0, 5, 3);
886        let overlay_lines = vec![vec![Segment::new("XXXXX")]];
887        compositor.add_layer(Layer::new(2, overlay_region, 10, overlay_lines));
888
889        let mut buf = ScreenBuffer::new(Size::new(20, 3));
890        compositor.compose(&mut buf);
891
892        // Positions 0-2: "Hel" from background
893        match buf.get(0, 0) {
894            Some(cell) => {
895                assert!(cell.grapheme == "H");
896            }
897            None => unreachable!(),
898        }
899        match buf.get(1, 0) {
900            Some(cell) => {
901                assert!(cell.grapheme == "e");
902            }
903            None => unreachable!(),
904        }
905        match buf.get(2, 0) {
906            Some(cell) => {
907                assert!(cell.grapheme == "l");
908            }
909            None => unreachable!(),
910        }
911
912        // Positions 3-7: "XXXXX" from overlay
913        match buf.get(3, 0) {
914            Some(cell) => {
915                assert!(cell.grapheme == "X");
916            }
917            None => unreachable!(),
918        }
919        match buf.get(7, 0) {
920            Some(cell) => {
921                assert!(cell.grapheme == "X");
922            }
923            None => unreachable!(),
924        }
925
926        // Position 8: "W" from background ("Hello World" offset by 8 = 'W')
927        // The background text "Hello World" has chars at positions:
928        // H(0) e(1) l(2) l(3) o(4) (5) W(6) o(7) r(8) l(9) d(10)
929        // But the overlay covers 3-7 of the layer, so background position 8
930        // should show "o" (position 8 in "Hello World" = 'r')
931        // Wait — "Hello World" is 11 chars, at layer origin x=0.
932        // Background position 8 maps to "Hello World"[8] = 'r'
933        match buf.get(8, 0) {
934            Some(cell) => {
935                assert!(cell.grapheme == "r");
936            }
937            None => unreachable!(),
938        }
939    }
940
941    /// Task 7, Test 8: Large number of layers — topmost always wins.
942    #[test]
943    fn many_layers_topmost_wins() {
944        let mut compositor = Compositor::new(20, 5);
945
946        // Create 25 layers, all covering the same region, with increasing z-index
947        // Each layer has its own single character
948        for i in 0u64..25 {
949            let ch = char::from(b'A' + (i as u8) % 26);
950            let region = Rect::new(0, 0, 20, 5);
951            let lines = vec![vec![Segment::new(ch.to_string())]];
952            compositor.add_layer(Layer::new(i + 1, region, i as i32, lines));
953        }
954
955        let mut buf = ScreenBuffer::new(Size::new(20, 5));
956        compositor.compose(&mut buf);
957
958        // The topmost layer (z=24) has character 'Y' (b'A' + 24)
959        match buf.get(0, 0) {
960            Some(cell) => {
961                assert!(cell.grapheme == "Y");
962            }
963            None => unreachable!(),
964        }
965    }
966}
967
968#[cfg(test)]
969mod unicode_pipeline_tests {
970    use super::*;
971    use crate::color::{Color, NamedColor};
972    use crate::geometry::{Rect, Size};
973    use crate::segment::Segment;
974    use crate::style::Style;
975
976    /// Test 1: CJK text layer produces correct primary and continuation cells.
977    #[test]
978    fn cjk_text_layer_correct_cells() {
979        let mut compositor = Compositor::new(20, 3);
980        let region = Rect::new(0, 0, 20, 3);
981        // Three CJK chars: 世界人 — each width 2, total width 6
982        let lines = vec![vec![Segment::new("\u{4e16}\u{754c}\u{4eba}")]];
983        compositor.add_layer(Layer::new(1, region, 0, lines));
984
985        let mut buf = ScreenBuffer::new(Size::new(20, 3));
986        compositor.compose(&mut buf);
987
988        // Column 0: primary "世" width 2
989        match buf.get(0, 0) {
990            Some(c) => {
991                assert_eq!(c.grapheme, "\u{4e16}");
992                assert_eq!(c.width, 2);
993            }
994            None => unreachable!(),
995        }
996        // Column 1: continuation
997        match buf.get(1, 0) {
998            Some(c) => assert_eq!(c.width, 0),
999            None => unreachable!(),
1000        }
1001        // Column 2: primary "界" width 2
1002        match buf.get(2, 0) {
1003            Some(c) => {
1004                assert_eq!(c.grapheme, "\u{754c}");
1005                assert_eq!(c.width, 2);
1006            }
1007            None => unreachable!(),
1008        }
1009        // Column 3: continuation
1010        match buf.get(3, 0) {
1011            Some(c) => assert_eq!(c.width, 0),
1012            None => unreachable!(),
1013        }
1014        // Column 4: primary "人" width 2
1015        match buf.get(4, 0) {
1016            Some(c) => {
1017                assert_eq!(c.grapheme, "\u{4eba}");
1018                assert_eq!(c.width, 2);
1019            }
1020            None => unreachable!(),
1021        }
1022        // Column 5: continuation
1023        match buf.get(5, 0) {
1024            Some(c) => assert_eq!(c.width, 0),
1025            None => unreachable!(),
1026        }
1027        // Column 6: blank
1028        match buf.get(6, 0) {
1029            Some(c) => assert!(c.is_blank()),
1030            None => unreachable!(),
1031        }
1032    }
1033
1034    /// Test 2: Emoji text layer produces correct cells.
1035    #[test]
1036    fn emoji_text_layer_correct_cells() {
1037        let mut compositor = Compositor::new(20, 3);
1038        let region = Rect::new(0, 0, 20, 3);
1039        // Two emoji: 😀🎉 — each width 2
1040        let lines = vec![vec![Segment::new("\u{1f600}\u{1f389}")]];
1041        compositor.add_layer(Layer::new(1, region, 0, lines));
1042
1043        let mut buf = ScreenBuffer::new(Size::new(20, 3));
1044        compositor.compose(&mut buf);
1045
1046        // Column 0: primary emoji, width 2
1047        match buf.get(0, 0) {
1048            Some(c) => {
1049                assert_eq!(c.grapheme, "\u{1f600}");
1050                assert_eq!(c.width, 2);
1051            }
1052            None => unreachable!(),
1053        }
1054        // Column 1: continuation
1055        match buf.get(1, 0) {
1056            Some(c) => assert_eq!(c.width, 0),
1057            None => unreachable!(),
1058        }
1059        // Column 2: second emoji, width 2
1060        match buf.get(2, 0) {
1061            Some(c) => {
1062                assert_eq!(c.grapheme, "\u{1f389}");
1063                assert_eq!(c.width, 2);
1064            }
1065            None => unreachable!(),
1066        }
1067        // Column 3: continuation
1068        match buf.get(3, 0) {
1069            Some(c) => assert_eq!(c.width, 0),
1070            None => unreachable!(),
1071        }
1072    }
1073
1074    /// Test 3: Mixed Latin + CJK + emoji in one layer — all widths correct.
1075    #[test]
1076    fn mixed_latin_cjk_emoji_widths() {
1077        let mut compositor = Compositor::new(20, 3);
1078        let region = Rect::new(0, 0, 20, 3);
1079        // "Hi" (2) + "世" (2) + "😀" (2) = total width 6
1080        let lines = vec![vec![Segment::new("Hi\u{4e16}\u{1f600}")]];
1081        compositor.add_layer(Layer::new(1, region, 0, lines));
1082
1083        let mut buf = ScreenBuffer::new(Size::new(20, 3));
1084        compositor.compose(&mut buf);
1085
1086        // Columns 0-1: "H", "i" (each width 1)
1087        match buf.get(0, 0) {
1088            Some(c) => {
1089                assert_eq!(c.grapheme, "H");
1090                assert_eq!(c.width, 1);
1091            }
1092            None => unreachable!(),
1093        }
1094        match buf.get(1, 0) {
1095            Some(c) => {
1096                assert_eq!(c.grapheme, "i");
1097                assert_eq!(c.width, 1);
1098            }
1099            None => unreachable!(),
1100        }
1101        // Columns 2-3: CJK "世" (width 2) + continuation
1102        match buf.get(2, 0) {
1103            Some(c) => {
1104                assert_eq!(c.grapheme, "\u{4e16}");
1105                assert_eq!(c.width, 2);
1106            }
1107            None => unreachable!(),
1108        }
1109        match buf.get(3, 0) {
1110            Some(c) => assert_eq!(c.width, 0),
1111            None => unreachable!(),
1112        }
1113        // Columns 4-5: emoji "😀" (width 2) + continuation
1114        match buf.get(4, 0) {
1115            Some(c) => {
1116                assert_eq!(c.grapheme, "\u{1f600}");
1117                assert_eq!(c.width, 2);
1118            }
1119            None => unreachable!(),
1120        }
1121        match buf.get(5, 0) {
1122            Some(c) => assert_eq!(c.width, 0),
1123            None => unreachable!(),
1124        }
1125    }
1126
1127    /// Test 4: Wide char at screen right edge — clipped correctly (no crash).
1128    #[test]
1129    fn wide_char_at_screen_right_edge_clipped() {
1130        // Screen width 5
1131        let mut compositor = Compositor::new(5, 1);
1132        let region = Rect::new(0, 0, 5, 1);
1133        // "ABCD世" = width 6: the CJK char starts at column 4 but needs column 5 too
1134        // The compositor writes 'A'(0), 'B'(1), 'C'(2), 'D'(3), then tries "世" at col 4
1135        // Since "世" is width 2 and the screen width is 5, column 5 is out of bounds
1136        // The buffer's set() will handle this by replacing with blank
1137        let lines = vec![vec![Segment::new("ABCD\u{4e16}")]];
1138        compositor.add_layer(Layer::new(1, region, 0, lines));
1139
1140        let mut buf = ScreenBuffer::new(Size::new(5, 1));
1141        compositor.compose(&mut buf);
1142
1143        // Columns 0-3 should have A, B, C, D
1144        match buf.get(0, 0) {
1145            Some(c) => assert_eq!(c.grapheme, "A"),
1146            None => unreachable!(),
1147        }
1148        match buf.get(3, 0) {
1149            Some(c) => assert_eq!(c.grapheme, "D"),
1150            None => unreachable!(),
1151        }
1152        // Column 4: the wide char can't fit, should be blank (buffer protection)
1153        match buf.get(4, 0) {
1154            Some(c) => {
1155                // Buffer set() replaces wide chars at last column with blank
1156                assert!(c.is_blank());
1157            }
1158            None => unreachable!(),
1159        }
1160        // No crash, no out-of-bounds
1161        assert!(buf.get(5, 0).is_none());
1162    }
1163
1164    /// Test 5: Combining marks in a layer — preserved in buffer cells.
1165    #[test]
1166    fn combining_marks_preserved_in_buffer() {
1167        let mut compositor = Compositor::new(20, 3);
1168        let region = Rect::new(0, 0, 20, 3);
1169        // "e\u{0301}" = e with combining acute accent = single grapheme, width 1
1170        let lines = vec![vec![Segment::new("e\u{0301}X")]];
1171        compositor.add_layer(Layer::new(1, region, 0, lines));
1172
1173        let mut buf = ScreenBuffer::new(Size::new(20, 3));
1174        compositor.compose(&mut buf);
1175
1176        // Column 0: the composed grapheme "e\u{0301}" should be there
1177        match buf.get(0, 0) {
1178            Some(c) => {
1179                assert_eq!(c.grapheme, "e\u{0301}");
1180                assert_eq!(c.width, 1);
1181            }
1182            None => unreachable!(),
1183        }
1184        // Column 1: "X"
1185        match buf.get(1, 0) {
1186            Some(c) => assert_eq!(c.grapheme, "X"),
1187            None => unreachable!(),
1188        }
1189    }
1190
1191    /// Test 6: Styled wide chars — style preserved in buffer.
1192    #[test]
1193    fn styled_wide_chars_preserved() {
1194        let mut compositor = Compositor::new(20, 3);
1195        let region = Rect::new(0, 0, 20, 3);
1196        let style = Style::new().fg(Color::Named(NamedColor::Red)).bold(true);
1197        // CJK text with style
1198        let lines = vec![vec![Segment::styled("\u{4e16}\u{754c}", style.clone())]];
1199        compositor.add_layer(Layer::new(1, region, 0, lines));
1200
1201        let mut buf = ScreenBuffer::new(Size::new(20, 3));
1202        compositor.compose(&mut buf);
1203
1204        // Column 0: "世" with red+bold style
1205        match buf.get(0, 0) {
1206            Some(c) => {
1207                assert_eq!(c.grapheme, "\u{4e16}");
1208                assert!(c.style.bold);
1209                assert!(matches!(c.style.fg, Some(Color::Named(NamedColor::Red))));
1210            }
1211            None => unreachable!(),
1212        }
1213        // Column 2: "界" with same style
1214        match buf.get(2, 0) {
1215            Some(c) => {
1216                assert_eq!(c.grapheme, "\u{754c}");
1217                assert!(c.style.bold);
1218                assert!(matches!(c.style.fg, Some(Color::Named(NamedColor::Red))));
1219            }
1220            None => unreachable!(),
1221        }
1222    }
1223
1224    /// Test 7: Overlapping layers with different Unicode scripts — topmost wins.
1225    #[test]
1226    fn overlapping_unicode_scripts_topmost_wins() {
1227        let mut compositor = Compositor::new(20, 3);
1228
1229        // Bottom layer (z=0): CJK text
1230        let bottom_region = Rect::new(0, 0, 20, 3);
1231        let bottom_lines = vec![vec![Segment::new("\u{4e16}\u{754c}\u{4eba}\u{6c11}")]];
1232        compositor.add_layer(Layer::new(1, bottom_region, 0, bottom_lines));
1233
1234        // Top layer (z=10): Latin text at same position, overlapping first 4 columns
1235        let top_region = Rect::new(0, 0, 4, 3);
1236        let top_lines = vec![vec![Segment::new("ABCD")]];
1237        compositor.add_layer(Layer::new(2, top_region, 10, top_lines));
1238
1239        let mut buf = ScreenBuffer::new(Size::new(20, 3));
1240        compositor.compose(&mut buf);
1241
1242        // Columns 0-3: should have "ABCD" from top layer
1243        match buf.get(0, 0) {
1244            Some(c) => assert_eq!(c.grapheme, "A"),
1245            None => unreachable!(),
1246        }
1247        match buf.get(1, 0) {
1248            Some(c) => assert_eq!(c.grapheme, "B"),
1249            None => unreachable!(),
1250        }
1251        match buf.get(2, 0) {
1252            Some(c) => assert_eq!(c.grapheme, "C"),
1253            None => unreachable!(),
1254        }
1255        match buf.get(3, 0) {
1256            Some(c) => assert_eq!(c.grapheme, "D"),
1257            None => unreachable!(),
1258        }
1259
1260        // Column 4 onwards: should have CJK from bottom layer
1261        // CJK "世界人民": 世(0-1), 界(2-3), 人(4-5), 民(6-7)
1262        // Since top layer covers 0-3, bottom layer columns 4-5 should show 人
1263        match buf.get(4, 0) {
1264            Some(c) => {
1265                assert_eq!(c.grapheme, "\u{4eba}");
1266                assert_eq!(c.width, 2);
1267            }
1268            None => unreachable!(),
1269        }
1270    }
1271
1272    /// Test 8: Multiple rows of CJK text — correct row-by-row rendering.
1273    #[test]
1274    fn multiple_rows_cjk_text() {
1275        let mut compositor = Compositor::new(20, 5);
1276        let region = Rect::new(0, 0, 20, 5);
1277        let lines = vec![
1278            vec![Segment::new("\u{4e16}\u{754c}")], // 世界 (row 0)
1279            vec![Segment::new("\u{4eba}\u{6c11}")], // 人民 (row 1)
1280            vec![Segment::new("\u{5927}\u{5b66}")], // 大学 (row 2)
1281        ];
1282        compositor.add_layer(Layer::new(1, region, 0, lines));
1283
1284        let mut buf = ScreenBuffer::new(Size::new(20, 5));
1285        compositor.compose(&mut buf);
1286
1287        // Row 0: "世" at col 0, "界" at col 2
1288        match buf.get(0, 0) {
1289            Some(c) => {
1290                assert_eq!(c.grapheme, "\u{4e16}");
1291                assert_eq!(c.width, 2);
1292            }
1293            None => unreachable!(),
1294        }
1295        match buf.get(2, 0) {
1296            Some(c) => {
1297                assert_eq!(c.grapheme, "\u{754c}");
1298                assert_eq!(c.width, 2);
1299            }
1300            None => unreachable!(),
1301        }
1302
1303        // Row 1: "人" at col 0, "民" at col 2
1304        match buf.get(0, 1) {
1305            Some(c) => {
1306                assert_eq!(c.grapheme, "\u{4eba}");
1307                assert_eq!(c.width, 2);
1308            }
1309            None => unreachable!(),
1310        }
1311        match buf.get(2, 1) {
1312            Some(c) => {
1313                assert_eq!(c.grapheme, "\u{6c11}");
1314                assert_eq!(c.width, 2);
1315            }
1316            None => unreachable!(),
1317        }
1318
1319        // Row 2: "大" at col 0, "学" at col 2
1320        match buf.get(0, 2) {
1321            Some(c) => {
1322                assert_eq!(c.grapheme, "\u{5927}");
1323                assert_eq!(c.width, 2);
1324            }
1325            None => unreachable!(),
1326        }
1327        match buf.get(2, 2) {
1328            Some(c) => {
1329                assert_eq!(c.grapheme, "\u{5b66}");
1330                assert_eq!(c.width, 2);
1331            }
1332            None => unreachable!(),
1333        }
1334
1335        // Row 3: should be all blank (no content)
1336        match buf.get(0, 3) {
1337            Some(c) => assert!(c.is_blank()),
1338            None => unreachable!(),
1339        }
1340    }
1341}