Skip to main content

rlvgl_core/
mask.rs

1//! Alpha mask primitives for LPAR-08 draw coverage composition.
2//!
3//! Masks write one scanline of `0..=255` coverage into caller-provided
4//! scratch. They are allocation-free, object-safe, and use the same absolute
5//! framebuffer coordinate convention as [`Renderer`](crate::renderer::Renderer).
6
7use crate::widget::Rect;
8
9/// Per-pixel alpha coverage source for masked fills.
10///
11/// Implementations write coverage for the scanline starting at `(x, y)` into
12/// `coverage`, one byte per pixel moving right. `coverage[0]` describes
13/// `(x, y)`, `coverage[1]` describes `(x + 1, y)`, and so on. Every element
14/// must be overwritten on every call so callers can safely reuse row scratch.
15///
16/// The trait is object-safe so renderers and higher-level draw helpers can
17/// accept `&dyn AlphaMask`.
18pub trait AlphaMask {
19    /// Write one row of alpha coverage into `coverage`.
20    fn row(&self, x: i32, y: i32, coverage: &mut [u8]);
21
22    /// Return coverage for a single pixel.
23    ///
24    /// This convenience method is also the allocation-free composition path
25    /// for combinators that need coverage from two child masks but receive
26    /// only one caller-owned row buffer.
27    fn alpha_at(&self, x: i32, y: i32) -> u8 {
28        let mut coverage = [0u8; 1];
29        self.row(x, y, &mut coverage);
30        coverage[0]
31    }
32}
33
34impl<T: AlphaMask + ?Sized> AlphaMask for &T {
35    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
36        (**self).row(x, y, coverage);
37    }
38
39    fn alpha_at(&self, x: i32, y: i32) -> u8 {
40        (**self).alpha_at(x, y)
41    }
42}
43
44/// Rectangular mask with full coverage inside and zero coverage outside.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct RectMask {
47    rect: Rect,
48}
49
50impl RectMask {
51    /// Create a rectangular mask over `rect`.
52    pub const fn new(rect: Rect) -> Self {
53        Self { rect }
54    }
55
56    /// Return the rectangle covered by this mask.
57    pub const fn rect(&self) -> Rect {
58        self.rect
59    }
60}
61
62impl From<Rect> for RectMask {
63    fn from(rect: Rect) -> Self {
64        Self::new(rect)
65    }
66}
67
68impl AlphaMask for RectMask {
69    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
70        let Some(edges) = rect_edges(self.rect) else {
71            coverage.fill(0);
72            return;
73        };
74
75        let (_, y0, _, y1) = edges;
76        let y = i64::from(y);
77        if y < y0 || y >= y1 {
78            coverage.fill(0);
79            return;
80        }
81
82        let (x0, _, x1, _) = edges;
83        for (offset, alpha) in coverage.iter_mut().enumerate() {
84            let px = absolute_x(x, offset);
85            *alpha = if px >= x0 && px < x1 { 255 } else { 0 };
86        }
87    }
88}
89
90/// Rounded-rectangle mask with deterministic integer edge coverage.
91///
92/// The rectangle uses the same half-open geometry as [`RectMask`]: pixels are
93/// tested against `x..x + width` and `y..y + height`. `radius` is clamped to
94/// half of the effective width and height. Edge coverage is evaluated with a
95/// fixed 4x4 integer subpixel grid so the result is deterministic across
96/// targets and does not require floating point.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct RoundedRectMask {
99    rect: Rect,
100    radius: u8,
101}
102
103impl RoundedRectMask {
104    /// Create a rounded-rectangle mask over `rect`.
105    pub const fn new(rect: Rect, radius: u8) -> Self {
106        Self { rect, radius }
107    }
108
109    /// Return the rectangle covered by this mask before radius clipping.
110    pub const fn rect(&self) -> Rect {
111        self.rect
112    }
113
114    /// Return the requested corner radius in pixels.
115    pub const fn radius(&self) -> u8 {
116        self.radius
117    }
118}
119
120impl AlphaMask for RoundedRectMask {
121    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
122        let Some(edges) = rect_edges(self.rect) else {
123            coverage.fill(0);
124            return;
125        };
126
127        let (x0, y0, x1, y1) = edges;
128        let y = i64::from(y);
129        if y < y0 || y >= y1 {
130            coverage.fill(0);
131            return;
132        }
133
134        let radius = rounded_radius(self.rect, self.radius);
135        if radius <= 0 {
136            RectMask::new(self.rect).row(x, i32::try_from(y).unwrap_or(i32::MAX), coverage);
137            return;
138        }
139
140        for (offset, alpha) in coverage.iter_mut().enumerate() {
141            let px = absolute_x(x, offset);
142            if px < x0 || px >= x1 {
143                *alpha = 0;
144            } else {
145                *alpha = rounded_rect_pixel_alpha(edges, radius, px, y);
146            }
147        }
148    }
149}
150
151/// Direction of a linear [`FadeMask`] ramp.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum FadeDirection {
154    /// Start opacity is at the left edge and end opacity is at the right edge.
155    LeftToRight,
156    /// Start opacity is at the right edge and end opacity is at the left edge.
157    RightToLeft,
158    /// Start opacity is at the top edge and end opacity is at the bottom edge.
159    TopToBottom,
160    /// Start opacity is at the bottom edge and end opacity is at the top edge.
161    BottomToTop,
162}
163
164/// Rectangular linear alpha ramp.
165///
166/// Pixels outside `rect` receive zero coverage. Pixels inside `rect` receive
167/// an integer linear interpolation from `start_opacity` to `end_opacity`
168/// along [`direction`](Self::direction).
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct FadeMask {
171    rect: Rect,
172    direction: FadeDirection,
173    start_opacity: u8,
174    end_opacity: u8,
175}
176
177impl FadeMask {
178    /// Create a fade mask over `rect`.
179    ///
180    /// `start_opacity` and `end_opacity` use the same `0..=255` coverage
181    /// scale as [`AlphaMask::row`].
182    pub const fn new(
183        rect: Rect,
184        direction: FadeDirection,
185        start_opacity: u8,
186        end_opacity: u8,
187    ) -> Self {
188        Self {
189            rect,
190            direction,
191            start_opacity,
192            end_opacity,
193        }
194    }
195
196    /// Return the rectangle over which this fade is active.
197    pub const fn rect(&self) -> Rect {
198        self.rect
199    }
200
201    /// Return the fade direction.
202    pub const fn direction(&self) -> FadeDirection {
203        self.direction
204    }
205
206    /// Return the opacity at the start edge of the fade direction.
207    pub const fn start_opacity(&self) -> u8 {
208        self.start_opacity
209    }
210
211    /// Return the opacity at the end edge of the fade direction.
212    pub const fn end_opacity(&self) -> u8 {
213        self.end_opacity
214    }
215}
216
217impl AlphaMask for FadeMask {
218    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
219        let Some((x0, y0, x1, y1)) = rect_edges(self.rect) else {
220            coverage.fill(0);
221            return;
222        };
223
224        let y = i64::from(y);
225        if y < y0 || y >= y1 {
226            coverage.fill(0);
227            return;
228        }
229
230        let span = match self.direction {
231            FadeDirection::LeftToRight | FadeDirection::RightToLeft => {
232                i64::from(self.rect.width - 1)
233            }
234            FadeDirection::TopToBottom | FadeDirection::BottomToTop => {
235                i64::from(self.rect.height - 1)
236            }
237        };
238
239        for (offset, alpha) in coverage.iter_mut().enumerate() {
240            let px = absolute_x(x, offset);
241            if px < x0 || px >= x1 {
242                *alpha = 0;
243                continue;
244            }
245
246            let pos = match self.direction {
247                FadeDirection::LeftToRight => px - x0,
248                FadeDirection::RightToLeft => x1 - 1 - px,
249                FadeDirection::TopToBottom => y - y0,
250                FadeDirection::BottomToTop => y1 - 1 - y,
251            };
252            *alpha = lerp_opacity(self.start_opacity, self.end_opacity, pos, span);
253        }
254    }
255}
256
257/// Annular arc or pie-slice mask with deterministic integer edge coverage.
258///
259/// Angles are degrees in framebuffer coordinates: `0` points right and
260/// positive degrees advance clockwise (`90` points down). The covered sweep is
261/// the clockwise range from `start_deg` to `end_deg`; ranges whose absolute
262/// difference is at least `360` cover the full annulus, while equal start and
263/// end angles cover no pixels. Radii are measured from `center`; `inner_radius`
264/// may be zero to produce a filled pie slice. Edge coverage uses the same fixed
265/// 4x4 integer subpixel grid as [`RoundedRectMask`].
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
267pub struct ArcMask {
268    center: (i32, i32),
269    outer_radius: u16,
270    inner_radius: u16,
271    start_deg: i16,
272    end_deg: i16,
273}
274
275impl ArcMask {
276    /// Create an arc mask.
277    ///
278    /// `outer_radius` is the outside radius of the annulus. `inner_radius`
279    /// carves out the center; when it is `0`, the mask becomes a pie slice.
280    /// If `outer_radius <= inner_radius`, the mask covers no pixels.
281    pub const fn new(
282        center: (i32, i32),
283        outer_radius: u16,
284        inner_radius: u16,
285        start_deg: i16,
286        end_deg: i16,
287    ) -> Self {
288        Self {
289            center,
290            outer_radius,
291            inner_radius,
292            start_deg,
293            end_deg,
294        }
295    }
296
297    /// Return the center point in framebuffer coordinates.
298    pub const fn center(&self) -> (i32, i32) {
299        self.center
300    }
301
302    /// Return the outside radius in pixels.
303    pub const fn outer_radius(&self) -> u16 {
304        self.outer_radius
305    }
306
307    /// Return the inside radius in pixels.
308    pub const fn inner_radius(&self) -> u16 {
309        self.inner_radius
310    }
311
312    /// Return the clockwise start angle in degrees.
313    pub const fn start_deg(&self) -> i16 {
314        self.start_deg
315    }
316
317    /// Return the clockwise end angle in degrees.
318    pub const fn end_deg(&self) -> i16 {
319        self.end_deg
320    }
321}
322
323impl AlphaMask for ArcMask {
324    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
325        if self.outer_radius <= self.inner_radius || arc_sweep(self.start_deg, self.end_deg) == 0 {
326            coverage.fill(0);
327            return;
328        }
329
330        let cy = i64::from(self.center.1);
331        let outer = i64::from(self.outer_radius);
332        let y = i64::from(y);
333        if y + 1 < cy - outer || y > cy + outer {
334            coverage.fill(0);
335            return;
336        }
337
338        for (offset, alpha) in coverage.iter_mut().enumerate() {
339            *alpha = arc_pixel_alpha(self, absolute_x(x, offset), y);
340        }
341    }
342}
343
344/// Mask combinator that takes the minimum coverage of two masks.
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub struct IntersectMask<A, B> {
347    first: A,
348    second: B,
349}
350
351impl<A, B> IntersectMask<A, B> {
352    /// Create a mask that intersects `first` and `second`.
353    pub const fn new(first: A, second: B) -> Self {
354        Self { first, second }
355    }
356
357    /// Return the two masks consumed by this combinator.
358    pub fn into_inner(self) -> (A, B) {
359        (self.first, self.second)
360    }
361}
362
363impl<A: AlphaMask, B: AlphaMask> AlphaMask for IntersectMask<A, B> {
364    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
365        for (offset, alpha) in coverage.iter_mut().enumerate() {
366            let px = absolute_x_i32(x, offset);
367            *alpha = self.first.alpha_at(px, y).min(self.second.alpha_at(px, y));
368        }
369    }
370}
371
372/// Mask combinator that takes the maximum coverage of two masks.
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub struct UnionMask<A, B> {
375    first: A,
376    second: B,
377}
378
379impl<A, B> UnionMask<A, B> {
380    /// Create a mask that unions `first` and `second`.
381    pub const fn new(first: A, second: B) -> Self {
382        Self { first, second }
383    }
384
385    /// Return the two masks consumed by this combinator.
386    pub fn into_inner(self) -> (A, B) {
387        (self.first, self.second)
388    }
389}
390
391impl<A: AlphaMask, B: AlphaMask> AlphaMask for UnionMask<A, B> {
392    fn row(&self, x: i32, y: i32, coverage: &mut [u8]) {
393        for (offset, alpha) in coverage.iter_mut().enumerate() {
394            let px = absolute_x_i32(x, offset);
395            *alpha = self.first.alpha_at(px, y).max(self.second.alpha_at(px, y));
396        }
397    }
398}
399
400fn rect_edges(rect: Rect) -> Option<(i64, i64, i64, i64)> {
401    if rect.width <= 0 || rect.height <= 0 {
402        return None;
403    }
404
405    let x0 = i64::from(rect.x);
406    let y0 = i64::from(rect.y);
407    Some((
408        x0,
409        y0,
410        x0 + i64::from(rect.width),
411        y0 + i64::from(rect.height),
412    ))
413}
414
415fn absolute_x(x: i32, offset: usize) -> i64 {
416    let offset = i64::try_from(offset).unwrap_or(i64::MAX);
417    i64::from(x).saturating_add(offset)
418}
419
420fn absolute_x_i32(x: i32, offset: usize) -> i32 {
421    let offset = i32::try_from(offset).unwrap_or(i32::MAX);
422    x.saturating_add(offset)
423}
424
425fn lerp_opacity(start: u8, end: u8, pos: i64, span: i64) -> u8 {
426    if span <= 0 {
427        return end;
428    }
429
430    let value = i64::from(start) + (i64::from(end) - i64::from(start)) * pos / span;
431    value.clamp(0, 255) as u8
432}
433
434const SUBPIXEL_SCALE: i64 = 8;
435const SUBPIXEL_OFFSETS: [i64; 4] = [1, 3, 5, 7];
436const SUBPIXEL_SAMPLES: u16 = 16;
437
438fn rounded_radius(rect: Rect, requested: u8) -> i64 {
439    let Some((x0, y0, x1, y1)) = rect_edges(rect) else {
440        return 0;
441    };
442    i64::from(requested).min((x1 - x0) / 2).min((y1 - y0) / 2)
443}
444
445fn rounded_rect_pixel_alpha(edges: (i64, i64, i64, i64), radius: i64, x: i64, y: i64) -> u8 {
446    let mut inside = 0u16;
447    for sy in SUBPIXEL_OFFSETS {
448        let sample_y = y.saturating_mul(SUBPIXEL_SCALE).saturating_add(sy);
449        for sx in SUBPIXEL_OFFSETS {
450            let sample_x = x.saturating_mul(SUBPIXEL_SCALE).saturating_add(sx);
451            if rounded_rect_sample_inside(edges, radius, sample_x, sample_y) {
452                inside += 1;
453            }
454        }
455    }
456    coverage_from_samples(inside)
457}
458
459fn rounded_rect_sample_inside(
460    (x0, y0, x1, y1): (i64, i64, i64, i64),
461    radius: i64,
462    sample_x: i64,
463    sample_y: i64,
464) -> bool {
465    let x0 = x0.saturating_mul(SUBPIXEL_SCALE);
466    let y0 = y0.saturating_mul(SUBPIXEL_SCALE);
467    let x1 = x1.saturating_mul(SUBPIXEL_SCALE);
468    let y1 = y1.saturating_mul(SUBPIXEL_SCALE);
469    if sample_x < x0 || sample_x >= x1 || sample_y < y0 || sample_y >= y1 {
470        return false;
471    }
472
473    let radius = radius.saturating_mul(SUBPIXEL_SCALE);
474    let left_center = x0.saturating_add(radius);
475    let right_center = x1.saturating_sub(radius);
476    let top_center = y0.saturating_add(radius);
477    let bottom_center = y1.saturating_sub(radius);
478
479    if (sample_x >= left_center && sample_x < right_center)
480        || (sample_y >= top_center && sample_y < bottom_center)
481    {
482        return true;
483    }
484
485    let center_x = if sample_x < left_center {
486        left_center
487    } else {
488        right_center
489    };
490    let center_y = if sample_y < top_center {
491        top_center
492    } else {
493        bottom_center
494    };
495    let dx = sample_x - center_x;
496    let dy = sample_y - center_y;
497    dx * dx + dy * dy <= radius * radius
498}
499
500fn arc_pixel_alpha(mask: &ArcMask, x: i64, y: i64) -> u8 {
501    let mut inside = 0u16;
502    for sy in SUBPIXEL_OFFSETS {
503        let sample_y = y.saturating_mul(SUBPIXEL_SCALE).saturating_add(sy);
504        for sx in SUBPIXEL_OFFSETS {
505            let sample_x = x.saturating_mul(SUBPIXEL_SCALE).saturating_add(sx);
506            if arc_sample_inside(mask, sample_x, sample_y) {
507                inside += 1;
508            }
509        }
510    }
511    coverage_from_samples(inside)
512}
513
514fn arc_sample_inside(mask: &ArcMask, sample_x: i64, sample_y: i64) -> bool {
515    let center_x = i64::from(mask.center.0).saturating_mul(SUBPIXEL_SCALE);
516    let center_y = i64::from(mask.center.1).saturating_mul(SUBPIXEL_SCALE);
517    let dx = sample_x - center_x;
518    let dy = sample_y - center_y;
519
520    let outer = i64::from(mask.outer_radius).saturating_mul(SUBPIXEL_SCALE);
521    if dx.abs() > outer || dy.abs() > outer {
522        return false;
523    }
524
525    let distance_sq = dx * dx + dy * dy;
526    if distance_sq > outer * outer {
527        return false;
528    }
529
530    let inner = i64::from(mask.inner_radius).saturating_mul(SUBPIXEL_SCALE);
531    if inner > 0 && distance_sq < inner * inner {
532        return false;
533    }
534
535    let sweep = arc_sweep(mask.start_deg, mask.end_deg);
536    if sweep >= 360 {
537        return true;
538    }
539    if dx == 0 && dy == 0 {
540        return mask.inner_radius == 0 && sweep > 0;
541    }
542
543    let angle = atan2_deg_clockwise(dx, dy);
544    let start = normalize_degrees(i32::from(mask.start_deg));
545    let relative = normalize_degrees(angle - start);
546    relative <= sweep
547}
548
549fn arc_sweep(start_deg: i16, end_deg: i16) -> i32 {
550    let raw = i32::from(end_deg) - i32::from(start_deg);
551    if raw >= 360 || raw <= -360 {
552        360
553    } else {
554        raw.rem_euclid(360)
555    }
556}
557
558fn atan2_deg_clockwise(dx: i64, dy: i64) -> i32 {
559    if dx == 0 {
560        return if dy > 0 {
561            90
562        } else if dy < 0 {
563            270
564        } else {
565            0
566        };
567    }
568
569    let abs_y = dy.abs();
570    let mut angle = if dx >= 0 {
571        let denom = dx + abs_y;
572        if denom == 0 {
573            0
574        } else {
575            45 - ((dx - abs_y) * 45 / denom) as i32
576        }
577    } else {
578        let denom = abs_y - dx;
579        135 - ((dx + abs_y) * 45 / denom) as i32
580    };
581
582    if dy < 0 {
583        angle = 360 - angle;
584    }
585    normalize_degrees(angle)
586}
587
588fn normalize_degrees(degrees: i32) -> i32 {
589    degrees.rem_euclid(360)
590}
591
592fn coverage_from_samples(inside: u16) -> u8 {
593    match inside {
594        0 => 0,
595        SUBPIXEL_SAMPLES => 255,
596        _ => ((inside * 255 + SUBPIXEL_SAMPLES / 2) / SUBPIXEL_SAMPLES) as u8,
597    }
598}