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
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}