Skip to main content

rlvgl_core/
draw.rs

1//! Drawing helpers that compose [`crate::renderer::Renderer`] calls to produce
2//! rounded rectangles and borders without extending the renderer trait.
3//!
4//! All functions work with any [`crate::renderer::Renderer`] implementation,
5//! making rounded corners available on every backend.
6
7use crate::renderer::Renderer;
8use crate::style::Style;
9use crate::widget::{Color, Rect};
10
11/// Maximum number of stops honored by [`GradientDesc`].
12pub const GRADIENT_MAX_STOPS: usize = 4;
13
14/// Shape of a gradient fill.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum GradientKind {
17    /// Linear gradient. Cardinal and diagonal angles are snapped to the
18    /// nearest 45-degree direction for deterministic integer sampling.
19    Linear {
20        /// Clockwise angle in degrees; `0` is left-to-right and `90` is
21        /// top-to-bottom.
22        angle_deg: i16,
23    },
24    /// Radial gradient from a center point expressed as fractions of `rect`.
25    Radial {
26        /// Center x coordinate as `0..=255` fraction of the target rect.
27        cx_frac: u8,
28        /// Center y coordinate as `0..=255` fraction of the target rect.
29        cy_frac: u8,
30    },
31}
32
33/// Linear or radial gradient descriptor.
34///
35/// Stops are `(position, color)` pairs where `position` is `0..=255`.
36/// At most [`GRADIENT_MAX_STOPS`] stops are considered; extra stops are
37/// ignored by the software reference path. Stops do not need to be sorted.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct GradientDesc<'a> {
40    /// Gradient geometry.
41    pub kind: GradientKind,
42    /// Color stops in `0..=255` position space.
43    pub stops: &'a [(u8, Color)],
44}
45
46impl<'a> GradientDesc<'a> {
47    /// Create a gradient descriptor.
48    pub const fn new(kind: GradientKind, stops: &'a [(u8, Color)]) -> Self {
49        Self { kind, stops }
50    }
51
52    /// Return the sampled color at an absolute pixel coordinate.
53    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/// Box-shadow descriptor consumed by [`Renderer::draw_shadow`].
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct ShadowDesc {
94    /// Horizontal shadow displacement.
95    pub offset_x: i16,
96    /// Vertical shadow displacement.
97    pub offset_y: i16,
98    /// Expansion beyond the source rect before blur.
99    pub spread: u8,
100    /// Approximate blur radius in pixels.
101    pub blur: u8,
102    /// Shadow color.
103    pub color: Color,
104}
105
106/// Integer square root (floor).
107fn 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
192/// Compute the arc x-extent at row `dy` for radius `r`, returning the integer
193/// part and a 0–255 fractional coverage for the boundary pixel.
194///
195/// Uses 4× oversampling: computes `isqrt((4r)² − (4dy+2)²)` to get the
196/// intersection at the pixel centre with 2 extra bits of precision.
197fn arc_dx(r: i32, dy: i32) -> (i32, u8) {
198    let r4 = r as u32 * 4;
199    let dy4 = dy as u32 * 4 + 2; // pixel centre
200    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; // 0, 64, 128, 192
208    (dx_int, frac)
209}
210
211/// Fill a rounded rectangle with anti-aliased corners.
212///
213/// For `radius == 0` this is a single [`Renderer::fill_rect`] call.  Nonzero
214/// radii are clamped to half the shorter side so pill shapes work correctly.
215/// Corner edges are anti-aliased via [`Renderer::blend_rect`].
216pub 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    // Clamp to half the shorter side for pill shapes.
223    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    // Body: full-width strip between top-radius and bottom-radius.
230    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    // Top strip between corners.
243    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        // Bottom strip between corners.
255        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    // Corner arcs with anti-aliased fringe.
267    //
268    // `arc_dx(r, dy)` returns the x-extent at `dy` rows from the arc's
269    // horizontal centre axis. The loop walks pixel rows from the *top edge*
270    // of the corner box, so dy=0 is the tangent row (extent ≈ 0) and dy=r-1
271    // is adjacent to the body (extent ≈ r). Pass `r - 1 - dy` to convert.
272    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        // --- fully opaque interior of each corner ---
277        if dx_int > 0 {
278            // top-left
279            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            // top-right
289            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            // bottom-left
299            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            // bottom-right
309            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        // --- AA fringe pixel at each corner ---
321        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            // top-left
325            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            // top-right
335            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            // bottom-left
345            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            // bottom-right
355            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
368/// Draw a border that follows rounded corners.
369///
370/// When `radius == 0` this draws four straight strips (same as
371/// [`draw_border_straight`]).  When `radius > 0`, corner arcs are drawn as
372/// the ring between an outer and inner radius, with AA fringe on both edges.
373pub 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    // --- Corner arcs (ring between outer and inner radius) ---
400    // See the matching comment in `fill_rounded_rect`: `arc_dx` measures
401    // from the arc centre axis, so we invert `dy` to turn the loop index
402    // (row from top of corner box) into the axis distance.
403    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            // arc_dx returns (0,0) when dy is outside the inner circle
409            (d, f)
410        } else {
411            (0i32, 0u8)
412        };
413
414        // Ring width: from outer edge inward to inner edge.
415        let ring_w = out_dx - in_dx;
416        if ring_w > 0 {
417            // top-left
418            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            // top-right
428            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            // bottom-left
438            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            // bottom-right
448            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        // --- Outer AA fringe ---
460        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        // --- Inner AA fringe ---
502        if in_dx > 0 && in_frac > 0 {
503            // Inner fringe: the pixel just inside the inner arc is partially covered
504            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    // --- Straight border segments between corners ---
546    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
591/// Draw a rectangular border as four straight `fill_rect` strips (no rounding).
592pub 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
635/// Draw a widget's background fill and border based on its [`Style`].
636///
637/// Respects `style.radius` for rounded corners, `style.border_width` for
638/// borders, and `style.alpha` for opacity.
639///
640/// Fully transparent backgrounds (`alpha == 0`) are skipped entirely so the
641/// underlying pixels show through. Partially transparent backgrounds are
642/// alpha-blended via [`Renderer::blend_rect`]; opaque backgrounds use
643/// [`Renderer::fill_rect`] for the fast overwrite path. Without this guard
644/// a transparent background would write zero-valued pixels over the
645/// framebuffer, which presents as solid black on backends whose surface
646/// has nothing to composite against (e.g. the `wgpu` simulator).
647pub 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}