freya_core/values/
gradient.rs

1use std::{
2    f32::consts::FRAC_PI_2,
3    fmt,
4};
5
6use freya_engine::prelude::*;
7use torin::{
8    prelude::Measure,
9    size::Rect,
10};
11
12use crate::{
13    parsing::{
14        ExtSplit,
15        Parse,
16        ParseError,
17    },
18    values::DisplayColor,
19};
20
21#[derive(Clone, Debug, Default, PartialEq)]
22pub struct GradientStop {
23    pub color: Color,
24    pub offset: f32,
25}
26
27impl Parse for GradientStop {
28    fn parse(value: &str) -> Result<Self, ParseError> {
29        let mut split = value.split_ascii_whitespace_excluding_group('(', ')');
30        let color_str = split.next().ok_or(ParseError)?;
31
32        let offset_str = split.next().ok_or(ParseError)?.trim();
33        if !offset_str.ends_with('%') || split.next().is_some() {
34            return Err(ParseError);
35        }
36
37        let offset = offset_str
38            .replacen('%', "", 1)
39            .parse::<f32>()
40            .map_err(|_| ParseError)?
41            / 100.0;
42
43        Ok(GradientStop {
44            color: Color::parse(color_str).map_err(|_| ParseError)?,
45            offset,
46        })
47    }
48}
49
50impl fmt::Display for GradientStop {
51    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52        _ = self.color.fmt_rgb(f);
53        write!(f, " {}%", self.offset * 100.0)
54    }
55}
56
57#[derive(Clone, Debug, Default, PartialEq)]
58pub struct LinearGradient {
59    pub stops: Vec<GradientStop>,
60    pub angle: f32,
61}
62
63impl LinearGradient {
64    pub fn into_shader(&self, bounds: Rect<f32, Measure>) -> Option<Shader> {
65        let colors: Vec<Color> = self.stops.iter().map(|stop| stop.color).collect();
66        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
67
68        let (dy, dx) = (self.angle.to_radians() + FRAC_PI_2).sin_cos();
69        let farthest_corner = Point::new(
70            if dx > 0.0 { bounds.width() } else { 0.0 },
71            if dy > 0.0 { bounds.height() } else { 0.0 },
72        );
73        let delta = farthest_corner - Point::new(bounds.width(), bounds.height()) / 2.0;
74        let u = delta.x * dy - delta.y * dx;
75        let endpoint = farthest_corner + Point::new(-u * dy, u * dx);
76
77        let origin = Point::new(bounds.min_x(), bounds.min_y());
78        Shader::linear_gradient(
79            (
80                Point::new(bounds.width(), bounds.height()) - endpoint + origin,
81                endpoint + origin,
82            ),
83            GradientShaderColors::Colors(&colors[..]),
84            Some(&offsets[..]),
85            TileMode::Clamp,
86            None,
87            None,
88        )
89    }
90}
91
92impl Parse for LinearGradient {
93    fn parse(value: &str) -> Result<Self, ParseError> {
94        if !value.starts_with("linear-gradient(") || !value.ends_with(')') {
95            return Err(ParseError);
96        }
97
98        let mut gradient = LinearGradient::default();
99        let mut value = value.replacen("linear-gradient(", "", 1);
100        value.remove(value.rfind(')').ok_or(ParseError)?);
101
102        let mut split = value.split_excluding_group(',', '(', ')');
103
104        let angle_or_first_stop = split.next().ok_or(ParseError)?.trim();
105
106        if angle_or_first_stop.ends_with("deg") {
107            if let Ok(angle) = angle_or_first_stop.replacen("deg", "", 1).parse::<f32>() {
108                gradient.angle = angle;
109            }
110        } else {
111            gradient
112                .stops
113                .push(GradientStop::parse(angle_or_first_stop)?);
114        }
115
116        for stop in split {
117            gradient.stops.push(GradientStop::parse(stop)?);
118        }
119
120        Ok(gradient)
121    }
122}
123
124impl fmt::Display for LinearGradient {
125    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126        write!(
127            f,
128            "linear-gradient({}deg, {})",
129            self.angle,
130            self.stops
131                .iter()
132                .map(|stop| stop.to_string())
133                .collect::<Vec<_>>()
134                .join(", ")
135        )
136    }
137}
138
139#[derive(Clone, Debug, Default, PartialEq)]
140pub struct RadialGradient {
141    pub stops: Vec<GradientStop>,
142}
143
144impl RadialGradient {
145    pub fn into_shader(&self, bounds: Rect<f32, Measure>) -> Option<Shader> {
146        let colors: Vec<Color> = self.stops.iter().map(|stop| stop.color).collect();
147        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
148
149        let center = bounds.center();
150
151        Shader::radial_gradient(
152            Point::new(center.x, center.y),
153            bounds.width().max(bounds.height()) / 2.0,
154            GradientShaderColors::Colors(&colors[..]),
155            Some(&offsets[..]),
156            TileMode::Clamp,
157            None,
158            None,
159        )
160    }
161}
162
163impl Parse for RadialGradient {
164    fn parse(value: &str) -> Result<Self, ParseError> {
165        if !value.starts_with("radial-gradient(") || !value.ends_with(')') {
166            return Err(ParseError);
167        }
168
169        let mut gradient = RadialGradient::default();
170        let mut value = value.replacen("radial-gradient(", "", 1);
171
172        value.remove(value.rfind(')').ok_or(ParseError)?);
173
174        for stop in value.split_excluding_group(',', '(', ')') {
175            gradient.stops.push(GradientStop::parse(stop)?);
176        }
177
178        Ok(gradient)
179    }
180}
181
182impl fmt::Display for RadialGradient {
183    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
184        write!(
185            f,
186            "radial-gradient({})",
187            self.stops
188                .iter()
189                .map(|stop| stop.to_string())
190                .collect::<Vec<_>>()
191                .join(", ")
192        )
193    }
194}
195
196#[derive(Clone, Debug, Default, PartialEq)]
197pub struct ConicGradient {
198    pub stops: Vec<GradientStop>,
199    pub angles: Option<(f32, f32)>,
200    pub angle: Option<f32>,
201}
202
203impl ConicGradient {
204    pub fn into_shader(&self, bounds: Rect<f32, Measure>) -> Option<Shader> {
205        let colors: Vec<Color> = self.stops.iter().map(|stop| stop.color).collect();
206        let offsets: Vec<f32> = self.stops.iter().map(|stop| stop.offset).collect();
207
208        let center = bounds.center();
209
210        let matrix =
211            Matrix::rotate_deg_pivot(-90.0 + self.angle.unwrap_or(0.0), (center.x, center.y));
212
213        Shader::sweep_gradient(
214            (center.x, center.y),
215            GradientShaderColors::Colors(&colors[..]),
216            Some(&offsets[..]),
217            TileMode::Clamp,
218            self.angles,
219            None,
220            Some(&matrix),
221        )
222    }
223}
224
225impl Parse for ConicGradient {
226    fn parse(value: &str) -> Result<Self, ParseError> {
227        if !value.starts_with("conic-gradient(") || !value.ends_with(')') {
228            return Err(ParseError);
229        }
230
231        let mut gradient = ConicGradient::default();
232        let mut value = value.replacen("conic-gradient(", "", 1);
233
234        value.remove(value.rfind(')').ok_or(ParseError)?);
235
236        let mut split = value.split_excluding_group(',', '(', ')');
237
238        let angle_or_first_stop = split.next().ok_or(ParseError)?.trim();
239
240        if angle_or_first_stop.ends_with("deg") {
241            if let Ok(angle) = angle_or_first_stop.replacen("deg", "", 1).parse::<f32>() {
242                gradient.angle = Some(angle);
243            }
244        } else {
245            gradient
246                .stops
247                .push(GradientStop::parse(angle_or_first_stop).map_err(|_| ParseError)?);
248        }
249
250        if let Some(angles_or_second_stop) = split.next().map(str::trim) {
251            if angles_or_second_stop.starts_with("from ") && angles_or_second_stop.ends_with("deg")
252            {
253                if let Some(start) = angles_or_second_stop
254                    .find("deg")
255                    .and_then(|index| angles_or_second_stop.get(5..index))
256                    .and_then(|slice| slice.parse::<f32>().ok())
257                {
258                    let end = angles_or_second_stop
259                        .find(" to ")
260                        .and_then(|index| angles_or_second_stop.get(index + 4..))
261                        .and_then(|slice| slice.find("deg").and_then(|index| slice.get(0..index)))
262                        .and_then(|slice| slice.parse::<f32>().ok())
263                        .unwrap_or(360.0);
264
265                    gradient.angles = Some((start, end));
266                }
267            } else {
268                gradient
269                    .stops
270                    .push(GradientStop::parse(angles_or_second_stop)?);
271            }
272        }
273
274        for stop in split {
275            gradient.stops.push(GradientStop::parse(stop)?);
276        }
277
278        Ok(gradient)
279    }
280}
281
282impl fmt::Display for ConicGradient {
283    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
284        write!(f, "conic-gradient(")?;
285
286        if let Some(angle) = self.angle {
287            write!(f, "{angle}deg, ")?;
288        }
289
290        if let Some((start, end)) = self.angles {
291            write!(f, "from {start}deg to {end}deg, ")?;
292        }
293
294        write!(
295            f,
296            "{})",
297            self.stops
298                .iter()
299                .map(|stop| stop.to_string())
300                .collect::<Vec<_>>()
301                .join(", ")
302        )
303    }
304}