Skip to main content

denise_render/
canvas.rs

1//! The drawing target: a borrowed frame plus a clip rectangle.
2
3pub use denise::PixelView;
4
5use denise::{Color, Frame, PixelFormat, Rect, Size};
6
7use crate::blend::{Paint, blend_pixel, blend_span};
8
9/// A clipped, writable pixel target.
10///
11/// Every operation is clipped to [`Canvas::clip`], which starts as the whole frame
12/// and only ever shrinks. Clipping is rectangular: that covers scrolling regions,
13/// damage-restricted repaint and nested panels, which is all a UI actually needs.
14/// Arbitrary clip shapes are not planned.
15///
16/// Coordinates are physical pixels relative to the frame origin, never relative to
17/// the clip.
18#[derive(Debug)]
19pub struct Canvas<'a> {
20    pixels: &'a mut [u32],
21    size: Size,
22    stride: usize,
23    format: PixelFormat,
24    clip: Rect,
25}
26
27impl<'a> Canvas<'a> {
28    /// Borrows a frame for drawing, clipped to the whole frame.
29    pub fn new(frame: &'a mut Frame<'_>) -> Self {
30        let size = frame.size();
31        let stride = frame.stride() as usize;
32        let format = frame.format();
33        Self {
34            pixels: frame.pixels_mut(),
35            size,
36            stride,
37            format,
38            clip: Rect::from_size(size),
39        }
40    }
41
42    /// Wraps a raw pixel slice. Returns `None` if it is too small for the geometry.
43    ///
44    /// Prefer [`Canvas::new`]; this exists for offscreen buffers and benchmarks that
45    /// have no [`Frame`] to hand.
46    pub fn from_pixels(
47        pixels: &'a mut [u32],
48        size: Size,
49        stride: u32,
50        format: PixelFormat,
51    ) -> Option<Self> {
52        if size.is_empty() || stride < size.width {
53            return None;
54        }
55        let stride = stride as usize;
56        let required = stride * (size.height as usize - 1) + size.width as usize;
57        (pixels.len() >= required).then_some(Self {
58            pixels,
59            size,
60            stride,
61            format,
62            clip: Rect::from_size(size),
63        })
64    }
65
66    /// A [`Pen`](crate::Pen) drawing through this canvas.
67    ///
68    /// The bridge from a concrete rasteriser to the painter-agnostic API widgets
69    /// and the text engine take.
70    #[inline]
71    pub fn pen(&mut self) -> crate::Pen<'_> {
72        crate::Pen::new(self)
73    }
74
75    /// Full extent of the underlying frame.
76    #[inline]
77    pub const fn size(&self) -> Size {
78        self.size
79    }
80
81    /// Word layout of the target.
82    #[inline]
83    pub const fn format(&self) -> PixelFormat {
84        self.format
85    }
86
87    /// The region operations are currently restricted to.
88    #[inline]
89    pub const fn clip(&self) -> Rect {
90        self.clip
91    }
92
93    /// Narrows the clip in place. Never widens it.
94    pub fn clip_to(&mut self, rect: Rect) {
95        self.clip = self.clip.intersect(&rect).unwrap_or(Rect::ZERO);
96    }
97
98    /// Puts back a clip that [`Painter::push_clip`](crate::Painter::push_clip)
99    /// narrowed.
100    ///
101    /// The one operation that may widen, which is why it is not public: the only
102    /// way to reach it is through a [`ClipToken`](crate::ClipToken), and the only
103    /// way to hold one of those is to have narrowed the clip first.
104    #[inline]
105    pub(crate) fn restore_clip(&mut self, rect: Rect) {
106        self.clip = rect;
107    }
108
109    /// A canvas over the same pixels with a tighter clip.
110    ///
111    /// The borrow ends when the returned canvas is dropped, so this is how a parent
112    /// hands a child a region to draw in without either being able to escape it.
113    pub fn with_clip(&mut self, rect: Rect) -> Canvas<'_> {
114        let clip = self.clip.intersect(&rect).unwrap_or(Rect::ZERO);
115        Canvas {
116            pixels: self.pixels,
117            size: self.size,
118            stride: self.stride,
119            format: self.format,
120            clip,
121        }
122    }
123
124    /// Returns `true` if the clip admits no pixels, so drawing can be skipped.
125    #[inline]
126    pub const fn is_clipped_out(&self) -> bool {
127        self.clip.is_empty()
128    }
129
130    /// The clipped, visible part of `rect`.
131    #[inline]
132    pub fn visible(&self, rect: Rect) -> Option<Rect> {
133        self.clip.intersect(&rect)
134    }
135
136    /// A writable span of row `y` from `x0` to `x1`, clipped. `None` if empty.
137    #[inline]
138    pub(crate) fn row_span(&mut self, y: i32, x0: i32, x1: i32) -> Option<&mut [u32]> {
139        if y < self.clip.y || y >= self.clip.bottom() {
140            return None;
141        }
142        let x0 = x0.max(self.clip.x);
143        let x1 = x1.min(self.clip.right());
144        if x0 >= x1 {
145            return None;
146        }
147        let base = y as usize * self.stride;
148        Some(&mut self.pixels[base + x0 as usize..base + x1 as usize])
149    }
150
151    /// Composites a paint over one pixel at `coverage` (`0..=255`), clipped.
152    #[inline]
153    pub(crate) fn blend_at(&mut self, x: i32, y: i32, paint: Paint, coverage: u32) {
154        if coverage == 0 {
155            return;
156        }
157        if let Some(span) = self.row_span(y, x, x + 1)
158            && let Some(px) = span.first_mut()
159        {
160            blend_pixel(px, paint, coverage);
161        }
162    }
163
164    /// Moves the pixels inside `rect` up by `dy` rows, or down by `-dy` rows
165    /// when `dy` is negative, within the clip, leaving the rows that came into
166    /// view as they were. [`Painter::scroll_rows`](denise::Painter::scroll_rows)
167    /// for a buffer of words, which is the one kind of target that can: a
168    /// `copy_within` per row, top to bottom when the content moves up and
169    /// bottom to top when it moves down, so the destination always trails the
170    /// source. Answers `false` only when there is nothing to copy — no rows
171    /// inside the clip, or a move further than the rectangle is tall.
172    pub fn scroll_rows(&mut self, rect: Rect, dy: i32) -> bool {
173        let Some(rect) = rect
174            .intersect(&self.clip)
175            .and_then(|r| r.intersect(&Rect::from_size(self.size)))
176        else {
177            return false;
178        };
179        let shift = dy.unsigned_abs() as usize;
180        let (width, height) = (rect.width as usize, rect.height as usize);
181        if dy == 0 || shift >= height || width == 0 {
182            return false;
183        }
184        let stride = self.stride;
185        let (left, top) = (rect.x as usize, rect.y as usize);
186        // The rectangle is inside the surface and the stride covers a row, so
187        // this holds; it is checked because a panic here would be a panic in
188        // the paint.
189        if (top + height - 1) * stride + left + width > self.pixels.len() {
190            return false;
191        }
192        if dy > 0 {
193            // The content moved up, so row `y` takes what row `y + shift` had.
194            for row in 0..height - shift {
195                let from = (top + row + shift) * stride + left;
196                self.pixels
197                    .copy_within(from..from + width, (top + row) * stride + left);
198            }
199        } else {
200            for row in (shift..height).rev() {
201                let from = (top + row - shift) * stride + left;
202                self.pixels
203                    .copy_within(from..from + width, (top + row) * stride + left);
204            }
205        }
206        true
207    }
208
209    /// Fills the entire clip with an opaque colour.
210    ///
211    /// This is the full-frame clear when the clip is untouched, and the
212    /// damage-restricted clear when it is not.
213    pub fn clear(&mut self, color: Color) {
214        let paint = Paint::new(Color::rgb(color.r, color.g, color.b));
215        let clip = self.clip;
216        for y in clip.y..clip.bottom() {
217            if let Some(span) = self.row_span(y, clip.x, clip.right()) {
218                blend_span(span, paint);
219            }
220        }
221    }
222
223    /// Copies matching regions out of another buffer.
224    ///
225    /// Source and destination coordinates are the same, so this is the
226    /// damage-driven "publish what changed" blit a double-buffered backend needs —
227    /// not a general blitter. Regions are clipped to both buffers.
228    pub fn copy_from(&mut self, src: &PixelView<'_>, regions: &[Rect]) {
229        let bounds = Rect::from_size(Size::new(
230            self.size.width.min(src.size().width),
231            self.size.height.min(src.size().height),
232        ));
233        for region in regions {
234            let Some(r) = region.intersect(&bounds) else {
235                continue;
236            };
237            for y in r.y..r.bottom() {
238                let Some(source) = src.row(y, r.x, r.right()) else {
239                    continue;
240                };
241                // Re-clip through row_span so the canvas clip still applies, then
242                // trim the source to whatever survived.
243                if let Some(dst) = self.row_span(y, r.x, r.right()) {
244                    let n = dst.len().min(source.len());
245                    dst[..n].copy_from_slice(&source[..n]);
246                }
247            }
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::testing::TestCanvas;
256
257    /// Rows moved by a scroll land exactly where a repaint would put them, in
258    /// both directions, and rows outside the rectangle and the clip stay.
259    #[test]
260    fn scrolled_rows_move_within_the_rectangle_and_the_clip() {
261        let mut t = TestCanvas::with_stride(8, 8, 10);
262        {
263            let mut c = t.canvas();
264            for y in 0..8 {
265                c.fill_rect(Rect::new(0, y, 8, 1), Color::rgb(y as u8, 0, 0));
266            }
267            // Rows 1..7 of columns 2..6, moved up two: row 1 takes row 3.
268            assert!(c.scroll_rows(Rect::new(2, 1, 4, 6), 2));
269        }
270        let row = |t: &TestCanvas, x: usize, y: usize| (t.pixels()[y * 10 + x] >> 16) & 0xFF;
271        assert_eq!(row(&t, 3, 1), 3);
272        assert_eq!(row(&t, 3, 4), 6);
273        // The two rows that came into view are left as they were.
274        assert_eq!(row(&t, 3, 5), 5);
275        assert_eq!(row(&t, 3, 6), 6);
276        // Outside the rectangle nothing moved.
277        assert_eq!(row(&t, 0, 1), 1);
278        assert_eq!(row(&t, 3, 0), 0);
279        assert_eq!(row(&t, 3, 7), 7);
280
281        // Down by one inside a clip narrower than the rectangle: only the
282        // clipped columns move, and the row that came into view at the top
283        // keeps what it had.
284        {
285            let mut c = t.canvas();
286            c.clip_to(Rect::new(0, 0, 4, 8));
287            assert!(c.scroll_rows(Rect::new(0, 0, 8, 8), -1));
288        }
289        assert_eq!(row(&t, 1, 2), 1);
290        assert_eq!(row(&t, 1, 0), 0);
291        assert_eq!(row(&t, 6, 2), 2);
292
293        // Nothing to copy is not a move.
294        let mut c = t.canvas();
295        assert!(!c.scroll_rows(Rect::new(0, 0, 8, 8), 8));
296        assert!(!c.scroll_rows(Rect::new(0, 0, 8, 8), 0));
297        assert!(!c.scroll_rows(Rect::new(20, 0, 8, 8), 1));
298    }
299
300    #[test]
301    fn clip_only_narrows() {
302        let mut t = TestCanvas::new(16, 16);
303        let mut c = t.canvas();
304        c.clip_to(Rect::new(4, 4, 8, 8));
305        c.clip_to(Rect::new(0, 0, 16, 16));
306        assert_eq!(c.clip(), Rect::new(4, 4, 8, 8));
307    }
308
309    #[test]
310    fn disjoint_clip_is_empty_not_negative() {
311        let mut t = TestCanvas::new(16, 16);
312        {
313            let mut c = t.canvas();
314            c.clip_to(Rect::new(0, 0, 4, 4));
315            c.clip_to(Rect::new(8, 8, 4, 4));
316            assert!(c.is_clipped_out());
317            c.clear(Color::WHITE);
318        }
319        assert!(t.pixels().iter().all(|&p| p == 0));
320    }
321
322    #[test]
323    fn nested_clip_borrow_restores_the_parent() {
324        let mut t = TestCanvas::new(16, 16);
325        let mut c = t.canvas();
326        {
327            let mut child = c.with_clip(Rect::new(0, 0, 4, 4));
328            child.clear(Color::WHITE);
329        }
330        assert_eq!(c.clip(), Rect::new(0, 0, 16, 16));
331    }
332
333    #[test]
334    fn clear_respects_the_clip_and_the_stride() {
335        let mut t = TestCanvas::with_stride(10, 4, 16);
336        {
337            let mut c = t.canvas();
338            c.clip_to(Rect::new(2, 1, 4, 2));
339            c.clear(Color::WHITE);
340        }
341        for (y, row) in t.pixels().chunks(16).enumerate() {
342            for (x, &px) in row.iter().enumerate() {
343                let inside = (2..6).contains(&x) && (1..3).contains(&y);
344                assert_eq!(px == 0xFFFF_FFFF, inside, "at {x},{y}");
345            }
346        }
347    }
348
349    #[test]
350    fn copy_from_moves_only_the_listed_regions() {
351        let mut source = TestCanvas::new(8, 8);
352        source.canvas().clear(Color::from_argb8888(0xFFAA_BBCC));
353
354        let mut t = TestCanvas::new(8, 8);
355        t.canvas()
356            .copy_from(&source.view(), &[Rect::new(2, 2, 3, 3)]);
357
358        for y in 0..8 {
359            for x in 0..8 {
360                let inside = (2..5).contains(&x) && (2..5).contains(&y);
361                let expected = if inside { 0xFFAA_BBCC } else { 0 };
362                assert_eq!(t.pixels()[y * 8 + x], expected, "at {x},{y}");
363            }
364        }
365    }
366
367    #[test]
368    fn copy_from_clips_to_the_smaller_buffer() {
369        let mut source = TestCanvas::new(4, 4);
370        source.canvas().clear(Color::from_argb8888(0xFFAA_BBCC));
371
372        let mut t = TestCanvas::new(8, 8);
373        t.canvas()
374            .copy_from(&source.view(), &[Rect::new(0, 0, 8, 8)]);
375
376        assert_eq!(t.pixels()[0], 0xFFAA_BBCC);
377        assert_eq!(t.pixels()[3], 0xFFAA_BBCC);
378        assert_eq!(t.pixels()[4], 0);
379        assert_eq!(t.pixels()[4 * 8], 0);
380    }
381
382    #[test]
383    fn pixel_view_rejects_undersized_slices() {
384        let pixels = [0u32; 10];
385        assert!(PixelView::new(&pixels, Size::new(4, 4), 4).is_none());
386        assert!(PixelView::new(&pixels, Size::new(4, 2), 2).is_none());
387    }
388}