Skip to main content

repose_core/
visual.rs

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
272fn scale_brush_alpha(brush: Brush, alpha: f32) -> Brush {
273    match brush {
274        Brush::Solid(color) => Brush::Solid(scale_color_alpha(color, alpha)),
275        Brush::Linear {
276            start,
277            end,
278            start_color,
279            end_color,
280        } => Brush::Linear {
281            start,
282            end,
283            start_color: scale_color_alpha(start_color, alpha),
284            end_color: scale_color_alpha(end_color, alpha),
285        },
286        Brush::LinearNormalized {
287            start,
288            end,
289            start_color,
290            end_color,
291        } => Brush::LinearNormalized {
292            start,
293            end,
294            start_color: scale_color_alpha(start_color, alpha),
295            end_color: scale_color_alpha(end_color, alpha),
296        },
297        Brush::Radial {
298            center,
299            radius,
300            start_color,
301            end_color,
302        } => Brush::Radial {
303            center,
304            radius,
305            start_color: scale_color_alpha(start_color, alpha),
306            end_color: scale_color_alpha(end_color, alpha),
307        },
308        Brush::Sweep {
309            center,
310            start_color,
311            end_color,
312        } => Brush::Sweep {
313            center,
314            start_color: scale_color_alpha(start_color, alpha),
315            end_color: scale_color_alpha(end_color, alpha),
316        },
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn control_visual_state_priority_is_stable() {
326        let set = ControlVisualSet {
327            normal: Some(ControlVisual::image(1, None, Color::WHITE)),
328            hovered: Some(ControlVisual::image(2, None, Color::WHITE)),
329            focused: Some(ControlVisual::image(3, None, Color::WHITE)),
330            dragged: Some(ControlVisual::image(4, None, Color::WHITE)),
331            disabled: Some(ControlVisual::image(5, None, Color::WHITE)),
332            ..Default::default()
333        };
334        let focused_hovered = ControlVisualState {
335            hovered: true,
336            focused: true,
337            ..Default::default()
338        };
339        assert!(matches!(
340            set.resolve(focused_hovered),
341            Some(ControlVisual::Image { handle: 3, .. })
342        ));
343        let disabled_dragged = ControlVisualState {
344            enabled: false,
345            dragged: true,
346            ..Default::default()
347        };
348        assert!(matches!(
349            set.resolve(disabled_dragged),
350            Some(ControlVisual::Image { handle: 5, .. })
351        ));
352    }
353}