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}