Skip to main content

repose_core/
visual.rs

1use std::rc::Rc;
2
3use crate::{
4    Brush, Color, ImageAlignment, ImageFilter, ImageFit, ImageHandle, ImageSourceRect, Px, Rect,
5    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                fit: *fit,
117                filter: *filter,
118                source_rect: *source_rect,
119                alignment: *alignment,
120            }),
121            Self::Custom(painter) => painter(scene, rect, state),
122        }
123    }
124
125    pub fn custom_identity(&self) -> Option<usize> {
126        match self {
127            Self::Custom(painter) => Some(Rc::as_ptr(painter) as *const () as usize),
128            _ => None,
129        }
130    }
131}
132
133impl std::fmt::Debug for ControlVisual {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            Self::Rect { brush, radius } => f
137                .debug_struct("Rect")
138                .field("brush", brush)
139                .field("radius", radius)
140                .finish(),
141            Self::Image {
142                handle,
143                source_rect,
144                tint,
145                fit,
146                filter,
147                alignment,
148            } => f
149                .debug_struct("Image")
150                .field("handle", handle)
151                .field("source_rect", source_rect)
152                .field("tint", tint)
153                .field("fit", fit)
154                .field("filter", filter)
155                .field("alignment", alignment)
156                .finish(),
157            Self::Custom(_) => f
158                .debug_struct("Custom")
159                .field("identity", &self.custom_identity())
160                .finish(),
161        }
162    }
163}
164
165impl PartialEq for ControlVisual {
166    fn eq(&self, other: &Self) -> bool {
167        match (self, other) {
168            (
169                Self::Rect {
170                    brush: a,
171                    radius: ra,
172                },
173                Self::Rect {
174                    brush: b,
175                    radius: rb,
176                },
177            ) => a == b && ra == rb,
178            (
179                Self::Image {
180                    handle: ah,
181                    source_rect: ar,
182                    tint: at,
183                    fit: af,
184                    filter: ax,
185                    alignment: aa,
186                },
187                Self::Image {
188                    handle: bh,
189                    source_rect: br,
190                    tint: bt,
191                    fit: bf,
192                    filter: bx,
193                    alignment: ba,
194                },
195            ) => ah == bh && ar == br && at == bt && af == bf && ax == bx && aa == ba,
196            (Self::Custom(a), Self::Custom(b)) => Rc::ptr_eq(a, b),
197            _ => false,
198        }
199    }
200}
201
202#[derive(Clone, Debug, Default, PartialEq)]
203pub struct ControlVisualSet {
204    pub normal: Option<ControlVisual>,
205    pub hovered: Option<ControlVisual>,
206    pub pressed: Option<ControlVisual>,
207    pub dragged: Option<ControlVisual>,
208    pub focused: Option<ControlVisual>,
209    pub disabled: Option<ControlVisual>,
210}
211
212impl ControlVisualSet {
213    pub fn resolve(&self, state: ControlVisualState) -> Option<&ControlVisual> {
214        let selected = if !state.enabled {
215            self.disabled.as_ref()
216        } else if state.dragged {
217            self.dragged.as_ref()
218        } else if state.pressed {
219            self.pressed.as_ref()
220        } else if state.focused {
221            self.focused.as_ref()
222        } else if state.hovered {
223            self.hovered.as_ref()
224        } else {
225            None
226        };
227        selected.or(self.normal.as_ref())
228    }
229
230    pub fn with_normal(mut self, visual: ControlVisual) -> Self {
231        self.normal = Some(visual);
232        self
233    }
234
235    pub fn with_hovered(mut self, visual: ControlVisual) -> Self {
236        self.hovered = Some(visual);
237        self
238    }
239
240    pub fn with_pressed(mut self, visual: ControlVisual) -> Self {
241        self.pressed = Some(visual);
242        self
243    }
244
245    pub fn with_dragged(mut self, visual: ControlVisual) -> Self {
246        self.dragged = Some(visual);
247        self
248    }
249
250    pub fn with_focused(mut self, visual: ControlVisual) -> Self {
251        self.focused = Some(visual);
252        self
253    }
254
255    pub fn with_disabled(mut self, visual: ControlVisual) -> Self {
256        self.disabled = Some(visual);
257        self
258    }
259}
260
261fn scale_color_alpha(color: Color, alpha: f32) -> Color {
262    Color(
263        color.0,
264        color.1,
265        color.2,
266        (color.3 as f32 * alpha.clamp(0.0, 1.0)) as u8,
267    )
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    #[test]
275    fn control_visual_state_priority_is_stable() {
276        let set = ControlVisualSet {
277            normal: Some(ControlVisual::image(1, None, Color::WHITE)),
278            hovered: Some(ControlVisual::image(2, None, Color::WHITE)),
279            focused: Some(ControlVisual::image(3, None, Color::WHITE)),
280            dragged: Some(ControlVisual::image(4, None, Color::WHITE)),
281            disabled: Some(ControlVisual::image(5, None, Color::WHITE)),
282            ..Default::default()
283        };
284        let focused_hovered = ControlVisualState {
285            hovered: true,
286            focused: true,
287            ..Default::default()
288        };
289        assert!(matches!(
290            set.resolve(focused_hovered),
291            Some(ControlVisual::Image { handle: 3, .. })
292        ));
293        let disabled_dragged = ControlVisualState {
294            enabled: false,
295            dragged: true,
296            ..Default::default()
297        };
298        assert!(matches!(
299            set.resolve(disabled_dragged),
300            Some(ControlVisual::Image { handle: 5, .. })
301        ));
302    }
303}
304
305fn scale_brush_alpha(brush: Brush, alpha: f32) -> Brush {
306    match brush {
307        Brush::Solid(color) => Brush::Solid(scale_color_alpha(color, alpha)),
308        Brush::Linear {
309            start,
310            end,
311            start_color,
312            end_color,
313        } => Brush::Linear {
314            start,
315            end,
316            start_color: scale_color_alpha(start_color, alpha),
317            end_color: scale_color_alpha(end_color, alpha),
318        },
319        Brush::LinearNormalized {
320            start,
321            end,
322            start_color,
323            end_color,
324        } => Brush::LinearNormalized {
325            start,
326            end,
327            start_color: scale_color_alpha(start_color, alpha),
328            end_color: scale_color_alpha(end_color, alpha),
329        },
330        Brush::Radial {
331            center,
332            radius,
333            start_color,
334            end_color,
335        } => Brush::Radial {
336            center,
337            radius,
338            start_color: scale_color_alpha(start_color, alpha),
339            end_color: scale_color_alpha(end_color, alpha),
340        },
341        Brush::Sweep {
342            center,
343            start_color,
344            end_color,
345        } => Brush::Sweep {
346            center,
347            start_color: scale_color_alpha(start_color, alpha),
348            end_color: scale_color_alpha(end_color, alpha),
349        },
350    }
351}