Skip to main content

argui_paint/
visual.rs

1use std::sync::{
2    Arc,
3    atomic::{AtomicU64, Ordering},
4};
5
6use argui_core::{Affine2D, Color, ColorInterpolation, Point, Rect};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct GradientStop {
10    pub offset: f32,
11    pub color: Color,
12}
13
14impl GradientStop {
15    #[must_use]
16    pub const fn new(offset: f32, color: Color) -> Self {
17        Self { offset, color }
18    }
19}
20
21impl Default for GradientStop {
22    fn default() -> Self {
23        Self::new(0.0, Color::TRANSPARENT)
24    }
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum GradientError {
29    TooFewStops,
30    InvalidOffset,
31    UnsortedStops,
32}
33
34impl core::fmt::Display for GradientError {
35    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
36        write!(formatter, "invalid gradient: {self:?}")
37    }
38}
39
40impl std::error::Error for GradientError {}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct GradientStops(Arc<[GradientStop]>);
44
45impl GradientStops {
46    pub fn new<const N: usize>(stops: [GradientStop; N]) -> Result<Self, GradientError> {
47        Self::from_vec(Vec::from(stops))
48    }
49
50    pub fn from_vec(stops: Vec<GradientStop>) -> Result<Self, GradientError> {
51        if stops.len() < 2 {
52            return Err(GradientError::TooFewStops);
53        }
54        let mut previous = 0.0;
55        for (index, stop) in stops.iter().enumerate() {
56            if !(0.0..=1.0).contains(&stop.offset) {
57                return Err(GradientError::InvalidOffset);
58            }
59            if index != 0 && stop.offset < previous {
60                return Err(GradientError::UnsortedStops);
61            }
62            previous = stop.offset;
63        }
64        Ok(Self(stops.into()))
65    }
66
67    #[must_use]
68    pub fn as_slice(&self) -> &[GradientStop] {
69        &self.0
70    }
71
72    #[must_use]
73    pub fn len(&self) -> usize {
74        self.0.len()
75    }
76
77    #[must_use]
78    pub fn is_empty(&self) -> bool {
79        self.0.is_empty()
80    }
81}
82
83#[derive(Clone, Debug, PartialEq)]
84pub struct BilinearGradient {
85    /// Interpolation space shared by both axes, with premultiplied alpha.
86    pub interpolation: ColorInterpolation,
87    corners: Arc<[Color; 4]>,
88}
89
90impl BilinearGradient {
91    /// Corners in top-left, top-right, bottom-left, bottom-right order.
92    #[must_use]
93    pub fn new(corners: [Color; 4], interpolation: ColorInterpolation) -> Self {
94        Self {
95            interpolation,
96            corners: Arc::new(corners),
97        }
98    }
99
100    #[must_use]
101    pub fn corners(&self) -> &[Color; 4] {
102        &self.corners
103    }
104}
105
106#[derive(Clone, Debug, PartialEq)]
107pub struct LinearGradient {
108    /// Relative coordinates where `(0, 0)` is the top-left of the primitive.
109    pub start: Point,
110    pub end: Point,
111    pub interpolation: ColorInterpolation,
112    pub stops: GradientStops,
113}
114
115impl LinearGradient {
116    pub fn new<const N: usize>(
117        start: Point,
118        end: Point,
119        interpolation: ColorInterpolation,
120        stops: [GradientStop; N],
121    ) -> Result<Self, GradientError> {
122        Ok(Self::with_stops(
123            start,
124            end,
125            interpolation,
126            GradientStops::new(stops)?,
127        ))
128    }
129
130    pub fn with_stops(
131        start: Point,
132        end: Point,
133        interpolation: ColorInterpolation,
134        stops: GradientStops,
135    ) -> Self {
136        Self {
137            start,
138            end,
139            interpolation,
140            stops,
141        }
142    }
143}
144
145#[derive(Clone, Debug, PartialEq)]
146pub struct RadialGradient {
147    /// Relative center and radii, allowing circular or elliptical gradients.
148    pub center: Point,
149    pub radius: Point,
150    pub interpolation: ColorInterpolation,
151    pub stops: GradientStops,
152}
153
154impl RadialGradient {
155    pub fn new<const N: usize>(
156        center: Point,
157        radius: Point,
158        interpolation: ColorInterpolation,
159        stops: [GradientStop; N],
160    ) -> Result<Self, GradientError> {
161        Ok(Self::with_stops(
162            center,
163            radius,
164            interpolation,
165            GradientStops::new(stops)?,
166        ))
167    }
168
169    pub fn with_stops(
170        center: Point,
171        radius: Point,
172        interpolation: ColorInterpolation,
173        stops: GradientStops,
174    ) -> Self {
175        Self {
176            center,
177            radius,
178            interpolation,
179            stops,
180        }
181    }
182}
183
184#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
185pub struct ImageId(pub u64);
186
187static NEXT_IMAGE_ID: AtomicU64 = AtomicU64::new(1);
188
189impl ImageId {
190    /// Allocates a process-local opaque handle. Asset libraries normally call
191    /// this on behalf of applications.
192    #[must_use]
193    pub fn fresh() -> Self {
194        let id = NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed);
195        assert_ne!(id, u64::MAX, "image handle space exhausted");
196        Self(id)
197    }
198}
199
200#[derive(Clone, Debug, PartialEq)]
201pub struct ImageAsset {
202    pub id: ImageId,
203    pub width: u32,
204    pub height: u32,
205    pub rgba8: Arc<[u8]>,
206}
207
208impl ImageAsset {
209    pub fn rgba8(
210        id: ImageId,
211        width: u32,
212        height: u32,
213        rgba8: impl Into<Arc<[u8]>>,
214    ) -> Result<Self, ImageAssetError> {
215        let rgba8 = rgba8.into();
216        let expected = usize::try_from(width)
217            .ok()
218            .and_then(|width| usize::try_from(height).ok().map(|height| width * height))
219            .and_then(|pixels| pixels.checked_mul(4))
220            .ok_or(ImageAssetError::InvalidDimensions)?;
221        if rgba8.len() != expected {
222            return Err(ImageAssetError::InvalidByteLength {
223                expected,
224                actual: rgba8.len(),
225            });
226        }
227        Ok(Self {
228            id,
229            width,
230            height,
231            rgba8,
232        })
233    }
234
235    #[must_use]
236    pub fn byte_len(&self) -> usize {
237        self.rgba8.len()
238    }
239}
240
241#[derive(Clone, Copy, Debug, Eq, PartialEq)]
242pub enum ImageAssetError {
243    InvalidDimensions,
244    InvalidByteLength { expected: usize, actual: usize },
245}
246
247impl core::fmt::Display for ImageAssetError {
248    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
249        write!(formatter, "invalid RGBA image: {self:?}")
250    }
251}
252
253impl std::error::Error for ImageAssetError {}
254
255#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
256pub enum ImageFit {
257    Fill,
258    Contain,
259    #[default]
260    Cover,
261}
262
263#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
264pub enum ImageSampling {
265    Nearest,
266    #[default]
267    Linear,
268}
269
270#[derive(Clone, Copy, Debug, PartialEq)]
271pub struct ClipRegion {
272    pub bounds: Rect,
273    pub transform: Affine2D,
274    pub radii: crate::CornerRadii,
275}
276
277impl ClipRegion {
278    #[must_use]
279    pub const fn new(bounds: Rect, transform: Affine2D) -> Self {
280        Self {
281            bounds,
282            transform,
283            radii: crate::CornerRadii::all(0.0),
284        }
285    }
286
287    #[must_use]
288    pub const fn rounded(bounds: Rect, transform: Affine2D, radii: crate::CornerRadii) -> Self {
289        Self {
290            bounds,
291            transform,
292            radii,
293        }
294    }
295
296    #[must_use]
297    pub fn contains(self, point: Point) -> bool {
298        self.transform.inverse().is_some_and(|inverse| {
299            let point = inverse.transform_point(point);
300            self.bounds.contains(point) && rounded_rect_contains(self.bounds, self.radii, point)
301        })
302    }
303}
304
305fn rounded_rect_contains(bounds: Rect, radii: crate::CornerRadii, point: Point) -> bool {
306    let local = Point::new(point.x - bounds.origin.x, point.y - bounds.origin.y);
307    let width = bounds.size.width.max(0.0);
308    let height = bounds.size.height.max(0.0);
309    let radius = if local.y < height * 0.5 {
310        if local.x < width * 0.5 {
311            radii.top_left
312        } else {
313            radii.top_right
314        }
315    } else if local.x < width * 0.5 {
316        radii.bottom_left
317    } else {
318        radii.bottom_right
319    }
320    .clamp(0.0, width.min(height) * 0.5);
321    let center = Point::new(
322        local.x.clamp(radius, width - radius),
323        local.y.clamp(radius, height - radius),
324    );
325    let delta = Point::new(local.x - center.x, local.y - center.y);
326    delta.x * delta.x + delta.y * delta.y <= radius * radius
327}
328
329#[derive(Clone, Debug, Default, PartialEq)]
330pub struct ClipChain(Arc<[ClipRegion]>);
331
332impl ClipChain {
333    #[must_use]
334    pub fn from_regions(regions: impl Into<Arc<[ClipRegion]>>) -> Self {
335        Self(regions.into())
336    }
337
338    #[must_use]
339    pub fn appended(&self, region: ClipRegion) -> Self {
340        let mut regions = self.0.to_vec();
341        regions.push(region);
342        Self(regions.into())
343    }
344
345    #[must_use]
346    pub fn regions(&self) -> &[ClipRegion] {
347        &self.0
348    }
349
350    #[must_use]
351    pub fn is_empty(&self) -> bool {
352        self.0
353            .iter()
354            .any(|region| region.bounds.size.width <= 0.0 || region.bounds.size.height <= 0.0)
355    }
356
357    #[must_use]
358    pub fn contains(&self, point: Point) -> bool {
359        !self.is_empty() && self.0.iter().all(|clip| clip.contains(point))
360    }
361}