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