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
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#[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 #[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 #[must_use]
97 pub fn with_color_mode(mut self, mode: ColorMode) -> Self {
98 self.color_mode = mode;
99 self
100 }
101
102 #[must_use]
104 pub const fn color_mode(&self) -> ColorMode {
105 self.color_mode
106 }
107
108 #[must_use]
110 pub fn width(&self) -> u16 {
111 self.buffer.width()
112 }
113
114 #[must_use]
116 pub fn height(&self) -> u16 {
117 self.buffer.height()
118 }
119
120 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let transformed: Vec<Point> = points.iter().map(|p| self.transform_point(*p)).collect();
510
511 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 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 self.clip_stack.push(ClipRect::new(0, 0, 0, 0));
562 }
563 } else {
564 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 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 }
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 }
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 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 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 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 assert_eq!(buffer.get(0, 0).unwrap().bg, Color::TRANSPARENT);
915 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 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 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 #[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 canvas.fill_rect(Rect::new(0.0, 1.0, 20.0, 1.0), Color::BLUE);
1062
1063 canvas.draw_text("Process 1234", Point::new(0.0, 1.0), &TextStyle::default());
1065
1066 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 #[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); let mut buffer = create_canvas(20, 5);
1090
1091 {
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 assert_eq!(buffer.get(0, 1).unwrap().bg, selection_bg);
1103 assert_eq!(buffer.get(0, 2).unwrap().bg, dimmed_bg);
1104
1105 {
1107 let mut canvas = DirectTerminalCanvas::new(&mut buffer);
1108 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 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 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]
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}