Skip to main content

sequoia_elements/
checkbox.rs

1use std::rc::Rc;
2
3use sequoia_core::{Color, Edges, Length};
4use sequoia_layout::{Align, LayoutStyle};
5use sequoia_reactive::Signal;
6use sequoia_style::CheckboxStyle;
7use sequoia_view::{Icon, Row, Text, Unit, View};
8
9use crate::container::Container;
10use crate::primitives::Deco;
11
12const BOX: f32 = 16.0;
13const GAP: f32 = 8.0;
14const BORDER_W: f32 = 1.5;
15
16pub struct Checkbox {
17    checked: Signal<bool>,
18    label: Option<String>,
19    style: Option<CheckboxStyle>,
20    background: Option<Color>,
21    checked_background: Option<Color>,
22    border: Option<Color>,
23    checked_border: Option<Color>,
24    check: Option<Color>,
25    text_color: Option<Color>,
26    font_size: Option<f32>,
27    radius: Option<f32>,
28    box_size: Option<f32>,
29    disabled: bool,
30    on_toggle: Option<Rc<dyn Fn(bool)>>,
31    margin: Edges,
32}
33
34impl Checkbox {
35    pub fn new(checked: Signal<bool>) -> Self {
36        Self {
37            checked,
38            label: None,
39            style: None,
40            background: None,
41            checked_background: None,
42            border: None,
43            checked_border: None,
44            check: None,
45            text_color: None,
46            font_size: None,
47            radius: None,
48            box_size: None,
49            disabled: false,
50            on_toggle: None,
51            margin: Edges::ZERO,
52        }
53    }
54
55    pub fn label(mut self, label: impl Into<String>) -> Self {
56        self.label = Some(label.into());
57        self
58    }
59
60    pub fn style(mut self, style: CheckboxStyle) -> Self {
61        self.style = Some(style);
62        self
63    }
64
65    pub fn background(mut self, color: Color) -> Self {
66        self.background = Some(color);
67        self
68    }
69
70    pub fn checked_background(mut self, color: Color) -> Self {
71        self.checked_background = Some(color);
72        self
73    }
74
75    pub fn border_color(mut self, color: Color) -> Self {
76        self.border = Some(color);
77        self
78    }
79
80    pub fn checked_border(mut self, color: Color) -> Self {
81        self.checked_border = Some(color);
82        self
83    }
84
85    pub fn check_color(mut self, color: Color) -> Self {
86        self.check = Some(color);
87        self
88    }
89
90    pub fn text_color(mut self, color: Color) -> Self {
91        self.text_color = Some(color);
92        self
93    }
94
95    pub fn font_size(mut self, size: f32) -> Self {
96        self.font_size = Some(size);
97        self
98    }
99
100    pub fn radius(mut self, radius: f32) -> Self {
101        self.radius = Some(radius);
102        self
103    }
104
105    pub fn box_size(mut self, size: f32) -> Self {
106        self.box_size = Some(size);
107        self
108    }
109
110    pub fn disabled(mut self, disabled: bool) -> Self {
111        self.disabled = disabled;
112        self
113    }
114
115    pub fn on_toggle(mut self, f: impl Fn(bool) + 'static) -> Self {
116        self.on_toggle = Some(Rc::new(f));
117        self
118    }
119
120    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
121        let m = margin.into();
122        self.margin = Edges::only(
123            sanitize_margin(m.left),
124            sanitize_margin(m.top),
125            sanitize_margin(m.right),
126            sanitize_margin(m.bottom),
127        );
128        self
129    }
130
131    pub fn margin_xy(self, x: f32, y: f32) -> Self {
132        self.margin(Edges::xy(x, y))
133    }
134
135    pub fn margin_x(mut self, x: f32) -> Self {
136        let x = sanitize_margin(x);
137        self.margin.left = x;
138        self.margin.right = x;
139        self
140    }
141
142    pub fn margin_y(mut self, y: f32) -> Self {
143        let y = sanitize_margin(y);
144        self.margin.top = y;
145        self.margin.bottom = y;
146        self
147    }
148
149    pub fn margin_left(mut self, v: f32) -> Self {
150        self.margin.left = sanitize_margin(v);
151        self
152    }
153
154    pub fn margin_right(mut self, v: f32) -> Self {
155        self.margin.right = sanitize_margin(v);
156        self
157    }
158
159    pub fn margin_top(mut self, v: f32) -> Self {
160        self.margin.top = sanitize_margin(v);
161        self
162    }
163
164    pub fn margin_bottom(mut self, v: f32) -> Self {
165        self.margin.bottom = sanitize_margin(v);
166        self
167    }
168
169    fn box_px(&self) -> f32 {
170        let style = self.style.unwrap_or_default();
171        self.box_size.or(style.box_size).unwrap_or(BOX).max(4.0)
172    }
173}
174
175fn sanitize_margin(v: f32) -> f32 {
176    if v.is_finite() { v.max(0.0) } else { 0.0 }
177}
178
179impl View for Checkbox {
180    fn accessibility(&self) -> Option<sequoia_view::Accessible> {
181        Some(sequoia_view::Accessible::new(
182            sequoia_view::AccessibleRole::Checkbox,
183            self.label
184                .clone()
185                .unwrap_or_else(|| "pole wyboru".to_string()),
186        ))
187    }
188
189    fn body(&self) -> impl View + 'static {
190        let selected = self.checked.get();
191        let style = self.style.unwrap_or_default();
192        let box_px = self.box_px();
193        let radius = self.radius.or(style.radius).unwrap_or(3.0);
194
195        let bg = if selected {
196            self.checked_background.or(style.checked)
197        } else {
198            self.background.or(style.background)
199        };
200        let border = if selected {
201            self.checked_border.or(style.checked_border)
202        } else {
203            self.border.or(style.border)
204        };
205        let check = self.check.or(style.check);
206
207        let check_view: sequoia_view::AnyView = if selected {
208            let icon_px = (box_px - 4.0).max(1.0);
209            let inset = ((box_px - icon_px) * 0.5).max(0.0);
210            let mut icon = Icon::new("check")
211                .size(icon_px)
212                .margin_xy(inset, inset)
213                .hit_testable(false);
214
215            icon = icon.color(check.unwrap_or(Color::hex(0x191919)));
216            sequoia_view::AnyView::new(icon)
217        } else {
218            sequoia_view::AnyView::new(Unit)
219        };
220        let checkbox_box = Deco::new(check_view, move |theme| {
221            let bg = bg.unwrap_or(if selected { theme.text } else { theme.surface });
222            let border = border.unwrap_or(if selected { theme.text } else { theme.border });
223            sequoia_style::BoxStyle::new()
224                .background(bg)
225                .border(BORDER_W, border)
226                .radius(radius)
227        })
228        .square_px(box_px)
229        .hit_testable(false);
230
231        let mut row = Row::new()
232            .main_gap(GAP)
233            .align_items(Align::Center)
234            .hit_testable(false)
235            .child(checkbox_box);
236        if let Some(label) = &self.label {
237            let mut t = Text::new(label.clone()).hit_testable(false);
238            if let Some(size) = self.font_size.or(style.font_size) {
239                t = t.size(size);
240            }
241            if let Some(c) = self.text_color.or(style.text) {
242                t = t.color(c);
243            }
244            row = row.child(t);
245        }
246
247        let checked = self.checked.clone();
248        let on_toggle = self.on_toggle.clone();
249        let disabled = self.disabled;
250        Container::new()
251            .child(row)
252            .disabled(disabled)
253            .on_click(move || {
254                if disabled {
255                    return;
256                }
257                let next = !checked.get_untracked();
258                checked.set(next);
259                if let Some(f) = &on_toggle {
260                    f(next);
261                }
262            })
263    }
264
265    fn layout_style(&self) -> LayoutStyle {
266        LayoutStyle {
267            width: Length::Fit,
268            height: Length::Fit,
269            margin: self.margin,
270            ..LayoutStyle::default()
271        }
272    }
273
274    fn hit_testable(&self) -> bool {
275        false
276    }
277}