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::vg::kurbo::{Affine, Rect, RoundedRect, RoundedRectRadii, Stroke};
8use maycoon_core::vg::peniko::{Brush, Fill};
9use maycoon_core::vg::Scene;
10use maycoon_core::widget::{Widget, WidgetLayoutExt};
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 {
25    layout_style: MaybeSignal<LayoutStyle>,
26    value: MaybeSignal<bool>,
27    on_change: MaybeSignal<Update>,
28}
29
30impl Checkbox {
31    /// Create a new checkbox with the given value.
32    ///
33    /// The value should be a signal, so it's mutable.
34    pub fn new(value: impl Into<MaybeSignal<bool>>) -> Self {
35        Self {
36            layout_style: 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            .into(),
47            value: value.into(),
48            on_change: Update::empty().into(),
49        }
50    }
51
52    /// Sets the value of the checkbox and returns itself.
53    pub fn with_value(mut self, value: impl Into<MaybeSignal<bool>>) -> Self {
54        self.value = value.into();
55        self
56    }
57
58    /// Sets the update value to apply on changes.
59    pub fn with_on_change(mut self, on_change: impl Into<MaybeSignal<Update>>) -> Self {
60        self.on_change = on_change.into();
61        self
62    }
63}
64
65impl WidgetLayoutExt for Checkbox {
66    fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
67        self.layout_style = layout_style.into();
68    }
69}
70
71impl Widget for Checkbox {
72    fn render(
73        &mut self,
74        scene: &mut Scene,
75        theme: &mut dyn Theme,
76        layout_node: &LayoutNode,
77        _: &AppInfo,
78        _: AppContext,
79    ) {
80        let checked = *self.value.get();
81
82        let color = if let Some(style) = theme.of(self.widget_id()) {
83            if checked {
84                style.get_color("color_checked").unwrap()
85            } else {
86                style.get_color("color_unchecked").unwrap()
87            }
88        } else if checked {
89            theme.defaults().interactive().active()
90        } else {
91            theme.defaults().interactive().inactive()
92        };
93
94        scene.stroke(
95            &Stroke::new(3.0),
96            Affine::default(),
97            &Brush::Solid(color),
98            None,
99            &RoundedRect::from_rect(
100                Rect::new(
101                    layout_node.layout.location.x as f64,
102                    layout_node.layout.location.y as f64,
103                    (layout_node.layout.location.x + layout_node.layout.size.width) as f64,
104                    (layout_node.layout.location.y + layout_node.layout.size.height) as f64,
105                ),
106                RoundedRectRadii::from_single_radius(5.0),
107            ),
108        );
109
110        if checked {
111            scene.fill(
112                Fill::NonZero,
113                Affine::default(),
114                &Brush::Solid(color),
115                None,
116                &RoundedRect::from_rect(
117                    Rect::new(
118                        layout_node.layout.location.x as f64 + 5.0,
119                        layout_node.layout.location.y as f64 + 5.0,
120                        (layout_node.layout.location.x + layout_node.layout.size.width) as f64
121                            - 5.0,
122                        (layout_node.layout.location.y + layout_node.layout.size.height) as f64
123                            - 5.0,
124                    ),
125                    RoundedRectRadii::from_single_radius(2.5),
126                ),
127            );
128        }
129    }
130
131    fn layout_style(&self) -> StyleNode {
132        StyleNode {
133            style: self.layout_style.get().clone(),
134            children: Vec::new(),
135        }
136    }
137
138    fn update(&mut self, layout: &LayoutNode, _: AppContext, info: &AppInfo) -> Update {
139        let mut update = Update::empty();
140
141        if let Some(cursor) = &info.cursor_pos {
142            if cursor.x as f32 >= layout.layout.location.x
143                && cursor.x as f32 <= layout.layout.location.x + layout.layout.size.width
144                && cursor.y as f32 >= layout.layout.location.y
145                && cursor.y as f32 <= layout.layout.location.y + layout.layout.size.height
146            {
147                for (_, btn, el) in &info.buttons {
148                    if btn == &MouseButton::Left && *el == ElementState::Released {
149                        update |= *self.on_change.get();
150                        update |= Update::DRAW;
151
152                        if let Some(sig) = self.value.as_signal() {
153                            sig.set(!*sig.get());
154                        }
155                    }
156                }
157            }
158        }
159
160        update
161    }
162
163    fn widget_id(&self) -> WidgetId {
164        WidgetId::new("maycoon-widgets", "Checkbox")
165    }
166}