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