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