Skip to main content

ferroui_controls/
toggle_switch.rs

1use crate::presenters::ContentPresenter;
2use crate::primitives::{TemplateAppliedEventArgs, TemplatedControlImpl, TemplatedControlImplExt, ToggleButton, ToggleButtonImpl};
3use crate::templates::IDataTemplate;
4use crate::{ButtonImpl, Canvas, ContentControlImpl, ContentControlImplExt, Control, ControlImpl, ControlImplExt, Panel};
5use ferroui_base::animation::Transitions;
6use ferroui_base::input::{
7    InputElement, InputElementImpl, PointerEventArgs, PointerPressedEventArgs, PointerReleasedEventArgs,
8};
9use ferroui_base::interactivity::{InteractiveImpl, RoutedEventArgs};
10use ferroui_base::layout::LayoutableImpl;
11use ferroui_base::utilities::math_utilities::{max, min};
12use ferroui_base::{
13    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, instantiate, BoxedValue, FerroObjectImpl,
14    FerroProperty, FerroPropertyChangedEventArgs, Point, Ref, StyledElement, StyledElementImpl, StyledProperty, Visual,
15    VisualImpl,
16};
17use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20const PC_DRAGGING: &str = ":dragging";
21
22/// A toggle switch control.
23#[repr(C)]
24pub struct ToggleSwitch {
25    base: ToggleButton,
26    knobs_panel: RefCell<Option<Ref<Panel>>>,
27    switch_knob: RefCell<Option<Ref<Panel>>>,
28    knobs_panel_pressed: Cell<bool>,
29    switch_start_point: Cell<Point>,
30    init_left: Cell<f64>,
31    is_dragging: Cell<bool>,
32    off_content_presenter: RefCell<Option<Ref<ContentPresenter>>>,
33    on_content_presenter: RefCell<Option<Ref<ContentPresenter>>>,
34}
35
36ferro_class!(ToggleSwitch: ToggleButton);
37
38ferro_class_info!(ToggleSwitch {
39    new: ToggleSwitch::new,
40    markup: {
41        attributes: [
42            TemplatePart("PART_MovingKnobs", type(Ref<Panel>), IsRequired = true),
43            TemplatePart("PART_OffContentPresenter", type(Ref<ContentPresenter>)),
44            TemplatePart("PART_OnContentPresenter", type(Ref<ContentPresenter>)),
45            TemplatePart("PART_SwitchKnob", type(Ref<Panel>)),
46            PseudoClasses(":dragging"),
47        ],
48    },
49});
50
51ferro_impl_classes!(
52    ToggleSwitch: FerroObjectImpl,
53    StyledElementImpl,
54    VisualImpl,
55    LayoutableImpl,
56    InteractiveImpl,
57    InputElementImpl,
58    ButtonImpl,
59    ToggleButtonImpl
60);
61
62impl ControlImpl for ToggleSwitch {
63    fn on_loaded(this: &Self, e: &RoutedEventArgs) {
64        Self::parent_on_loaded(this, e);
65        this.update_knob_transitions();
66    }
67}
68
69impl TemplatedControlImpl for ToggleSwitch {
70    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
71        Self::parent_on_apply_template(this, e);
72
73        *this.switch_knob.borrow_mut() = e.name_scope().find_as::<Panel>("PART_SwitchKnob");
74        let knobs_panel = e.name_scope().get_as::<Panel>("PART_MovingKnobs");
75        *this.knobs_panel.borrow_mut() = Some(knobs_panel.clone());
76
77        // The handlers refer to the switch weakly: the panel is a part of
78        // the switch and must not keep it alive.
79        let weak = this.to_ref().downgrade();
80        knobs_panel.add_handler(InputElement::pointer_pressed_event(), {
81            let weak = weak.clone();
82            move |_, e: &PointerPressedEventArgs| {
83                if let Some(this) = weak.upgrade() {
84                    this.knobs_panel_pointer_pressed(e);
85                }
86            }
87        });
88        knobs_panel.add_handler(InputElement::pointer_released_event(), {
89            let weak = weak.clone();
90            move |_, e: &PointerReleasedEventArgs| {
91                if let Some(this) = weak.upgrade() {
92                    this.knobs_panel_pointer_released(e);
93                }
94            }
95        });
96        knobs_panel.add_handler(InputElement::pointer_moved_event(), move |_, e: &PointerEventArgs| {
97            if let Some(this) = weak.upgrade() {
98                this.knobs_panel_pointer_moved(e);
99            }
100        });
101
102        if let Some(is_checked) = this.is_checked() {
103            this.update_knob_pos(is_checked);
104        }
105    }
106}
107
108impl ContentControlImpl for ToggleSwitch {
109    fn register_content_presenter(this: &Self, presenter: &ContentPresenter) -> bool {
110        let mut result = Self::parent_register_content_presenter(this, presenter);
111
112        let name = presenter.name();
113        if name.as_deref() == Some("PART_OnContentPresenter") {
114            *this.on_content_presenter.borrow_mut() = Some(presenter.to_ref());
115            result = true;
116        } else if name.as_deref() == Some("PART_OffContentPresenter") {
117            *this.off_content_presenter.borrow_mut() = Some(presenter.to_ref());
118            result = true;
119        }
120
121        result
122    }
123}
124
125ferro_properties! {
126    impl ToggleSwitch {
127        /// Defines the `OffContent` property.
128        pub fn off_content_property() -> StyledProperty<Option<BoxedValue>> {
129            let default: BoxedValue = Rc::new("Off".to_string());
130            FerroProperty::register::<ToggleSwitch, _>("OffContent", Some(default))
131        }
132
133        /// Defines the `OffContentTemplate` property.
134        pub fn off_content_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
135            FerroProperty::register::<ToggleSwitch, _>("OffContentTemplate", None)
136        }
137
138        /// Defines the `OnContent` property.
139        pub fn on_content_property() -> StyledProperty<Option<BoxedValue>> {
140            let default: BoxedValue = Rc::new("On".to_string());
141            FerroProperty::register::<ToggleSwitch, _>("OnContent", Some(default))
142        }
143
144        /// Defines the `OnContentTemplate` property.
145        pub fn on_content_template_property() -> StyledProperty<Option<Rc<dyn IDataTemplate>>> {
146            FerroProperty::register::<ToggleSwitch, _>("OnContentTemplate", None)
147        }
148
149        /// Defines the `KnobTransitions` property.
150        pub fn knob_transitions_property() -> StyledProperty<Option<Transitions>> {
151            FerroProperty::register::<ToggleSwitch, _>("KnobTransitions", None)
152        }
153    }
154}
155
156impl ToggleSwitch {
157    fn static_constructor() {
158        Self::off_content_property().changed().add_class_handler::<ToggleSwitch>(|x, e| x.off_content_changed(e));
159        Self::on_content_property().changed().add_class_handler::<ToggleSwitch>(|x, e| x.on_content_changed(e));
160        ToggleButton::is_checked_property().changed().add_class_handler::<ToggleSwitch>(|x, e| {
161            if let Some(value) = e.get_new_value::<Option<bool>>() {
162                x.update_knob_pos(value);
163            }
164        });
165
166        Visual::bounds_property().changed().add_class_handler::<ToggleSwitch>(|x, _| {
167            if let Some(is_checked) = x.is_checked() {
168                x.update_knob_pos(is_checked);
169            }
170        });
171        Self::knob_transitions_property().changed().add_class_handler::<ToggleSwitch>(|x, _| {
172            x.update_knob_transitions();
173        });
174    }
175
176    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
177    pub fn construct() -> Self {
178        Self {
179            base: ToggleButton::construct(),
180            knobs_panel: RefCell::new(None),
181            switch_knob: RefCell::new(None),
182            knobs_panel_pressed: Cell::new(false),
183            switch_start_point: Cell::new(Point::default()),
184            init_left: Cell::new(-1.0),
185            is_dragging: Cell::new(false),
186            off_content_presenter: RefCell::new(None),
187            on_content_presenter: RefCell::new(None),
188        }
189    }
190
191    pub fn new() -> Ref<Self> {
192        instantiate(Self::construct())
193    }
194
195    /// The content that is displayed when in the on state.
196    pub fn on_content(&self) -> Option<BoxedValue> {
197        self.get_value(Self::on_content_property())
198    }
199
200    pub fn set_on_content(&self, value: Option<BoxedValue>) {
201        self.set_value(Self::on_content_property(), value)
202    }
203
204    /// The content that is displayed when in the off state.
205    pub fn off_content(&self) -> Option<BoxedValue> {
206        self.get_value(Self::off_content_property())
207    }
208
209    pub fn set_off_content(&self, value: Option<BoxedValue>) {
210        self.set_value(Self::off_content_property(), value)
211    }
212
213    /// The presenter of the off content from the control's template.
214    pub fn off_content_presenter(&self) -> Option<Ref<ContentPresenter>> {
215        self.off_content_presenter.borrow().clone()
216    }
217
218    /// The presenter of the on content from the control's template.
219    pub fn on_content_presenter(&self) -> Option<Ref<ContentPresenter>> {
220        self.on_content_presenter.borrow().clone()
221    }
222
223    /// The data template used to display the off content.
224    pub fn off_content_template(&self) -> Option<Rc<dyn IDataTemplate>> {
225        self.get_value(Self::off_content_template_property())
226    }
227
228    pub fn set_off_content_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
229        self.set_value(Self::off_content_template_property(), value)
230    }
231
232    /// The data template used to display the on content.
233    pub fn on_content_template(&self) -> Option<Rc<dyn IDataTemplate>> {
234        self.get_value(Self::on_content_template_property())
235    }
236
237    pub fn set_on_content_template(&self, value: Option<Rc<dyn IDataTemplate>>) {
238        self.set_value(Self::on_content_template_property(), value)
239    }
240
241    /// The transitions of the switching knob.
242    pub fn knob_transitions(&self) -> Option<Transitions> {
243        self.get_value(Self::knob_transitions_property())
244    }
245
246    pub fn set_knob_transitions(&self, value: Option<Transitions>) {
247        self.set_value(Self::knob_transitions_property(), value)
248    }
249
250    fn off_content_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
251        self.logical_content_changed(e);
252    }
253
254    fn on_content_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
255        self.logical_content_changed(e);
256    }
257
258    /// The body shared by the two content changed handlers.
259    fn logical_content_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
260        let (old_value, new_value) = e.get_old_and_new_value::<Option<BoxedValue>>();
261
262        if let Some(old_child) = old_value.as_ref().and_then(Control::logical_from_boxed) {
263            StyledElement::logical_children(self).remove(&old_child);
264        }
265
266        if let Some(new_child) = new_value.as_ref().and_then(Control::logical_from_boxed) {
267            StyledElement::logical_children(self).add(new_child);
268        }
269    }
270
271    fn update_knob_transitions(&self) {
272        let knobs_panel = self.knobs_panel.borrow().clone();
273        if let Some(knobs_panel) = knobs_panel {
274            knobs_panel.set_transitions(self.knob_transitions());
275        }
276    }
277
278    fn switch_knob_visual(&self) -> Option<Ref<Visual>> {
279        self.switch_knob.borrow().clone().map(Ref::upcast)
280    }
281
282    fn knobs_panel_pointer_pressed(&self, e: &PointerPressedEventArgs) {
283        let knobs_panel = self.knobs_panel.borrow().clone().expect("the knobs panel raised the event");
284        self.switch_start_point.set(e.get_position(self.switch_knob_visual().as_deref()));
285        self.init_left.set(Canvas::get_left(&knobs_panel));
286        self.is_dragging.set(false);
287        self.knobs_panel_pressed.set(true);
288    }
289
290    fn knobs_panel_pointer_released(&self, e: &PointerReleasedEventArgs) {
291        if self.is_dragging.get() {
292            e.set_handled(true);
293
294            let knobs_panel = self.knobs_panel.borrow().clone().expect("the knobs panel raised the event");
295            let switch_knob = self.switch_knob.borrow().clone().expect("dragging requires the switch knob");
296            let should_become_checked = Canvas::get_left(&knobs_panel) >= (switch_knob.bounds().width / 2.0);
297            knobs_panel.clear_value(Canvas::left_property());
298
299            self.pseudo_classes().set(PC_DRAGGING, false);
300
301            if Some(should_become_checked) == self.is_checked() {
302                self.update_knob_pos(should_become_checked);
303            } else {
304                self.set_current_value(ToggleButton::is_checked_property(), Some(should_become_checked));
305            }
306            self.update_knob_transitions();
307        }
308
309        self.is_dragging.set(false);
310
311        self.knobs_panel_pressed.set(false);
312    }
313
314    fn knobs_panel_pointer_moved(&self, e: &PointerEventArgs) {
315        if self.knobs_panel_pressed.get() {
316            let difference = e.get_position(self.switch_knob_visual().as_deref()) - self.switch_start_point.get();
317
318            if !self.is_dragging.get() && difference.x.abs() > 3.0 {
319                let knobs_panel = self.knobs_panel.borrow().clone();
320                if let Some(knobs_panel) = knobs_panel {
321                    knobs_panel.set_transitions(None);
322                }
323                self.is_dragging.set(true);
324                self.pseudo_classes().set(PC_DRAGGING, true);
325            }
326
327            if self.is_dragging.get() {
328                let knobs_panel = self.knobs_panel.borrow().clone().expect("the knobs panel raised the event");
329                let switch_knob = self.switch_knob.borrow().clone().expect("dragging requires the switch knob");
330                // The minimum and the maximum propagate a position that is
331                // not a number (the knob of an indeterminate switch has none).
332                Canvas::set_left(
333                    &knobs_panel,
334                    min(switch_knob.bounds().width, max(0.0, self.init_left.get() + difference.x)),
335                );
336            }
337        }
338    }
339
340    fn update_knob_pos(&self, value: bool) {
341        let switch_knob = self.switch_knob.borrow().clone();
342        let knobs_panel = self.knobs_panel.borrow().clone();
343        if let (Some(switch_knob), Some(knobs_panel)) = (switch_knob, knobs_panel) {
344            if value {
345                Canvas::set_left(&knobs_panel, switch_knob.bounds().width);
346            } else {
347                Canvas::set_left(&knobs_panel, 0.0);
348            }
349        }
350    }
351}