yakui_widgets/widgets/
checkbox.rs

1use yakui_core::event::{EventInterest, EventResponse, WidgetEvent};
2use yakui_core::geometry::{Constraints, Vec2};
3use yakui_core::input::MouseButton;
4use yakui_core::widget::{EventContext, LayoutContext, PaintContext, Widget};
5use yakui_core::Response;
6
7use crate::shapes::RoundedRectangle;
8use crate::{colors, shapes};
9
10const OUTER_SIZE: f32 = 24.0;
11const INNER_SIZE: f32 = 16.0;
12
13/**
14A checkbox with a provided value.
15
16Responds with [CheckboxResponse].
17
18Shorthand:
19```rust
20# let _handle = yakui_widgets::DocTest::start();
21let mut value = false;
22
23value = yakui::checkbox(value).checked;
24```
25*/
26#[derive(Debug)]
27#[non_exhaustive]
28#[must_use = "yakui widgets do nothing if you don't `show` them"]
29pub struct Checkbox {
30    pub checked: bool,
31}
32
33impl Checkbox {
34    pub fn new(checked: bool) -> Self {
35        Self { checked }
36    }
37
38    pub fn show(self) -> Response<CheckboxResponse> {
39        crate::util::widget::<CheckboxWidget>(self)
40    }
41}
42
43#[derive(Debug)]
44pub struct CheckboxWidget {
45    props: Checkbox,
46    hovering: bool,
47    mouse_down: bool,
48    just_toggled: bool,
49}
50
51#[derive(Debug)]
52#[non_exhaustive]
53pub struct CheckboxResponse {
54    pub checked: bool,
55}
56
57impl Widget for CheckboxWidget {
58    type Props<'a> = Checkbox;
59    type Response = CheckboxResponse;
60
61    fn new() -> Self {
62        Self {
63            props: Checkbox::new(false),
64            hovering: false,
65            mouse_down: false,
66            just_toggled: false,
67        }
68    }
69
70    fn update(&mut self, props: Self::Props<'_>) -> Self::Response {
71        self.props = props;
72
73        let mut checked = self.props.checked;
74        if self.just_toggled {
75            checked = !checked;
76            self.just_toggled = false;
77        }
78
79        CheckboxResponse { checked }
80    }
81
82    fn paint(&self, ctx: PaintContext<'_>) {
83        let layout_node = ctx.layout.get(ctx.dom.current()).unwrap();
84
85        let padding = Vec2::splat(OUTER_SIZE - INNER_SIZE);
86        let mut check_rect = layout_node.rect;
87        check_rect.set_pos(check_rect.pos() + padding / 2.0);
88        check_rect.set_size(check_rect.size() - padding);
89
90        let mut bg = RoundedRectangle::new(layout_node.rect, 6.0);
91        bg.color = colors::BACKGROUND_3;
92        bg.add(ctx.paint);
93
94        if self.props.checked {
95            shapes::cross(ctx.paint, check_rect, colors::TEXT);
96        }
97    }
98
99    fn layout(&self, _ctx: LayoutContext<'_>, constraints: Constraints) -> Vec2 {
100        constraints.constrain_min(Vec2::splat(OUTER_SIZE))
101    }
102
103    fn event_interest(&self) -> EventInterest {
104        EventInterest::MOUSE_INSIDE | EventInterest::MOUSE_OUTSIDE
105    }
106
107    fn event(&mut self, _ctx: EventContext<'_>, event: &WidgetEvent) -> EventResponse {
108        match event {
109            WidgetEvent::MouseEnter => {
110                self.hovering = true;
111                EventResponse::Sink
112            }
113            WidgetEvent::MouseLeave => {
114                self.hovering = false;
115                EventResponse::Sink
116            }
117            WidgetEvent::MouseButtonChanged {
118                button: MouseButton::One,
119                down,
120                inside,
121                ..
122            } => {
123                if *inside {
124                    if *down {
125                        self.mouse_down = true;
126                        EventResponse::Sink
127                    } else if self.mouse_down {
128                        self.mouse_down = false;
129                        self.just_toggled = true;
130                        EventResponse::Sink
131                    } else {
132                        EventResponse::Bubble
133                    }
134                } else {
135                    self.mouse_down = false;
136                    EventResponse::Bubble
137                }
138            }
139            _ => EventResponse::Bubble,
140        }
141    }
142}