freya_core/style/
gradient.rs

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