Skip to main content

ftui_render/
drawing.rs

1#![forbid(unsafe_code)]
2
3//! Drawing primitives for the buffer.
4//!
5//! Provides ergonomic, well-tested helpers on top of `Buffer::set()` so
6//! widgets can draw borders, lines, text, and filled regions without
7//! duplicating low-level cell loops.
8//!
9//! All operations respect the buffer's scissor stack (clipping) and
10//! opacity stack automatically via `Buffer::set()`.
11
12use crate::buffer::Buffer;
13use crate::cell::{Cell, CellContent};
14use crate::grapheme_width;
15use ftui_core::geometry::Rect;
16
17/// Characters used to draw a border around a rectangle.
18///
19/// This is a render-level type that holds raw characters.
20/// Higher-level crates (e.g. ftui-widgets) provide presets.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct BorderChars {
23    /// Top-left corner character.
24    pub top_left: char,
25    /// Top-right corner character.
26    pub top_right: char,
27    /// Bottom-left corner character.
28    pub bottom_left: char,
29    /// Bottom-right corner character.
30    pub bottom_right: char,
31    /// Horizontal line character.
32    pub horizontal: char,
33    /// Vertical line character.
34    pub vertical: char,
35}
36
37impl BorderChars {
38    /// Simple box-drawing characters (U+250x).
39    pub const SQUARE: Self = Self {
40        top_left: '┌',
41        top_right: '┐',
42        bottom_left: '└',
43        bottom_right: '┘',
44        horizontal: '─',
45        vertical: '│',
46    };
47
48    /// Rounded corners.
49    pub const ROUNDED: Self = Self {
50        top_left: '╭',
51        top_right: '╮',
52        bottom_left: '╰',
53        bottom_right: '╯',
54        horizontal: '─',
55        vertical: '│',
56    };
57
58    /// Double-line border.
59    pub const DOUBLE: Self = Self {
60        top_left: '╔',
61        top_right: '╗',
62        bottom_left: '╚',
63        bottom_right: '╝',
64        horizontal: '═',
65        vertical: '║',
66    };
67
68    /// Heavy (thick) border.
69    pub const HEAVY: Self = Self {
70        top_left: '┏',
71        top_right: '┓',
72        bottom_left: '┗',
73        bottom_right: '┛',
74        horizontal: '━',
75        vertical: '┃',
76    };
77
78    /// ASCII-only border.
79    pub const ASCII: Self = Self {
80        top_left: '+',
81        top_right: '+',
82        bottom_left: '+',
83        bottom_right: '+',
84        horizontal: '-',
85        vertical: '|',
86    };
87}
88
89/// Extension trait for drawing on a Buffer.
90pub trait Draw {
91    /// Draw a horizontal line of cells.
92    fn draw_horizontal_line(&mut self, x: u16, y: u16, width: u16, cell: Cell);
93
94    /// Draw a vertical line of cells.
95    fn draw_vertical_line(&mut self, x: u16, y: u16, height: u16, cell: Cell);
96
97    /// Draw a filled rectangle.
98    fn draw_rect_filled(&mut self, rect: Rect, cell: Cell);
99
100    /// Draw a rectangle outline using a single cell character.
101    fn draw_rect_outline(&mut self, rect: Rect, cell: Cell);
102
103    /// Print text at the given coordinates using the cell's colors/attrs.
104    ///
105    /// Characters replace the cell content; fg/bg/attrs come from `base_cell`.
106    /// Stops at the buffer edge. Returns the x position after the last character.
107    fn print_text(&mut self, x: u16, y: u16, text: &str, base_cell: Cell) -> u16;
108
109    /// Print text with a right-side clipping boundary.
110    ///
111    /// Like `print_text` but stops at `max_x` (exclusive) instead of the
112    /// buffer edge. Returns the x position after the last character.
113    fn print_text_clipped(
114        &mut self,
115        x: u16,
116        y: u16,
117        text: &str,
118        base_cell: Cell,
119        max_x: u16,
120    ) -> u16;
121
122    /// Draw a border around a rectangle using the given characters.
123    ///
124    /// The border is drawn inside the rectangle (edges + corners).
125    /// The cell's fg/bg/attrs are applied to all border characters.
126    fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell);
127
128    /// Draw a border and fill the interior.
129    ///
130    /// Draws a border using `border_chars` and fills the interior with
131    /// `fill_cell`. If the rect is too small for an interior (width or
132    /// height <= 2), only the border is drawn.
133    fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell);
134
135    /// Set all cells in a rectangular area to the given fg/bg/attrs without
136    /// changing cell content.
137    ///
138    /// Useful for painting backgrounds or selection highlights.
139    fn paint_area(
140        &mut self,
141        rect: Rect,
142        fg: Option<crate::cell::PackedRgba>,
143        bg: Option<crate::cell::PackedRgba>,
144    );
145}
146
147impl Draw for Buffer {
148    fn draw_horizontal_line(&mut self, x: u16, y: u16, width: u16, cell: Cell) {
149        for i in 0..width {
150            self.set_fast(x.saturating_add(i), y, cell);
151        }
152    }
153
154    fn draw_vertical_line(&mut self, x: u16, y: u16, height: u16, cell: Cell) {
155        for i in 0..height {
156            self.set_fast(x, y.saturating_add(i), cell);
157        }
158    }
159
160    fn draw_rect_filled(&mut self, rect: Rect, cell: Cell) {
161        self.fill(rect, cell);
162    }
163
164    fn draw_rect_outline(&mut self, rect: Rect, cell: Cell) {
165        if rect.is_empty() {
166            return;
167        }
168
169        // Top
170        self.draw_horizontal_line(rect.x, rect.y, rect.width, cell);
171
172        // Bottom
173        if rect.height > 1 {
174            self.draw_horizontal_line(rect.x, rect.bottom().saturating_sub(1), rect.width, cell);
175        }
176
177        // Left (excluding corners)
178        if rect.height > 2 {
179            self.draw_vertical_line(rect.x, rect.y.saturating_add(1), rect.height - 2, cell);
180        }
181
182        // Right (excluding corners)
183        if rect.width > 1 && rect.height > 2 {
184            self.draw_vertical_line(
185                rect.right().saturating_sub(1),
186                rect.y.saturating_add(1),
187                rect.height - 2,
188                cell,
189            );
190        }
191    }
192
193    fn print_text(&mut self, x: u16, y: u16, text: &str, base_cell: Cell) -> u16 {
194        self.print_text_clipped(x, y, text, base_cell, self.width())
195    }
196
197    fn print_text_clipped(
198        &mut self,
199        x: u16,
200        y: u16,
201        text: &str,
202        base_cell: Cell,
203        max_x: u16,
204    ) -> u16 {
205        use unicode_segmentation::UnicodeSegmentation;
206
207        let mut cx = x;
208        for grapheme in text.graphemes(true) {
209            if cx >= max_x {
210                break;
211            }
212
213            let Some(first) = grapheme.chars().next() else {
214                continue;
215            };
216
217            // Buffer has no GraphemePool, so multi-codepoint graphemes must fall back to a
218            // single char. We still preserve the grapheme's display width to keep column
219            // alignment deterministic, but we *must* also fill the extra cells so we don't
220            // leave "holes" that can retain stale content (borders, old text, etc.).
221            let rendered_content = CellContent::from_char(first);
222            let rendered_width = rendered_content.width();
223            let mut width = grapheme_width(grapheme);
224            if width == 0 {
225                width = rendered_width;
226            }
227            width = width.max(rendered_width);
228            if width == 0 {
229                continue;
230            }
231
232            // Don't start a wide char if it won't fit
233            if cx as u32 + width as u32 > max_x as u32 {
234                break;
235            }
236
237            let cell = Cell {
238                content: rendered_content,
239                fg: base_cell.fg,
240                bg: base_cell.bg,
241                attrs: base_cell.attrs,
242            };
243            self.set_fast(cx, y, cell);
244
245            // If we preserved extra display width (e.g., VS16 emoji sequences like "⚙️"),
246            // explicitly clear the trailing cells with spaces in the same style.
247            if rendered_width < width {
248                let filler = Cell {
249                    content: CellContent::from_char(' '),
250                    fg: base_cell.fg,
251                    bg: base_cell.bg,
252                    attrs: base_cell.attrs,
253                };
254                for offset in rendered_width..width {
255                    self.set_fast(cx.saturating_add(offset as u16), y, filler);
256                }
257            }
258
259            cx = cx.saturating_add(width as u16);
260        }
261        cx
262    }
263
264    fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell) {
265        if rect.is_empty() {
266            return;
267        }
268
269        let make_cell = |c: char| -> Cell {
270            Cell {
271                content: CellContent::from_char(c),
272                fg: base_cell.fg,
273                bg: base_cell.bg,
274                attrs: base_cell.attrs,
275            }
276        };
277
278        let h_cell = make_cell(chars.horizontal);
279        let v_cell = make_cell(chars.vertical);
280
281        // Top edge
282        for x in rect.left()..rect.right() {
283            self.set_fast(x, rect.top(), h_cell);
284        }
285
286        // Bottom edge
287        if rect.height > 1 {
288            for x in rect.left()..rect.right() {
289                self.set_fast(x, rect.bottom().saturating_sub(1), h_cell);
290            }
291        }
292
293        // Left edge (excluding corners)
294        if rect.height > 2 {
295            for y in (rect.top().saturating_add(1))..(rect.bottom().saturating_sub(1)) {
296                self.set_fast(rect.left(), y, v_cell);
297            }
298        }
299
300        // Right edge (excluding corners)
301        if rect.width > 1 && rect.height > 2 {
302            for y in (rect.top().saturating_add(1))..(rect.bottom().saturating_sub(1)) {
303                self.set_fast(rect.right().saturating_sub(1), y, v_cell);
304            }
305        }
306
307        // Corners (drawn last to overwrite edge chars at corners)
308        self.set_fast(rect.left(), rect.top(), make_cell(chars.top_left));
309
310        if rect.width > 1 {
311            self.set_fast(
312                rect.right().saturating_sub(1),
313                rect.top(),
314                make_cell(chars.top_right),
315            );
316        }
317
318        if rect.height > 1 {
319            self.set_fast(
320                rect.left(),
321                rect.bottom().saturating_sub(1),
322                make_cell(chars.bottom_left),
323            );
324        }
325
326        if rect.width > 1 && rect.height > 1 {
327            self.set_fast(
328                rect.right().saturating_sub(1),
329                rect.bottom().saturating_sub(1),
330                make_cell(chars.bottom_right),
331            );
332        }
333    }
334
335    fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell) {
336        if rect.is_empty() {
337            return;
338        }
339
340        // Fill interior first
341        if rect.width > 2 && rect.height > 2 {
342            let inner = Rect::new(
343                rect.x.saturating_add(1),
344                rect.y.saturating_add(1),
345                rect.width - 2,
346                rect.height - 2,
347            );
348            self.fill(inner, fill_cell);
349        }
350
351        // Draw border on top
352        self.draw_border(rect, chars, border_cell);
353    }
354
355    fn paint_area(
356        &mut self,
357        rect: Rect,
358        fg: Option<crate::cell::PackedRgba>,
359        bg: Option<crate::cell::PackedRgba>,
360    ) {
361        for y in rect.y..rect.bottom() {
362            for x in rect.x..rect.right() {
363                if let Some(cell) = self.get_mut(x, y) {
364                    if let Some(fg_color) = fg {
365                        cell.fg = fg_color;
366                    }
367                    if let Some(bg_color) = bg {
368                        cell.bg = bg_color;
369                    }
370                }
371            }
372        }
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::cell::PackedRgba;
380
381    // --- Helper ---
382
383    fn char_at(buf: &Buffer, x: u16, y: u16) -> Option<char> {
384        buf.get(x, y).and_then(|c| {
385            if c.is_empty() {
386                None
387            } else {
388                c.content.as_char()
389            }
390        })
391    }
392
393    // --- Horizontal line ---
394
395    #[test]
396    fn horizontal_line_basic() {
397        let mut buf = Buffer::new(10, 1);
398        let cell = Cell::from_char('─');
399        buf.draw_horizontal_line(2, 0, 5, cell);
400        assert_eq!(char_at(&buf, 1, 0), None);
401        assert_eq!(char_at(&buf, 2, 0), Some('─'));
402        assert_eq!(char_at(&buf, 6, 0), Some('─'));
403        assert_eq!(char_at(&buf, 7, 0), None);
404    }
405
406    #[test]
407    fn horizontal_line_zero_width() {
408        let mut buf = Buffer::new(10, 1);
409        buf.draw_horizontal_line(0, 0, 0, Cell::from_char('x'));
410        // Nothing should be written
411        assert!(buf.get(0, 0).unwrap().is_empty());
412    }
413
414    #[test]
415    fn horizontal_line_clipped_by_scissor() {
416        let mut buf = Buffer::new(10, 1);
417        buf.push_scissor(Rect::new(0, 0, 3, 1));
418        buf.draw_horizontal_line(0, 0, 10, Cell::from_char('x'));
419        assert_eq!(char_at(&buf, 0, 0), Some('x'));
420        assert_eq!(char_at(&buf, 2, 0), Some('x'));
421        // Outside scissor: not written (still empty)
422        assert!(buf.get(3, 0).unwrap().is_empty());
423    }
424
425    // --- Vertical line ---
426
427    #[test]
428    fn vertical_line_basic() {
429        let mut buf = Buffer::new(1, 10);
430        let cell = Cell::from_char('│');
431        buf.draw_vertical_line(0, 1, 4, cell);
432        assert!(buf.get(0, 0).unwrap().is_empty());
433        assert_eq!(char_at(&buf, 0, 1), Some('│'));
434        assert_eq!(char_at(&buf, 0, 4), Some('│'));
435        assert!(buf.get(0, 5).unwrap().is_empty());
436    }
437
438    #[test]
439    fn vertical_line_zero_height() {
440        let mut buf = Buffer::new(1, 10);
441        buf.draw_vertical_line(0, 0, 0, Cell::from_char('x'));
442        assert!(buf.get(0, 0).unwrap().is_empty());
443    }
444
445    // --- Rect filled ---
446
447    #[test]
448    fn rect_filled() {
449        let mut buf = Buffer::new(5, 5);
450        let cell = Cell::from_char('█');
451        buf.draw_rect_filled(Rect::new(1, 1, 3, 3), cell);
452        // Inside
453        assert_eq!(char_at(&buf, 1, 1), Some('█'));
454        assert_eq!(char_at(&buf, 3, 3), Some('█'));
455        // Outside
456        assert!(buf.get(0, 0).unwrap().is_empty());
457        assert!(buf.get(4, 4).unwrap().is_empty());
458    }
459
460    #[test]
461    fn rect_filled_empty() {
462        let mut buf = Buffer::new(5, 5);
463        buf.draw_rect_filled(Rect::new(0, 0, 0, 0), Cell::from_char('x'));
464        assert!(buf.get(0, 0).unwrap().is_empty());
465    }
466
467    // --- Rect outline ---
468
469    #[test]
470    fn rect_outline_basic() {
471        let mut buf = Buffer::new(5, 5);
472        let cell = Cell::from_char('#');
473        buf.draw_rect_outline(Rect::new(0, 0, 5, 5), cell);
474
475        // Corners
476        assert_eq!(char_at(&buf, 0, 0), Some('#'));
477        assert_eq!(char_at(&buf, 4, 0), Some('#'));
478        assert_eq!(char_at(&buf, 0, 4), Some('#'));
479        assert_eq!(char_at(&buf, 4, 4), Some('#'));
480
481        // Edges
482        assert_eq!(char_at(&buf, 2, 0), Some('#'));
483        assert_eq!(char_at(&buf, 0, 2), Some('#'));
484
485        // Interior is empty
486        assert!(buf.get(2, 2).unwrap().is_empty());
487    }
488
489    #[test]
490    fn rect_outline_1x1() {
491        let mut buf = Buffer::new(5, 5);
492        buf.draw_rect_outline(Rect::new(1, 1, 1, 1), Cell::from_char('o'));
493        assert_eq!(char_at(&buf, 1, 1), Some('o'));
494    }
495
496    #[test]
497    fn rect_outline_2x2() {
498        let mut buf = Buffer::new(5, 5);
499        buf.draw_rect_outline(Rect::new(0, 0, 2, 2), Cell::from_char('#'));
500        assert_eq!(char_at(&buf, 0, 0), Some('#'));
501        assert_eq!(char_at(&buf, 1, 0), Some('#'));
502        assert_eq!(char_at(&buf, 0, 1), Some('#'));
503        assert_eq!(char_at(&buf, 1, 1), Some('#'));
504    }
505
506    // --- Print text ---
507
508    #[test]
509    fn print_text_basic() {
510        let mut buf = Buffer::new(20, 1);
511        let cell = Cell::from_char(' '); // base cell, content overridden
512        let end_x = buf.print_text(2, 0, "Hello", cell);
513        assert_eq!(char_at(&buf, 2, 0), Some('H'));
514        assert_eq!(char_at(&buf, 3, 0), Some('e'));
515        assert_eq!(char_at(&buf, 6, 0), Some('o'));
516        assert_eq!(end_x, 7);
517    }
518
519    #[test]
520    fn print_text_preserves_style() {
521        let mut buf = Buffer::new(10, 1);
522        let cell = Cell::from_char(' ')
523            .with_fg(PackedRgba::rgb(255, 0, 0))
524            .with_bg(PackedRgba::rgb(0, 0, 255));
525        buf.print_text(0, 0, "AB", cell);
526        let a = buf.get(0, 0).unwrap();
527        assert_eq!(a.fg, PackedRgba::rgb(255, 0, 0));
528        assert_eq!(a.bg, PackedRgba::rgb(0, 0, 255));
529    }
530
531    #[test]
532    fn print_text_clips_at_buffer_edge() {
533        let mut buf = Buffer::new(5, 1);
534        let end_x = buf.print_text(0, 0, "Hello World", Cell::from_char(' '));
535        assert_eq!(char_at(&buf, 4, 0), Some('o'));
536        assert_eq!(end_x, 5);
537    }
538
539    #[test]
540    fn print_text_clipped_stops_at_max_x() {
541        let mut buf = Buffer::new(20, 1);
542        let end_x = buf.print_text_clipped(0, 0, "Hello World", Cell::from_char(' '), 5);
543        assert_eq!(char_at(&buf, 4, 0), Some('o'));
544        assert_eq!(end_x, 5);
545        // Beyond max_x not written
546        assert!(buf.get(5, 0).unwrap().is_empty());
547    }
548
549    #[test]
550    fn print_text_multi_codepoint_grapheme_fills_width() {
551        // "👍🏽" is a single grapheme cluster with display width 2 (skin-tone
552        // modifier sequence), and is multi-codepoint.  The buffer should write
553        // the head cell and a continuation marker, clearing any stale content.
554        let mut buf = Buffer::new(4, 1);
555
556        // Seed a "stale border" sentinel that should be cleared.
557        buf.set_raw(1, 0, Cell::from_char('|'));
558
559        let base = Cell::from_char(' ')
560            .with_fg(PackedRgba::rgb(255, 0, 0))
561            .with_bg(PackedRgba::rgb(0, 0, 255));
562
563        let end_x = buf.print_text_clipped(0, 0, "👍🏽", base, 4);
564        assert_eq!(end_x, 2);
565        assert_eq!(char_at(&buf, 0, 0), Some('👍'));
566
567        // The trailing cell is a continuation marker (stale sentinel cleared).
568        let c1 = buf.get(1, 0).unwrap();
569        assert!(c1.is_continuation());
570    }
571
572    #[test]
573    fn print_text_wide_chars() {
574        let mut buf = Buffer::new(10, 1);
575        let end_x = buf.print_text(0, 0, "AB", Cell::from_char(' '));
576        // A=1w, B=1w
577        assert_eq!(end_x, 2);
578        assert_eq!(char_at(&buf, 0, 0), Some('A'));
579        assert_eq!(char_at(&buf, 1, 0), Some('B'));
580    }
581
582    #[test]
583    fn print_text_wide_char_clipped() {
584        let mut buf = Buffer::new(10, 1);
585        // Wide char '中' (width=2) at position 4 with max_x=5 won't fit
586        let end_x = buf.print_text_clipped(4, 0, "中", Cell::from_char(' '), 5);
587        // Can't fit: 4 + 2 > 5
588        assert_eq!(end_x, 4);
589    }
590
591    #[test]
592    fn print_text_empty_string() {
593        let mut buf = Buffer::new(10, 1);
594        let end_x = buf.print_text(0, 0, "", Cell::from_char(' '));
595        assert_eq!(end_x, 0);
596    }
597
598    // --- Border drawing ---
599
600    #[test]
601    fn draw_border_square() {
602        let mut buf = Buffer::new(5, 3);
603        buf.draw_border(
604            Rect::new(0, 0, 5, 3),
605            BorderChars::SQUARE,
606            Cell::from_char(' '),
607        );
608
609        // Corners
610        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
611        assert_eq!(char_at(&buf, 4, 0), Some('┐'));
612        assert_eq!(char_at(&buf, 0, 2), Some('└'));
613        assert_eq!(char_at(&buf, 4, 2), Some('┘'));
614
615        // Horizontal edges
616        assert_eq!(char_at(&buf, 1, 0), Some('─'));
617        assert_eq!(char_at(&buf, 2, 0), Some('─'));
618        assert_eq!(char_at(&buf, 3, 0), Some('─'));
619
620        // Vertical edges
621        assert_eq!(char_at(&buf, 0, 1), Some('│'));
622        assert_eq!(char_at(&buf, 4, 1), Some('│'));
623
624        // Interior empty
625        assert!(buf.get(2, 1).unwrap().is_empty());
626    }
627
628    #[test]
629    fn draw_border_rounded() {
630        let mut buf = Buffer::new(4, 3);
631        buf.draw_border(
632            Rect::new(0, 0, 4, 3),
633            BorderChars::ROUNDED,
634            Cell::from_char(' '),
635        );
636        assert_eq!(char_at(&buf, 0, 0), Some('╭'));
637        assert_eq!(char_at(&buf, 3, 0), Some('╮'));
638        assert_eq!(char_at(&buf, 0, 2), Some('╰'));
639        assert_eq!(char_at(&buf, 3, 2), Some('╯'));
640    }
641
642    #[test]
643    fn draw_border_1x1() {
644        let mut buf = Buffer::new(5, 5);
645        buf.draw_border(
646            Rect::new(1, 1, 1, 1),
647            BorderChars::SQUARE,
648            Cell::from_char(' '),
649        );
650        // Only top-left corner drawn (since width=1, height=1)
651        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
652    }
653
654    #[test]
655    fn draw_border_2x2() {
656        let mut buf = Buffer::new(5, 5);
657        buf.draw_border(
658            Rect::new(0, 0, 2, 2),
659            BorderChars::SQUARE,
660            Cell::from_char(' '),
661        );
662        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
663        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
664        assert_eq!(char_at(&buf, 0, 1), Some('└'));
665        assert_eq!(char_at(&buf, 1, 1), Some('┘'));
666    }
667
668    #[test]
669    fn draw_border_empty_rect() {
670        let mut buf = Buffer::new(5, 5);
671        buf.draw_border(
672            Rect::new(0, 0, 0, 0),
673            BorderChars::SQUARE,
674            Cell::from_char(' '),
675        );
676        // Nothing drawn
677        assert!(buf.get(0, 0).unwrap().is_empty());
678    }
679
680    #[test]
681    fn draw_border_preserves_style() {
682        let mut buf = Buffer::new(5, 3);
683        let cell = Cell::from_char(' ')
684            .with_fg(PackedRgba::rgb(0, 255, 0))
685            .with_bg(PackedRgba::rgb(0, 0, 128));
686        buf.draw_border(Rect::new(0, 0, 5, 3), BorderChars::SQUARE, cell);
687
688        let corner = buf.get(0, 0).unwrap();
689        assert_eq!(corner.fg, PackedRgba::rgb(0, 255, 0));
690        assert_eq!(corner.bg, PackedRgba::rgb(0, 0, 128));
691
692        let edge = buf.get(2, 0).unwrap();
693        assert_eq!(edge.fg, PackedRgba::rgb(0, 255, 0));
694    }
695
696    #[test]
697    fn draw_border_clipped_by_scissor() {
698        let mut buf = Buffer::new(10, 5);
699        buf.push_scissor(Rect::new(0, 0, 3, 3));
700        buf.draw_border(
701            Rect::new(0, 0, 6, 4),
702            BorderChars::SQUARE,
703            Cell::from_char(' '),
704        );
705
706        // Inside scissor: drawn
707        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
708        assert_eq!(char_at(&buf, 2, 0), Some('─'));
709
710        // Outside scissor: not drawn
711        assert!(buf.get(5, 0).unwrap().is_empty());
712        assert!(buf.get(0, 3).unwrap().is_empty());
713    }
714
715    // --- Draw box ---
716
717    #[test]
718    fn draw_box_basic() {
719        let mut buf = Buffer::new(5, 4);
720        let border = Cell::from_char(' ').with_fg(PackedRgba::rgb(255, 255, 255));
721        let fill = Cell::from_char('.').with_bg(PackedRgba::rgb(50, 50, 50));
722        buf.draw_box(Rect::new(0, 0, 5, 4), BorderChars::SQUARE, border, fill);
723
724        // Border
725        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
726        assert_eq!(char_at(&buf, 4, 3), Some('┘'));
727
728        // Interior filled
729        assert_eq!(char_at(&buf, 1, 1), Some('.'));
730        assert_eq!(char_at(&buf, 3, 2), Some('.'));
731        assert_eq!(buf.get(2, 1).unwrap().bg, PackedRgba::rgb(50, 50, 50));
732    }
733
734    #[test]
735    fn draw_box_too_small_for_interior() {
736        let mut buf = Buffer::new(5, 5);
737        let border = Cell::from_char(' ');
738        let fill = Cell::from_char('X');
739        buf.draw_box(Rect::new(0, 0, 2, 2), BorderChars::SQUARE, border, fill);
740
741        // Only border, no fill (width=2, height=2 → interior is 0x0)
742        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
743        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
744    }
745
746    #[test]
747    fn draw_box_empty() {
748        let mut buf = Buffer::new(5, 5);
749        buf.draw_box(
750            Rect::new(0, 0, 0, 0),
751            BorderChars::SQUARE,
752            Cell::from_char(' '),
753            Cell::from_char('.'),
754        );
755        assert!(buf.get(0, 0).unwrap().is_empty());
756    }
757
758    // --- Paint area ---
759
760    #[test]
761    fn paint_area_sets_colors() {
762        let mut buf = Buffer::new(5, 3);
763        // Pre-fill with content
764        buf.set(1, 1, Cell::from_char('X'));
765        buf.set(2, 1, Cell::from_char('Y'));
766
767        buf.paint_area(
768            Rect::new(0, 0, 5, 3),
769            None,
770            Some(PackedRgba::rgb(30, 30, 30)),
771        );
772
773        // Content preserved
774        assert_eq!(char_at(&buf, 1, 1), Some('X'));
775        // Background changed
776        assert_eq!(buf.get(1, 1).unwrap().bg, PackedRgba::rgb(30, 30, 30));
777        assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(30, 30, 30));
778    }
779
780    #[test]
781    fn paint_area_sets_fg() {
782        let mut buf = Buffer::new(3, 1);
783        buf.set(0, 0, Cell::from_char('A'));
784
785        buf.paint_area(
786            Rect::new(0, 0, 3, 1),
787            Some(PackedRgba::rgb(200, 100, 50)),
788            None,
789        );
790
791        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(200, 100, 50));
792    }
793
794    #[test]
795    fn paint_area_empty_rect() {
796        let mut buf = Buffer::new(5, 5);
797        buf.set(0, 0, Cell::from_char('A'));
798        let original_fg = buf.get(0, 0).unwrap().fg;
799
800        buf.paint_area(
801            Rect::new(0, 0, 0, 0),
802            Some(PackedRgba::rgb(255, 0, 0)),
803            None,
804        );
805
806        // Nothing changed
807        assert_eq!(buf.get(0, 0).unwrap().fg, original_fg);
808    }
809
810    // --- All border presets compile ---
811
812    #[test]
813    fn all_border_presets() {
814        let mut buf = Buffer::new(6, 4);
815        let cell = Cell::from_char(' ');
816        let rect = Rect::new(0, 0, 6, 4);
817
818        for chars in [
819            BorderChars::SQUARE,
820            BorderChars::ROUNDED,
821            BorderChars::DOUBLE,
822            BorderChars::HEAVY,
823            BorderChars::ASCII,
824        ] {
825            buf.clear();
826            buf.draw_border(rect, chars, cell);
827            // Corners should be set
828            assert!(buf.get(0, 0).unwrap().content.as_char().is_some());
829            assert!(buf.get(5, 3).unwrap().content.as_char().is_some());
830        }
831    }
832
833    // --- Wider integration tests ---
834
835    #[test]
836    fn draw_border_then_print_title() {
837        let mut buf = Buffer::new(12, 3);
838        let cell = Cell::from_char(' ');
839
840        // Draw border
841        buf.draw_border(Rect::new(0, 0, 12, 3), BorderChars::SQUARE, cell);
842
843        // Print title inside top edge
844        buf.print_text(1, 0, "Title", cell);
845
846        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
847        assert_eq!(char_at(&buf, 1, 0), Some('T'));
848        assert_eq!(char_at(&buf, 5, 0), Some('e'));
849        assert_eq!(char_at(&buf, 6, 0), Some('─'));
850        assert_eq!(char_at(&buf, 11, 0), Some('┐'));
851    }
852
853    #[test]
854    fn draw_nested_borders() {
855        let mut buf = Buffer::new(10, 6);
856        let cell = Cell::from_char(' ');
857
858        buf.draw_border(Rect::new(0, 0, 10, 6), BorderChars::DOUBLE, cell);
859        buf.draw_border(Rect::new(1, 1, 8, 4), BorderChars::SQUARE, cell);
860
861        // Outer corners
862        assert_eq!(char_at(&buf, 0, 0), Some('╔'));
863        assert_eq!(char_at(&buf, 9, 5), Some('╝'));
864
865        // Inner corners
866        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
867        assert_eq!(char_at(&buf, 8, 4), Some('┘'));
868    }
869
870    // --- BorderChars trait coverage ---
871
872    #[test]
873    fn border_chars_debug_clone_copy_eq() {
874        let a = BorderChars::SQUARE;
875        let dbg = format!("{:?}", a);
876        assert!(dbg.contains("BorderChars"), "Debug: {dbg}");
877        let copied: BorderChars = a; // Copy
878        assert_eq!(a, copied);
879        assert_ne!(a, BorderChars::ROUNDED);
880        assert_ne!(BorderChars::DOUBLE, BorderChars::HEAVY);
881    }
882
883    #[test]
884    fn border_chars_double_characters() {
885        let d = BorderChars::DOUBLE;
886        assert_eq!(d.top_left, '╔');
887        assert_eq!(d.top_right, '╗');
888        assert_eq!(d.bottom_left, '╚');
889        assert_eq!(d.bottom_right, '╝');
890        assert_eq!(d.horizontal, '═');
891        assert_eq!(d.vertical, '║');
892    }
893
894    #[test]
895    fn border_chars_heavy_characters() {
896        let h = BorderChars::HEAVY;
897        assert_eq!(h.top_left, '┏');
898        assert_eq!(h.top_right, '┓');
899        assert_eq!(h.bottom_left, '┗');
900        assert_eq!(h.bottom_right, '┛');
901        assert_eq!(h.horizontal, '━');
902        assert_eq!(h.vertical, '┃');
903    }
904
905    #[test]
906    fn border_chars_ascii_characters() {
907        let a = BorderChars::ASCII;
908        assert_eq!(a.top_left, '+');
909        assert_eq!(a.top_right, '+');
910        assert_eq!(a.bottom_left, '+');
911        assert_eq!(a.bottom_right, '+');
912        assert_eq!(a.horizontal, '-');
913        assert_eq!(a.vertical, '|');
914    }
915
916    // --- draw_rect_outline edge cases ---
917
918    #[test]
919    fn rect_outline_empty_rect() {
920        let mut buf = Buffer::new(5, 5);
921        buf.draw_rect_outline(Rect::new(0, 0, 0, 0), Cell::from_char('#'));
922        // Nothing drawn
923        assert!(buf.get(0, 0).unwrap().is_empty());
924    }
925
926    #[test]
927    fn rect_outline_1xn_tall() {
928        let mut buf = Buffer::new(5, 5);
929        buf.draw_rect_outline(Rect::new(1, 0, 1, 4), Cell::from_char('#'));
930        // Width=1: only top and bottom, no left/right separation
931        assert_eq!(char_at(&buf, 1, 0), Some('#'));
932        assert_eq!(char_at(&buf, 1, 3), Some('#'));
933        // Left side (excluding corners) when height > 2
934        assert_eq!(char_at(&buf, 1, 1), Some('#'));
935        assert_eq!(char_at(&buf, 1, 2), Some('#'));
936    }
937
938    #[test]
939    fn rect_outline_nx1_wide() {
940        let mut buf = Buffer::new(5, 5);
941        buf.draw_rect_outline(Rect::new(0, 1, 4, 1), Cell::from_char('#'));
942        // Height=1: only top row
943        for x in 0..4 {
944            assert_eq!(char_at(&buf, x, 1), Some('#'));
945        }
946        // Nothing below
947        assert!(buf.get(0, 2).unwrap().is_empty());
948    }
949
950    #[test]
951    fn rect_outline_3x3() {
952        let mut buf = Buffer::new(5, 5);
953        buf.draw_rect_outline(Rect::new(0, 0, 3, 3), Cell::from_char('#'));
954        // All border cells filled
955        for &(x, y) in &[
956            (0, 0),
957            (1, 0),
958            (2, 0),
959            (0, 1),
960            (2, 1),
961            (0, 2),
962            (1, 2),
963            (2, 2),
964        ] {
965            assert_eq!(char_at(&buf, x, y), Some('#'), "({x},{y})");
966        }
967        // Interior empty
968        assert!(buf.get(1, 1).unwrap().is_empty());
969    }
970
971    // --- draw_border edge cases ---
972
973    #[test]
974    fn draw_border_1x3_narrow() {
975        // Width=1, height=3: top-left corner, vertical edge, bottom-left corner
976        let mut buf = Buffer::new(5, 5);
977        buf.draw_border(
978            Rect::new(1, 0, 1, 3),
979            BorderChars::SQUARE,
980            Cell::from_char(' '),
981        );
982        assert_eq!(char_at(&buf, 1, 0), Some('┌'));
983        assert_eq!(char_at(&buf, 1, 1), Some('│'));
984        assert_eq!(char_at(&buf, 1, 2), Some('└'));
985    }
986
987    #[test]
988    fn draw_border_3x1_flat() {
989        // Width=3, height=1: only top row with corners + horizontal
990        let mut buf = Buffer::new(5, 5);
991        buf.draw_border(
992            Rect::new(0, 0, 3, 1),
993            BorderChars::SQUARE,
994            Cell::from_char(' '),
995        );
996        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
997        assert_eq!(char_at(&buf, 1, 0), Some('─'));
998        assert_eq!(char_at(&buf, 2, 0), Some('┐'));
999    }
1000
1001    #[test]
1002    fn draw_border_2x1() {
1003        // Width=2, height=1: top-left and top-right corners only
1004        let mut buf = Buffer::new(5, 5);
1005        buf.draw_border(
1006            Rect::new(0, 0, 2, 1),
1007            BorderChars::SQUARE,
1008            Cell::from_char(' '),
1009        );
1010        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1011        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
1012    }
1013
1014    #[test]
1015    fn draw_border_1x2() {
1016        // Width=1, height=2: top-left and bottom-left (no right column)
1017        let mut buf = Buffer::new(5, 5);
1018        buf.draw_border(
1019            Rect::new(0, 0, 1, 2),
1020            BorderChars::SQUARE,
1021            Cell::from_char(' '),
1022        );
1023        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1024        assert_eq!(char_at(&buf, 0, 1), Some('└'));
1025    }
1026
1027    // --- draw_box edge cases ---
1028
1029    #[test]
1030    fn draw_box_3x3_minimal_interior() {
1031        let mut buf = Buffer::new(5, 5);
1032        let border = Cell::from_char(' ');
1033        let fill = Cell::from_char('.');
1034        buf.draw_box(Rect::new(0, 0, 3, 3), BorderChars::SQUARE, border, fill);
1035        // Border corners
1036        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1037        assert_eq!(char_at(&buf, 2, 2), Some('┘'));
1038        // Interior: 1 cell
1039        assert_eq!(char_at(&buf, 1, 1), Some('.'));
1040    }
1041
1042    #[test]
1043    fn draw_box_1x1() {
1044        let mut buf = Buffer::new(5, 5);
1045        let border = Cell::from_char(' ');
1046        let fill = Cell::from_char('X');
1047        buf.draw_box(Rect::new(1, 1, 1, 1), BorderChars::SQUARE, border, fill);
1048        // Only corner drawn, no fill
1049        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
1050    }
1051
1052    #[test]
1053    fn draw_box_border_overwrites_fill() {
1054        // Ensure border is drawn on top of fill
1055        let mut buf = Buffer::new(5, 5);
1056        let border = Cell::from_char(' ').with_fg(PackedRgba::rgb(255, 0, 0));
1057        let fill = Cell::from_char('.').with_fg(PackedRgba::rgb(0, 255, 0));
1058        buf.draw_box(Rect::new(0, 0, 4, 4), BorderChars::SQUARE, border, fill);
1059        // Corner should have border style, not fill
1060        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1061        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(255, 0, 0));
1062        // Interior should have fill style
1063        assert_eq!(char_at(&buf, 1, 1), Some('.'));
1064        assert_eq!(buf.get(1, 1).unwrap().fg, PackedRgba::rgb(0, 255, 0));
1065    }
1066
1067    // --- paint_area edge cases ---
1068
1069    #[test]
1070    fn paint_area_sets_both_fg_and_bg() {
1071        let mut buf = Buffer::new(3, 3);
1072        buf.set(1, 1, Cell::from_char('X'));
1073        buf.paint_area(
1074            Rect::new(0, 0, 3, 3),
1075            Some(PackedRgba::rgb(100, 200, 50)),
1076            Some(PackedRgba::rgb(10, 20, 30)),
1077        );
1078        let cell = buf.get(1, 1).unwrap();
1079        assert_eq!(cell.content.as_char(), Some('X'));
1080        assert_eq!(cell.fg, PackedRgba::rgb(100, 200, 50));
1081        assert_eq!(cell.bg, PackedRgba::rgb(10, 20, 30));
1082    }
1083
1084    #[test]
1085    fn paint_area_beyond_buffer() {
1086        let mut buf = Buffer::new(3, 3);
1087        // Rect extends past buffer — should silently handle via get_mut returning None
1088        buf.paint_area(
1089            Rect::new(0, 0, 100, 100),
1090            Some(PackedRgba::rgb(255, 0, 0)),
1091            None,
1092        );
1093        // Only cells within buffer should be painted
1094        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(255, 0, 0));
1095        assert_eq!(buf.get(2, 2).unwrap().fg, PackedRgba::rgb(255, 0, 0));
1096    }
1097
1098    #[test]
1099    fn paint_area_no_colors() {
1100        let mut buf = Buffer::new(3, 1);
1101        let cell = Cell::from_char('A').with_fg(PackedRgba::rgb(10, 20, 30));
1102        buf.set(0, 0, cell);
1103        // Paint with neither fg nor bg — nothing changes
1104        buf.paint_area(Rect::new(0, 0, 3, 1), None, None);
1105        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(10, 20, 30));
1106    }
1107
1108    // --- print_text edge cases ---
1109
1110    #[test]
1111    fn print_text_max_x_zero() {
1112        let mut buf = Buffer::new(10, 1);
1113        let end_x = buf.print_text_clipped(0, 0, "Hello", Cell::from_char(' '), 0);
1114        assert_eq!(end_x, 0);
1115        assert!(buf.get(0, 0).unwrap().is_empty());
1116    }
1117
1118    #[test]
1119    fn print_text_start_past_max_x() {
1120        let mut buf = Buffer::new(10, 1);
1121        let end_x = buf.print_text_clipped(5, 0, "Hello", Cell::from_char(' '), 3);
1122        assert_eq!(end_x, 5); // cx starts at 5 >= max_x=3, immediately breaks
1123    }
1124
1125    #[test]
1126    fn print_text_single_char() {
1127        let mut buf = Buffer::new(10, 1);
1128        let end_x = buf.print_text(0, 0, "X", Cell::from_char(' '));
1129        assert_eq!(end_x, 1);
1130        assert_eq!(char_at(&buf, 0, 0), Some('X'));
1131    }
1132
1133    // --- Horizontal/vertical line edge cases ---
1134
1135    #[test]
1136    fn horizontal_line_at_buffer_bottom() {
1137        let mut buf = Buffer::new(5, 3);
1138        buf.draw_horizontal_line(0, 2, 5, Cell::from_char('='));
1139        for x in 0..5 {
1140            assert_eq!(char_at(&buf, x, 2), Some('='));
1141        }
1142    }
1143
1144    #[test]
1145    fn vertical_line_at_buffer_right_edge() {
1146        let mut buf = Buffer::new(5, 5);
1147        buf.draw_vertical_line(4, 0, 5, Cell::from_char('|'));
1148        for y in 0..5 {
1149            assert_eq!(char_at(&buf, 4, y), Some('|'));
1150        }
1151    }
1152
1153    #[test]
1154    fn horizontal_line_exceeds_buffer() {
1155        // Line extends beyond buffer width; set() should silently clip
1156        let mut buf = Buffer::new(3, 1);
1157        buf.draw_horizontal_line(0, 0, 100, Cell::from_char('-'));
1158        for x in 0..3 {
1159            assert_eq!(char_at(&buf, x, 0), Some('-'));
1160        }
1161    }
1162
1163    #[test]
1164    fn vertical_line_exceeds_buffer() {
1165        let mut buf = Buffer::new(1, 3);
1166        buf.draw_vertical_line(0, 0, 100, Cell::from_char('|'));
1167        for y in 0..3 {
1168            assert_eq!(char_at(&buf, 0, y), Some('|'));
1169        }
1170    }
1171
1172    // --- Scissor + drawing ops ---
1173
1174    #[test]
1175    fn rect_filled_clipped_by_scissor() {
1176        let mut buf = Buffer::new(10, 10);
1177        buf.push_scissor(Rect::new(2, 2, 3, 3));
1178        buf.draw_rect_filled(Rect::new(0, 0, 10, 10), Cell::from_char('#'));
1179        // Inside scissor
1180        assert_eq!(char_at(&buf, 2, 2), Some('#'));
1181        assert_eq!(char_at(&buf, 4, 4), Some('#'));
1182        // Outside scissor
1183        assert!(buf.get(0, 0).unwrap().is_empty());
1184        assert!(buf.get(5, 5).unwrap().is_empty());
1185        buf.pop_scissor();
1186    }
1187
1188    #[test]
1189    fn vertical_line_clipped_by_scissor() {
1190        let mut buf = Buffer::new(5, 10);
1191        buf.push_scissor(Rect::new(0, 2, 5, 3));
1192        buf.draw_vertical_line(2, 0, 10, Cell::from_char('|'));
1193        // Inside scissor
1194        assert_eq!(char_at(&buf, 2, 2), Some('|'));
1195        assert_eq!(char_at(&buf, 2, 4), Some('|'));
1196        // Outside scissor
1197        assert!(buf.get(2, 0).unwrap().is_empty());
1198        assert!(buf.get(2, 5).unwrap().is_empty());
1199        buf.pop_scissor();
1200    }
1201
1202    // --- 1x1 buffer stress ---
1203
1204    #[test]
1205    fn drawing_on_1x1_buffer() {
1206        let mut buf = Buffer::new(1, 1);
1207        buf.draw_horizontal_line(0, 0, 1, Cell::from_char('H'));
1208        assert_eq!(char_at(&buf, 0, 0), Some('H'));
1209
1210        buf.clear();
1211        buf.draw_vertical_line(0, 0, 1, Cell::from_char('V'));
1212        assert_eq!(char_at(&buf, 0, 0), Some('V'));
1213
1214        buf.clear();
1215        buf.draw_rect_outline(Rect::new(0, 0, 1, 1), Cell::from_char('O'));
1216        assert_eq!(char_at(&buf, 0, 0), Some('O'));
1217
1218        buf.clear();
1219        buf.draw_rect_filled(Rect::new(0, 0, 1, 1), Cell::from_char('F'));
1220        assert_eq!(char_at(&buf, 0, 0), Some('F'));
1221
1222        buf.clear();
1223        buf.draw_border(
1224            Rect::new(0, 0, 1, 1),
1225            BorderChars::SQUARE,
1226            Cell::from_char(' '),
1227        );
1228        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1229
1230        buf.clear();
1231        buf.draw_box(
1232            Rect::new(0, 0, 1, 1),
1233            BorderChars::ASCII,
1234            Cell::from_char(' '),
1235            Cell::from_char('.'),
1236        );
1237        assert_eq!(char_at(&buf, 0, 0), Some('+'));
1238
1239        buf.clear();
1240        let end = buf.print_text(0, 0, "X", Cell::from_char(' '));
1241        assert_eq!(end, 1);
1242        assert_eq!(char_at(&buf, 0, 0), Some('X'));
1243    }
1244}