1use teksilo_canvas::{GradientStop, Paint, Point, Size};
20
21use crate::binding::{BindingLevel, BindingRegistry};
22use crate::color_prop::ColorProp;
23use crate::styles::{FillRecipe, Theme};
24use crate::widget_id::WidgetId;
25
26#[derive(Clone, Debug)]
29pub struct GradientStopProp {
30 pub offset: f32,
32 pub color: ColorProp,
33}
34
35#[derive(Clone, Debug)]
37pub enum PaintProp {
38 Solid(ColorProp),
41 Linear {
44 stops: Vec<GradientStopProp>,
45 angle_deg: f32,
46 },
47 Radial {
50 stops: Vec<GradientStopProp>,
51 center: (f32, f32),
52 radius: f32,
53 },
54}
55
56impl PaintProp {
57 pub fn solid(color: impl Into<ColorProp>) -> Self {
59 PaintProp::Solid(color.into())
60 }
61
62 pub fn from_fill(fill: &FillRecipe, colors: &teksilo_tokens::ColorTokens) -> Self {
70 match fill {
71 FillRecipe::Solid(c) => PaintProp::Solid((*c).into()),
72 FillRecipe::None => {
73 PaintProp::Solid(ColorProp::Static(teksilo_tokens::Color::TRANSPARENT))
74 }
75 FillRecipe::StateLayer { .. } => {
76 let flat = fill
78 .resolve_flat(colors)
79 .unwrap_or(teksilo_tokens::Color::TRANSPARENT);
80 PaintProp::Solid(ColorProp::Static(flat))
81 }
82 FillRecipe::LinearGradient { stops, angle_deg } => PaintProp::Linear {
83 stops: stops.iter().map(stop_to_prop).collect(),
84 angle_deg: *angle_deg,
85 },
86 FillRecipe::RadialGradient {
87 stops,
88 center,
89 radius,
90 } => PaintProp::Radial {
91 stops: stops.iter().map(stop_to_prop).collect(),
92 center: *center,
93 radius: *radius,
94 },
95 }
96 }
97
98 pub fn resolve(&self, theme: &Theme, enabled: bool, size: Size) -> Paint {
101 match self {
102 PaintProp::Solid(c) => Paint::Solid(c.resolve(theme, enabled)),
103 PaintProp::Linear { stops, angle_deg } => {
104 let (start, end) = angle_to_endpoints(*angle_deg, size);
105 Paint::LinearGradient {
106 start,
107 end,
108 stops: resolve_stops(stops, theme, enabled),
109 }
110 }
111 PaintProp::Radial {
112 stops,
113 center,
114 radius,
115 } => Paint::RadialGradient {
116 center: Point::new(center.0 * size.width, center.1 * size.height),
117 radius: radius * size.width.max(size.height),
118 stops: resolve_stops(stops, theme, enabled),
119 },
120 }
121 }
122
123 pub fn register_if_bound(
126 &self,
127 widget_id: WidgetId,
128 registry: &BindingRegistry,
129 level: BindingLevel,
130 ) {
131 match self {
132 PaintProp::Solid(c) => c.register_if_bound(widget_id, registry, level),
133 PaintProp::Linear { stops, .. } | PaintProp::Radial { stops, .. } => {
134 for s in stops {
135 s.color.register_if_bound(widget_id, registry, level);
136 }
137 }
138 }
139 }
140}
141
142fn stop_to_prop(s: &crate::styles::GradientStop) -> GradientStopProp {
143 GradientStopProp {
144 offset: s.offset,
145 color: s.color.into(),
146 }
147}
148
149fn resolve_stops(stops: &[GradientStopProp], theme: &Theme, enabled: bool) -> Vec<GradientStop> {
150 stops
151 .iter()
152 .map(|s| GradientStop {
153 offset: s.offset,
154 color: s.color.resolve(theme, enabled),
155 })
156 .collect()
157}
158
159pub fn angle_to_endpoints(angle_deg: f32, size: Size) -> (Point, Point) {
163 let rad = angle_deg.to_radians();
164 let dx = rad.sin();
166 let dy = rad.cos();
167 let cx = size.width * 0.5;
168 let cy = size.height * 0.5;
169 let ex = dx * size.width * 0.5;
170 let ey = dy * size.height * 0.5;
171 (Point::new(cx - ex, cy - ey), Point::new(cx + ex, cy + ey))
172}
173
174impl<T: Into<ColorProp>> From<T> for PaintProp {
178 fn from(t: T) -> Self {
179 PaintProp::Solid(t.into())
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::presets::intui;
187 use crate::styles::{GradientStop as RecipeStop, RecipeColor};
188 use teksilo_tokens::{Color, SurfaceRole};
189
190 #[test]
191 fn solid_from_color_resolves() {
192 let theme = intui::light();
193 let p = PaintProp::solid(Color::RED);
194 match p.resolve(&theme, true, Size::new(10.0, 10.0)) {
195 Paint::Solid(c) => assert_eq!(c, Color::RED),
196 _ => panic!("expected solid"),
197 }
198 }
199
200 #[test]
201 fn vertical_angle_endpoints() {
202 let (s, e) = angle_to_endpoints(0.0, Size::new(100.0, 40.0));
204 assert_eq!((s.x, s.y), (50.0, 0.0));
205 assert_eq!((e.x, e.y), (50.0, 40.0));
206 let (s, e) = angle_to_endpoints(90.0, Size::new(100.0, 40.0));
208 assert!((s.x - 0.0).abs() < 1e-4 && (s.y - 20.0).abs() < 1e-4);
209 assert!((e.x - 100.0).abs() < 1e-4 && (e.y - 20.0).abs() < 1e-4);
210 }
211
212 #[test]
213 fn linear_gradient_from_fill_resolves_role_stops() {
214 let theme = intui::light();
215 let fill = FillRecipe::LinearGradient {
216 stops: vec![
217 RecipeStop {
218 offset: 0.0,
219 color: RecipeColor::Surface(SurfaceRole::Accent),
220 },
221 RecipeStop {
222 offset: 1.0,
223 color: RecipeColor::Static(Color::WHITE),
224 },
225 ],
226 angle_deg: 0.0,
227 };
228 let p = PaintProp::from_fill(&fill, &theme.colors);
229 match p.resolve(&theme, true, Size::new(20.0, 20.0)) {
230 Paint::LinearGradient { stops, .. } => {
231 assert_eq!(stops.len(), 2);
232 assert_eq!(stops[0].color, theme.colors.accent);
233 assert_eq!(stops[1].color, Color::WHITE);
234 }
235 _ => panic!("expected linear gradient"),
236 }
237 }
238}