Skip to main content

presentar_terminal/direct/
direct_canvas.rs

1//! Direct terminal canvas implementing the Canvas trait.
2//!
3//! This writes directly to a `CellBuffer`,
4//! which is then rendered via the `DiffRenderer`.
5
6use super::cell_buffer::{CellBuffer, Modifiers};
7use crate::color::ColorMode;
8use presentar_core::{Canvas, Color, Point, Rect, TextStyle, Transform2D};
9use unicode_segmentation::UnicodeSegmentation;
10use unicode_width::UnicodeWidthStr;
11
12/// Direct terminal canvas that implements presentar's Canvas trait.
13///
14/// This canvas writes directly to a `CellBuffer`,
15/// enabling zero-allocation steady-state rendering.
16pub struct DirectTerminalCanvas<'a> {
17    /// The cell buffer to write to.
18    buffer: &'a mut CellBuffer,
19    /// Clip region stack.
20    clip_stack: Vec<ClipRect>,
21    /// Transform stack.
22    transform_stack: Vec<Transform2D>,
23    /// Current accumulated transform.
24    current_transform: Transform2D,
25    /// Color mode for palette mapping.
26    color_mode: ColorMode,
27}
28
29impl std::fmt::Debug for DirectTerminalCanvas<'_> {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        f.debug_struct("DirectTerminalCanvas")
32            .field("clip_stack", &self.clip_stack.len())
33            .field("transform_stack", &self.transform_stack.len())
34            .field("color_mode", &self.color_mode)
35            .finish_non_exhaustive()
36    }
37}
38
39/// Simple clip rectangle.
40#[derive(Clone, Copy, Debug)]
41struct ClipRect {
42    x: u16,
43    y: u16,
44    width: u16,
45    height: u16,
46}
47
48impl ClipRect {
49    const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
50        Self {
51            x,
52            y,
53            width,
54            height,
55        }
56    }
57
58    const fn contains(self, x: u16, y: u16) -> bool {
59        x >= self.x && x < self.x + self.width && y >= self.y && y < self.y + self.height
60    }
61
62    #[allow(clippy::cast_possible_wrap)]
63    fn intersect(self, other: Self) -> Option<Self> {
64        let x1 = self.x.max(other.x);
65        let y1 = self.y.max(other.y);
66        let x2 = (self.x + self.width).min(other.x + other.width);
67        let y2 = (self.y + self.height).min(other.y + other.height);
68
69        if x2 > x1 && y2 > y1 {
70            Some(Self::new(x1, y1, x2 - x1, y2 - y1))
71        } else {
72            None
73        }
74    }
75
76    const fn is_empty(self) -> bool {
77        self.width == 0 || self.height == 0
78    }
79}
80
81impl<'a> DirectTerminalCanvas<'a> {
82    /// Create a new direct canvas.
83    #[must_use]
84    pub fn new(buffer: &'a mut CellBuffer) -> Self {
85        let clip = ClipRect::new(0, 0, buffer.width(), buffer.height());
86        Self {
87            buffer,
88            clip_stack: vec![clip],
89            transform_stack: Vec::new(),
90            current_transform: Transform2D::IDENTITY,
91            color_mode: ColorMode::detect(),
92        }
93    }
94
95    /// Create a canvas with a specific color mode.
96    #[must_use]
97    pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
98        self.color_mode = mode;
99        self
100    }
101
102    /// Get the current color mode.
103    #[must_use]
104    pub const fn color_mode(&self) -> ColorMode {
105        self.color_mode
106    }
107
108    /// Get the buffer width.
109    #[must_use]
110    pub fn width(&self) -> u16 {
111        self.buffer.width()
112    }
113
114    /// Get the buffer height.
115    #[must_use]
116    pub fn height(&self) -> u16 {
117        self.buffer.height()
118    }
119
120    /// Get the current clip region.
121    fn clip(&self) -> ClipRect {
122        self.clip_stack
123            .last()
124            .copied()
125            .unwrap_or_else(|| ClipRect::new(0, 0, self.buffer.width(), self.buffer.height()))
126    }
127
128    /// Transform a point using the current transform.
129    fn transform_point(&self, p: Point) -> Point {
130        let m = &self.current_transform.matrix;
131        Point::new(
132            m[0] * p.x + m[2] * p.y + m[4],
133            m[1] * p.x + m[3] * p.y + m[5],
134        )
135    }
136
137    /// Convert a presentar Rect to terminal coordinates, applying transform and clipping.
138    fn to_terminal_rect(&self, rect: Rect) -> Option<ClipRect> {
139        let top_left = self.transform_point(Point::new(rect.x, rect.y));
140        let bottom_right =
141            self.transform_point(Point::new(rect.x + rect.width, rect.y + rect.height));
142
143        let x = top_left.x.round() as i32;
144        let y = top_left.y.round() as i32;
145        let w = (bottom_right.x - top_left.x).round() as i32;
146        let h = (bottom_right.y - top_left.y).round() as i32;
147
148        if x < 0 || y < 0 || w <= 0 || h <= 0 {
149            // Handle negative coordinates by adjusting
150            let x = x.max(0) as u16;
151            let y = y.max(0) as u16;
152            let w = w.max(0) as u16;
153            let h = h.max(0) as u16;
154
155            if w == 0 || h == 0 {
156                return None;
157            }
158
159            let rect = ClipRect::new(x, y, w, h);
160            self.clip().intersect(rect)
161        } else {
162            let rect = ClipRect::new(x as u16, y as u16, w as u16, h as u16);
163            self.clip().intersect(rect)
164        }
165    }
166
167    /// Set a cell with clipping.
168    fn set_cell(
169        &mut self,
170        x: u16,
171        y: u16,
172        symbol: &str,
173        fg: Color,
174        bg: Color,
175        modifiers: Modifiers,
176    ) {
177        let clip = self.clip();
178        if clip.contains(x, y) && x < self.buffer.width() && y < self.buffer.height() {
179            self.buffer.update(x, y, symbol, fg, bg, modifiers);
180
181            // Handle wide characters
182            let width = UnicodeWidthStr::width(symbol);
183            if width > 1 && x + 1 < self.buffer.width() {
184                if let Some(cell) = self.buffer.get_mut(x + 1, y) {
185                    cell.make_continuation();
186                }
187                self.buffer.mark_dirty(x + 1, y);
188            }
189        }
190    }
191
192    /// Convert text style to modifiers.
193    fn style_to_modifiers(style: &TextStyle) -> Modifiers {
194        let mut modifiers = Modifiers::NONE;
195        if matches!(style.weight, presentar_core::FontWeight::Bold) {
196            modifiers = modifiers.with(Modifiers::BOLD);
197        }
198        if matches!(style.style, presentar_core::FontStyle::Italic) {
199            modifiers = modifiers.with(Modifiers::ITALIC);
200        }
201        modifiers
202    }
203}
204
205impl Canvas for DirectTerminalCanvas<'_> {
206    fn fill_rect(&mut self, rect: Rect, color: Color) {
207        let Some(r) = self.to_terminal_rect(rect) else {
208            return;
209        };
210
211        // Skip if clipped to empty
212        if r.is_empty() {
213            return;
214        }
215
216        for y in r.y..r.y + r.height {
217            for x in r.x..r.x + r.width {
218                self.set_cell(x, y, " ", color, color, Modifiers::NONE);
219            }
220        }
221    }
222
223    fn stroke_rect(&mut self, rect: Rect, color: Color, _width: f32) {
224        let Some(r) = self.to_terminal_rect(rect) else {
225            return;
226        };
227
228        // Top and bottom edges
229        for x in r.x..r.x + r.width {
230            self.set_cell(x, r.y, "─", color, Color::TRANSPARENT, Modifiers::NONE);
231            if r.height > 1 {
232                self.set_cell(
233                    x,
234                    r.y + r.height - 1,
235                    "─",
236                    color,
237                    Color::TRANSPARENT,
238                    Modifiers::NONE,
239                );
240            }
241        }
242
243        // Left and right edges
244        for y in r.y..r.y + r.height {
245            self.set_cell(r.x, y, "│", color, Color::TRANSPARENT, Modifiers::NONE);
246            if r.width > 1 {
247                self.set_cell(
248                    r.x + r.width - 1,
249                    y,
250                    "│",
251                    color,
252                    Color::TRANSPARENT,
253                    Modifiers::NONE,
254                );
255            }
256        }
257
258        // Corners
259        self.set_cell(r.x, r.y, "┌", color, Color::TRANSPARENT, Modifiers::NONE);
260        if r.width > 1 {
261            self.set_cell(
262                r.x + r.width - 1,
263                r.y,
264                "┐",
265                color,
266                Color::TRANSPARENT,
267                Modifiers::NONE,
268            );
269        }
270        if r.height > 1 {
271            self.set_cell(
272                r.x,
273                r.y + r.height - 1,
274                "└",
275                color,
276                Color::TRANSPARENT,
277                Modifiers::NONE,
278            );
279            if r.width > 1 {
280                self.set_cell(
281                    r.x + r.width - 1,
282                    r.y + r.height - 1,
283                    "┘",
284                    color,
285                    Color::TRANSPARENT,
286                    Modifiers::NONE,
287                );
288            }
289        }
290    }
291
292    #[allow(clippy::cast_possible_wrap)]
293    fn draw_text(&mut self, text: &str, position: Point, style: &TextStyle) {
294        let p = self.transform_point(position);
295        let mut x = p.x.round() as i32;
296        let y = p.y.round() as i32;
297
298        if y < 0 {
299            return;
300        }
301        let y = y as u16;
302
303        let clip = self.clip();
304        if y < clip.y || y >= clip.y + clip.height {
305            return;
306        }
307
308        let modifiers = Self::style_to_modifiers(style);
309        let fg = style.color;
310
311        // Render grapheme by grapheme
312        for grapheme in text.graphemes(true) {
313            if x < 0 {
314                x += UnicodeWidthStr::width(grapheme) as i32;
315                continue;
316            }
317
318            let xu = x as u16;
319            if xu >= clip.x + clip.width {
320                break;
321            }
322
323            if xu >= clip.x {
324                // CRITICAL: Preserve existing background when drawing text.
325                // Text rendering should NOT overwrite background colors set by fill_rect.
326                // Without this, backgrounds would be lost during text rendering.
327                let existing_bg = self
328                    .buffer
329                    .get(xu, y)
330                    .map(|c| c.bg)
331                    .unwrap_or(Color::TRANSPARENT);
332                self.set_cell(xu, y, grapheme, fg, existing_bg, modifiers);
333            }
334
335            x += UnicodeWidthStr::width(grapheme) as i32;
336        }
337    }
338
339    fn draw_line(&mut self, from: Point, to: Point, color: Color, _width: f32) {
340        let from = self.transform_point(from);
341        let to = self.transform_point(to);
342
343        // Bresenham's line algorithm
344        let x0 = from.x.round() as i32;
345        let y0 = from.y.round() as i32;
346        let x1 = to.x.round() as i32;
347        let y1 = to.y.round() as i32;
348
349        let dx = (x1 - x0).abs();
350        let dy = -(y1 - y0).abs();
351        let sx = if x0 < x1 { 1 } else { -1 };
352        let sy = if y0 < y1 { 1 } else { -1 };
353        let mut err = dx + dy;
354
355        let mut x = x0;
356        let mut y = y0;
357
358        loop {
359            if x >= 0 && y >= 0 {
360                let ch = if dx > (-dy) * 2 {
361                    "─"
362                } else if (-dy) > dx * 2 {
363                    "│"
364                } else if (sx > 0) == (sy > 0) {
365                    "╲"
366                } else {
367                    "╱"
368                };
369                self.set_cell(
370                    x as u16,
371                    y as u16,
372                    ch,
373                    color,
374                    Color::TRANSPARENT,
375                    Modifiers::NONE,
376                );
377            }
378
379            if x == x1 && y == y1 {
380                break;
381            }
382
383            let e2 = 2 * err;
384            if e2 >= dy {
385                if x == x1 {
386                    break;
387                }
388                err += dy;
389                x += sx;
390            }
391            if e2 <= dx {
392                if y == y1 {
393                    break;
394                }
395                err += dx;
396                y += sy;
397            }
398        }
399    }
400
401    fn fill_circle(&mut self, center: Point, radius: f32, color: Color) {
402        let c = self.transform_point(center);
403        let r = radius.round() as i32;
404        let cx = c.x.round() as i32;
405        let cy = c.y.round() as i32;
406
407        // Midpoint circle algorithm with fill
408        for y in (cy - r)..=(cy + r) {
409            let dy = (y - cy).abs();
410            let dx = ((r * r - dy * dy) as f32).sqrt() as i32;
411            for x in (cx - dx)..=(cx + dx) {
412                if x >= 0 && y >= 0 {
413                    self.set_cell(x as u16, y as u16, " ", color, color, Modifiers::NONE);
414                }
415            }
416        }
417    }
418
419    fn stroke_circle(&mut self, center: Point, radius: f32, color: Color, _width: f32) {
420        let c = self.transform_point(center);
421        let r = radius.round() as i32;
422        let cx = c.x.round() as i32;
423        let cy = c.y.round() as i32;
424
425        // Midpoint circle algorithm
426        let mut x = r;
427        let mut y = 0;
428        let mut err = 0;
429
430        while x >= y {
431            let points = [
432                (cx + x, cy + y),
433                (cx + y, cy + x),
434                (cx - y, cy + x),
435                (cx - x, cy + y),
436                (cx - x, cy - y),
437                (cx - y, cy - x),
438                (cx + y, cy - x),
439                (cx + x, cy - y),
440            ];
441
442            for (px, py) in points {
443                if px >= 0 && py >= 0 {
444                    self.set_cell(
445                        px as u16,
446                        py as u16,
447                        "●",
448                        color,
449                        Color::TRANSPARENT,
450                        Modifiers::NONE,
451                    );
452                }
453            }
454
455            y += 1;
456            err += 1 + 2 * y;
457            if 2 * (err - x) + 1 > 0 {
458                x -= 1;
459                err += 1 - 2 * x;
460            }
461        }
462    }
463
464    fn fill_arc(
465        &mut self,
466        center: Point,
467        radius: f32,
468        start_angle: f32,
469        end_angle: f32,
470        color: Color,
471    ) {
472        let c = self.transform_point(center);
473        let cx = c.x.round() as i32;
474        let cy = c.y.round() as i32;
475
476        let steps = (radius * 4.0) as i32;
477        if steps <= 0 {
478            return;
479        }
480
481        let angle_step = (end_angle - start_angle) / steps as f32;
482
483        for i in 0..=steps {
484            let angle = start_angle + i as f32 * angle_step;
485            let x = cx + (radius * angle.cos()).round() as i32;
486            let y = cy + (radius * angle.sin()).round() as i32;
487            if x >= 0 && y >= 0 {
488                self.set_cell(x as u16, y as u16, " ", color, color, Modifiers::NONE);
489            }
490        }
491    }
492
493    fn draw_path(&mut self, points: &[Point], color: Color, width: f32) {
494        if points.len() < 2 {
495            return;
496        }
497
498        for window in points.windows(2) {
499            self.draw_line(window[0], window[1], color, width);
500        }
501    }
502
503    fn fill_polygon(&mut self, points: &[Point], color: Color) {
504        if points.len() < 3 {
505            return;
506        }
507
508        // Transform points
509        let transformed: Vec<Point> = points.iter().map(|p| self.transform_point(*p)).collect();
510
511        // Find bounding box
512        let min_y = transformed
513            .iter()
514            .map(|p| p.y.round() as i32)
515            .min()
516            .unwrap_or(0);
517        let max_y = transformed
518            .iter()
519            .map(|p| p.y.round() as i32)
520            .max()
521            .unwrap_or(0);
522
523        // Scanline fill
524        for y in min_y..=max_y {
525            let mut intersections: Vec<i32> = Vec::new();
526
527            for i in 0..transformed.len() {
528                let p1 = transformed[i];
529                let p2 = transformed[(i + 1) % transformed.len()];
530
531                let y1 = p1.y.round() as i32;
532                let y2 = p2.y.round() as i32;
533
534                if (y1 <= y && y < y2) || (y2 <= y && y < y1) {
535                    let t = (y as f32 - p1.y) / (p2.y - p1.y);
536                    let x = (p1.x + t * (p2.x - p1.x)).round() as i32;
537                    intersections.push(x);
538                }
539            }
540
541            intersections.sort_unstable();
542
543            for chunk in intersections.chunks(2) {
544                if chunk.len() == 2 {
545                    for x in chunk[0]..=chunk[1] {
546                        if x >= 0 && y >= 0 {
547                            self.set_cell(x as u16, y as u16, " ", color, color, Modifiers::NONE);
548                        }
549                    }
550                }
551            }
552        }
553    }
554
555    fn push_clip(&mut self, rect: Rect) {
556        if let Some(r) = self.to_terminal_rect(rect) {
557            if let Some(clipped) = self.clip().intersect(r) {
558                self.clip_stack.push(clipped);
559            } else {
560                // Empty clip
561                self.clip_stack.push(ClipRect::new(0, 0, 0, 0));
562            }
563        } else {
564            // Empty clip
565            self.clip_stack.push(ClipRect::new(0, 0, 0, 0));
566        }
567    }
568
569    fn pop_clip(&mut self) {
570        if self.clip_stack.len() > 1 {
571            self.clip_stack.pop();
572        }
573    }
574
575    fn push_transform(&mut self, transform: Transform2D) {
576        self.transform_stack.push(self.current_transform);
577
578        // Multiply transforms
579        let a = &self.current_transform.matrix;
580        let b = &transform.matrix;
581        self.current_transform = Transform2D {
582            matrix: [
583                a[0] * b[0] + a[2] * b[1],
584                a[1] * b[0] + a[3] * b[1],
585                a[0] * b[2] + a[2] * b[3],
586                a[1] * b[2] + a[3] * b[3],
587                a[0] * b[4] + a[2] * b[5] + a[4],
588                a[1] * b[4] + a[3] * b[5] + a[5],
589            ],
590        };
591    }
592
593    fn pop_transform(&mut self) {
594        if let Some(t) = self.transform_stack.pop() {
595            self.current_transform = t;
596        }
597    }
598}
599
600#[cfg(test)]
601#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
602mod tests {
603    use super::*;
604    use presentar_core::{FontStyle, FontWeight};
605
606    fn create_canvas(width: u16, height: u16) -> CellBuffer {
607        CellBuffer::new(width, height)
608    }
609
610    #[test]
611    fn test_canvas_creation() {
612        let mut buffer = create_canvas(80, 24);
613        let canvas = DirectTerminalCanvas::new(&mut buffer);
614        assert_eq!(canvas.width(), 80);
615        assert_eq!(canvas.height(), 24);
616    }
617
618    #[test]
619    fn test_canvas_with_color_mode() {
620        let mut buffer = create_canvas(80, 24);
621        let canvas = DirectTerminalCanvas::new(&mut buffer).with_color_mode(ColorMode::Color256);
622        assert_eq!(canvas.color_mode(), ColorMode::Color256);
623    }
624
625    #[test]
626    fn test_fill_rect() {
627        let mut buffer = create_canvas(20, 10);
628        {
629            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
630            canvas.fill_rect(Rect::new(1.0, 1.0, 3.0, 3.0), Color::RED);
631        }
632
633        let cell = buffer.get(2, 2).unwrap();
634        assert_eq!(cell.bg, Color::RED);
635    }
636
637    #[test]
638    fn test_fill_rect_outside_bounds() {
639        let mut buffer = create_canvas(10, 10);
640        {
641            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
642            canvas.fill_rect(Rect::new(100.0, 100.0, 3.0, 3.0), Color::RED);
643        }
644        // Should not panic
645    }
646
647    #[test]
648    fn test_stroke_rect() {
649        let mut buffer = create_canvas(20, 10);
650        {
651            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
652            canvas.stroke_rect(Rect::new(1.0, 1.0, 5.0, 5.0), Color::GREEN, 1.0);
653        }
654
655        assert_eq!(buffer.get(1, 1).unwrap().symbol.as_str(), "┌");
656        assert_eq!(buffer.get(5, 1).unwrap().symbol.as_str(), "┐");
657        assert_eq!(buffer.get(1, 5).unwrap().symbol.as_str(), "└");
658        assert_eq!(buffer.get(5, 5).unwrap().symbol.as_str(), "┘");
659    }
660
661    #[test]
662    fn test_stroke_rect_single_cell() {
663        let mut buffer = create_canvas(10, 10);
664        {
665            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
666            canvas.stroke_rect(Rect::new(1.0, 1.0, 1.0, 1.0), Color::GREEN, 1.0);
667        }
668    }
669
670    #[test]
671    fn test_draw_text() {
672        let mut buffer = create_canvas(20, 5);
673        {
674            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
675            canvas.draw_text("Hello", Point::new(0.0, 0.0), &TextStyle::default());
676        }
677
678        assert_eq!(buffer.get(0, 0).unwrap().symbol.as_str(), "H");
679        assert_eq!(buffer.get(1, 0).unwrap().symbol.as_str(), "e");
680    }
681
682    #[test]
683    fn test_draw_text_bold_italic() {
684        let mut buffer = create_canvas(20, 5);
685        {
686            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
687            let style = TextStyle {
688                weight: FontWeight::Bold,
689                style: FontStyle::Italic,
690                ..Default::default()
691            };
692            canvas.draw_text("Hi", Point::new(0.0, 0.0), &style);
693        }
694
695        let cell = buffer.get(0, 0).unwrap();
696        assert!(cell.modifiers.contains(Modifiers::BOLD));
697        assert!(cell.modifiers.contains(Modifiers::ITALIC));
698    }
699
700    #[test]
701    fn test_draw_text_clipped_y() {
702        let mut buffer = create_canvas(20, 5);
703        {
704            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
705            canvas.draw_text("Hello", Point::new(0.0, 10.0), &TextStyle::default());
706        }
707        // Should not render
708    }
709
710    #[test]
711    fn test_draw_text_negative_y() {
712        let mut buffer = create_canvas(20, 5);
713        {
714            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
715            canvas.draw_text("Hello", Point::new(0.0, -5.0), &TextStyle::default());
716        }
717    }
718
719    #[test]
720    fn test_draw_text_partial_clip() {
721        let mut buffer = create_canvas(5, 5);
722        {
723            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
724            canvas.draw_text("Hello World", Point::new(0.0, 0.0), &TextStyle::default());
725        }
726        // Should clip at width
727        assert_eq!(buffer.get(4, 0).unwrap().symbol.as_str(), "o");
728    }
729
730    #[test]
731    fn test_draw_text_negative_x() {
732        let mut buffer = create_canvas(10, 5);
733        {
734            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
735            canvas.draw_text("Hello", Point::new(-2.0, 0.0), &TextStyle::default());
736        }
737        // First visible should be 'l'
738        assert_eq!(buffer.get(0, 0).unwrap().symbol.as_str(), "l");
739    }
740
741    #[test]
742    fn test_draw_line_horizontal() {
743        let mut buffer = create_canvas(20, 10);
744        {
745            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
746            canvas.draw_line(
747                Point::new(0.0, 5.0),
748                Point::new(10.0, 5.0),
749                Color::WHITE,
750                1.0,
751            );
752        }
753    }
754
755    #[test]
756    fn test_draw_line_vertical() {
757        let mut buffer = create_canvas(20, 20);
758        {
759            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
760            canvas.draw_line(
761                Point::new(5.0, 0.0),
762                Point::new(5.0, 10.0),
763                Color::WHITE,
764                1.0,
765            );
766        }
767    }
768
769    #[test]
770    fn test_draw_line_diagonal() {
771        let mut buffer = create_canvas(20, 20);
772        {
773            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
774            canvas.draw_line(
775                Point::new(0.0, 0.0),
776                Point::new(10.0, 10.0),
777                Color::WHITE,
778                1.0,
779            );
780        }
781    }
782
783    #[test]
784    fn test_draw_line_same_point() {
785        let mut buffer = create_canvas(20, 20);
786        {
787            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
788            canvas.draw_line(
789                Point::new(5.0, 5.0),
790                Point::new(5.0, 5.0),
791                Color::WHITE,
792                1.0,
793            );
794        }
795    }
796
797    #[test]
798    fn test_fill_circle() {
799        let mut buffer = create_canvas(20, 20);
800        {
801            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
802            canvas.fill_circle(Point::new(10.0, 10.0), 5.0, Color::BLUE);
803        }
804        // Center should be filled
805        assert_eq!(buffer.get(10, 10).unwrap().bg, Color::BLUE);
806    }
807
808    #[test]
809    fn test_stroke_circle() {
810        let mut buffer = create_canvas(20, 20);
811        {
812            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
813            canvas.stroke_circle(Point::new(10.0, 10.0), 5.0, Color::GREEN, 1.0);
814        }
815    }
816
817    #[test]
818    fn test_fill_arc() {
819        let mut buffer = create_canvas(20, 20);
820        {
821            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
822            canvas.fill_arc(
823                Point::new(10.0, 10.0),
824                5.0,
825                0.0,
826                std::f32::consts::PI,
827                Color::RED,
828            );
829        }
830    }
831
832    #[test]
833    fn test_fill_arc_zero_radius() {
834        let mut buffer = create_canvas(20, 20);
835        {
836            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
837            canvas.fill_arc(
838                Point::new(10.0, 10.0),
839                0.0,
840                0.0,
841                std::f32::consts::PI,
842                Color::RED,
843            );
844        }
845    }
846
847    #[test]
848    fn test_draw_path() {
849        let mut buffer = create_canvas(20, 20);
850        {
851            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
852            let points = [
853                Point::new(0.0, 0.0),
854                Point::new(5.0, 5.0),
855                Point::new(10.0, 0.0),
856            ];
857            canvas.draw_path(&points, Color::WHITE, 1.0);
858        }
859    }
860
861    #[test]
862    fn test_draw_path_empty() {
863        let mut buffer = create_canvas(20, 20);
864        {
865            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
866            canvas.draw_path(&[], Color::WHITE, 1.0);
867        }
868    }
869
870    #[test]
871    fn test_draw_path_single_point() {
872        let mut buffer = create_canvas(20, 20);
873        {
874            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
875            canvas.draw_path(&[Point::new(5.0, 5.0)], Color::WHITE, 1.0);
876        }
877    }
878
879    #[test]
880    fn test_fill_polygon() {
881        let mut buffer = create_canvas(20, 20);
882        {
883            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
884            let points = [
885                Point::new(5.0, 0.0),
886                Point::new(10.0, 10.0),
887                Point::new(0.0, 10.0),
888            ];
889            canvas.fill_polygon(&points, Color::BLUE);
890        }
891    }
892
893    #[test]
894    fn test_fill_polygon_insufficient_points() {
895        let mut buffer = create_canvas(20, 20);
896        {
897            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
898            canvas.fill_polygon(&[Point::new(5.0, 5.0)], Color::BLUE);
899            canvas.fill_polygon(&[Point::new(5.0, 5.0), Point::new(10.0, 10.0)], Color::BLUE);
900        }
901    }
902
903    #[test]
904    fn test_push_pop_clip() {
905        let mut buffer = create_canvas(20, 10);
906        {
907            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
908            canvas.push_clip(Rect::new(5.0, 5.0, 10.0, 5.0));
909            canvas.fill_rect(Rect::new(0.0, 0.0, 20.0, 10.0), Color::RED);
910            canvas.pop_clip();
911        }
912
913        // Outside clip should be unchanged
914        assert_eq!(buffer.get(0, 0).unwrap().bg, Color::TRANSPARENT);
915        // Inside clip should be filled
916        assert_eq!(buffer.get(7, 7).unwrap().bg, Color::RED);
917    }
918
919    #[test]
920    fn test_push_clip_empty() {
921        let mut buffer = create_canvas(20, 10);
922        {
923            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
924            canvas.push_clip(Rect::new(100.0, 100.0, 10.0, 10.0));
925            canvas.fill_rect(Rect::new(0.0, 0.0, 20.0, 10.0), Color::RED);
926            canvas.pop_clip();
927        }
928        // Nothing should be filled
929        assert_eq!(buffer.get(0, 0).unwrap().bg, Color::TRANSPARENT);
930    }
931
932    #[test]
933    fn test_pop_clip_at_root() {
934        let mut buffer = create_canvas(20, 10);
935        {
936            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
937            canvas.pop_clip();
938            canvas.pop_clip();
939        }
940    }
941
942    #[test]
943    fn test_push_pop_transform() {
944        let mut buffer = create_canvas(20, 10);
945        {
946            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
947            canvas.push_transform(Transform2D::translate(5.0, 5.0));
948            canvas.fill_rect(Rect::new(0.0, 0.0, 2.0, 2.0), Color::BLUE);
949            canvas.pop_transform();
950        }
951
952        assert_eq!(buffer.get(5, 5).unwrap().bg, Color::BLUE);
953        assert_eq!(buffer.get(0, 0).unwrap().bg, Color::TRANSPARENT);
954    }
955
956    #[test]
957    fn test_transform_stack() {
958        let mut buffer = create_canvas(20, 20);
959        {
960            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
961            canvas.push_transform(Transform2D::translate(5.0, 5.0));
962            canvas.push_transform(Transform2D::translate(2.0, 2.0));
963            canvas.fill_rect(Rect::new(0.0, 0.0, 2.0, 2.0), Color::GREEN);
964            canvas.pop_transform();
965            canvas.pop_transform();
966        }
967
968        assert_eq!(buffer.get(7, 7).unwrap().bg, Color::GREEN);
969    }
970
971    #[test]
972    fn test_pop_transform_empty() {
973        let mut buffer = create_canvas(20, 10);
974        {
975            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
976            canvas.pop_transform();
977        }
978    }
979
980    #[test]
981    fn test_wide_character() {
982        let mut buffer = create_canvas(20, 5);
983        {
984            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
985            canvas.draw_text("日本", Point::new(0.0, 0.0), &TextStyle::default());
986        }
987
988        assert_eq!(buffer.get(0, 0).unwrap().symbol.as_str(), "日");
989        assert!(buffer.get(1, 0).unwrap().is_continuation());
990        assert_eq!(buffer.get(2, 0).unwrap().symbol.as_str(), "本");
991    }
992
993    #[test]
994    fn test_clip_rect_methods() {
995        let r1 = ClipRect::new(0, 0, 10, 10);
996        let r2 = ClipRect::new(5, 5, 10, 10);
997
998        assert!(r1.contains(5, 5));
999        assert!(!r1.contains(10, 10));
1000
1001        let intersect = r1.intersect(r2).unwrap();
1002        assert_eq!(intersect.x, 5);
1003        assert_eq!(intersect.y, 5);
1004        assert_eq!(intersect.width, 5);
1005        assert_eq!(intersect.height, 5);
1006    }
1007
1008    #[test]
1009    fn test_clip_rect_no_intersect() {
1010        let r1 = ClipRect::new(0, 0, 5, 5);
1011        let r2 = ClipRect::new(10, 10, 5, 5);
1012
1013        assert!(r1.intersect(r2).is_none());
1014    }
1015
1016    #[test]
1017    fn test_clip_rect_empty() {
1018        let r = ClipRect::new(0, 0, 0, 0);
1019        assert!(r.is_empty());
1020    }
1021
1022    #[test]
1023    fn test_negative_rect() {
1024        let mut buffer = create_canvas(20, 10);
1025        {
1026            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1027            canvas.fill_rect(Rect::new(-5.0, -5.0, 10.0, 10.0), Color::RED);
1028        }
1029        // Should still fill visible portion
1030        assert_eq!(buffer.get(0, 0).unwrap().bg, Color::RED);
1031    }
1032
1033    #[test]
1034    fn test_to_terminal_rect_zero_size() {
1035        let mut buffer = create_canvas(20, 10);
1036        {
1037            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1038            canvas.fill_rect(Rect::new(5.0, 5.0, 0.0, 0.0), Color::RED);
1039        }
1040    }
1041
1042    // =========================================================================
1043    // REGRESSION TESTS
1044    // =========================================================================
1045
1046    /// Regression test for selection artifact bug.
1047    ///
1048    /// Bug: When a row was selected (blue background), then deselected,
1049    /// the blue background persisted because draw_text was overwriting
1050    /// the fill_rect background with TRANSPARENT.
1051    ///
1052    /// Fix: draw_text now preserves the existing background color instead
1053    /// of setting it to TRANSPARENT.
1054    #[test]
1055    fn test_draw_text_preserves_fill_rect_background() {
1056        let mut buffer = create_canvas(20, 5);
1057        {
1058            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1059
1060            // Step 1: Fill row with selection background (simulates selected row)
1061            canvas.fill_rect(Rect::new(0.0, 1.0, 20.0, 1.0), Color::BLUE);
1062
1063            // Step 2: Draw text on top (simulates rendering row content)
1064            canvas.draw_text("Process 1234", Point::new(0.0, 1.0), &TextStyle::default());
1065
1066            // CRITICAL: Text characters MUST preserve the BLUE background
1067            // This was the bug - draw_text was setting bg to TRANSPARENT
1068            let cell = buffer.get(0, 1).unwrap();
1069            assert_eq!(
1070                cell.bg,
1071                Color::BLUE,
1072                "Text draw MUST preserve fill_rect background, got {:?}",
1073                cell.bg
1074            );
1075        }
1076    }
1077
1078    /// Regression test: Selection change clears old selection properly.
1079    ///
1080    /// Simulates the full selection lifecycle:
1081    /// 1. Row 1 selected (highlighted bg)
1082    /// 2. Selection moves to row 2
1083    /// 3. Row 1 should now have dimmed bg (not highlighted!)
1084    #[test]
1085    fn test_selection_change_clears_old_background() {
1086        let dimmed_bg = Color::new(0.08, 0.08, 0.1, 1.0);
1087        let selection_bg = Color::new(0.15, 0.12, 0.22, 1.0); // ttop-style subtle selection
1088
1089        let mut buffer = create_canvas(20, 5);
1090
1091        // Frame 1: Row 1 selected
1092        {
1093            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1094            canvas.fill_rect(Rect::new(0.0, 1.0, 20.0, 1.0), selection_bg);
1095            canvas.draw_text("Selected Row", Point::new(0.0, 1.0), &TextStyle::default());
1096
1097            canvas.fill_rect(Rect::new(0.0, 2.0, 20.0, 1.0), dimmed_bg);
1098            canvas.draw_text("Normal Row", Point::new(0.0, 2.0), &TextStyle::default());
1099        }
1100
1101        // Verify frame 1
1102        assert_eq!(buffer.get(0, 1).unwrap().bg, selection_bg);
1103        assert_eq!(buffer.get(0, 2).unwrap().bg, dimmed_bg);
1104
1105        // Frame 2: Selection moves to row 2
1106        {
1107            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1108            // Row 1 now dimmed
1109            canvas.fill_rect(Rect::new(0.0, 1.0, 20.0, 1.0), dimmed_bg);
1110            canvas.draw_text("Normal Row", Point::new(0.0, 1.0), &TextStyle::default());
1111
1112            // Row 2 now selected
1113            canvas.fill_rect(Rect::new(0.0, 2.0, 20.0, 1.0), selection_bg);
1114            canvas.draw_text("Selected Row", Point::new(0.0, 2.0), &TextStyle::default());
1115        }
1116
1117        // CRITICAL: Row 1 must now have dimmed background, NOT selection_bg
1118        assert_eq!(
1119            buffer.get(0, 1).unwrap().bg,
1120            dimmed_bg,
1121            "Old selection must be cleared to dimmed_bg"
1122        );
1123        assert_eq!(
1124            buffer.get(0, 2).unwrap().bg,
1125            selection_bg,
1126            "New selection must have selection_bg"
1127        );
1128    }
1129
1130    /// Test that text color is set correctly while preserving background.
1131    #[test]
1132    fn test_draw_text_sets_foreground_color() {
1133        let mut buffer = create_canvas(20, 5);
1134        {
1135            let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1136            canvas.fill_rect(Rect::new(0.0, 0.0, 20.0, 1.0), Color::BLACK);
1137
1138            let style = TextStyle {
1139                color: Color::YELLOW,
1140                ..Default::default()
1141            };
1142            canvas.draw_text("Test", Point::new(0.0, 0.0), &style);
1143        }
1144
1145        let cell = buffer.get(0, 0).unwrap();
1146        assert_eq!(cell.fg, Color::YELLOW);
1147        assert_eq!(cell.bg, Color::BLACK);
1148    }
1149}