Skip to main content

denise_render/
canvas.rs

1//! The drawing target: a borrowed frame plus a clip rectangle.
2
3use denise::{Color, Frame, PixelFormat, Rect, Size};
4
5use crate::blend::{Paint, blend_pixel, blend_span};
6
7/// A read-only view of somebody else's pixels, for [`Canvas::copy_from`].
8#[derive(Clone, Copy, Debug)]
9pub struct PixelView<'a> {
10    pixels: &'a [u32],
11    size: Size,
12    stride: usize,
13}
14
15impl<'a> PixelView<'a> {
16    /// Wraps a pixel slice. Returns `None` if `pixels` is too small for the
17    /// geometry, or if `stride` is narrower than `size.width`.
18    pub fn new(pixels: &'a [u32], size: Size, stride: u32) -> Option<Self> {
19        if size.is_empty() || stride < size.width {
20            return None;
21        }
22        // `required_words` computes in `u64` on purpose; see its documentation for
23        // the 32-bit wrap this avoids.
24        let required = denise::required_words(size, stride);
25        let stride = stride as usize;
26        (pixels.len() as u64 >= required).then_some(Self {
27            pixels,
28            size,
29            stride,
30        })
31    }
32
33    /// Borrows a frame's pixels for reading.
34    pub fn from_frame(frame: &'a Frame<'_>) -> Option<Self> {
35        Self::new(frame.pixels(), frame.size(), frame.stride())
36    }
37
38    /// Visible extent.
39    #[inline]
40    pub const fn size(&self) -> Size {
41        self.size
42    }
43
44    #[inline]
45    pub(crate) fn row(&self, y: i32, x0: i32, x1: i32) -> Option<&[u32]> {
46        if y < 0 || y >= self.size.height as i32 || x0 >= x1 || x0 < 0 {
47            return None;
48        }
49        let base = y as usize * self.stride;
50        self.pixels.get(base + x0 as usize..base + x1 as usize)
51    }
52}
53
54/// A clipped, writable pixel target.
55///
56/// Every operation is clipped to [`Canvas::clip`], which starts as the whole frame
57/// and only ever shrinks. Clipping is rectangular: that covers scrolling regions,
58/// damage-restricted repaint and nested panels, which is all a UI actually needs.
59/// Arbitrary clip shapes are not planned.
60///
61/// Coordinates are physical pixels relative to the frame origin, never relative to
62/// the clip.
63#[derive(Debug)]
64pub struct Canvas<'a> {
65    pixels: &'a mut [u32],
66    size: Size,
67    stride: usize,
68    format: PixelFormat,
69    clip: Rect,
70}
71
72impl<'a> Canvas<'a> {
73    /// Borrows a frame for drawing, clipped to the whole frame.
74    pub fn new(frame: &'a mut Frame<'_>) -> Self {
75        let size = frame.size();
76        let stride = frame.stride() as usize;
77        let format = frame.format();
78        Self {
79            pixels: frame.pixels_mut(),
80            size,
81            stride,
82            format,
83            clip: Rect::from_size(size),
84        }
85    }
86
87    /// Wraps a raw pixel slice. Returns `None` if it is too small for the geometry.
88    ///
89    /// Prefer [`Canvas::new`]; this exists for offscreen buffers and benchmarks that
90    /// have no [`Frame`] to hand.
91    pub fn from_pixels(
92        pixels: &'a mut [u32],
93        size: Size,
94        stride: u32,
95        format: PixelFormat,
96    ) -> Option<Self> {
97        if size.is_empty() || stride < size.width {
98            return None;
99        }
100        let stride = stride as usize;
101        let required = stride * (size.height as usize - 1) + size.width as usize;
102        (pixels.len() >= required).then_some(Self {
103            pixels,
104            size,
105            stride,
106            format,
107            clip: Rect::from_size(size),
108        })
109    }
110
111    /// Full extent of the underlying frame.
112    #[inline]
113    pub const fn size(&self) -> Size {
114        self.size
115    }
116
117    /// Word layout of the target.
118    #[inline]
119    pub const fn format(&self) -> PixelFormat {
120        self.format
121    }
122
123    /// The region operations are currently restricted to.
124    #[inline]
125    pub const fn clip(&self) -> Rect {
126        self.clip
127    }
128
129    /// Narrows the clip in place. Never widens it.
130    pub fn clip_to(&mut self, rect: Rect) {
131        self.clip = self.clip.intersect(&rect).unwrap_or(Rect::ZERO);
132    }
133
134    /// A canvas over the same pixels with a tighter clip.
135    ///
136    /// The borrow ends when the returned canvas is dropped, so this is how a parent
137    /// hands a child a region to draw in without either being able to escape it.
138    pub fn with_clip(&mut self, rect: Rect) -> Canvas<'_> {
139        let clip = self.clip.intersect(&rect).unwrap_or(Rect::ZERO);
140        Canvas {
141            pixels: self.pixels,
142            size: self.size,
143            stride: self.stride,
144            format: self.format,
145            clip,
146        }
147    }
148
149    /// Returns `true` if the clip admits no pixels, so drawing can be skipped.
150    #[inline]
151    pub const fn is_clipped_out(&self) -> bool {
152        self.clip.is_empty()
153    }
154
155    /// The clipped, visible part of `rect`.
156    #[inline]
157    pub fn visible(&self, rect: Rect) -> Option<Rect> {
158        self.clip.intersect(&rect)
159    }
160
161    /// A writable span of row `y` from `x0` to `x1`, clipped. `None` if empty.
162    #[inline]
163    pub(crate) fn row_span(&mut self, y: i32, x0: i32, x1: i32) -> Option<&mut [u32]> {
164        if y < self.clip.y || y >= self.clip.bottom() {
165            return None;
166        }
167        let x0 = x0.max(self.clip.x);
168        let x1 = x1.min(self.clip.right());
169        if x0 >= x1 {
170            return None;
171        }
172        let base = y as usize * self.stride;
173        Some(&mut self.pixels[base + x0 as usize..base + x1 as usize])
174    }
175
176    /// Composites a paint over one pixel at `coverage` (`0..=255`), clipped.
177    #[inline]
178    pub(crate) fn blend_at(&mut self, x: i32, y: i32, paint: Paint, coverage: u32) {
179        if coverage == 0 {
180            return;
181        }
182        if let Some(span) = self.row_span(y, x, x + 1)
183            && let Some(px) = span.first_mut()
184        {
185            blend_pixel(px, paint, coverage);
186        }
187    }
188
189    /// Fills the entire clip with an opaque colour.
190    ///
191    /// This is the full-frame clear when the clip is untouched, and the
192    /// damage-restricted clear when it is not.
193    pub fn clear(&mut self, color: Color) {
194        let paint = Paint::new(Color::rgb(color.r, color.g, color.b));
195        let clip = self.clip;
196        for y in clip.y..clip.bottom() {
197            if let Some(span) = self.row_span(y, clip.x, clip.right()) {
198                blend_span(span, paint);
199            }
200        }
201    }
202
203    /// Copies matching regions out of another buffer.
204    ///
205    /// Source and destination coordinates are the same, so this is the
206    /// damage-driven "publish what changed" blit a double-buffered backend needs —
207    /// not a general blitter. Regions are clipped to both buffers.
208    pub fn copy_from(&mut self, src: &PixelView<'_>, regions: &[Rect]) {
209        let bounds = Rect::from_size(Size::new(
210            self.size.width.min(src.size.width),
211            self.size.height.min(src.size.height),
212        ));
213        for region in regions {
214            let Some(r) = region.intersect(&bounds) else {
215                continue;
216            };
217            for y in r.y..r.bottom() {
218                let Some(source) = src.row(y, r.x, r.right()) else {
219                    continue;
220                };
221                // Re-clip through row_span so the canvas clip still applies, then
222                // trim the source to whatever survived.
223                if let Some(dst) = self.row_span(y, r.x, r.right()) {
224                    let n = dst.len().min(source.len());
225                    dst[..n].copy_from_slice(&source[..n]);
226                }
227            }
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::testing::TestCanvas;
236
237    #[test]
238    fn clip_only_narrows() {
239        let mut t = TestCanvas::new(16, 16);
240        let mut c = t.canvas();
241        c.clip_to(Rect::new(4, 4, 8, 8));
242        c.clip_to(Rect::new(0, 0, 16, 16));
243        assert_eq!(c.clip(), Rect::new(4, 4, 8, 8));
244    }
245
246    #[test]
247    fn disjoint_clip_is_empty_not_negative() {
248        let mut t = TestCanvas::new(16, 16);
249        {
250            let mut c = t.canvas();
251            c.clip_to(Rect::new(0, 0, 4, 4));
252            c.clip_to(Rect::new(8, 8, 4, 4));
253            assert!(c.is_clipped_out());
254            c.clear(Color::WHITE);
255        }
256        assert!(t.pixels().iter().all(|&p| p == 0));
257    }
258
259    #[test]
260    fn nested_clip_borrow_restores_the_parent() {
261        let mut t = TestCanvas::new(16, 16);
262        let mut c = t.canvas();
263        {
264            let mut child = c.with_clip(Rect::new(0, 0, 4, 4));
265            child.clear(Color::WHITE);
266        }
267        assert_eq!(c.clip(), Rect::new(0, 0, 16, 16));
268    }
269
270    #[test]
271    fn clear_respects_the_clip_and_the_stride() {
272        let mut t = TestCanvas::with_stride(10, 4, 16);
273        {
274            let mut c = t.canvas();
275            c.clip_to(Rect::new(2, 1, 4, 2));
276            c.clear(Color::WHITE);
277        }
278        for (y, row) in t.pixels().chunks(16).enumerate() {
279            for (x, &px) in row.iter().enumerate() {
280                let inside = (2..6).contains(&x) && (1..3).contains(&y);
281                assert_eq!(px == 0xFFFF_FFFF, inside, "at {x},{y}");
282            }
283        }
284    }
285
286    #[test]
287    fn copy_from_moves_only_the_listed_regions() {
288        let mut source = TestCanvas::new(8, 8);
289        source.canvas().clear(Color::from_argb8888(0xFFAA_BBCC));
290
291        let mut t = TestCanvas::new(8, 8);
292        t.canvas()
293            .copy_from(&source.view(), &[Rect::new(2, 2, 3, 3)]);
294
295        for y in 0..8 {
296            for x in 0..8 {
297                let inside = (2..5).contains(&x) && (2..5).contains(&y);
298                let expected = if inside { 0xFFAA_BBCC } else { 0 };
299                assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
300            }
301        }
302    }
303
304    #[test]
305    fn copy_from_clips_to_the_smaller_buffer() {
306        let mut source = TestCanvas::new(4, 4);
307        source.canvas().clear(Color::from_argb8888(0xFFAA_BBCC));
308
309        let mut t = TestCanvas::new(8, 8);
310        t.canvas()
311            .copy_from(&source.view(), &[Rect::new(0, 0, 8, 8)]);
312
313        assert_eq!(t.pixels()[0], 0xFFAA_BBCC);
314        assert_eq!(t.pixels()[3], 0xFFAA_BBCC);
315        assert_eq!(t.pixels()[4], 0);
316        assert_eq!(t.pixels()[4 * 8], 0);
317    }
318
319    #[test]
320    fn pixel_view_rejects_undersized_slices() {
321        let pixels = [0u32; 10];
322        assert!(PixelView::new(&pixels, Size::new(4, 4), 4).is_none());
323        assert!(PixelView::new(&pixels, Size::new(4, 2), 2).is_none());
324    }
325}