Skip to main content

ferroui_controls/primitives/
toggle_button.rs

1use super::TemplatedControlImpl;
2use crate::metadata::PseudoClassesAttribute;
3use crate::{Button, ButtonImpl, ButtonImplExt, ContentControlImpl, ControlImpl};
4use ferroui_base::data::BindingMode;
5use ferroui_base::input::InputElementImpl;
6use ferroui_base::interactivity::{
7    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
8};
9use ferroui_base::layout::LayoutableImpl;
10use ferroui_base::{
11    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, FerroObjectImpl,
12    FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Ref, StyledElementImpl, StyledProperty,
13    StyledPropertyOptions, VisualImpl,
14};
15
16const PC_CHECKED: &str = ":checked";
17const PC_UNCHECKED: &str = ":unchecked";
18const PC_INDETERMINATE: &str = ":indeterminate";
19
20/// Represents a control that a user can select (check) or clear (uncheck).
21/// Base class for controls that can switch states.
22#[repr(C)]
23pub struct ToggleButton {
24    base: Button,
25}
26
27ferro_class! {
28    ToggleButton: Button, virtuals ToggleButtonImpl: ButtonImpl {
29        /// Toggles the `IsChecked` property.
30        fn toggle(this);
31        /// Called when `IsChecked` changes. `e` are the event arguments for
32        /// the routed event that is raised by the default implementation of
33        /// this method.
34        fn on_is_checked_changed(this, e: &RoutedEventArgs);
35    }
36}
37ferroui_base::ferro_class_info!(ToggleButton { new: ToggleButton::new });
38
39ferro_impl_classes!(
40    ToggleButton: StyledElementImpl,
41    VisualImpl,
42    LayoutableImpl,
43    InteractiveImpl,
44    InputElementImpl,
45    TemplatedControlImpl,
46    ContentControlImpl
47);
48
49impl ControlImpl for ToggleButton {
50    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
51        crate::automation::peers::ToggleButtonAutomationPeer::new(this).upcast()
52    }
53}
54
55impl FerroObjectImpl for ToggleButton {
56    fn constructed(this: &Self) {
57        Self::parent_constructed(this);
58        this.update_pseudo_classes(this.is_checked());
59    }
60
61    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
62        Self::parent_on_property_changed(this, change);
63
64        if change.property() == Self::is_checked_property().as_property() {
65            let new_value = change.get_new_value::<Option<bool>>();
66            this.update_pseudo_classes(new_value);
67            this.on_is_checked_changed(&RoutedEventArgs::with_event(Self::is_checked_changed_event()));
68        }
69    }
70}
71
72impl ButtonImpl for ToggleButton {
73    fn on_click(this: &Self) {
74        if !this.is_effectively_enabled() {
75            return;
76        }
77
78        this.toggle();
79        Self::parent_on_click(this);
80    }
81}
82
83impl ToggleButtonImpl for ToggleButton {
84    fn toggle(this: &Self) {
85        let new_value = match this.is_checked() {
86            Some(true) => {
87                if this.is_three_state() {
88                    None
89                } else {
90                    Some(false)
91                }
92            }
93            Some(false) => Some(true),
94            None => Some(false),
95        };
96
97        this.set_current_value(Self::is_checked_property(), new_value);
98    }
99
100    fn on_is_checked_changed(this: &Self, e: &RoutedEventArgs) {
101        this.raise_event(e);
102    }
103}
104
105impl ToggleButton {
106    /// The pseudoclasses set by the class.
107    pub const PSEUDO_CLASSES: PseudoClassesAttribute =
108        PseudoClassesAttribute::new(&[PC_CHECKED, PC_UNCHECKED, PC_INDETERMINATE]);
109}
110
111ferroui_base::ferro_properties! { impl ToggleButton {
112    ferro_property!(
113        /// Defines the `IsChecked` property.
114        pub fn is_checked_property() -> StyledProperty<Option<bool>> {
115            FerroProperty::register_with::<ToggleButton, _>(
116                "IsChecked",
117                StyledPropertyOptions::new(Some(false)).default_binding_mode(BindingMode::TwoWay),
118            )
119        }
120    );
121
122    ferro_property!(
123        /// Defines the `IsThreeState` property.
124        pub fn is_three_state_property() -> StyledProperty<bool> {
125            FerroProperty::register::<ToggleButton, _>("IsThreeState", false)
126        }
127    );
128} }
129
130impl ToggleButton {
131    ferro_routed_event!(
132        /// Defines the `IsCheckedChanged` event.
133        pub fn is_checked_changed_event() -> RoutedEvent<RoutedEventArgs> {
134            RoutedEvent::register::<ToggleButton, _>("IsCheckedChanged", RoutingStrategies::BUBBLE)
135        }
136    );
137
138    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
139    pub fn construct() -> Self {
140        Self { base: Button::construct() }
141    }
142
143    /// Creates a toggle button.
144    pub fn new() -> Ref<Self> {
145        instantiate(Self::construct())
146    }
147
148    /// Raised when the `IsChecked` property value changes.
149    pub fn is_checked_changed(
150        &self,
151        handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static,
152    ) -> RoutedEventHandlerToken {
153        self.add_handler(Self::is_checked_changed_event(), handler)
154    }
155
156    /// Whether the toggle button is checked; `None` is the indeterminate
157    /// state.
158    pub fn is_checked(&self) -> Option<bool> {
159        self.get_value(Self::is_checked_property())
160    }
161
162    pub fn set_is_checked(&self, value: Option<bool>) {
163        self.set_value(Self::is_checked_property(), value)
164    }
165
166    /// A value that indicates whether the control supports three states.
167    pub fn is_three_state(&self) -> bool {
168        self.get_value(Self::is_three_state_property())
169    }
170
171    pub fn set_is_three_state(&self, value: bool) {
172        self.set_value(Self::is_three_state_property(), value)
173    }
174
175    fn update_pseudo_classes(&self, is_checked: Option<bool>) {
176        self.pseudo_classes().set(PC_CHECKED, is_checked == Some(true));
177        self.pseudo_classes().set(PC_UNCHECKED, is_checked == Some(false));
178        self.pseudo_classes().set(PC_INDETERMINATE, is_checked.is_none());
179    }
180}