1use denise::Rect;
10
11use crate::blend::{Paint, blend_span};
12use crate::canvas::Canvas;
13pub(crate) use denise::angle::{COORD_LIMIT, ONE, to_fx};
14
15pub(crate) const SUBSAMPLES: usize = 4;
18
19pub(crate) const SUB_STEP: i32 = ONE / SUBSAMPLES as i32;
21
22#[inline]
23pub(crate) fn floor_px(v: i32) -> i32 {
24 v.div_euclid(ONE)
25}
26
27#[inline]
28pub(crate) fn ceil_px(v: i32) -> i32 {
29 (v + ONE - 1).div_euclid(ONE)
30}
31
32#[inline]
34fn arc_half_width(radius: i32, dy: i32) -> i32 {
35 let r2 = (radius as i64 * radius as i64) as u64;
36 let d2 = (dy as i64 * dy as i64) as u64;
37 r2.saturating_sub(d2).isqrt() as i32
38}
39
40#[derive(Clone, Copy, Debug)]
43pub(crate) struct Scan {
44 pub(crate) left: [i32; SUBSAMPLES],
45 pub(crate) right: [i32; SUBSAMPLES],
46}
47
48impl Scan {
49 pub(crate) fn new(rect: Rect, radius: i32, y: i32) -> Self {
51 let rad = to_fx(radius);
52 let top = to_fx(rect.y);
53 let bottom = to_fx(rect.bottom());
54 let left_edge = to_fx(rect.x);
55 let right_edge = to_fx(rect.right());
56
57 let mut left = [0; SUBSAMPLES];
58 let mut right = [0; SUBSAMPLES];
59
60 for k in 0..SUBSAMPLES {
61 let sy = to_fx(y) + k as i32 * SUB_STEP + SUB_STEP / 2;
62
63 let from_top = sy - top;
67 let from_bottom = bottom - sy;
68 let dy = if from_top < rad {
69 rad - from_top
70 } else if from_bottom < rad {
71 rad - from_bottom
72 } else {
73 0
74 };
75
76 let inset = if dy > 0 {
77 rad - arc_half_width(rad, dy)
78 } else {
79 0
80 };
81
82 left[k] = left_edge + inset;
83 right[k] = right_edge - inset;
84 }
85
86 Scan { left, right }
87 }
88
89 pub(crate) fn coverage(&self, x: i32) -> u32 {
91 let px0 = to_fx(x);
92 let px1 = px0 + ONE;
93 let mut covered: i32 = 0;
94 for k in 0..SUBSAMPLES {
95 let l = self.left[k].max(px0);
96 let r = self.right[k].min(px1);
97 covered += (r - l).max(0);
98 }
99 let total = ONE as u32 * SUBSAMPLES as u32;
103 ((covered as u32 * 255 + total / 2) / total).min(255)
104 }
105
106 #[inline]
107 pub(crate) fn min_left(&self) -> i32 {
108 *self.left.iter().min().expect("SUBSAMPLES > 0")
109 }
110
111 #[inline]
112 pub(crate) fn max_left(&self) -> i32 {
113 *self.left.iter().max().expect("SUBSAMPLES > 0")
114 }
115
116 #[inline]
117 pub(crate) fn min_right(&self) -> i32 {
118 *self.right.iter().min().expect("SUBSAMPLES > 0")
119 }
120
121 #[inline]
122 pub(crate) fn max_right(&self) -> i32 {
123 *self.right.iter().max().expect("SUBSAMPLES > 0")
124 }
125}
126
127struct Coverage<'a> {
129 outer: &'a Scan,
130 inner: Option<&'a Scan>,
131}
132
133impl Coverage<'_> {
134 #[inline]
135 fn at(&self, x: i32) -> u32 {
136 let outer = self.outer.coverage(x);
137 match self.inner {
138 None => outer,
139 Some(inner) => {
140 let hole = inner.coverage(x);
141 if hole == 0 {
142 outer
143 } else {
144 (outer * (255 - hole) + 127) / 255
147 }
148 }
149 }
150 }
151}
152
153impl Canvas<'_> {
154 pub fn fill_rounded_rect(&mut self, rect: Rect, radius: i32, color: impl Into<Paint>) {
159 let paint = color.into();
160 if paint.is_invisible() || rect.is_empty() {
161 return;
162 }
163 let radius = radius.clamp(0, rect.width.min(rect.height) / 2);
164 if radius == 0 {
165 self.fill_rect(rect, paint);
166 return;
167 }
168 let Some(vis) = self.visible(rect) else {
169 return;
170 };
171
172 for y in vis.y..vis.bottom() {
173 let outer = Scan::new(rect, radius, y);
174 let cov = Coverage {
175 outer: &outer,
176 inner: None,
177 };
178 self.emit_run(
179 y,
180 floor_px(outer.min_left()),
181 ceil_px(outer.max_right()),
182 Some((ceil_px(outer.max_left()), floor_px(outer.min_right()))),
183 &cov,
184 paint,
185 );
186 }
187 }
188
189 pub fn stroke_rounded_rect(
194 &mut self,
195 rect: Rect,
196 radius: i32,
197 thickness: i32,
198 color: impl Into<Paint>,
199 ) {
200 let paint = color.into();
201 let t = thickness.max(0);
202 if t == 0 || rect.is_empty() || paint.is_invisible() {
203 return;
204 }
205 let radius = radius.clamp(0, rect.width.min(rect.height) / 2);
206 if t * 2 >= rect.width.min(rect.height) {
207 self.fill_rounded_rect(rect, radius, paint);
208 return;
209 }
210
211 let inner = Rect::new(
212 rect.x + t,
213 rect.y + t,
214 rect.width - 2 * t,
215 rect.height - 2 * t,
216 );
217 let inner_radius = (radius - t).max(0);
218
219 let Some(vis) = self.visible(rect) else {
220 return;
221 };
222
223 for y in vis.y..vis.bottom() {
224 let outer = Scan::new(rect, radius, y);
225
226 if y < inner.y || y >= inner.bottom() {
228 let cov = Coverage {
229 outer: &outer,
230 inner: None,
231 };
232 self.emit_run(
233 y,
234 floor_px(outer.min_left()),
235 ceil_px(outer.max_right()),
236 Some((ceil_px(outer.max_left()), floor_px(outer.min_right()))),
237 &cov,
238 paint,
239 );
240 continue;
241 }
242
243 let inner_scan = Scan::new(inner, inner_radius, y);
244 let cov = Coverage {
245 outer: &outer,
246 inner: Some(&inner_scan),
247 };
248
249 self.emit_run(
253 y,
254 floor_px(outer.min_left()),
255 ceil_px(inner_scan.max_left()),
256 Some((ceil_px(outer.max_left()), floor_px(inner_scan.min_left()))),
257 &cov,
258 paint,
259 );
260 self.emit_run(
261 y,
262 floor_px(inner_scan.min_right()),
263 ceil_px(outer.max_right()),
264 Some((ceil_px(inner_scan.max_right()), floor_px(outer.min_right()))),
265 &cov,
266 paint,
267 );
268 }
269 }
270
271 fn emit_run(
274 &mut self,
275 y: i32,
276 from: i32,
277 to: i32,
278 solid: Option<(i32, i32)>,
279 cov: &Coverage<'_>,
280 paint: Paint,
281 ) {
282 let clip = self.clip();
283 let from = from.max(clip.x);
284 let to = to.min(clip.right());
285 if from >= to {
286 return;
287 }
288
289 let (s0, s1) = match solid {
290 Some((s0, s1)) if s0 < s1 => (s0.clamp(from, to), s1.clamp(from, to)),
291 _ => (to, to),
292 };
293
294 for x in from..s0 {
295 self.blend_at(x, y, paint, cov.at(x));
296 }
297 if s0 < s1
298 && let Some(span) = self.row_span(y, s0, s1)
299 {
300 blend_span(span, paint);
301 }
302 for x in s1..to {
303 self.blend_at(x, y, paint, cov.at(x));
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::testing::TestCanvas;
312 use denise::Color;
313
314 fn alpha_of(px: u32) -> u32 {
315 px & 0xFF
317 }
318
319 #[test]
320 fn zero_radius_is_exactly_a_fill() {
321 let mut rounded = TestCanvas::new(16, 16);
322 rounded
323 .canvas()
324 .fill_rounded_rect(Rect::new(2, 2, 12, 12), 0, Color::WHITE);
325
326 let mut square = TestCanvas::new(16, 16);
327 square
328 .canvas()
329 .fill_rect(Rect::new(2, 2, 12, 12), Color::WHITE);
330
331 assert_eq!(rounded.pixels(), square.pixels());
332 }
333
334 #[test]
335 fn corners_are_cut_and_the_middle_is_not() {
336 let mut t = TestCanvas::new(32, 32);
337 t.canvas()
338 .fill_rounded_rect(Rect::new(0, 0, 32, 32), 8, Color::WHITE);
339
340 assert_eq!(alpha_of(t.at(0, 0)), 0, "corner must be empty");
341 assert_eq!(alpha_of(t.at(16, 0)), 255, "top edge must be solid");
342 assert_eq!(alpha_of(t.at(0, 16)), 255, "left edge must be solid");
343 assert_eq!(alpha_of(t.at(16, 16)), 255, "centre must be solid");
344 assert_eq!(alpha_of(t.at(31, 31)), 0, "corner must be empty");
345 }
346
347 #[test]
348 fn corners_are_antialiased_not_stepped() {
349 let radius = 10;
350 let mut t = TestCanvas::new(32, 32);
351 t.canvas()
352 .fill_rounded_rect(Rect::new(0, 0, 32, 32), radius, Color::WHITE);
353
354 let partial = (0..radius)
359 .flat_map(|y| (0..radius).map(move |x| (x, y)))
360 .filter(|&(x, y)| (1..255).contains(&alpha_of(t.at(x, y))))
361 .count();
362
363 assert!(
366 partial >= radius as usize,
367 "only {partial} partially covered pixels in a radius-{radius} corner"
368 );
369 }
370
371 #[test]
372 fn coverage_reaches_both_extremes() {
373 let mut t = TestCanvas::new(32, 32);
376 t.canvas()
377 .fill_rounded_rect(Rect::new(0, 0, 32, 32), 10, Color::WHITE);
378 assert_eq!(alpha_of(t.at(0, 0)), 0);
379 assert_eq!(alpha_of(t.at(16, 16)), 255);
380 }
381
382 #[test]
383 fn shape_is_symmetric() {
384 let mut t = TestCanvas::new(32, 24);
385 t.canvas()
386 .fill_rounded_rect(Rect::new(0, 0, 32, 24), 7, Color::WHITE);
387
388 for y in 0..24 {
389 for x in 0..16 {
390 assert_eq!(t.at(x, y), t.at(31 - x, y), "mirror at {x},{y}");
391 }
392 }
393 for y in 0..12 {
394 for x in 0..32 {
395 assert_eq!(t.at(x, y), t.at(x, 23 - y), "flip at {x},{y}");
396 }
397 }
398 }
399
400 #[test]
401 fn radius_is_clamped_to_half_the_shorter_side() {
402 let mut t = TestCanvas::new(40, 20);
404 t.canvas()
405 .fill_rounded_rect(Rect::new(0, 0, 40, 20), 999, Color::WHITE);
406 assert_eq!(alpha_of(t.at(20, 10)), 255);
407 assert_eq!(alpha_of(t.at(0, 0)), 0);
408 assert_eq!(alpha_of(t.at(20, 0)), 255);
409 }
410
411 #[test]
412 fn fill_stays_inside_its_bounds() {
413 let mut t = TestCanvas::new(32, 32);
414 t.canvas()
415 .fill_rounded_rect(Rect::new(8, 8, 16, 16), 4, Color::WHITE);
416 for y in 0..32 {
417 for x in 0..32 {
418 let inside = (8..24).contains(&x) && (8..24).contains(&y);
419 if !inside {
420 assert_eq!(t.at(x, y), 0, "spilled at {x},{y}");
421 }
422 }
423 }
424 }
425
426 #[test]
427 fn clipping_a_rounded_fill_matches_the_unclipped_result() {
428 let region = Rect::new(4, 4, 10, 10);
429
430 let mut full = TestCanvas::new(32, 32);
431 full.canvas()
432 .fill_rounded_rect(Rect::new(2, 2, 24, 24), 6, Color::WHITE);
433
434 let mut clipped = TestCanvas::new(32, 32);
435 {
436 let mut c = clipped.canvas();
437 c.clip_to(region);
438 c.fill_rounded_rect(Rect::new(2, 2, 24, 24), 6, Color::WHITE);
439 }
440
441 for y in 0..32 {
442 for x in 0..32 {
443 let expected = if region.contains(denise::Point::new(x, y)) {
444 full.at(x, y)
445 } else {
446 0
447 };
448 assert_eq!(clipped.at(x, y), expected, "at {x},{y}");
449 }
450 }
451 }
452
453 #[test]
454 fn stroke_leaves_the_interior_alone() {
455 let mut t = TestCanvas::new(32, 32);
456 t.canvas()
457 .stroke_rounded_rect(Rect::new(2, 2, 28, 28), 8, 3, Color::WHITE);
458 assert_eq!(alpha_of(t.at(16, 16)), 0, "interior must be untouched");
459 assert_eq!(alpha_of(t.at(16, 2)), 255, "top band must be solid");
460 assert_eq!(alpha_of(t.at(2, 16)), 255, "left band must be solid");
461 assert_eq!(alpha_of(t.at(16, 6)), 0, "just inside the band");
462 }
463
464 #[test]
465 fn stroke_covers_the_band_without_seams() {
466 let mut t = TestCanvas::new(40, 40);
470 t.canvas()
471 .stroke_rounded_rect(Rect::new(4, 4, 32, 32), 10, 4, Color::WHITE);
472 for x in 4..8 {
473 assert_eq!(alpha_of(t.at(x, 20)), 255, "seam at x={x}");
474 }
475 }
476
477 #[test]
478 fn stroke_thicker_than_the_shape_is_a_fill() {
479 let mut t = TestCanvas::new(32, 32);
480 t.canvas()
481 .stroke_rounded_rect(Rect::new(4, 4, 16, 16), 4, 99, Color::WHITE);
482 assert_eq!(alpha_of(t.at(12, 12)), 255);
483 assert_eq!(alpha_of(t.at(0, 0)), 0);
484 }
485
486 #[test]
487 fn stroke_alpha_does_not_double_up_anywhere() {
488 let mut t = TestCanvas::new(40, 40);
490 t.canvas().stroke_rounded_rect(
491 Rect::new(4, 4, 32, 32),
492 10,
493 3,
494 Color::rgba(255, 255, 255, 128),
495 );
496 let single = alpha_of(t.at(20, 4));
497 for y in 0..40 {
498 for x in 0..40 {
499 assert!(
500 alpha_of(t.at(x, y)) <= single,
501 "double-composited at {x},{y}"
502 );
503 }
504 }
505 }
506
507 #[test]
508 fn degenerate_rects_do_not_panic() {
509 let mut t = TestCanvas::new(16, 16);
510 let mut c = t.canvas();
511 c.fill_rounded_rect(Rect::new(0, 0, 1, 1), 4, Color::WHITE);
512 c.fill_rounded_rect(Rect::new(0, 0, 0, 10), 4, Color::WHITE);
513 c.fill_rounded_rect(Rect::new(-100, -100, 8, 8), 3, Color::WHITE);
514 c.stroke_rounded_rect(Rect::new(0, 0, 2, 2), 1, 1, Color::WHITE);
515 c.stroke_rounded_rect(Rect::new(1_000_000, 0, 8, 8), 3, 1, Color::WHITE);
516 }
517}