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