Skip to main content

fui/controls/
switch.rs

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