1use std::rc::Rc;
2
3use crate::{
4 Brush, Color, ImageAlignment, ImageFilter, ImageFit, ImageHandle, ImagePaintStyle,
5 ImageSourceRect, Px, Rect, Scene, SceneNode,
6};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct ControlVisualState {
10 pub alpha: f32,
11 pub enabled: bool,
12 pub hovered: bool,
13 pub pressed: bool,
14 pub dragged: bool,
15 pub focused: bool,
16}
17
18impl Default for ControlVisualState {
19 fn default() -> Self {
20 Self {
21 alpha: 1.0,
22 enabled: true,
23 hovered: false,
24 pressed: false,
25 dragged: false,
26 focused: false,
27 }
28 }
29}
30
31pub type ControlPainter = Rc<dyn Fn(&mut Scene, Rect, ControlVisualState)>;
32
33#[derive(Clone)]
34#[non_exhaustive]
35pub enum ControlVisual {
36 Rect {
37 brush: Brush,
38 radius: [Px; 4],
39 },
40 Image {
41 handle: ImageHandle,
42 source_rect: Option<ImageSourceRect>,
43 tint: Color,
44 fit: ImageFit,
45 filter: ImageFilter,
46 alignment: ImageAlignment,
47 },
48 Custom(ControlPainter),
49}
50
51impl ControlVisual {
52 pub fn rect(brush: Brush, radius: [Px; 4]) -> Self {
53 Self::Rect { brush, radius }
54 }
55
56 pub fn image(handle: ImageHandle, source_rect: Option<ImageSourceRect>, tint: Color) -> Self {
57 Self::Image {
58 handle,
59 source_rect,
60 tint,
61 fit: ImageFit::FillBounds,
62 filter: ImageFilter::Linear,
63 alignment: ImageAlignment::Center,
64 }
65 }
66
67 pub fn custom(painter: impl Fn(&mut Scene, Rect, ControlVisualState) + 'static) -> Self {
68 Self::Custom(Rc::new(painter))
69 }
70
71 pub fn image_fit(mut self, fit: ImageFit) -> Self {
72 if let Self::Image { fit: current, .. } = &mut self {
73 *current = fit;
74 }
75 self
76 }
77
78 pub fn image_filter(mut self, filter: ImageFilter) -> Self {
79 if let Self::Image {
80 filter: current, ..
81 } = &mut self
82 {
83 *current = filter;
84 }
85 self
86 }
87
88 pub fn image_alignment(mut self, alignment: ImageAlignment) -> Self {
89 if let Self::Image {
90 alignment: current, ..
91 } = &mut self
92 {
93 *current = alignment;
94 }
95 self
96 }
97
98 pub fn paint(&self, scene: &mut Scene, rect: Rect, state: ControlVisualState) {
99 match self {
100 Self::Rect { brush, radius } => scene.nodes.push(SceneNode::Rect {
101 rect,
102 brush: scale_brush_alpha(*brush, state.alpha),
103 radius: *radius,
104 }),
105 Self::Image {
106 handle,
107 source_rect,
108 tint,
109 fit,
110 filter,
111 alignment,
112 } => scene.nodes.push(SceneNode::Image {
113 rect,
114 handle: *handle,
115 tint: scale_color_alpha(*tint, state.alpha),
116 style: ImagePaintStyle {
117 fit: *fit,
118 filter: *filter,
119 source_rect: *source_rect,
120 alignment: *alignment,
121 },
122 }),
123 Self::Custom(painter) => painter(scene, rect, state),
124 }
125 }
126
127 pub fn custom_identity(&self) -> Option<usize> {
128 match self {
129 Self::Custom(painter) => Some(Rc::as_ptr(painter) as *const () as usize),
130 _ => None,
131 }
132 }
133}
134
135impl std::fmt::Debug for ControlVisual {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 match self {
138 Self::Rect { brush, radius } => f
139 .debug_struct("Rect")
140 .field("brush", brush)
141 .field("radius", radius)
142 .finish(),
143 Self::Image {
144 handle,
145 source_rect,
146 tint,
147 fit,
148 filter,
149 alignment,
150 } => f
151 .debug_struct("Image")
152 .field("handle", handle)
153 .field("source_rect", source_rect)
154 .field("tint", tint)
155 .field("fit", fit)
156 .field("filter", filter)
157 .field("alignment", alignment)
158 .finish(),
159 Self::Custom(_) => f
160 .debug_struct("Custom")
161 .field("identity", &self.custom_identity())
162 .finish(),
163 }
164 }
165}
166
167impl PartialEq for ControlVisual {
168 fn eq(&self, other: &Self) -> bool {
169 match (self, other) {
170 (
171 Self::Rect {
172 brush: a,
173 radius: ra,
174 },
175 Self::Rect {
176 brush: b,
177 radius: rb,
178 },
179 ) => a == b && ra == rb,
180 (
181 Self::Image {
182 handle: ah,
183 source_rect: ar,
184 tint: at,
185 fit: af,
186 filter: ax,
187 alignment: aa,
188 },
189 Self::Image {
190 handle: bh,
191 source_rect: br,
192 tint: bt,
193 fit: bf,
194 filter: bx,
195 alignment: ba,
196 },
197 ) => ah == bh && ar == br && at == bt && af == bf && ax == bx && aa == ba,
198 (Self::Custom(a), Self::Custom(b)) => Rc::ptr_eq(a, b),
199 _ => false,
200 }
201 }
202}
203
204#[derive(Clone, Debug, Default, PartialEq)]
205pub struct ControlVisualSet {
206 pub normal: Option<ControlVisual>,
207 pub hovered: Option<ControlVisual>,
208 pub pressed: Option<ControlVisual>,
209 pub dragged: Option<ControlVisual>,
210 pub focused: Option<ControlVisual>,
211 pub disabled: Option<ControlVisual>,
212}
213
214impl ControlVisualSet {
215 pub fn resolve(&self, state: ControlVisualState) -> Option<&ControlVisual> {
216 let selected = if !state.enabled {
217 self.disabled.as_ref()
218 } else if state.dragged {
219 self.dragged.as_ref()
220 } else if state.pressed {
221 self.pressed.as_ref()
222 } else if state.focused {
223 self.focused.as_ref()
224 } else if state.hovered {
225 self.hovered.as_ref()
226 } else {
227 None
228 };
229 selected.or(self.normal.as_ref())
230 }
231
232 pub fn with_normal(mut self, visual: ControlVisual) -> Self {
233 self.normal = Some(visual);
234 self
235 }
236
237 pub fn with_hovered(mut self, visual: ControlVisual) -> Self {
238 self.hovered = Some(visual);
239 self
240 }
241
242 pub fn with_pressed(mut self, visual: ControlVisual) -> Self {
243 self.pressed = Some(visual);
244 self
245 }
246
247 pub fn with_dragged(mut self, visual: ControlVisual) -> Self {
248 self.dragged = Some(visual);
249 self
250 }
251
252 pub fn with_focused(mut self, visual: ControlVisual) -> Self {
253 self.focused = Some(visual);
254 self
255 }
256
257 pub fn with_disabled(mut self, visual: ControlVisual) -> Self {
258 self.disabled = Some(visual);
259 self
260 }
261}
262
263fn scale_color_alpha(color: Color, alpha: f32) -> Color {
264 Color(
265 color.0,
266 color.1,
267 color.2,
268 (color.3 as f32 * alpha.clamp(0.0, 1.0)) as u8,
269 )
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn control_visual_state_priority_is_stable() {
278 let set = ControlVisualSet {
279 normal: Some(ControlVisual::image(1, None, Color::WHITE)),
280 hovered: Some(ControlVisual::image(2, None, Color::WHITE)),
281 focused: Some(ControlVisual::image(3, None, Color::WHITE)),
282 dragged: Some(ControlVisual::image(4, None, Color::WHITE)),
283 disabled: Some(ControlVisual::image(5, None, Color::WHITE)),
284 ..Default::default()
285 };
286 let focused_hovered = ControlVisualState {
287 hovered: true,
288 focused: true,
289 ..Default::default()
290 };
291 assert!(matches!(
292 set.resolve(focused_hovered),
293 Some(ControlVisual::Image { handle: 3, .. })
294 ));
295 let disabled_dragged = ControlVisualState {
296 enabled: false,
297 dragged: true,
298 ..Default::default()
299 };
300 assert!(matches!(
301 set.resolve(disabled_dragged),
302 Some(ControlVisual::Image { handle: 5, .. })
303 ));
304 }
305}
306
307fn scale_brush_alpha(brush: Brush, alpha: f32) -> Brush {
308 match brush {
309 Brush::Solid(color) => Brush::Solid(scale_color_alpha(color, alpha)),
310 Brush::Linear {
311 start,
312 end,
313 start_color,
314 end_color,
315 } => Brush::Linear {
316 start,
317 end,
318 start_color: scale_color_alpha(start_color, alpha),
319 end_color: scale_color_alpha(end_color, alpha),
320 },
321 Brush::LinearNormalized {
322 start,
323 end,
324 start_color,
325 end_color,
326 } => Brush::LinearNormalized {
327 start,
328 end,
329 start_color: scale_color_alpha(start_color, alpha),
330 end_color: scale_color_alpha(end_color, alpha),
331 },
332 Brush::Radial {
333 center,
334 radius,
335 start_color,
336 end_color,
337 } => Brush::Radial {
338 center,
339 radius,
340 start_color: scale_color_alpha(start_color, alpha),
341 end_color: scale_color_alpha(end_color, alpha),
342 },
343 Brush::Sweep {
344 center,
345 start_color,
346 end_color,
347 } => Brush::Sweep {
348 center,
349 start_color: scale_color_alpha(start_color, alpha),
350 end_color: scale_color_alpha(end_color, alpha),
351 },
352 }
353}