maycoon_widgets/
switch.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::{Circle, Point, 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 switch widget to toggle between two states.
16///
17/// Similar to a checkbox, but with slightly different visuals.
18///
19/// See the [switch](https://github.com/maycoon-ui/maycoon/blob/master/examples/switch/src/main.rs) example for how to use it in practice.
20///
21/// ### Theming
22/// Styling the checkbox requires following properties:
23/// - `color_unchecked` -  The color of the switch, when it's not checked (inner value is false).
24/// - `color_checked` - The color of the switch, when it's checked (inner value is true).
25///
26/// The [WidgetId] is equal to `maycoon-widgets:Switch`.
27pub struct Switch {
28    layout: MaybeSignal<LayoutStyle>,
29    value: MaybeSignal<bool>,
30    on_change: MaybeSignal<Update>,
31}
32
33impl Switch {
34    /// Create a new switch with the given value.
35    ///
36    /// The value should be a signal, so it's mutable.
37    #[inline(always)]
38    pub fn new(value: impl Into<MaybeSignal<bool>>) -> Self {
39        Self {
40            layout: LayoutStyle {
41                size: Vector2::new(Dimension::length(60.0), Dimension::length(30.0)),
42                margin: layout::Rect::<LengthPercentageAuto> {
43                    left: LengthPercentageAuto::length(2.5),
44                    right: LengthPercentageAuto::length(2.5),
45                    top: LengthPercentageAuto::length(2.5),
46                    bottom: LengthPercentageAuto::length(2.5),
47                },
48                ..Default::default()
49            }
50            .into(),
51            value: value.into(),
52            on_change: Update::empty().into(),
53        }
54    }
55
56    /// Sets the value of the checkbox and returns self.
57    #[inline(always)]
58    pub fn with_value(mut self, value: impl Into<MaybeSignal<bool>>) -> Self {
59        self.value = value.into();
60        self
61    }
62
63    /// Sets the update value to apply on changes and returns self.
64    #[inline(always)]
65    pub fn with_on_change(mut self, update: impl Into<MaybeSignal<Update>>) -> Self {
66        self.on_change = update.into();
67        self
68    }
69}
70
71impl WidgetLayoutExt for Switch {
72    #[inline(always)]
73    fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
74        self.layout = layout_style.into();
75    }
76}
77
78impl Widget for Switch {
79    fn render(
80        &mut self,
81        scene: &mut dyn Scene,
82        theme: &mut dyn Theme,
83        layout_node: &LayoutNode,
84        _: &AppInfo,
85        _: AppContext,
86    ) {
87        let checked = *self.value.get();
88
89        let color = if let Some(style) = theme.of(self.widget_id()) {
90            if checked {
91                style.get_color("color_checked").unwrap()
92            } else {
93                style.get_color("color_unchecked").unwrap()
94            }
95        } else if checked {
96            theme.defaults().interactive().active()
97        } else {
98            theme.defaults().interactive().inactive()
99        };
100
101        scene.draw_rounded_rect(
102            &Brush::Solid(color),
103            None,
104            Some(&Stroke::new(5.0)),
105            &RoundedRect::from_rect(
106                Rect::new(
107                    layout_node.layout.location.x as f64,
108                    layout_node.layout.location.y as f64,
109                    (layout_node.layout.location.x + layout_node.layout.size.width) as f64,
110                    (layout_node.layout.location.y + layout_node.layout.size.height) as f64,
111                ),
112                RoundedRectRadii::from_single_radius(60.0),
113            ),
114        );
115
116        let offset = if checked { 15.0 } else { -15.0 };
117
118        scene.draw_circle(
119            &Brush::Solid(color),
120            None,
121            None,
122            &Circle::new(
123                Point::new(
124                    (layout_node.layout.location.x + (layout_node.layout.size.width / 2.0)) as f64
125                        + offset,
126                    (layout_node.layout.location.y + (layout_node.layout.size.height / 2.0)) as f64,
127                ),
128                10.0,
129            ),
130        );
131    }
132
133    #[inline(always)]
134    fn layout_style(&self) -> StyleNode {
135        StyleNode {
136            style: self.layout.get().clone(),
137            children: Vec::new(),
138        }
139    }
140
141    fn update(&mut self, layout: &LayoutNode, _: AppContext, info: &AppInfo) -> Update {
142        let mut update = Update::empty();
143
144        if let Some(cursor) = info.cursor_pos
145            && layout::intersects(cursor, &layout.layout)
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                        let checked = *sig.get();
154                        sig.set(!checked);
155                    }
156                }
157            }
158        }
159
160        update
161    }
162
163    #[inline(always)]
164    fn widget_id(&self) -> WidgetId {
165        WidgetId::new("maycoon-widgets", "Switch")
166    }
167}