sequoia_elements/
checkbox.rs1use 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}