Skip to main content

denise_render/
rect.rs

1//! Axis-aligned rectangles.
2
3use denise::Rect;
4
5use crate::blend::{Paint, blend_span};
6use crate::canvas::Canvas;
7
8impl Canvas<'_> {
9    /// Fills `rect`, compositing with the destination if the paint has alpha.
10    pub fn fill_rect(&mut self, rect: Rect, color: impl Into<Paint>) {
11        let paint = color.into();
12        if paint.is_invisible() {
13            return;
14        }
15        let Some(r) = self.visible(rect) else {
16            return;
17        };
18        for y in r.y..r.bottom() {
19            if let Some(span) = self.row_span(y, r.x, r.right()) {
20                blend_span(span, paint);
21            }
22        }
23    }
24
25    /// Fills every rectangle. Overlapping rectangles composite twice.
26    pub fn fill_rects(&mut self, rects: &[Rect], color: impl Into<Paint>) {
27        let paint = color.into();
28        for rect in rects {
29            self.fill_rect(*rect, paint);
30        }
31    }
32
33    /// Draws a border of `thickness` pixels *inside* `rect`.
34    ///
35    /// The four bands are cut so they do not overlap. Drawing them as four
36    /// full-length rectangles would composite the corners twice, which is invisible
37    /// at full alpha and obvious at anything less.
38    pub fn stroke_rect(&mut self, rect: Rect, thickness: i32, color: impl Into<Paint>) {
39        let paint = color.into();
40        let t = thickness.max(0);
41        if t == 0 || rect.is_empty() || paint.is_invisible() {
42            return;
43        }
44        if t * 2 >= rect.width.min(rect.height) {
45            self.fill_rect(rect, paint);
46            return;
47        }
48
49        let inner_height = rect.height - 2 * t;
50        self.fill_rect(Rect::new(rect.x, rect.y, rect.width, t), paint);
51        self.fill_rect(Rect::new(rect.x, rect.bottom() - t, rect.width, t), paint);
52        self.fill_rect(Rect::new(rect.x, rect.y + t, t, inner_height), paint);
53        self.fill_rect(
54            Rect::new(rect.right() - t, rect.y + t, t, inner_height),
55            paint,
56        );
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::testing::TestCanvas;
64    use denise::Color;
65
66    #[test]
67    fn fill_clips_to_the_canvas() {
68        let mut t = TestCanvas::new(8, 8);
69        t.canvas().fill_rect(Rect::new(-4, -4, 8, 8), Color::WHITE);
70        assert_eq!(t.at(0, 0), 0xFFFF_FFFF);
71        assert_eq!(t.at(3, 3), 0xFFFF_FFFF);
72        assert_eq!(t.at(4, 0), 0);
73        assert_eq!(t.at(0, 4), 0);
74    }
75
76    #[test]
77    fn fill_never_touches_stride_padding() {
78        let mut t = TestCanvas::with_stride(10, 4, 16);
79        t.canvas().fill_rect(Rect::new(0, 0, 10, 4), Color::WHITE);
80        for row in t.pixels().chunks(16) {
81            assert!(row[..10].iter().all(|&p| p == 0xFFFF_FFFF));
82            assert!(row[10..].iter().all(|&p| p == 0));
83        }
84    }
85
86    #[test]
87    fn wholly_offscreen_fill_is_a_noop() {
88        let mut t = TestCanvas::new(8, 8);
89        t.canvas()
90            .fill_rect(Rect::new(100, 100, 8, 8), Color::WHITE);
91        assert!(t.pixels().iter().all(|&p| p == 0));
92    }
93
94    #[test]
95    fn half_alpha_twice_is_not_full_alpha() {
96        let mut t = TestCanvas::new(4, 4);
97        t.canvas()
98            .fill_rect(Rect::new(0, 0, 4, 4), Color::rgba(255, 255, 255, 128));
99        let once = t.at(0, 0);
100        t.canvas()
101            .fill_rect(Rect::new(0, 0, 4, 4), Color::rgba(255, 255, 255, 128));
102        let twice = t.at(0, 0);
103        assert!(twice > once, "second coat must lighten further");
104        assert!(twice < 0xFFFF_FFFF, "two half coats must not reach opaque");
105    }
106
107    #[test]
108    fn stroke_corners_are_not_composited_twice() {
109        let mut t = TestCanvas::new(16, 16);
110        t.canvas()
111            .stroke_rect(Rect::new(0, 0, 16, 16), 2, Color::rgba(255, 255, 255, 128));
112        // A corner pixel and an edge pixel get exactly one coat each.
113        assert_eq!(t.at(0, 0), t.at(8, 0));
114        assert_eq!(t.at(0, 0), t.at(0, 8));
115    }
116
117    #[test]
118    fn stroke_leaves_the_interior_alone() {
119        let mut t = TestCanvas::new(16, 16);
120        t.canvas()
121            .stroke_rect(Rect::new(2, 2, 12, 12), 3, Color::WHITE);
122        assert_eq!(t.at(2, 2), 0xFFFF_FFFF);
123        assert_eq!(t.at(4, 4), 0xFFFF_FFFF);
124        assert_eq!(t.at(5, 5), 0, "interior must be untouched");
125        assert_eq!(t.at(1, 1), 0, "outside must be untouched");
126    }
127
128    #[test]
129    fn stroke_thicker_than_the_rect_becomes_a_fill() {
130        let mut t = TestCanvas::new(8, 8);
131        t.canvas()
132            .stroke_rect(Rect::new(1, 1, 4, 4), 9, Color::WHITE);
133        assert_eq!(t.at(2, 2), 0xFFFF_FFFF);
134        assert_eq!(t.at(0, 0), 0);
135    }
136}