1use crate::renderer::Renderer;
8use crate::style::Style;
9use crate::widget::{Color, Rect};
10
11pub const GRADIENT_MAX_STOPS: usize = 4;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum GradientKind {
17 Linear {
20 angle_deg: i16,
23 },
24 Radial {
26 cx_frac: u8,
28 cy_frac: u8,
30 },
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct GradientDesc<'a> {
40 pub kind: GradientKind,
42 pub stops: &'a [(u8, Color)],
44}
45
46impl<'a> GradientDesc<'a> {
47 pub const fn new(kind: GradientKind, stops: &'a [(u8, Color)]) -> Self {
49 Self { kind, stops }
50 }
51
52 pub fn color_at(&self, rect: Rect, x: i32, y: i32) -> Option<Color> {
54 if rect.width <= 0 || rect.height <= 0 || self.stops.is_empty() {
55 return None;
56 }
57 Some(self.color_at_fraction(gradient_position(self.kind, rect, x, y)))
58 }
59
60 fn color_at_fraction(&self, t: u8) -> Color {
61 let stops = &self.stops[..self.stops.len().min(GRADIENT_MAX_STOPS)];
62 if stops.len() == 1 {
63 return stops[0].1;
64 }
65
66 let mut lower: Option<(u8, Color)> = None;
67 let mut upper: Option<(u8, Color)> = None;
68
69 for &(pos, color) in stops {
70 if pos <= t && lower.is_none_or(|(best, _)| pos >= best) {
71 lower = Some((pos, color));
72 }
73 if pos >= t && upper.is_none_or(|(best, _)| pos <= best) {
74 upper = Some((pos, color));
75 }
76 }
77
78 match (lower, upper) {
79 (Some((lp, lc)), Some((up, uc))) if up != lp => {
80 let num = i32::from(t.saturating_sub(lp));
81 let den = i32::from(up - lp);
82 lc.lerp(uc, num, den)
83 }
84 (Some((_, c)), _) => c,
85 (_, Some((_, c))) => c,
86 (None, None) => Color(0, 0, 0, 0),
87 }
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct ShadowDesc {
94 pub offset_x: i16,
96 pub offset_y: i16,
98 pub spread: u8,
100 pub blur: u8,
102 pub color: Color,
104}
105
106fn isqrt(n: u32) -> u32 {
108 if n == 0 {
109 return 0;
110 }
111 let mut x = n;
112 let mut y = x.div_ceil(2);
113 while y < x {
114 x = y;
115 y = (x + n / x) / 2;
116 }
117 x
118}
119
120fn gradient_position(kind: GradientKind, rect: Rect, x: i32, y: i32) -> u8 {
121 match kind {
122 GradientKind::Linear { angle_deg } => linear_gradient_position(angle_deg, rect, x, y),
123 GradientKind::Radial { cx_frac, cy_frac } => {
124 radial_gradient_position(cx_frac, cy_frac, rect, x, y)
125 }
126 }
127}
128
129fn linear_gradient_position(angle_deg: i16, rect: Rect, x: i32, y: i32) -> u8 {
130 let xf = axis_fraction(x - rect.x, rect.width);
131 let yf = axis_fraction(y - rect.y, rect.height);
132 match snapped_octant(angle_deg) {
133 0 => xf,
134 1 => avg_u8(xf, yf),
135 2 => yf,
136 3 => avg_u8(255u8.saturating_sub(xf), yf),
137 4 => 255u8.saturating_sub(xf),
138 5 => avg_u8(255u8.saturating_sub(xf), 255u8.saturating_sub(yf)),
139 6 => 255u8.saturating_sub(yf),
140 _ => avg_u8(xf, 255u8.saturating_sub(yf)),
141 }
142}
143
144fn radial_gradient_position(cx_frac: u8, cy_frac: u8, rect: Rect, x: i32, y: i32) -> u8 {
145 let cx = rect.x + fraction_to_axis(cx_frac, rect.width);
146 let cy = rect.y + fraction_to_axis(cy_frac, rect.height);
147 let dx = (x - cx).unsigned_abs();
148 let dy = (y - cy).unsigned_abs();
149 let dist = isqrt(dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy)));
150
151 let corners = [
152 (rect.x, rect.y),
153 (rect.x + rect.width - 1, rect.y),
154 (rect.x, rect.y + rect.height - 1),
155 (rect.x + rect.width - 1, rect.y + rect.height - 1),
156 ];
157 let mut max_dist = 1u32;
158 for &(corner_x, corner_y) in &corners {
159 let dx = (corner_x - cx).unsigned_abs();
160 let dy = (corner_y - cy).unsigned_abs();
161 max_dist = max_dist.max(isqrt(
162 dx.saturating_mul(dx).saturating_add(dy.saturating_mul(dy)),
163 ));
164 }
165
166 ((dist.min(max_dist) * 255) / max_dist) as u8
167}
168
169fn snapped_octant(angle_deg: i16) -> u8 {
170 let angle = i32::from(angle_deg).rem_euclid(360);
171 (((angle + 22) / 45) % 8) as u8
172}
173
174fn axis_fraction(pos: i32, len: i32) -> u8 {
175 if len <= 1 {
176 return 255;
177 }
178 ((pos.clamp(0, len - 1) * 255) / (len - 1)) as u8
179}
180
181fn fraction_to_axis(frac: u8, len: i32) -> i32 {
182 if len <= 1 {
183 return 0;
184 }
185 (i32::from(frac) * (len - 1)) / 255
186}
187
188fn avg_u8(a: u8, b: u8) -> u8 {
189 ((u16::from(a) + u16::from(b)) / 2) as u8
190}
191
192fn arc_dx(r: i32, dy: i32) -> (i32, u8) {
198 let r4 = r as u32 * 4;
199 let dy4 = dy as u32 * 4 + 2; let sq = r4 * r4;
201 let dysq = dy4 * dy4;
202 if dysq >= sq {
203 return (0, 0);
204 }
205 let dx4 = isqrt(sq - dysq);
206 let dx_int = (dx4 / 4) as i32;
207 let frac = (dx4 % 4) as u8 * 64; (dx_int, frac)
209}
210
211pub fn fill_rounded_rect(renderer: &mut dyn Renderer, rect: Rect, color: Color, radius: u8) {
217 let r = radius as i32;
218 if r == 0 {
219 renderer.fill_rect(rect, color);
220 return;
221 }
222 let r = r.min(rect.width / 2).min(rect.height / 2);
224 if r <= 0 {
225 renderer.fill_rect(rect, color);
226 return;
227 }
228
229 if rect.height - 2 * r > 0 {
231 renderer.fill_rect(
232 Rect {
233 x: rect.x,
234 y: rect.y + r,
235 width: rect.width,
236 height: rect.height - 2 * r,
237 },
238 color,
239 );
240 }
241
242 if rect.width - 2 * r > 0 {
244 renderer.fill_rect(
245 Rect {
246 x: rect.x + r,
247 y: rect.y,
248 width: rect.width - 2 * r,
249 height: r,
250 },
251 color,
252 );
253
254 renderer.fill_rect(
256 Rect {
257 x: rect.x + r,
258 y: rect.y + rect.height - r,
259 width: rect.width - 2 * r,
260 height: r,
261 },
262 color,
263 );
264 }
265
266 let base_alpha = color.3 as u16;
273 for dy in 0..r {
274 let (dx_int, frac) = arc_dx(r, r - 1 - dy);
275
276 if dx_int > 0 {
278 renderer.fill_rect(
280 Rect {
281 x: rect.x + r - dx_int,
282 y: rect.y + dy,
283 width: dx_int,
284 height: 1,
285 },
286 color,
287 );
288 renderer.fill_rect(
290 Rect {
291 x: rect.x + rect.width - r,
292 y: rect.y + dy,
293 width: dx_int,
294 height: 1,
295 },
296 color,
297 );
298 renderer.fill_rect(
300 Rect {
301 x: rect.x + r - dx_int,
302 y: rect.y + rect.height - 1 - dy,
303 width: dx_int,
304 height: 1,
305 },
306 color,
307 );
308 renderer.fill_rect(
310 Rect {
311 x: rect.x + rect.width - r,
312 y: rect.y + rect.height - 1 - dy,
313 width: dx_int,
314 height: 1,
315 },
316 color,
317 );
318 }
319
320 if frac > 0 {
322 let aa_alpha = ((frac as u16 * base_alpha) / 255) as u8;
323 let aa = Color(color.0, color.1, color.2, aa_alpha);
324 renderer.blend_rect(
326 Rect {
327 x: rect.x + r - dx_int - 1,
328 y: rect.y + dy,
329 width: 1,
330 height: 1,
331 },
332 aa,
333 );
334 renderer.blend_rect(
336 Rect {
337 x: rect.x + rect.width - r + dx_int,
338 y: rect.y + dy,
339 width: 1,
340 height: 1,
341 },
342 aa,
343 );
344 renderer.blend_rect(
346 Rect {
347 x: rect.x + r - dx_int - 1,
348 y: rect.y + rect.height - 1 - dy,
349 width: 1,
350 height: 1,
351 },
352 aa,
353 );
354 renderer.blend_rect(
356 Rect {
357 x: rect.x + rect.width - r + dx_int,
358 y: rect.y + rect.height - 1 - dy,
359 width: 1,
360 height: 1,
361 },
362 aa,
363 );
364 }
365 }
366}
367
368pub fn draw_rounded_border(
374 renderer: &mut dyn Renderer,
375 rect: Rect,
376 color: Color,
377 border_width: u8,
378 radius: u8,
379) {
380 let bw = border_width as i32;
381 if bw == 0 {
382 return;
383 }
384
385 let r = radius as i32;
386 if r == 0 {
387 draw_border_straight(renderer, rect, color, border_width);
388 return;
389 }
390
391 let rout = r.min(rect.width / 2).min(rect.height / 2);
392 if rout <= 0 {
393 draw_border_straight(renderer, rect, color, border_width);
394 return;
395 }
396 let rin = (rout - bw).max(0);
397 let base_alpha = color.3 as u16;
398
399 for dy in 0..rout {
404 let axis_dy = rout - 1 - dy;
405 let (out_dx, out_frac) = arc_dx(rout, axis_dy);
406 let (in_dx, in_frac) = if rin > 0 {
407 let (d, f) = arc_dx(rin, axis_dy);
408 (d, f)
410 } else {
411 (0i32, 0u8)
412 };
413
414 let ring_w = out_dx - in_dx;
416 if ring_w > 0 {
417 renderer.fill_rect(
419 Rect {
420 x: rect.x + rout - out_dx,
421 y: rect.y + dy,
422 width: ring_w,
423 height: 1,
424 },
425 color,
426 );
427 renderer.fill_rect(
429 Rect {
430 x: rect.x + rect.width - rout + in_dx,
431 y: rect.y + dy,
432 width: ring_w,
433 height: 1,
434 },
435 color,
436 );
437 renderer.fill_rect(
439 Rect {
440 x: rect.x + rout - out_dx,
441 y: rect.y + rect.height - 1 - dy,
442 width: ring_w,
443 height: 1,
444 },
445 color,
446 );
447 renderer.fill_rect(
449 Rect {
450 x: rect.x + rect.width - rout + in_dx,
451 y: rect.y + rect.height - 1 - dy,
452 width: ring_w,
453 height: 1,
454 },
455 color,
456 );
457 }
458
459 if out_frac > 0 {
461 let aa_alpha = ((out_frac as u16 * base_alpha) / 255) as u8;
462 let aa = Color(color.0, color.1, color.2, aa_alpha);
463 renderer.blend_rect(
464 Rect {
465 x: rect.x + rout - out_dx - 1,
466 y: rect.y + dy,
467 width: 1,
468 height: 1,
469 },
470 aa,
471 );
472 renderer.blend_rect(
473 Rect {
474 x: rect.x + rect.width - rout + out_dx,
475 y: rect.y + dy,
476 width: 1,
477 height: 1,
478 },
479 aa,
480 );
481 renderer.blend_rect(
482 Rect {
483 x: rect.x + rout - out_dx - 1,
484 y: rect.y + rect.height - 1 - dy,
485 width: 1,
486 height: 1,
487 },
488 aa,
489 );
490 renderer.blend_rect(
491 Rect {
492 x: rect.x + rect.width - rout + out_dx,
493 y: rect.y + rect.height - 1 - dy,
494 width: 1,
495 height: 1,
496 },
497 aa,
498 );
499 }
500
501 if in_dx > 0 && in_frac > 0 {
503 let aa_alpha = (((255 - in_frac as u16) * base_alpha) / 255) as u8;
505 let aa = Color(color.0, color.1, color.2, aa_alpha);
506 renderer.blend_rect(
507 Rect {
508 x: rect.x + rout - in_dx,
509 y: rect.y + dy,
510 width: 1,
511 height: 1,
512 },
513 aa,
514 );
515 renderer.blend_rect(
516 Rect {
517 x: rect.x + rect.width - rout + in_dx - 1,
518 y: rect.y + dy,
519 width: 1,
520 height: 1,
521 },
522 aa,
523 );
524 renderer.blend_rect(
525 Rect {
526 x: rect.x + rout - in_dx,
527 y: rect.y + rect.height - 1 - dy,
528 width: 1,
529 height: 1,
530 },
531 aa,
532 );
533 renderer.blend_rect(
534 Rect {
535 x: rect.x + rect.width - rout + in_dx - 1,
536 y: rect.y + rect.height - 1 - dy,
537 width: 1,
538 height: 1,
539 },
540 aa,
541 );
542 }
543 }
544
545 let straight_h = rect.height - 2 * rout;
547 if straight_h > 0 {
548 renderer.fill_rect(
549 Rect {
550 x: rect.x,
551 y: rect.y + rout,
552 width: bw,
553 height: straight_h,
554 },
555 color,
556 );
557 renderer.fill_rect(
558 Rect {
559 x: rect.x + rect.width - bw,
560 y: rect.y + rout,
561 width: bw,
562 height: straight_h,
563 },
564 color,
565 );
566 }
567
568 let straight_w = rect.width - 2 * rout;
569 if straight_w > 0 {
570 renderer.fill_rect(
571 Rect {
572 x: rect.x + rout,
573 y: rect.y,
574 width: straight_w,
575 height: bw,
576 },
577 color,
578 );
579 renderer.fill_rect(
580 Rect {
581 x: rect.x + rout,
582 y: rect.y + rect.height - bw,
583 width: straight_w,
584 height: bw,
585 },
586 color,
587 );
588 }
589}
590
591pub fn draw_border_straight(renderer: &mut dyn Renderer, rect: Rect, color: Color, width: u8) {
593 let w = width as i32;
594 if w == 0 {
595 return;
596 }
597 renderer.fill_rect(
598 Rect {
599 x: rect.x,
600 y: rect.y,
601 width: rect.width,
602 height: w,
603 },
604 color,
605 );
606 renderer.fill_rect(
607 Rect {
608 x: rect.x,
609 y: rect.y + rect.height - w,
610 width: rect.width,
611 height: w,
612 },
613 color,
614 );
615 renderer.fill_rect(
616 Rect {
617 x: rect.x,
618 y: rect.y + w,
619 width: w,
620 height: rect.height - 2 * w,
621 },
622 color,
623 );
624 renderer.fill_rect(
625 Rect {
626 x: rect.x + rect.width - w,
627 y: rect.y + w,
628 width: w,
629 height: rect.height - 2 * w,
630 },
631 color,
632 );
633}
634
635pub fn draw_widget_bg(renderer: &mut dyn Renderer, rect: Rect, style: &Style) {
648 let bg = style.bg_color.with_alpha(style.alpha);
649 if bg.3 != 0 {
650 if style.radius > 0 {
651 fill_rounded_rect(renderer, rect, bg, style.radius);
652 } else if bg.3 == 255 {
653 renderer.fill_rect(rect, bg);
654 } else {
655 renderer.blend_rect(rect, bg);
656 }
657 }
658 if style.border_width > 0 {
659 let border = style.border_color.with_alpha(style.alpha);
660 if border.3 != 0 {
661 draw_rounded_border(renderer, rect, border, style.border_width, style.radius);
662 }
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 struct RecordRenderer {
671 fill_rects: alloc::vec::Vec<(Rect, Color)>,
672 blend_rects: alloc::vec::Vec<(Rect, Color)>,
673 }
674
675 impl RecordRenderer {
676 fn new() -> Self {
677 Self {
678 fill_rects: alloc::vec::Vec::new(),
679 blend_rects: alloc::vec::Vec::new(),
680 }
681 }
682 }
683
684 impl Renderer for RecordRenderer {
685 fn fill_rect(&mut self, rect: Rect, color: Color) {
686 self.fill_rects.push((rect, color));
687 }
688 fn blend_rect(&mut self, rect: Rect, color: Color) {
689 self.blend_rects.push((rect, color));
690 }
691 fn draw_text(&mut self, _pos: (i32, i32), _text: &str, _color: Color) {}
692 }
693
694 struct CountRenderer {
695 fills: u32,
696 blends: u32,
697 }
698
699 impl Renderer for CountRenderer {
700 fn fill_rect(&mut self, _rect: Rect, _color: Color) {
701 self.fills += 1;
702 }
703 fn blend_rect(&mut self, _rect: Rect, _color: Color) {
704 self.blends += 1;
705 }
706 fn draw_text(&mut self, _pos: (i32, i32), _text: &str, _color: Color) {}
707 }
708
709 #[test]
710 fn zero_radius_single_fill() {
711 let mut r = CountRenderer {
712 fills: 0,
713 blends: 0,
714 };
715 let rect = Rect {
716 x: 0,
717 y: 0,
718 width: 100,
719 height: 50,
720 };
721 fill_rounded_rect(&mut r, rect, Color(0, 0, 0, 255), 0);
722 assert_eq!(r.fills, 1);
723 assert_eq!(r.blends, 0);
724 }
725
726 #[test]
727 fn radius_clamped_for_pill_shape() {
728 let mut r = CountRenderer {
729 fills: 0,
730 blends: 0,
731 };
732 let rect = Rect {
733 x: 0,
734 y: 0,
735 width: 40,
736 height: 20,
737 };
738 fill_rounded_rect(&mut r, rect, Color(0, 0, 0, 255), 30);
739 assert!(r.fills > 1, "expected corners, got {} fills", r.fills);
740 }
741
742 #[test]
743 fn aa_fringe_produces_blend_calls() {
744 let mut r = CountRenderer {
745 fills: 0,
746 blends: 0,
747 };
748 let rect = Rect {
749 x: 0,
750 y: 0,
751 width: 100,
752 height: 100,
753 };
754 fill_rounded_rect(&mut r, rect, Color(255, 0, 0, 255), 10);
755 assert!(r.blends > 0, "expected AA blend calls, got 0");
756 }
757
758 #[test]
759 fn rounded_border_produces_ring() {
760 let mut r = RecordRenderer::new();
761 let rect = Rect {
762 x: 0,
763 y: 0,
764 width: 60,
765 height: 60,
766 };
767 draw_rounded_border(&mut r, rect, Color(0, 0, 0, 255), 2, 8);
768 assert!(!r.fill_rects.is_empty(), "expected border fills");
769 }
770
771 #[test]
772 fn straight_border_four_strips() {
773 let mut r = CountRenderer {
774 fills: 0,
775 blends: 0,
776 };
777 let rect = Rect {
778 x: 0,
779 y: 0,
780 width: 100,
781 height: 50,
782 };
783 draw_border_straight(&mut r, rect, Color(0, 0, 0, 255), 2);
784 assert_eq!(r.fills, 4);
785 }
786
787 #[test]
788 fn draw_widget_bg_uses_radius() {
789 let mut r = CountRenderer {
790 fills: 0,
791 blends: 0,
792 };
793 let rect = Rect {
794 x: 0,
795 y: 0,
796 width: 80,
797 height: 40,
798 };
799 let style = Style {
800 bg_color: Color(100, 100, 100, 255),
801 border_color: Color(0, 0, 0, 255),
802 border_width: 1,
803 alpha: 255,
804 radius: 6,
805 };
806 draw_widget_bg(&mut r, rect, &style);
807 assert!(r.fills > 1);
808 }
809
810 #[test]
811 fn linear_gradient_samples_cardinal_axis() {
812 let stops = [(0, Color(0, 0, 0, 255)), (255, Color(255, 0, 0, 255))];
813 let gradient = GradientDesc::new(GradientKind::Linear { angle_deg: 0 }, &stops);
814 let rect = Rect {
815 x: 10,
816 y: 20,
817 width: 3,
818 height: 2,
819 };
820
821 assert_eq!(gradient.color_at(rect, 10, 20), Some(Color(0, 0, 0, 255)));
822 assert_eq!(gradient.color_at(rect, 11, 20), Some(Color(127, 0, 0, 255)));
823 assert_eq!(gradient.color_at(rect, 12, 20), Some(Color(255, 0, 0, 255)));
824 }
825
826 #[test]
827 fn gradient_stops_do_not_need_sorting() {
828 let stops = [(255, Color(255, 0, 0, 255)), (0, Color(0, 0, 0, 255))];
829 let gradient = GradientDesc::new(GradientKind::Linear { angle_deg: 90 }, &stops);
830 let rect = Rect {
831 x: 0,
832 y: 0,
833 width: 2,
834 height: 3,
835 };
836
837 assert_eq!(gradient.color_at(rect, 0, 0), Some(Color(0, 0, 0, 255)));
838 assert_eq!(gradient.color_at(rect, 0, 1), Some(Color(127, 0, 0, 255)));
839 assert_eq!(gradient.color_at(rect, 0, 2), Some(Color(255, 0, 0, 255)));
840 }
841
842 #[test]
843 fn radial_gradient_reaches_outer_stop_at_corner() {
844 let stops = [(0, Color(0, 0, 0, 255)), (255, Color(0, 0, 255, 255))];
845 let gradient = GradientDesc::new(
846 GradientKind::Radial {
847 cx_frac: 128,
848 cy_frac: 128,
849 },
850 &stops,
851 );
852 let rect = Rect {
853 x: 0,
854 y: 0,
855 width: 5,
856 height: 5,
857 };
858
859 assert_eq!(gradient.color_at(rect, 2, 2), Some(Color(0, 0, 0, 255)));
860 assert_eq!(gradient.color_at(rect, 0, 0), Some(Color(0, 0, 255, 255)));
861 }
862}