maycoon_widgets/
checkbox.rs

1use crate::ext::WidgetLayoutExt;
2use maycoon_core::app::info::AppInfo;
3use maycoon_core::app::update::Update;
4use maycoon_core::layout;
5use maycoon_core::layout::{Dimension, LayoutNode, LayoutStyle, LengthPercentageAuto, StyleNode};
6use maycoon_core::state::{State, Val};
7use maycoon_core::vg::kurbo::{Affine, Rect, RoundedRect, RoundedRectRadii, Stroke};
8use maycoon_core::vg::peniko::{Brush, Fill};
9use maycoon_core::vg::Scene;
10use maycoon_core::widget::Widget;
11use maycoon_core::window::{ElementState, MouseButton};
12use maycoon_theme::id::WidgetId;
13use maycoon_theme::theme::Theme;
14use nalgebra::Vector2;
15
16/// A checkbox widget. Changes state when it's clicked.
17///
18/// See the [checkbox](https://github.com/maycoon-ui/maycoon/blob/master/examples/checkbox/src/main.rs) example for how to use it in practice.
19///
20/// ### Theming
21/// Styling the checkbox require following properties:
22/// - `color_unchecked` -  The color of the checkbox, when it's not checked (inner value is false).
23/// - `color_checked` - The color of the checkbox, when it's checked (inner value is true).
24pub struct Checkbox<S: State> {
25    layout_style: Val<S, LayoutStyle>,
26    on_change: Box<dyn FnMut(&mut S) -> Update>,
27    value: Val<S, bool>,
28}
29
30impl<S: State> Checkbox<S> {
31    /// Create a new checkbox with the given value.
32    ///
33    /// The value should be state dependent, so you can mutate it on change.
34    pub fn new(value: Val<S, bool>) -> Self {
35        Self {
36            layout_style: Val::new_val(LayoutStyle {
37                size: Vector2::<Dimension>::new(Dimension::Length(20.0), Dimension::Length(20.0)),
38                margin: layout::Rect::<LengthPercentageAuto> {
39                    left: LengthPercentageAuto::Length(0.5),
40                    right: LengthPercentageAuto::Length(0.5),
41                    top: LengthPercentageAuto::Length(0.5),
42                    bottom: LengthPercentageAuto::Length(0.5),
43                },
44                ..Default::default()
45            }),
46            on_change: Box::new(|_| Update::empty()),
47            value,
48        }
49    }
50
51    /// Sets the function to be called when the checkbox is clicked/changed.
52    ///
53    /// You should mutate the inner value of the checkbox using the provided state.
54    pub fn with_on_change(mut self, on_change: impl FnMut(&mut S) -> Update + 'static) -> Self {
55        self.on_change = Box::new(on_change);
56        self
57    }
58
59    /// Sets the value of the checkbox and returns itself.
60    ///
61    /// The [Val] should be state dependent, so you can mutate it on change.
62    pub fn with_value(mut self, value: impl Into<Val<S, bool>>) -> Self {
63        self.value = value.into();
64        self
65    }
66}
67
68impl<S: State> WidgetLayoutExt<S> for Checkbox<S> {
69    fn set_layout_style(&mut self, layout_style: impl Into<Val<S, LayoutStyle>>) {
70        self.layout_style = layout_style.into();
71    }
72}
73
74impl<S: State> Widget<S> for Checkbox<S> {
75    fn render(
76        &mut self,
77        scene: &mut Scene,
78        theme: &mut dyn Theme,
79        _: &AppInfo,
80        layout_node: &LayoutNode,
81        state: &S,
82    ) {
83        let checked = *self.value.get_ref(state);
84
85        let color = if let Some(style) = theme.of(self.widget_id()) {
86            if checked {
87                style.get_color("color_checked").unwrap()
88            } else {
89                style.get_color("color_unchecked").unwrap()
90            }
91        } else if checked {
92            theme.defaults().interactive().active()
93        } else {
94            theme.defaults().interactive().inactive()
95        };
96
97        scene.stroke(
98            &Stroke::new(3.0),
99            Affine::default(),
100            &Brush::Solid(color),
101            None,
102            &RoundedRect::from_rect(
103                Rect::new(
104                    layout_node.layout.location.x as f64,
105                    layout_node.layout.location.y as f64,
106                    (layout_node.layout.location.x + layout_node.layout.size.width) as f64,
107                    (layout_node.layout.location.y + layout_node.layout.size.height) as f64,
108                ),
109                RoundedRectRadii::from_single_radius(5.0),
110            ),
111        );
112
113        if checked {
114            scene.fill(
115                Fill::NonZero,
116                Affine::default(),
117                &Brush::Solid(color),
118                None,
119                &RoundedRect::from_rect(
120                    Rect::new(
121                        layout_node.layout.location.x as f64 + 5.0,
122                        layout_node.layout.location.y as f64 + 5.0,
123                        (layout_node.layout.location.x + layout_node.layout.size.width) as f64
124                            - 5.0,
125                        (layout_node.layout.location.y + layout_node.layout.size.height) as f64
126                            - 5.0,
127                    ),
128                    RoundedRectRadii::from_single_radius(2.5),
129                ),
130            );
131        }
132    }
133
134    fn layout_style(&mut self, state: &S) -> StyleNode {
135        StyleNode {
136            style: self.layout_style.get_ref(state).clone(),
137            children: Vec::new(),
138        }
139    }
140
141    fn update(&mut self, layout: &LayoutNode, state: &mut S, info: &AppInfo) -> Update {
142        self.value.invalidate();
143        self.layout_style.invalidate();
144
145        let mut update = Update::empty();
146
147        if let Some(cursor) = &info.cursor_pos {
148            if cursor.x as f32 >= layout.layout.location.x
149                && cursor.x as f32 <= layout.layout.location.x + layout.layout.size.width
150                && cursor.y as f32 >= layout.layout.location.y
151                && cursor.y as f32 <= layout.layout.location.y + layout.layout.size.height
152            {
153                for (_, btn, el) in &info.buttons {
154                    if btn == &MouseButton::Left && *el == ElementState::Released {
155                        update |= (self.on_change)(state);
156                    }
157                }
158            }
159        }
160
161        update
162    }
163
164    fn widget_id(&self) -> WidgetId {
165        WidgetId::new("maycoon-widgets", "Checkbox")
166    }
167}