1use super::*;
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
4pub struct Color {
5 pub red: u8,
6 pub green: u8,
7 pub blue: u8,
8 pub alpha: u8,
9}
10
11impl Color {
12 pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
13 pub const BLACK: Self = Self::rgb(0, 0, 0);
14 pub const WHITE: Self = Self::rgb(255, 255, 255);
15
16 pub const fn rgb(red: u8, green: u8, blue: u8) -> Self {
17 Self::rgba(red, green, blue, 255)
18 }
19
20 pub const fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
21 Self {
22 red,
23 green,
24 blue,
25 alpha,
26 }
27 }
28
29 pub const fn to_rgba(self) -> [u8; 4] {
30 [self.red, self.green, self.blue, self.alpha]
31 }
32
33 pub fn from_hex(value: &str) -> Result<Self, ColorParseError> {
34 value.parse()
35 }
36}
37
38impl From<[u8; 4]> for Color {
39 fn from([red, green, blue, alpha]: [u8; 4]) -> Self {
40 Self::rgba(red, green, blue, alpha)
41 }
42}
43
44impl From<[u8; 3]> for Color {
45 fn from([red, green, blue]: [u8; 3]) -> Self {
46 Self::rgb(red, green, blue)
47 }
48}
49
50impl From<Color> for [u8; 4] {
51 fn from(color: Color) -> Self {
52 color.to_rgba()
53 }
54}
55
56impl FromStr for Color {
57 type Err = ColorParseError;
58
59 fn from_str(value: &str) -> Result<Self, Self::Err> {
60 let hex = value.strip_prefix('#').unwrap_or(value);
61 if !hex.is_ascii() || !matches!(hex.len(), 6 | 8) {
62 return Err(ColorParseError);
63 }
64
65 let parse =
66 |offset| u8::from_str_radix(&hex[offset..offset + 2], 16).map_err(|_| ColorParseError);
67 let red = parse(0)?;
68 let green = parse(2)?;
69 let blue = parse(4)?;
70 let alpha = if hex.len() == 8 { parse(6)? } else { 255 };
71
72 Ok(Self::rgba(red, green, blue, alpha))
73 }
74}
75
76impl TryFrom<&str> for Color {
77 type Error = ColorParseError;
78
79 fn try_from(value: &str) -> Result<Self, Self::Error> {
80 value.parse()
81 }
82}
83
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct ColorParseError;
86
87impl fmt::Display for ColorParseError {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 formatter.write_str("expected #RRGGBB or #RRGGBBAA color")
90 }
91}
92
93impl Error for ColorParseError {}
94
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96pub enum GradientDirection {
97 #[default]
98 Horizontal,
99 Vertical,
100 Diagonal,
101 DiagonalReverse,
102}
103
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct PaintTransform {
106 pub scale: f32,
107 pub translate_x: i32,
108 pub translate_y: i32,
109}
110
111impl PaintTransform {
112 pub const IDENTITY: Self = Self {
113 scale: 1.0,
114 translate_x: 0,
115 translate_y: 0,
116 };
117
118 pub const fn new(scale: f32, translate_x: i32, translate_y: i32) -> Self {
119 Self {
120 scale,
121 translate_x,
122 translate_y,
123 }
124 }
125
126 pub const fn scale(scale: f32) -> Self {
127 Self {
128 scale,
129 ..Self::IDENTITY
130 }
131 }
132
133 pub const fn translate(translate_x: i32, translate_y: i32) -> Self {
134 Self {
135 translate_x,
136 translate_y,
137 ..Self::IDENTITY
138 }
139 }
140
141 pub fn compose(self, next: Self) -> Self {
142 let scale = sanitize_scale(self.scale) * sanitize_scale(next.scale);
143 Self {
144 scale,
145 translate_x: scale_i32_f32(next.translate_x, sanitize_scale(self.scale))
146 .saturating_add(self.translate_x),
147 translate_y: scale_i32_f32(next.translate_y, sanitize_scale(self.scale))
148 .saturating_add(self.translate_y),
149 }
150 }
151
152 pub fn is_identity(self) -> bool {
153 self.scale == 1.0 && self.translate_x == 0 && self.translate_y == 0
154 }
155}
156
157impl Default for PaintTransform {
158 fn default() -> Self {
159 Self::IDENTITY
160 }
161}
162
163pub(in crate::ui) fn sanitize_scale(scale: f32) -> f32 {
164 if scale.is_finite() {
165 scale.max(0.0)
166 } else {
167 0.0
168 }
169}
170
171pub(in crate::ui) fn scale_i32_f32(value: i32, scale: f32) -> i32 {
172 let scaled = f64::from(value) * f64::from(scale);
173 if !scaled.is_finite() {
174 return if scaled.is_sign_negative() {
175 i32::MIN
176 } else {
177 i32::MAX
178 };
179 }
180 scaled
181 .round()
182 .clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
183}
184
185pub(in crate::ui::types) fn interpolate_color(
186 from: Color,
187 to: Color,
188 position: u64,
189 max_position: u64,
190) -> Color {
191 Color::rgba(
192 interpolate_component(from.red, to.red, position, max_position),
193 interpolate_component(from.green, to.green, position, max_position),
194 interpolate_component(from.blue, to.blue, position, max_position),
195 interpolate_component(from.alpha, to.alpha, position, max_position),
196 )
197}
198
199fn interpolate_component(from: u8, to: u8, position: u64, max_position: u64) -> u8 {
200 let from = u64::from(from);
201 let to = u64::from(to);
202 if to >= from {
203 (from + (to - from) * position / max_position) as u8
204 } else {
205 (from - (from - to) * position / max_position) as u8
206 }
207}
208
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub enum Paint {
211 Solid(Color),
212 Gradient(Arc<[Color]>),
213}
214
215impl Paint {
216 pub fn solid(color: impl Into<Color>) -> Self {
217 Self::Solid(color.into())
218 }
219
220 pub fn gradient(colors: impl Into<Arc<[Color]>>) -> Self {
221 let colors = colors.into();
222 if colors.is_empty() {
223 Self::Solid(Color::TRANSPARENT)
224 } else {
225 Self::Gradient(colors)
226 }
227 }
228
229 pub fn first(&self) -> Color {
230 match self {
231 Self::Solid(color) => *color,
232 Self::Gradient(colors) => colors[0],
233 }
234 }
235
236 pub fn at(
237 &self,
238 x: u32,
239 y: u32,
240 width: u32,
241 height: u32,
242 direction: GradientDirection,
243 ) -> Color {
244 let colors = match self {
245 Self::Solid(color) => return *color,
246 Self::Gradient(colors) if colors.len() == 1 => return colors[0],
247 Self::Gradient(colors) => colors,
248 };
249
250 let (position, max_position) = match direction {
251 GradientDirection::Horizontal => (x, width.saturating_sub(1)),
252 GradientDirection::Vertical => (y, height.saturating_sub(1)),
253 GradientDirection::Diagonal => (
254 x.saturating_add(y),
255 width
256 .saturating_sub(1)
257 .saturating_add(height.saturating_sub(1)),
258 ),
259 GradientDirection::DiagonalReverse => (
260 width.saturating_sub(1).saturating_sub(x).saturating_add(y),
261 width
262 .saturating_sub(1)
263 .saturating_add(height.saturating_sub(1)),
264 ),
265 };
266 if max_position == 0 {
267 return colors[0];
268 }
269
270 let stop_count = colors.len() - 1;
271 let scaled = u64::from(position.min(max_position)) * stop_count as u64;
272 let max_position = u64::from(max_position);
273 let index = (scaled / max_position) as usize;
274 if index >= stop_count {
275 return colors[stop_count];
276 }
277
278 let remainder = scaled % max_position;
279 interpolate_color(colors[index], colors[index + 1], remainder, max_position)
280 }
281}
282
283impl Default for Paint {
284 fn default() -> Self {
285 Self::Solid(Color::TRANSPARENT)
286 }
287}
288
289impl From<Color> for Paint {
290 fn from(color: Color) -> Self {
291 Self::Solid(color)
292 }
293}
294
295impl From<[u8; 4]> for Paint {
296 fn from(color: [u8; 4]) -> Self {
297 Self::Solid(color.into())
298 }
299}
300
301impl From<[u8; 3]> for Paint {
302 fn from(color: [u8; 3]) -> Self {
303 Self::Solid(color.into())
304 }
305}