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