Skip to main content

freya_core/style/
gradient.rs

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