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 pub interpolation: ColorInterpolation,
87 corners: Arc<[Color; 4]>,
88}
89
90impl BilinearGradient {
91 #[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 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 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 #[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}