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(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(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            // If we can't render the full grapheme, fall back to the first char
218            // but preserve the grapheme's display width to avoid layout gaps.
219            let mut width = grapheme_width(grapheme);
220            if width == 0 {
221                width = CellContent::from_char(first).width();
222            }
223            if width == 0 {
224                continue;
225            }
226
227            // Don't start a wide char if it won't fit
228            if cx as u32 + width as u32 > max_x as u32 {
229                break;
230            }
231
232            let cell = Cell {
233                content: CellContent::from_char(first),
234                fg: base_cell.fg,
235                bg: base_cell.bg,
236                attrs: base_cell.attrs,
237            };
238            self.set(cx, y, cell);
239
240            cx = cx.saturating_add(width as u16);
241        }
242        cx
243    }
244
245    fn draw_border(&mut self, rect: Rect, chars: BorderChars, base_cell: Cell) {
246        if rect.is_empty() {
247            return;
248        }
249
250        let make_cell = |c: char| -> Cell {
251            Cell {
252                content: CellContent::from_char(c),
253                fg: base_cell.fg,
254                bg: base_cell.bg,
255                attrs: base_cell.attrs,
256            }
257        };
258
259        let h_cell = make_cell(chars.horizontal);
260        let v_cell = make_cell(chars.vertical);
261
262        // Top edge
263        for x in rect.left()..rect.right() {
264            self.set(x, rect.top(), h_cell);
265        }
266
267        // Bottom edge
268        if rect.height > 1 {
269            for x in rect.left()..rect.right() {
270                self.set(x, rect.bottom().saturating_sub(1), h_cell);
271            }
272        }
273
274        // Left edge (excluding corners)
275        if rect.height > 2 {
276            for y in (rect.top().saturating_add(1))..(rect.bottom().saturating_sub(1)) {
277                self.set(rect.left(), y, v_cell);
278            }
279        }
280
281        // Right edge (excluding corners)
282        if rect.width > 1 && rect.height > 2 {
283            for y in (rect.top().saturating_add(1))..(rect.bottom().saturating_sub(1)) {
284                self.set(rect.right().saturating_sub(1), y, v_cell);
285            }
286        }
287
288        // Corners (drawn last to overwrite edge chars at corners)
289        self.set(rect.left(), rect.top(), make_cell(chars.top_left));
290
291        if rect.width > 1 {
292            self.set(
293                rect.right().saturating_sub(1),
294                rect.top(),
295                make_cell(chars.top_right),
296            );
297        }
298
299        if rect.height > 1 {
300            self.set(
301                rect.left(),
302                rect.bottom().saturating_sub(1),
303                make_cell(chars.bottom_left),
304            );
305        }
306
307        if rect.width > 1 && rect.height > 1 {
308            self.set(
309                rect.right().saturating_sub(1),
310                rect.bottom().saturating_sub(1),
311                make_cell(chars.bottom_right),
312            );
313        }
314    }
315
316    fn draw_box(&mut self, rect: Rect, chars: BorderChars, border_cell: Cell, fill_cell: Cell) {
317        if rect.is_empty() {
318            return;
319        }
320
321        // Fill interior first
322        if rect.width > 2 && rect.height > 2 {
323            let inner = Rect::new(
324                rect.x.saturating_add(1),
325                rect.y.saturating_add(1),
326                rect.width - 2,
327                rect.height - 2,
328            );
329            self.fill(inner, fill_cell);
330        }
331
332        // Draw border on top
333        self.draw_border(rect, chars, border_cell);
334    }
335
336    fn paint_area(
337        &mut self,
338        rect: Rect,
339        fg: Option<crate::cell::PackedRgba>,
340        bg: Option<crate::cell::PackedRgba>,
341    ) {
342        for y in rect.y..rect.bottom() {
343            for x in rect.x..rect.right() {
344                if let Some(cell) = self.get_mut(x, y) {
345                    if let Some(fg_color) = fg {
346                        cell.fg = fg_color;
347                    }
348                    if let Some(bg_color) = bg {
349                        cell.bg = bg_color;
350                    }
351                }
352            }
353        }
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360    use crate::cell::PackedRgba;
361
362    // --- Helper ---
363
364    fn char_at(buf: &Buffer, x: u16, y: u16) -> Option<char> {
365        buf.get(x, y).and_then(|c| {
366            if c.is_empty() {
367                None
368            } else {
369                c.content.as_char()
370            }
371        })
372    }
373
374    // --- Horizontal line ---
375
376    #[test]
377    fn horizontal_line_basic() {
378        let mut buf = Buffer::new(10, 1);
379        let cell = Cell::from_char('─');
380        buf.draw_horizontal_line(2, 0, 5, cell);
381        assert_eq!(char_at(&buf, 1, 0), None);
382        assert_eq!(char_at(&buf, 2, 0), Some('─'));
383        assert_eq!(char_at(&buf, 6, 0), Some('─'));
384        assert_eq!(char_at(&buf, 7, 0), None);
385    }
386
387    #[test]
388    fn horizontal_line_zero_width() {
389        let mut buf = Buffer::new(10, 1);
390        buf.draw_horizontal_line(0, 0, 0, Cell::from_char('x'));
391        // Nothing should be written
392        assert!(buf.get(0, 0).unwrap().is_empty());
393    }
394
395    #[test]
396    fn horizontal_line_clipped_by_scissor() {
397        let mut buf = Buffer::new(10, 1);
398        buf.push_scissor(Rect::new(0, 0, 3, 1));
399        buf.draw_horizontal_line(0, 0, 10, Cell::from_char('x'));
400        assert_eq!(char_at(&buf, 0, 0), Some('x'));
401        assert_eq!(char_at(&buf, 2, 0), Some('x'));
402        // Outside scissor: not written (still empty)
403        assert!(buf.get(3, 0).unwrap().is_empty());
404    }
405
406    // --- Vertical line ---
407
408    #[test]
409    fn vertical_line_basic() {
410        let mut buf = Buffer::new(1, 10);
411        let cell = Cell::from_char('│');
412        buf.draw_vertical_line(0, 1, 4, cell);
413        assert!(buf.get(0, 0).unwrap().is_empty());
414        assert_eq!(char_at(&buf, 0, 1), Some('│'));
415        assert_eq!(char_at(&buf, 0, 4), Some('│'));
416        assert!(buf.get(0, 5).unwrap().is_empty());
417    }
418
419    #[test]
420    fn vertical_line_zero_height() {
421        let mut buf = Buffer::new(1, 10);
422        buf.draw_vertical_line(0, 0, 0, Cell::from_char('x'));
423        assert!(buf.get(0, 0).unwrap().is_empty());
424    }
425
426    // --- Rect filled ---
427
428    #[test]
429    fn rect_filled() {
430        let mut buf = Buffer::new(5, 5);
431        let cell = Cell::from_char('█');
432        buf.draw_rect_filled(Rect::new(1, 1, 3, 3), cell);
433        // Inside
434        assert_eq!(char_at(&buf, 1, 1), Some('█'));
435        assert_eq!(char_at(&buf, 3, 3), Some('█'));
436        // Outside
437        assert!(buf.get(0, 0).unwrap().is_empty());
438        assert!(buf.get(4, 4).unwrap().is_empty());
439    }
440
441    #[test]
442    fn rect_filled_empty() {
443        let mut buf = Buffer::new(5, 5);
444        buf.draw_rect_filled(Rect::new(0, 0, 0, 0), Cell::from_char('x'));
445        assert!(buf.get(0, 0).unwrap().is_empty());
446    }
447
448    // --- Rect outline ---
449
450    #[test]
451    fn rect_outline_basic() {
452        let mut buf = Buffer::new(5, 5);
453        let cell = Cell::from_char('#');
454        buf.draw_rect_outline(Rect::new(0, 0, 5, 5), cell);
455
456        // Corners
457        assert_eq!(char_at(&buf, 0, 0), Some('#'));
458        assert_eq!(char_at(&buf, 4, 0), Some('#'));
459        assert_eq!(char_at(&buf, 0, 4), Some('#'));
460        assert_eq!(char_at(&buf, 4, 4), Some('#'));
461
462        // Edges
463        assert_eq!(char_at(&buf, 2, 0), Some('#'));
464        assert_eq!(char_at(&buf, 0, 2), Some('#'));
465
466        // Interior is empty
467        assert!(buf.get(2, 2).unwrap().is_empty());
468    }
469
470    #[test]
471    fn rect_outline_1x1() {
472        let mut buf = Buffer::new(5, 5);
473        buf.draw_rect_outline(Rect::new(1, 1, 1, 1), Cell::from_char('o'));
474        assert_eq!(char_at(&buf, 1, 1), Some('o'));
475    }
476
477    #[test]
478    fn rect_outline_2x2() {
479        let mut buf = Buffer::new(5, 5);
480        buf.draw_rect_outline(Rect::new(0, 0, 2, 2), Cell::from_char('#'));
481        assert_eq!(char_at(&buf, 0, 0), Some('#'));
482        assert_eq!(char_at(&buf, 1, 0), Some('#'));
483        assert_eq!(char_at(&buf, 0, 1), Some('#'));
484        assert_eq!(char_at(&buf, 1, 1), Some('#'));
485    }
486
487    // --- Print text ---
488
489    #[test]
490    fn print_text_basic() {
491        let mut buf = Buffer::new(20, 1);
492        let cell = Cell::from_char(' '); // base cell, content overridden
493        let end_x = buf.print_text(2, 0, "Hello", cell);
494        assert_eq!(char_at(&buf, 2, 0), Some('H'));
495        assert_eq!(char_at(&buf, 3, 0), Some('e'));
496        assert_eq!(char_at(&buf, 6, 0), Some('o'));
497        assert_eq!(end_x, 7);
498    }
499
500    #[test]
501    fn print_text_preserves_style() {
502        let mut buf = Buffer::new(10, 1);
503        let cell = Cell::from_char(' ')
504            .with_fg(PackedRgba::rgb(255, 0, 0))
505            .with_bg(PackedRgba::rgb(0, 0, 255));
506        buf.print_text(0, 0, "AB", cell);
507        let a = buf.get(0, 0).unwrap();
508        assert_eq!(a.fg, PackedRgba::rgb(255, 0, 0));
509        assert_eq!(a.bg, PackedRgba::rgb(0, 0, 255));
510    }
511
512    #[test]
513    fn print_text_clips_at_buffer_edge() {
514        let mut buf = Buffer::new(5, 1);
515        let end_x = buf.print_text(0, 0, "Hello World", Cell::from_char(' '));
516        assert_eq!(char_at(&buf, 4, 0), Some('o'));
517        assert_eq!(end_x, 5);
518    }
519
520    #[test]
521    fn print_text_clipped_stops_at_max_x() {
522        let mut buf = Buffer::new(20, 1);
523        let end_x = buf.print_text_clipped(0, 0, "Hello World", Cell::from_char(' '), 5);
524        assert_eq!(char_at(&buf, 4, 0), Some('o'));
525        assert_eq!(end_x, 5);
526        // Beyond max_x not written
527        assert!(buf.get(5, 0).unwrap().is_empty());
528    }
529
530    #[test]
531    fn print_text_wide_chars() {
532        let mut buf = Buffer::new(10, 1);
533        let end_x = buf.print_text(0, 0, "AB", Cell::from_char(' '));
534        // A=1w, B=1w
535        assert_eq!(end_x, 2);
536        assert_eq!(char_at(&buf, 0, 0), Some('A'));
537        assert_eq!(char_at(&buf, 1, 0), Some('B'));
538    }
539
540    #[test]
541    fn print_text_wide_char_clipped() {
542        let mut buf = Buffer::new(10, 1);
543        // Wide char '中' (width=2) at position 4 with max_x=5 won't fit
544        let end_x = buf.print_text_clipped(4, 0, "中", Cell::from_char(' '), 5);
545        // Can't fit: 4 + 2 > 5
546        assert_eq!(end_x, 4);
547    }
548
549    #[test]
550    fn print_text_empty_string() {
551        let mut buf = Buffer::new(10, 1);
552        let end_x = buf.print_text(0, 0, "", Cell::from_char(' '));
553        assert_eq!(end_x, 0);
554    }
555
556    // --- Border drawing ---
557
558    #[test]
559    fn draw_border_square() {
560        let mut buf = Buffer::new(5, 3);
561        buf.draw_border(
562            Rect::new(0, 0, 5, 3),
563            BorderChars::SQUARE,
564            Cell::from_char(' '),
565        );
566
567        // Corners
568        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
569        assert_eq!(char_at(&buf, 4, 0), Some('┐'));
570        assert_eq!(char_at(&buf, 0, 2), Some('└'));
571        assert_eq!(char_at(&buf, 4, 2), Some('┘'));
572
573        // Horizontal edges
574        assert_eq!(char_at(&buf, 1, 0), Some('─'));
575        assert_eq!(char_at(&buf, 2, 0), Some('─'));
576        assert_eq!(char_at(&buf, 3, 0), Some('─'));
577
578        // Vertical edges
579        assert_eq!(char_at(&buf, 0, 1), Some('│'));
580        assert_eq!(char_at(&buf, 4, 1), Some('│'));
581
582        // Interior empty
583        assert!(buf.get(2, 1).unwrap().is_empty());
584    }
585
586    #[test]
587    fn draw_border_rounded() {
588        let mut buf = Buffer::new(4, 3);
589        buf.draw_border(
590            Rect::new(0, 0, 4, 3),
591            BorderChars::ROUNDED,
592            Cell::from_char(' '),
593        );
594        assert_eq!(char_at(&buf, 0, 0), Some('╭'));
595        assert_eq!(char_at(&buf, 3, 0), Some('╮'));
596        assert_eq!(char_at(&buf, 0, 2), Some('╰'));
597        assert_eq!(char_at(&buf, 3, 2), Some('╯'));
598    }
599
600    #[test]
601    fn draw_border_1x1() {
602        let mut buf = Buffer::new(5, 5);
603        buf.draw_border(
604            Rect::new(1, 1, 1, 1),
605            BorderChars::SQUARE,
606            Cell::from_char(' '),
607        );
608        // Only top-left corner drawn (since width=1, height=1)
609        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
610    }
611
612    #[test]
613    fn draw_border_2x2() {
614        let mut buf = Buffer::new(5, 5);
615        buf.draw_border(
616            Rect::new(0, 0, 2, 2),
617            BorderChars::SQUARE,
618            Cell::from_char(' '),
619        );
620        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
621        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
622        assert_eq!(char_at(&buf, 0, 1), Some('└'));
623        assert_eq!(char_at(&buf, 1, 1), Some('┘'));
624    }
625
626    #[test]
627    fn draw_border_empty_rect() {
628        let mut buf = Buffer::new(5, 5);
629        buf.draw_border(
630            Rect::new(0, 0, 0, 0),
631            BorderChars::SQUARE,
632            Cell::from_char(' '),
633        );
634        // Nothing drawn
635        assert!(buf.get(0, 0).unwrap().is_empty());
636    }
637
638    #[test]
639    fn draw_border_preserves_style() {
640        let mut buf = Buffer::new(5, 3);
641        let cell = Cell::from_char(' ')
642            .with_fg(PackedRgba::rgb(0, 255, 0))
643            .with_bg(PackedRgba::rgb(0, 0, 128));
644        buf.draw_border(Rect::new(0, 0, 5, 3), BorderChars::SQUARE, cell);
645
646        let corner = buf.get(0, 0).unwrap();
647        assert_eq!(corner.fg, PackedRgba::rgb(0, 255, 0));
648        assert_eq!(corner.bg, PackedRgba::rgb(0, 0, 128));
649
650        let edge = buf.get(2, 0).unwrap();
651        assert_eq!(edge.fg, PackedRgba::rgb(0, 255, 0));
652    }
653
654    #[test]
655    fn draw_border_clipped_by_scissor() {
656        let mut buf = Buffer::new(10, 5);
657        buf.push_scissor(Rect::new(0, 0, 3, 3));
658        buf.draw_border(
659            Rect::new(0, 0, 6, 4),
660            BorderChars::SQUARE,
661            Cell::from_char(' '),
662        );
663
664        // Inside scissor: drawn
665        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
666        assert_eq!(char_at(&buf, 2, 0), Some('─'));
667
668        // Outside scissor: not drawn
669        assert!(buf.get(5, 0).unwrap().is_empty());
670        assert!(buf.get(0, 3).unwrap().is_empty());
671    }
672
673    // --- Draw box ---
674
675    #[test]
676    fn draw_box_basic() {
677        let mut buf = Buffer::new(5, 4);
678        let border = Cell::from_char(' ').with_fg(PackedRgba::rgb(255, 255, 255));
679        let fill = Cell::from_char('.').with_bg(PackedRgba::rgb(50, 50, 50));
680        buf.draw_box(Rect::new(0, 0, 5, 4), BorderChars::SQUARE, border, fill);
681
682        // Border
683        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
684        assert_eq!(char_at(&buf, 4, 3), Some('┘'));
685
686        // Interior filled
687        assert_eq!(char_at(&buf, 1, 1), Some('.'));
688        assert_eq!(char_at(&buf, 3, 2), Some('.'));
689        assert_eq!(buf.get(2, 1).unwrap().bg, PackedRgba::rgb(50, 50, 50));
690    }
691
692    #[test]
693    fn draw_box_too_small_for_interior() {
694        let mut buf = Buffer::new(5, 5);
695        let border = Cell::from_char(' ');
696        let fill = Cell::from_char('X');
697        buf.draw_box(Rect::new(0, 0, 2, 2), BorderChars::SQUARE, border, fill);
698
699        // Only border, no fill (width=2, height=2 → interior is 0x0)
700        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
701        assert_eq!(char_at(&buf, 1, 0), Some('┐'));
702    }
703
704    #[test]
705    fn draw_box_empty() {
706        let mut buf = Buffer::new(5, 5);
707        buf.draw_box(
708            Rect::new(0, 0, 0, 0),
709            BorderChars::SQUARE,
710            Cell::from_char(' '),
711            Cell::from_char('.'),
712        );
713        assert!(buf.get(0, 0).unwrap().is_empty());
714    }
715
716    // --- Paint area ---
717
718    #[test]
719    fn paint_area_sets_colors() {
720        let mut buf = Buffer::new(5, 3);
721        // Pre-fill with content
722        buf.set(1, 1, Cell::from_char('X'));
723        buf.set(2, 1, Cell::from_char('Y'));
724
725        buf.paint_area(
726            Rect::new(0, 0, 5, 3),
727            None,
728            Some(PackedRgba::rgb(30, 30, 30)),
729        );
730
731        // Content preserved
732        assert_eq!(char_at(&buf, 1, 1), Some('X'));
733        // Background changed
734        assert_eq!(buf.get(1, 1).unwrap().bg, PackedRgba::rgb(30, 30, 30));
735        assert_eq!(buf.get(0, 0).unwrap().bg, PackedRgba::rgb(30, 30, 30));
736    }
737
738    #[test]
739    fn paint_area_sets_fg() {
740        let mut buf = Buffer::new(3, 1);
741        buf.set(0, 0, Cell::from_char('A'));
742
743        buf.paint_area(
744            Rect::new(0, 0, 3, 1),
745            Some(PackedRgba::rgb(200, 100, 50)),
746            None,
747        );
748
749        assert_eq!(buf.get(0, 0).unwrap().fg, PackedRgba::rgb(200, 100, 50));
750    }
751
752    #[test]
753    fn paint_area_empty_rect() {
754        let mut buf = Buffer::new(5, 5);
755        buf.set(0, 0, Cell::from_char('A'));
756        let original_fg = buf.get(0, 0).unwrap().fg;
757
758        buf.paint_area(
759            Rect::new(0, 0, 0, 0),
760            Some(PackedRgba::rgb(255, 0, 0)),
761            None,
762        );
763
764        // Nothing changed
765        assert_eq!(buf.get(0, 0).unwrap().fg, original_fg);
766    }
767
768    // --- All border presets compile ---
769
770    #[test]
771    fn all_border_presets() {
772        let mut buf = Buffer::new(6, 4);
773        let cell = Cell::from_char(' ');
774        let rect = Rect::new(0, 0, 6, 4);
775
776        for chars in [
777            BorderChars::SQUARE,
778            BorderChars::ROUNDED,
779            BorderChars::DOUBLE,
780            BorderChars::HEAVY,
781            BorderChars::ASCII,
782        ] {
783            buf.clear();
784            buf.draw_border(rect, chars, cell);
785            // Corners should be set
786            assert!(buf.get(0, 0).unwrap().content.as_char().is_some());
787            assert!(buf.get(5, 3).unwrap().content.as_char().is_some());
788        }
789    }
790
791    // --- Wider integration tests ---
792
793    #[test]
794    fn draw_border_then_print_title() {
795        let mut buf = Buffer::new(12, 3);
796        let cell = Cell::from_char(' ');
797
798        // Draw border
799        buf.draw_border(Rect::new(0, 0, 12, 3), BorderChars::SQUARE, cell);
800
801        // Print title inside top edge
802        buf.print_text(1, 0, "Title", cell);
803
804        assert_eq!(char_at(&buf, 0, 0), Some('┌'));
805        assert_eq!(char_at(&buf, 1, 0), Some('T'));
806        assert_eq!(char_at(&buf, 5, 0), Some('e'));
807        assert_eq!(char_at(&buf, 6, 0), Some('─'));
808        assert_eq!(char_at(&buf, 11, 0), Some('┐'));
809    }
810
811    #[test]
812    fn draw_nested_borders() {
813        let mut buf = Buffer::new(10, 6);
814        let cell = Cell::from_char(' ');
815
816        buf.draw_border(Rect::new(0, 0, 10, 6), BorderChars::DOUBLE, cell);
817        buf.draw_border(Rect::new(1, 1, 8, 4), BorderChars::SQUARE, cell);
818
819        // Outer corners
820        assert_eq!(char_at(&buf, 0, 0), Some('╔'));
821        assert_eq!(char_at(&buf, 9, 5), Some('╝'));
822
823        // Inner corners
824        assert_eq!(char_at(&buf, 1, 1), Some('┌'));
825        assert_eq!(char_at(&buf, 8, 4), Some('┘'));
826    }
827}