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        let clipped = self.current_scissor().intersection(&rect);
362        if clipped.is_empty() {
363            return;
364        }
365
366        let opacity = self.current_opacity();
367
368        for y in clipped.y..clipped.bottom() {
369            self.mark_dirty_span(y, clipped.x, clipped.right());
370            for x in clipped.x..clipped.right() {
371                let idx = self.index_unchecked(x, y);
372                let cell = self.cell_mut_unchecked(idx);
373
374                if let Some(fg_color) = fg {
375                    if opacity < 1.0 {
376                        cell.fg = fg_color.with_opacity(opacity);
377                    } else {
378                        cell.fg = fg_color;
379                    }
380                }
381                if let Some(bg_color) = bg {
382                    if opacity < 1.0 {
383                        cell.bg = bg_color.with_opacity(opacity).over(cell.bg);
384                    } else {
385                        cell.bg = bg_color;
386                    }
387                }
388            }
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use crate::cell::PackedRgba;
397
398    // --- Helper ---
399
400    fn char_at(buf: &Buffer, x: u16, y: u16) -> Option<char> {
401        buf.get(x, y).and_then(|c| {
402            if c.is_empty() {
403                None
404            } else {
405                c.content.as_char()
406            }
407        })
408    }
409
410    // --- Horizontal line ---
411
412    #[test]
413    fn horizontal_line_basic() {
414        let mut buf = Buffer::new(10, 1);
415        let cell = Cell::from_char('─');
416        buf.draw_horizontal_line(2, 0, 5, cell);
417        assert_eq!(char_at(&buf, 1, 0), None);
418        assert_eq!(char_at(&buf, 2, 0), Some('─'));
419        assert_eq!(char_at(&buf, 6, 0), Some('─'));
420        assert_eq!(char_at(&buf, 7, 0), None);
421    }
422
423    #[test]
424    fn horizontal_line_zero_width() {
425        let mut buf = Buffer::new(10, 1);
426        buf.draw_horizontal_line(0, 0, 0, Cell::from_char('x'));
427        // Nothing should be written
428        assert!(buf.get(0, 0).unwrap().is_empty());
429    }
430
431    #[test]
432    fn horizontal_line_clipped_by_scissor() {
433        let mut buf = Buffer::new(10, 1);
434        buf.push_scissor(Rect::new(0, 0, 3, 1));
435        buf.draw_horizontal_line(0, 0, 10, Cell::from_char('x'));
436        assert_eq!(char_at(&buf, 0, 0), Some('x'));
437        assert_eq!(char_at(&buf, 2, 0), Some('x'));
438        // Outside scissor: not written (still empty)
439        assert!(buf.get(3, 0).unwrap().is_empty());
440    }
441
442    // --- Vertical line ---
443
444    #[test]
445    fn vertical_line_basic() {
446        let mut buf = Buffer::new(1, 10);
447        let cell = Cell::from_char('│');
448        buf.draw_vertical_line(0, 1, 4, cell);
449        assert!(buf.get(0, 0).unwrap().is_empty());
450        assert_eq!(char_at(&buf, 0, 1), Some('│'));
451        assert_eq!(char_at(&buf, 0, 4), Some('│'));
452        assert!(buf.get(0, 5).unwrap().is_empty());
453    }
454
455    #[test]
456    fn vertical_line_zero_height() {
457        let mut buf = Buffer::new(1, 10);
458        buf.draw_vertical_line(0, 0, 0, Cell::from_char('x'));
459        assert!(buf.get(0, 0).unwrap().is_empty());
460    }
461
462    // --- Rect filled ---
463
464    #[test]
465    fn rect_filled() {
466        let mut buf = Buffer::new(5, 5);
467        let cell = Cell::from_char('█');
468        buf.draw_rect_filled(Rect::new(1, 1, 3, 3), cell);
469        // Inside
470        assert_eq!(char_at(&buf, 1, 1), Some('█'));
471        assert_eq!(char_at(&buf, 3, 3), Some('█'));
472        // Outside
473        assert!(buf.get(0, 0).unwrap().is_empty());
474        assert!(buf.get(4, 4).unwrap().is_empty());
475    }
476
477    #[test]
478    fn rect_filled_empty() {
479        let mut buf = Buffer::new(5, 5);
480        buf.draw_rect_filled(Rect::new(0, 0, 0, 0), Cell::from_char('x'));
481        assert!(buf.get(0, 0).unwrap().is_empty());
482    }
483
484    // --- Rect outline ---
485
486    #[test]
487    fn rect_outline_basic() {
488        let mut buf = Buffer::new(5, 5);
489        let cell = Cell::from_char('#');
490        buf.draw_rect_outline(Rect::new(0, 0, 5, 5), cell);
491
492        // Corners
493        assert_eq!(char_at(&buf, 0, 0), Some('#'));
494        assert_eq!(char_at(&buf, 4, 0), Some('#'));
495        assert_eq!(char_at(&buf, 0, 4), Some('#'));
496        assert_eq!(char_at(&buf, 4, 4), Some('#'));
497
498        // Edges
499        assert_eq!(char_at(&buf, 2, 0), Some('#'));
500        assert_eq!(char_at(&buf, 0, 2), Some('#'));
501
502        // Interior is empty
503        assert!(buf.get(2, 2).unwrap().is_empty());
504    }
505
506    #[test]
507    fn rect_outline_1x1() {
508        let mut buf = Buffer::new(5, 5);
509        buf.draw_rect_outline(Rect::new(1, 1, 1, 1), Cell::from_char('o'));
510        assert_eq!(char_at(&buf, 1, 1), Some('o'));
511    }
512
513    #[test]
514    fn rect_outline_2x2() {
515        let mut buf = Buffer::new(5, 5);
516        buf.draw_rect_outline(Rect::new(0, 0, 2, 2), Cell::from_char('#'));
517        assert_eq!(char_at(&buf, 0, 0), Some('#'));
518        assert_eq!(char_at(&buf, 1, 0), Some('#'));
519        assert_eq!(char_at(&buf, 0, 1), Some('#'));
520        assert_eq!(char_at(&buf, 1, 1), Some('#'));
521    }
522
523    // --- Print text ---
524
525    #[test]
526    fn print_text_basic() {
527        let mut buf = Buffer::new(20, 1);
528        let cell = Cell::from_char(' '); // base cell, content overridden
529        let end_x = buf.print_text(2, 0, "Hello", cell);
530        assert_eq!(char_at(&buf, 2, 0), Some('H'));
531        assert_eq!(char_at(&buf, 3, 0), Some('e'));
532        assert_eq!(char_at(&buf, 6, 0), Some('o'));
533        assert_eq!(end_x, 7);
534    }
535
536    #[test]
537    fn print_text_preserves_style() {
538        let mut buf = Buffer::new(10, 1);
539        let cell = Cell::from_char(' ')
540            .with_fg(PackedRgba::rgb(255, 0, 0))
541            .with_bg(PackedRgba::rgb(0, 0, 255));
542        buf.print_text(0, 0, "AB", cell);
543        let a = buf.get(0, 0).unwrap();
544        assert_eq!(a.fg, PackedRgba::rgb(255, 0, 0));
545        assert_eq!(a.bg, PackedRgba::rgb(0, 0, 255));
546    }
547
548    #[test]
549    fn print_text_clips_at_buffer_edge() {
550        let mut buf = Buffer::new(5, 1);
551        let end_x = buf.print_text(0, 0, "Hello World", Cell::from_char(' '));
552        assert_eq!(char_at(&buf, 4, 0), Some('o'));
553        assert_eq!(end_x, 5);
554    }
555
556    #[test]
557    fn print_text_clipped_stops_at_max_x() {
558        let mut buf = Buffer::new(20, 1);
559        let end_x = buf.print_text_clipped(0, 0, "Hello World", Cell::from_char(' '), 5);
560        assert_eq!(char_at(&buf, 4, 0), Some('o'));
561        assert_eq!(end_x, 5);
562        // Beyond max_x not written
563        assert!(buf.get(5, 0).unwrap().is_empty());
564    }
565
566    #[test]
567    fn print_text_multi_codepoint_grapheme_fills_width() {
568        // "👍🏽" is a single grapheme cluster with display width 2 (skin-tone
569        // modifier sequence), and is multi-codepoint.  The buffer should write
570        // the head cell and a continuation marker, clearing any stale content.
571        let mut buf = Buffer::new(4, 1);
572
573        // Seed a "stale border" sentinel that should be cleared.
574        buf.set_raw(1, 0, Cell::from_char('|'));
575
576        let base = Cell::from_char(' ')
577            .with_fg(PackedRgba::rgb(255, 0, 0))
578            .with_bg(PackedRgba::rgb(0, 0, 255));
579
580        let end_x = buf.print_text_clipped(0, 0, "👍🏽", base, 4);
581        assert_eq!(end_x, 2);
582        assert_eq!(char_at(&buf, 0, 0), Some('👍'));
583
584        // The trailing cell is a continuation marker (stale sentinel cleared).
585        let c1 = buf.get(1, 0).unwrap();
586        assert!(c1.is_continuation());
587    }
588
589    #[test]
590    fn print_text_wide_chars() {
591        let mut buf = Buffer::new(10, 1);
592        let end_x = buf.print_text(0, 0, "AB", Cell::from_char(' '));
593        // A=1w, B=1w
594        assert_eq!(end_x, 2);
595        assert_eq!(char_at(&buf, 0, 0), Some('A'));
596        assert_eq!(char_at(&buf, 1, 0), Some('B'));
597    }
598
599    #[test]
600    fn print_text_wide_char_clipped() {
601        let mut buf = Buffer::new(10, 1);
602        // Wide char '中' (width=2) at position 4 with max_x=5 won't fit
603        let end_x = buf.print_text_clipped(4, 0, "中", Cell::from_char(' '), 5);
604        // Can't fit: 4 + 2 > 5
605        assert_eq!(end_x, 4);
606    }
607
608    #[test]
609    fn print_text_empty_string() {
610        let mut buf = Buffer::new(10, 1);
611        let end_x = buf.print_text(0, 0, "", Cell::from_char(' '));
612        assert_eq!(end_x, 0);
613    }
614
615    // --- Border drawing ---
616
617    #[test]
618    fn draw_border_square() {
619        let mut buf = Buffer::new(5, 3);
620        buf.draw_border(
621            Rect::new(0, 0, 5, 3),
622            BorderChars::SQUARE,
623            Cell::from_char(' '),
624        );
625
626        // Corners
627        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
628        assert_eq!(char_at(&buf, 4, 0), Some('┐'));
629        assert_eq!(char_at(&buf, 0, 2), Some('└'));
630        assert_eq!(char_at(&buf, 4, 2), Some('┘'));
631
632        // Horizontal edges
633        assert_eq!(char_at(&buf, 1, 0), Some('─'));
634        assert_eq!(char_at(&buf, 2, 0), Some('─'));
635        assert_eq!(char_at(&buf, 3, 0), Some('─'));
636
637        // Vertical edges
638        assert_eq!(char_at(&buf, 0, 1), Some('│'));
639        assert_eq!(char_at(&buf, 4, 1), Some('│'));
640
641        // Interior empty
642        assert!(buf.get(2, 1).unwrap().is_empty());
643    }
644
645    #[test]
646    fn draw_border_rounded() {
647        let mut buf = Buffer::new(4, 3);
648        buf.draw_border(
649            Rect::new(0, 0, 4, 3),
650            BorderChars::ROUNDED,
651            Cell::from_char(' '),
652        );
653        assert_eq!(char_at(&buf, 0, 0), Some('╭'));
654        assert_eq!(char_at(&buf, 3, 0), Some('╮'));
655        assert_eq!(char_at(&buf, 0, 2), Some('╰'));
656        assert_eq!(char_at(&buf, 3, 2), Some('╯'));
657    }
658
659    #[test]
660    fn draw_border_1x1() {
661        let mut buf = Buffer::new(5, 5);
662        buf.draw_border(
663            Rect::new(1, 1, 1, 1),
664            BorderChars::SQUARE,
665            Cell::from_char(' '),
666        );
667        // Only top-left corner drawn (since width=1, height=1)
668        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
669    }
670
671    #[test]
672    fn draw_border_2x2() {
673        let mut buf = Buffer::new(5, 5);
674        buf.draw_border(
675            Rect::new(0, 0, 2, 2),
676            BorderChars::SQUARE,
677            Cell::from_char(' '),
678        );
679        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
680        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
681        assert_eq!(char_at(&buf, 0, 1), Some('└'));
682        assert_eq!(char_at(&buf, 1, 1), Some('┘'));
683    }
684
685    #[test]
686    fn draw_border_empty_rect() {
687        let mut buf = Buffer::new(5, 5);
688        buf.draw_border(
689            Rect::new(0, 0, 0, 0),
690            BorderChars::SQUARE,
691            Cell::from_char(' '),
692        );
693        // Nothing drawn
694        assert!(buf.get(0, 0).unwrap().is_empty());
695    }
696
697    #[test]
698    fn draw_border_preserves_style() {
699        let mut buf = Buffer::new(5, 3);
700        let cell = Cell::from_char(' ')
701            .with_fg(PackedRgba::rgb(0, 255, 0))
702            .with_bg(PackedRgba::rgb(0, 0, 128));
703        buf.draw_border(Rect::new(0, 0, 5, 3), BorderChars::SQUARE, cell);
704
705        let corner = buf.get(0, 0).unwrap();
706        assert_eq!(corner.fg, PackedRgba::rgb(0, 255, 0));
707        assert_eq!(corner.bg, PackedRgba::rgb(0, 0, 128));
708
709        let edge = buf.get(2, 0).unwrap();
710        assert_eq!(edge.fg, PackedRgba::rgb(0, 255, 0));
711    }
712
713    #[test]
714    fn draw_border_clipped_by_scissor() {
715        let mut buf = Buffer::new(10, 5);
716        buf.push_scissor(Rect::new(0, 0, 3, 3));
717        buf.draw_border(
718            Rect::new(0, 0, 6, 4),
719            BorderChars::SQUARE,
720            Cell::from_char(' '),
721        );
722
723        // Inside scissor: drawn
724        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
725        assert_eq!(char_at(&buf, 2, 0), Some('─'));
726
727        // Outside scissor: not drawn
728        assert!(buf.get(5, 0).unwrap().is_empty());
729        assert!(buf.get(0, 3).unwrap().is_empty());
730    }
731
732    // --- Draw box ---
733
734    #[test]
735    fn draw_box_basic() {
736        let mut buf = Buffer::new(5, 4);
737        let border = Cell::from_char(' ').with_fg(PackedRgba::rgb(255, 255, 255));
738        let fill = Cell::from_char('.').with_bg(PackedRgba::rgb(50, 50, 50));
739        buf.draw_box(Rect::new(0, 0, 5, 4), BorderChars::SQUARE, border, fill);
740
741        // Border
742        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
743        assert_eq!(char_at(&buf, 4, 3), Some('┘'));
744
745        // Interior filled
746        assert_eq!(char_at(&buf, 1, 1), Some('.'));
747        assert_eq!(char_at(&buf, 3, 2), Some('.'));
748        assert_eq!(buf.get(2, 1).unwrap().bg, PackedRgba::rgb(50, 50, 50));
749    }
750
751    #[test]
752    fn draw_box_too_small_for_interior() {
753        let mut buf = Buffer::new(5, 5);
754        let border = Cell::from_char(' ');
755        let fill = Cell::from_char('X');
756        buf.draw_box(Rect::new(0, 0, 2, 2), BorderChars::SQUARE, border, fill);
757
758        // Only border, no fill (width=2, height=2 → interior is 0x0)
759        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
760        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
761    }
762
763    #[test]
764    fn draw_box_empty() {
765        let mut buf = Buffer::new(5, 5);
766        buf.draw_box(
767            Rect::new(0, 0, 0, 0),
768            BorderChars::SQUARE,
769            Cell::from_char(' '),
770            Cell::from_char('.'),
771        );
772        assert!(buf.get(0, 0).unwrap().is_empty());
773    }
774
775    // --- Paint area ---
776
777    #[test]
778    fn paint_area_sets_colors() {
779        let mut buf = Buffer::new(5, 3);
780        // Pre-fill with content
781        buf.set(1, 1, Cell::from_char('X'));
782        buf.set(2, 1, Cell::from_char('Y'));
783
784        buf.paint_area(
785            Rect::new(0, 0, 5, 3),
786            None,
787            Some(PackedRgba::rgb(30, 30, 30)),
788        );
789
790        // Content preserved
791        assert_eq!(char_at(&buf, 1, 1), Some('X'));
792        // Background changed
793        assert_eq!(buf.get(1, 1).unwrap().bg, PackedRgba::rgb(30, 30, 30));
794        assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(30, 30, 30));
795    }
796
797    #[test]
798    fn paint_area_sets_fg() {
799        let mut buf = Buffer::new(3, 1);
800        buf.set(0, 0, Cell::from_char('A'));
801
802        buf.paint_area(
803            Rect::new(0, 0, 3, 1),
804            Some(PackedRgba::rgb(200, 100, 50)),
805            None,
806        );
807
808        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(200, 100, 50));
809    }
810
811    #[test]
812    fn paint_area_empty_rect() {
813        let mut buf = Buffer::new(5, 5);
814        buf.set(0, 0, Cell::from_char('A'));
815        let original_fg = buf.get(0, 0).unwrap().fg;
816
817        buf.paint_area(
818            Rect::new(0, 0, 0, 0),
819            Some(PackedRgba::rgb(255, 0, 0)),
820            None,
821        );
822
823        // Nothing changed
824        assert_eq!(buf.get(0, 0).unwrap().fg, original_fg);
825    }
826
827    // --- All border presets compile ---
828
829    #[test]
830    fn all_border_presets() {
831        let mut buf = Buffer::new(6, 4);
832        let cell = Cell::from_char(' ');
833        let rect = Rect::new(0, 0, 6, 4);
834
835        for chars in [
836            BorderChars::SQUARE,
837            BorderChars::ROUNDED,
838            BorderChars::DOUBLE,
839            BorderChars::HEAVY,
840            BorderChars::ASCII,
841        ] {
842            buf.clear();
843            buf.draw_border(rect, chars, cell);
844            // Corners should be set
845            assert!(buf.get(0, 0).unwrap().content.as_char().is_some());
846            assert!(buf.get(5, 3).unwrap().content.as_char().is_some());
847        }
848    }
849
850    // --- Wider integration tests ---
851
852    #[test]
853    fn draw_border_then_print_title() {
854        let mut buf = Buffer::new(12, 3);
855        let cell = Cell::from_char(' ');
856
857        // Draw border
858        buf.draw_border(Rect::new(0, 0, 12, 3), BorderChars::SQUARE, cell);
859
860        // Print title inside top edge
861        buf.print_text(1, 0, "Title", cell);
862
863        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
864        assert_eq!(char_at(&buf, 1, 0), Some('T'));
865        assert_eq!(char_at(&buf, 5, 0), Some('e'));
866        assert_eq!(char_at(&buf, 6, 0), Some('─'));
867        assert_eq!(char_at(&buf, 11, 0), Some('┐'));
868    }
869
870    #[test]
871    fn draw_nested_borders() {
872        let mut buf = Buffer::new(10, 6);
873        let cell = Cell::from_char(' ');
874
875        buf.draw_border(Rect::new(0, 0, 10, 6), BorderChars::DOUBLE, cell);
876        buf.draw_border(Rect::new(1, 1, 8, 4), BorderChars::SQUARE, cell);
877
878        // Outer corners
879        assert_eq!(char_at(&buf, 0, 0), Some('╔'));
880        assert_eq!(char_at(&buf, 9, 5), Some('╝'));
881
882        // Inner corners
883        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
884        assert_eq!(char_at(&buf, 8, 4), Some('┘'));
885    }
886
887    // --- BorderChars trait coverage ---
888
889    #[test]
890    fn border_chars_debug_clone_copy_eq() {
891        let a = BorderChars::SQUARE;
892        let dbg = format!("{:?}", a);
893        assert!(dbg.contains("BorderChars"), "Debug: {dbg}");
894        let copied: BorderChars = a; // Copy
895        assert_eq!(a, copied);
896        assert_ne!(a, BorderChars::ROUNDED);
897        assert_ne!(BorderChars::DOUBLE, BorderChars::HEAVY);
898    }
899
900    #[test]
901    fn border_chars_double_characters() {
902        let d = BorderChars::DOUBLE;
903        assert_eq!(d.top_left, '╔');
904        assert_eq!(d.top_right, '╗');
905        assert_eq!(d.bottom_left, '╚');
906        assert_eq!(d.bottom_right, '╝');
907        assert_eq!(d.horizontal, '═');
908        assert_eq!(d.vertical, '║');
909    }
910
911    #[test]
912    fn border_chars_heavy_characters() {
913        let h = BorderChars::HEAVY;
914        assert_eq!(h.top_left, '┏');
915        assert_eq!(h.top_right, '┓');
916        assert_eq!(h.bottom_left, '┗');
917        assert_eq!(h.bottom_right, '┛');
918        assert_eq!(h.horizontal, '━');
919        assert_eq!(h.vertical, '┃');
920    }
921
922    #[test]
923    fn border_chars_ascii_characters() {
924        let a = BorderChars::ASCII;
925        assert_eq!(a.top_left, '+');
926        assert_eq!(a.top_right, '+');
927        assert_eq!(a.bottom_left, '+');
928        assert_eq!(a.bottom_right, '+');
929        assert_eq!(a.horizontal, '-');
930        assert_eq!(a.vertical, '|');
931    }
932
933    // --- draw_rect_outline edge cases ---
934
935    #[test]
936    fn rect_outline_empty_rect() {
937        let mut buf = Buffer::new(5, 5);
938        buf.draw_rect_outline(Rect::new(0, 0, 0, 0), Cell::from_char('#'));
939        // Nothing drawn
940        assert!(buf.get(0, 0).unwrap().is_empty());
941    }
942
943    #[test]
944    fn rect_outline_1xn_tall() {
945        let mut buf = Buffer::new(5, 5);
946        buf.draw_rect_outline(Rect::new(1, 0, 1, 4), Cell::from_char('#'));
947        // Width=1: only top and bottom, no left/right separation
948        assert_eq!(char_at(&buf, 1, 0), Some('#'));
949        assert_eq!(char_at(&buf, 1, 3), Some('#'));
950        // Left side (excluding corners) when height > 2
951        assert_eq!(char_at(&buf, 1, 1), Some('#'));
952        assert_eq!(char_at(&buf, 1, 2), Some('#'));
953    }
954
955    #[test]
956    fn rect_outline_nx1_wide() {
957        let mut buf = Buffer::new(5, 5);
958        buf.draw_rect_outline(Rect::new(0, 1, 4, 1), Cell::from_char('#'));
959        // Height=1: only top row
960        for x in 0..4 {
961            assert_eq!(char_at(&buf, x, 1), Some('#'));
962        }
963        // Nothing below
964        assert!(buf.get(0, 2).unwrap().is_empty());
965    }
966
967    #[test]
968    fn rect_outline_3x3() {
969        let mut buf = Buffer::new(5, 5);
970        buf.draw_rect_outline(Rect::new(0, 0, 3, 3), Cell::from_char('#'));
971        // All border cells filled
972        for &(x, y) in &[
973            (0, 0),
974            (1, 0),
975            (2, 0),
976            (0, 1),
977            (2, 1),
978            (0, 2),
979            (1, 2),
980            (2, 2),
981        ] {
982            assert_eq!(char_at(&buf, x, y), Some('#'), "({x},{y})");
983        }
984        // Interior empty
985        assert!(buf.get(1, 1).unwrap().is_empty());
986    }
987
988    // --- draw_border edge cases ---
989
990    #[test]
991    fn draw_border_1x3_narrow() {
992        // Width=1, height=3: top-left corner, vertical edge, bottom-left corner
993        let mut buf = Buffer::new(5, 5);
994        buf.draw_border(
995            Rect::new(1, 0, 1, 3),
996            BorderChars::SQUARE,
997            Cell::from_char(' '),
998        );
999        assert_eq!(char_at(&buf, 1, 0), Some('┌'));
1000        assert_eq!(char_at(&buf, 1, 1), Some('│'));
1001        assert_eq!(char_at(&buf, 1, 2), Some('└'));
1002    }
1003
1004    #[test]
1005    fn draw_border_3x1_flat() {
1006        // Width=3, height=1: only top row with corners + horizontal
1007        let mut buf = Buffer::new(5, 5);
1008        buf.draw_border(
1009            Rect::new(0, 0, 3, 1),
1010            BorderChars::SQUARE,
1011            Cell::from_char(' '),
1012        );
1013        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1014        assert_eq!(char_at(&buf, 1, 0), Some('─'));
1015        assert_eq!(char_at(&buf, 2, 0), Some('┐'));
1016    }
1017
1018    #[test]
1019    fn draw_border_2x1() {
1020        // Width=2, height=1: top-left and top-right corners only
1021        let mut buf = Buffer::new(5, 5);
1022        buf.draw_border(
1023            Rect::new(0, 0, 2, 1),
1024            BorderChars::SQUARE,
1025            Cell::from_char(' '),
1026        );
1027        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1028        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
1029    }
1030
1031    #[test]
1032    fn draw_border_1x2() {
1033        // Width=1, height=2: top-left and bottom-left (no right column)
1034        let mut buf = Buffer::new(5, 5);
1035        buf.draw_border(
1036            Rect::new(0, 0, 1, 2),
1037            BorderChars::SQUARE,
1038            Cell::from_char(' '),
1039        );
1040        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1041        assert_eq!(char_at(&buf, 0, 1), Some('└'));
1042    }
1043
1044    // --- draw_box edge cases ---
1045
1046    #[test]
1047    fn draw_box_3x3_minimal_interior() {
1048        let mut buf = Buffer::new(5, 5);
1049        let border = Cell::from_char(' ');
1050        let fill = Cell::from_char('.');
1051        buf.draw_box(Rect::new(0, 0, 3, 3), BorderChars::SQUARE, border, fill);
1052        // Border corners
1053        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1054        assert_eq!(char_at(&buf, 2, 2), Some('┘'));
1055        // Interior: 1 cell
1056        assert_eq!(char_at(&buf, 1, 1), Some('.'));
1057    }
1058
1059    #[test]
1060    fn draw_box_1x1() {
1061        let mut buf = Buffer::new(5, 5);
1062        let border = Cell::from_char(' ');
1063        let fill = Cell::from_char('X');
1064        buf.draw_box(Rect::new(1, 1, 1, 1), BorderChars::SQUARE, border, fill);
1065        // Only corner drawn, no fill
1066        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
1067    }
1068
1069    #[test]
1070    fn draw_box_border_overwrites_fill() {
1071        // Ensure border is drawn on top of fill
1072        let mut buf = Buffer::new(5, 5);
1073        let border = Cell::from_char(' ').with_fg(PackedRgba::rgb(255, 0, 0));
1074        let fill = Cell::from_char('.').with_fg(PackedRgba::rgb(0, 255, 0));
1075        buf.draw_box(Rect::new(0, 0, 4, 4), BorderChars::SQUARE, border, fill);
1076        // Corner should have border style, not fill
1077        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1078        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(255, 0, 0));
1079        // Interior should have fill style
1080        assert_eq!(char_at(&buf, 1, 1), Some('.'));
1081        assert_eq!(buf.get(1, 1).unwrap().fg, PackedRgba::rgb(0, 255, 0));
1082    }
1083
1084    // --- paint_area edge cases ---
1085
1086    #[test]
1087    fn paint_area_sets_both_fg_and_bg() {
1088        let mut buf = Buffer::new(3, 3);
1089        buf.set(1, 1, Cell::from_char('X'));
1090        buf.paint_area(
1091            Rect::new(0, 0, 3, 3),
1092            Some(PackedRgba::rgb(100, 200, 50)),
1093            Some(PackedRgba::rgb(10, 20, 30)),
1094        );
1095        let cell = buf.get(1, 1).unwrap();
1096        assert_eq!(cell.content.as_char(), Some('X'));
1097        assert_eq!(cell.fg, PackedRgba::rgb(100, 200, 50));
1098        assert_eq!(cell.bg, PackedRgba::rgb(10, 20, 30));
1099    }
1100
1101    #[test]
1102    fn paint_area_beyond_buffer() {
1103        let mut buf = Buffer::new(3, 3);
1104        // Rect extends past buffer — should silently handle via get_mut returning None
1105        buf.paint_area(
1106            Rect::new(0, 0, 100, 100),
1107            Some(PackedRgba::rgb(255, 0, 0)),
1108            None,
1109        );
1110        // Only cells within buffer should be painted
1111        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(255, 0, 0));
1112        assert_eq!(buf.get(2, 2).unwrap().fg, PackedRgba::rgb(255, 0, 0));
1113    }
1114
1115    #[test]
1116    fn paint_area_no_colors() {
1117        let mut buf = Buffer::new(3, 1);
1118        let cell = Cell::from_char('A').with_fg(PackedRgba::rgb(10, 20, 30));
1119        buf.set(0, 0, cell);
1120        // Paint with neither fg nor bg — nothing changes
1121        buf.paint_area(Rect::new(0, 0, 3, 1), None, None);
1122        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(10, 20, 30));
1123    }
1124
1125    // --- print_text edge cases ---
1126
1127    #[test]
1128    fn print_text_max_x_zero() {
1129        let mut buf = Buffer::new(10, 1);
1130        let end_x = buf.print_text_clipped(0, 0, "Hello", Cell::from_char(' '), 0);
1131        assert_eq!(end_x, 0);
1132        assert!(buf.get(0, 0).unwrap().is_empty());
1133    }
1134
1135    #[test]
1136    fn print_text_start_past_max_x() {
1137        let mut buf = Buffer::new(10, 1);
1138        let end_x = buf.print_text_clipped(5, 0, "Hello", Cell::from_char(' '), 3);
1139        assert_eq!(end_x, 5); // cx starts at 5 >= max_x=3, immediately breaks
1140    }
1141
1142    #[test]
1143    fn print_text_single_char() {
1144        let mut buf = Buffer::new(10, 1);
1145        let end_x = buf.print_text(0, 0, "X", Cell::from_char(' '));
1146        assert_eq!(end_x, 1);
1147        assert_eq!(char_at(&buf, 0, 0), Some('X'));
1148    }
1149
1150    // --- Horizontal/vertical line edge cases ---
1151
1152    #[test]
1153    fn horizontal_line_at_buffer_bottom() {
1154        let mut buf = Buffer::new(5, 3);
1155        buf.draw_horizontal_line(0, 2, 5, Cell::from_char('='));
1156        for x in 0..5 {
1157            assert_eq!(char_at(&buf, x, 2), Some('='));
1158        }
1159    }
1160
1161    #[test]
1162    fn vertical_line_at_buffer_right_edge() {
1163        let mut buf = Buffer::new(5, 5);
1164        buf.draw_vertical_line(4, 0, 5, Cell::from_char('|'));
1165        for y in 0..5 {
1166            assert_eq!(char_at(&buf, 4, y), Some('|'));
1167        }
1168    }
1169
1170    #[test]
1171    fn horizontal_line_exceeds_buffer() {
1172        // Line extends beyond buffer width; set() should silently clip
1173        let mut buf = Buffer::new(3, 1);
1174        buf.draw_horizontal_line(0, 0, 100, Cell::from_char('-'));
1175        for x in 0..3 {
1176            assert_eq!(char_at(&buf, x, 0), Some('-'));
1177        }
1178    }
1179
1180    #[test]
1181    fn vertical_line_exceeds_buffer() {
1182        let mut buf = Buffer::new(1, 3);
1183        buf.draw_vertical_line(0, 0, 100, Cell::from_char('|'));
1184        for y in 0..3 {
1185            assert_eq!(char_at(&buf, 0, y), Some('|'));
1186        }
1187    }
1188
1189    // --- Scissor + drawing ops ---
1190
1191    #[test]
1192    fn rect_filled_clipped_by_scissor() {
1193        let mut buf = Buffer::new(10, 10);
1194        buf.push_scissor(Rect::new(2, 2, 3, 3));
1195        buf.draw_rect_filled(Rect::new(0, 0, 10, 10), Cell::from_char('#'));
1196        // Inside scissor
1197        assert_eq!(char_at(&buf, 2, 2), Some('#'));
1198        assert_eq!(char_at(&buf, 4, 4), Some('#'));
1199        // Outside scissor
1200        assert!(buf.get(0, 0).unwrap().is_empty());
1201        assert!(buf.get(5, 5).unwrap().is_empty());
1202        buf.pop_scissor();
1203    }
1204
1205    #[test]
1206    fn vertical_line_clipped_by_scissor() {
1207        let mut buf = Buffer::new(5, 10);
1208        buf.push_scissor(Rect::new(0, 2, 5, 3));
1209        buf.draw_vertical_line(2, 0, 10, Cell::from_char('|'));
1210        // Inside scissor
1211        assert_eq!(char_at(&buf, 2, 2), Some('|'));
1212        assert_eq!(char_at(&buf, 2, 4), Some('|'));
1213        // Outside scissor
1214        assert!(buf.get(2, 0).unwrap().is_empty());
1215        assert!(buf.get(2, 5).unwrap().is_empty());
1216        buf.pop_scissor();
1217    }
1218
1219    // --- 1x1 buffer stress ---
1220
1221    #[test]
1222    fn drawing_on_1x1_buffer() {
1223        let mut buf = Buffer::new(1, 1);
1224        buf.draw_horizontal_line(0, 0, 1, Cell::from_char('H'));
1225        assert_eq!(char_at(&buf, 0, 0), Some('H'));
1226
1227        buf.clear();
1228        buf.draw_vertical_line(0, 0, 1, Cell::from_char('V'));
1229        assert_eq!(char_at(&buf, 0, 0), Some('V'));
1230
1231        buf.clear();
1232        buf.draw_rect_outline(Rect::new(0, 0, 1, 1), Cell::from_char('O'));
1233        assert_eq!(char_at(&buf, 0, 0), Some('O'));
1234
1235        buf.clear();
1236        buf.draw_rect_filled(Rect::new(0, 0, 1, 1), Cell::from_char('F'));
1237        assert_eq!(char_at(&buf, 0, 0), Some('F'));
1238
1239        buf.clear();
1240        buf.draw_border(
1241            Rect::new(0, 0, 1, 1),
1242            BorderChars::SQUARE,
1243            Cell::from_char(' '),
1244        );
1245        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
1246
1247        buf.clear();
1248        buf.draw_box(
1249            Rect::new(0, 0, 1, 1),
1250            BorderChars::ASCII,
1251            Cell::from_char(' '),
1252            Cell::from_char('.'),
1253        );
1254        assert_eq!(char_at(&buf, 0, 0), Some('+'));
1255
1256        buf.clear();
1257        let end = buf.print_text(0, 0, "X", Cell::from_char(' '));
1258        assert_eq!(end, 1);
1259        assert_eq!(char_at(&buf, 0, 0), Some('X'));
1260    }
1261}