maycoon_widgets/
checkbox.rs

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