Skip to main content

fui/controls/
checkbox.rs

1use super::internal::checkbox_indicator_presenter::{
2    create_default_checkbox_indicator_presenter, CheckboxIndicatorPresenter,
3    CheckboxIndicatorTemplate, CheckboxIndicatorVisualState,
4};
5use super::internal::pressable_labeled_control::{
6    PressableLabeledControlState, WeakPressableLabeledControl,
7};
8use super::*;
9use std::cell::{Cell, RefCell};
10use std::rc::Rc;
11
12fn create_indicator_presenter(
13    template: Option<Rc<dyn CheckboxIndicatorTemplate>>,
14    sizing: Option<LabeledControlSizing>,
15) -> Rc<dyn CheckboxIndicatorPresenter> {
16    if let Some(template) = template {
17        return template.create(sizing);
18    }
19    create_default_checkbox_indicator_presenter(sizing)
20}
21
22#[derive(Clone)]
23pub struct Checkbox {
24    base: PressableLabeledControl,
25    root: FlexBox,
26    indicator_presenter: Rc<RefCell<Rc<dyn CheckboxIndicatorPresenter>>>,
27    template_override: Rc<RefCell<Option<Rc<dyn CheckboxIndicatorTemplate>>>>,
28    sizing_value: Rc<Cell<Option<LabeledControlSizing>>>,
29    state: Rc<Cell<CheckState>>,
30    tri_state: Rc<Cell<bool>>,
31    colors_value: Rc<Cell<Option<LabeledControlColors>>>,
32    changed: Rc<RefCell<Option<CheckboxChangedCallback>>>,
33    weak_root: Rc<WeakNodeRef>,
34}
35
36impl Checkbox {
37    pub fn new(label: impl Into<String>) -> Self {
38        let label = label.into();
39        let indicator_presenter = create_indicator_presenter(None, None);
40        let base =
41            PressableLabeledControl::new(SemanticRole::Checkbox, label, indicator_presenter.root());
42        let root = base.root();
43        let weak_root = Rc::new(root.node_ref().downgrade());
44        let control = Self {
45            base,
46            root,
47            indicator_presenter: Rc::new(RefCell::new(indicator_presenter)),
48            template_override: Rc::new(RefCell::new(None)),
49            sizing_value: Rc::new(Cell::new(None)),
50            state: Rc::new(Cell::new(CheckState::False)),
51            tri_state: Rc::new(Cell::new(false)),
52            colors_value: Rc::new(Cell::new(None)),
53            changed: Rc::new(RefCell::new(None)),
54            weak_root,
55        };
56        control.root.semantic_checked(SemanticCheckedState::False);
57        let activation = CheckboxEventTarget::from_checkbox(&control);
58        control.base.set_activation(move |state| {
59            activation.activate(state);
60        });
61        let state_sync = CheckboxEventTarget::from_checkbox(&control);
62        control.base.set_state_changed(move |state| {
63            state_sync.sync_visual(state, false, false);
64        });
65        let snapshot = control.base.state_snapshotter();
66        let state = control.state.clone();
67        let tri_state = control.tri_state.clone();
68        let weak_root = control.weak_root.clone();
69        let presenter = control.indicator_presenter.clone();
70        let colors_value = control.colors_value.clone();
71        let changed = control.changed.clone();
72        control.persist_state(crate::persisted::persisted_value_adapter(
73            "checkbox-checked-state",
74            crate::persisted::PersistedInt32Codec,
75            1,
76            {
77                let state = state.clone();
78                move || {
79                    Some(match state.get() {
80                        CheckState::False => 0,
81                        CheckState::True => 1,
82                        CheckState::Mixed => 2,
83                    })
84                }
85            },
86            move |value| {
87                let next = match value {
88                    1 => CheckState::True,
89                    2 => CheckState::Mixed,
90                    _ => CheckState::False,
91                };
92                let effective = if !tri_state.get() && next == CheckState::Mixed {
93                    CheckState::False
94                } else {
95                    next
96                };
97                apply_checkbox_state(
98                    &presenter,
99                    &weak_root,
100                    &state,
101                    effective,
102                    true,
103                    false,
104                    &changed,
105                    colors_value.get(),
106                    snapshot(),
107                );
108            },
109        ));
110        control.sync_visual(control.base.snapshot_state(), false, false);
111        control
112    }
113
114    pub fn checked(&self, checked: bool) -> &Self {
115        self.check(checked)
116    }
117
118    pub fn check(&self, checked: bool) -> &Self {
119        self.set_state(
120            if checked {
121                CheckState::True
122            } else {
123                CheckState::False
124            },
125            true,
126            false,
127        );
128        self
129    }
130
131    pub fn mixed(&self, mixed: bool) -> &Self {
132        if !self.tri_state.get() {
133            return self;
134        }
135        self.set_state(
136            if mixed {
137                CheckState::Mixed
138            } else {
139                CheckState::False
140            },
141            true,
142            false,
143        );
144        self
145    }
146
147    pub fn check_state(&self, state: CheckState) -> &Self {
148        self.set_state(state, true, false);
149        self
150    }
151
152    pub fn tri_state(&self, enabled: bool) -> &Self {
153        self.tri_state.set(enabled);
154        if !enabled && self.state.get() == CheckState::Mixed {
155            self.set_state(CheckState::False, true, false);
156        }
157        self
158    }
159
160    pub fn sizing(&self, sizing: LabeledControlSizing) -> &Self {
161        self.set_sizing(Some(sizing))
162    }
163
164    pub fn clear_sizing(&self) -> &Self {
165        self.set_sizing(None)
166    }
167
168    fn set_sizing(&self, sizing: Option<LabeledControlSizing>) -> &Self {
169        self.sizing_value.set(sizing);
170        self.base.set_label_font_size_override(
171            sizing
172                .filter(|sizing| sizing.has_label_font_size())
173                .map(|sizing| sizing.label_font_size_px())
174                .unwrap_or(0.0),
175        );
176        self.replace_indicator_presenter(create_indicator_presenter(
177            self.template_override.borrow().clone(),
178            self.sizing_value.get(),
179        ));
180        self.sync_visual(self.base.snapshot_state(), false, false);
181        self
182    }
183
184    pub fn template(&self, template: Rc<dyn CheckboxIndicatorTemplate>) -> &Self {
185        self.set_template(Some(template))
186    }
187
188    pub fn clear_template(&self) -> &Self {
189        self.set_template(None)
190    }
191
192    fn set_template(&self, template: Option<Rc<dyn CheckboxIndicatorTemplate>>) -> &Self {
193        self.template_override.replace(template.clone());
194        self.replace_indicator_presenter(create_indicator_presenter(
195            template,
196            self.sizing_value.get(),
197        ));
198        self.sync_visual(self.base.snapshot_state(), false, false);
199        self
200    }
201
202    pub fn colors(&self, colors: LabeledControlColors) -> &Self {
203        self.set_colors(Some(colors))
204    }
205
206    pub fn clear_colors(&self) -> &Self {
207        self.set_colors(None)
208    }
209
210    fn set_colors(&self, colors: Option<LabeledControlColors>) -> &Self {
211        self.colors_value.set(colors);
212        self.base.colors(colors);
213        self.sync_visual(self.base.snapshot_state(), false, false);
214        self
215    }
216
217    pub fn on_changed(&self, handler: impl Fn(CheckboxChangedEventArgs) + 'static) -> &Self {
218        *self.changed.borrow_mut() = Some(Rc::new(handler));
219        self
220    }
221
222    pub fn checked_state(&self) -> CheckState {
223        self.state.get()
224    }
225
226    pub fn state(&self) -> CheckState {
227        self.checked_state()
228    }
229
230    pub fn is_checked(&self) -> bool {
231        self.state.get().is_checked()
232    }
233
234    pub fn enabled(&self, enabled: bool) -> &Self {
235        self.base.enabled(enabled);
236        self
237    }
238
239    #[cfg(test)]
240    pub(crate) fn test_parts(&self) -> (FlexBox, FlexBox, FlexBox, FlexBox) {
241        self.base.test_parts()
242    }
243
244    fn replace_indicator_presenter(&self, next_presenter: Rc<dyn CheckboxIndicatorPresenter>) {
245        let previous = self.indicator_presenter.borrow().clone();
246        if Rc::ptr_eq(&previous, &next_presenter) {
247            return;
248        }
249        self.base.replace_indicator_root(next_presenter.root());
250        self.indicator_presenter.replace(next_presenter);
251    }
252
253    fn set_state(&self, state: CheckState, emit: bool, announce: bool) {
254        let effective = if !self.tri_state.get() && state == CheckState::Mixed {
255            CheckState::False
256        } else {
257            state
258        };
259        apply_checkbox_state(
260            &self.indicator_presenter,
261            &self.weak_root,
262            &self.state,
263            effective,
264            emit,
265            announce,
266            &self.changed,
267            self.colors_value.get(),
268            self.base.snapshot_state(),
269        );
270    }
271
272    fn sync_visual(&self, base_state: PressableLabeledControlState, emit: bool, announce: bool) {
273        apply_checkbox_state(
274            &self.indicator_presenter,
275            &self.weak_root,
276            &self.state,
277            self.state.get(),
278            emit,
279            announce,
280            &self.changed,
281            self.colors_value.get(),
282            base_state,
283        );
284    }
285}
286
287impl Clickable for Checkbox {
288    fn on_click(&self, handler: impl Fn(ClickEventArgs) + 'static) -> &Self {
289        self.base.on_click(handler);
290        self
291    }
292}
293
294impl LabeledControlTextStyle for Checkbox {
295    fn set_label_font_family(&self, family: crate::FontFamily) {
296        self.base.font_family(family);
297    }
298
299    fn set_label_font_size(&self, size: f32) {
300        self.base.font_size(size);
301    }
302
303    fn set_label_text_color(&self, color: u32) {
304        self.base.text_color(color);
305    }
306}
307
308impl Node for Checkbox {
309    fn retained_node_ref(&self) -> NodeRef {
310        self.root.retained_node_ref()
311    }
312
313    fn build_self(&self) {
314        self.sync_visual(self.base.snapshot_state(), false, false);
315        self.root.build_self();
316    }
317}
318
319impl HasFlexBoxRoot for Checkbox {
320    fn flex_box_root(&self) -> &FlexBox {
321        &self.root
322    }
323}
324
325impl ThemeBindable for Checkbox {
326    fn theme_binding_node(&self) -> NodeRef {
327        self.root.retained_node_ref()
328    }
329
330    fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
331        let target = CheckboxEventTarget::from_checkbox(self);
332        Box::new(move || target.upgrade())
333    }
334}
335
336#[derive(Clone)]
337struct CheckboxEventTarget {
338    base: WeakPressableLabeledControl,
339    presenter: Rc<RefCell<Rc<dyn CheckboxIndicatorPresenter>>>,
340    template_override: Rc<RefCell<Option<Rc<dyn CheckboxIndicatorTemplate>>>>,
341    sizing_value: Rc<Cell<Option<LabeledControlSizing>>>,
342    state: Rc<Cell<CheckState>>,
343    tri_state: Rc<Cell<bool>>,
344    colors_value: Rc<Cell<Option<LabeledControlColors>>>,
345    changed: Rc<RefCell<Option<CheckboxChangedCallback>>>,
346    weak_root: Rc<WeakNodeRef>,
347}
348
349impl CheckboxEventTarget {
350    fn from_checkbox(checkbox: &Checkbox) -> Self {
351        Self {
352            base: checkbox.base.downgrade(),
353            presenter: checkbox.indicator_presenter.clone(),
354            template_override: checkbox.template_override.clone(),
355            sizing_value: checkbox.sizing_value.clone(),
356            state: checkbox.state.clone(),
357            tri_state: checkbox.tri_state.clone(),
358            colors_value: checkbox.colors_value.clone(),
359            changed: checkbox.changed.clone(),
360            weak_root: checkbox.weak_root.clone(),
361        }
362    }
363
364    fn upgrade(&self) -> Option<Checkbox> {
365        let base = self.base.upgrade()?;
366        Some(Checkbox {
367            root: base.root(),
368            base,
369            indicator_presenter: self.presenter.clone(),
370            template_override: self.template_override.clone(),
371            sizing_value: self.sizing_value.clone(),
372            state: self.state.clone(),
373            tri_state: self.tri_state.clone(),
374            colors_value: self.colors_value.clone(),
375            changed: self.changed.clone(),
376            weak_root: self.weak_root.clone(),
377        })
378    }
379
380    fn activate(&self, base_state: PressableLabeledControlState) {
381        let next = match (self.state.get(), self.tri_state.get()) {
382            (CheckState::False, _) => CheckState::True,
383            (CheckState::True, true) => CheckState::Mixed,
384            (CheckState::Mixed, _) | (CheckState::True, false) => CheckState::False,
385        };
386        apply_checkbox_state(
387            &self.presenter,
388            &self.weak_root,
389            &self.state,
390            next,
391            true,
392            true,
393            &self.changed,
394            self.colors_value.get(),
395            base_state,
396        );
397    }
398
399    fn sync_visual(&self, base_state: PressableLabeledControlState, emit: bool, announce: bool) {
400        apply_checkbox_state(
401            &self.presenter,
402            &self.weak_root,
403            &self.state,
404            self.state.get(),
405            emit,
406            announce,
407            &self.changed,
408            self.colors_value.get(),
409            base_state,
410        );
411    }
412}
413
414#[allow(clippy::too_many_arguments)]
415fn apply_checkbox_state(
416    presenter: &Rc<RefCell<Rc<dyn CheckboxIndicatorPresenter>>>,
417    weak_root: &Rc<WeakNodeRef>,
418    state_cell: &Rc<Cell<CheckState>>,
419    state: CheckState,
420    emit: bool,
421    announce: bool,
422    changed: &Rc<RefCell<Option<CheckboxChangedCallback>>>,
423    colors: Option<LabeledControlColors>,
424    base_state: PressableLabeledControlState,
425) {
426    let previous = state_cell.get();
427    state_cell.set(state);
428    update_semantic_checked(weak_root, state.semantic(), announce && previous != state);
429    presenter.borrow().apply(
430        current_theme(),
431        CheckboxIndicatorVisualState::new(
432            state.semantic(),
433            base_state.hovered,
434            base_state.pressed,
435            base_state.focused,
436            base_state.enabled,
437        ),
438        colors,
439    );
440    if emit && previous != state {
441        if let Some(callback) = changed.borrow().clone() {
442            callback(CheckboxChangedEventArgs {
443                state,
444                checked: state.is_checked(),
445            });
446        }
447    }
448}