Skip to main content

ferroui_controls/
button_spinner.rs

1use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
2use crate::primitives::{TemplateAppliedEventArgs, TemplatedControlImpl};
3use crate::{
4    Button, ContentControlImpl, ControlImpl, SpinDirection, SpinEventArgs, Spinner, SpinnerImpl,
5    ValidSpinDirections,
6};
7use ferroui_base::input::navigation::XYFocusHelpers;
8use ferroui_base::input::{
9    InputElementImpl, InputElementImplExt, Key, KeyEventArgs, PointerReleasedEventArgs, PointerWheelEventArgs,
10};
11use ferroui_base::interactivity::{Interactive, InteractiveImpl, RoutedEventHandlerToken};
12use ferroui_base::layout::LayoutableImpl;
13use ferroui_base::{
14    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt,
15    FerroProperty, FerroPropertyChangedEventArgs, Ref, StaticType, StyledElementImpl, StyledProperty, VisualImpl,
16};
17use std::cell::RefCell;
18
19const PC_LEFT: &str = ":left";
20const PC_RIGHT: &str = ":right";
21
22/// Represents the location of the spin buttons of a [`ButtonSpinner`].
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum Location {
25    /// The buttons are placed at the left.
26    Left,
27    /// The buttons are placed at the right.
28    Right,
29}
30
31/// A template button of the spinner and the click handler attached to it.
32type SpinButton = (Ref<Button>, RoutedEventHandlerToken);
33
34/// Represents a spinner control that includes two buttons.
35#[repr(C)]
36pub struct ButtonSpinner {
37    base: Spinner,
38    decrease_button: RefCell<Option<SpinButton>>,
39    increase_button: RefCell<Option<SpinButton>>,
40}
41
42ferro_class! {
43    ButtonSpinner: Spinner, virtuals ButtonSpinnerImpl: SpinnerImpl {
44        /// Called when the `AllowSpin` property value changed.
45        fn on_allow_spin_changed(this, old_value: bool, new_value: bool);
46    }
47}
48ferroui_base::ferro_class_info!(ButtonSpinner { new: ButtonSpinner::new });
49
50ferro_impl_classes!(
51    ButtonSpinner: StyledElementImpl,
52    VisualImpl,
53    LayoutableImpl,
54    InteractiveImpl,
55    ControlImpl,
56    ContentControlImpl
57);
58
59impl FerroObjectImpl for ButtonSpinner {
60    fn constructed(this: &Self) {
61        Self::parent_constructed(this);
62        this.update_pseudo_classes(this.button_spinner_location());
63    }
64
65    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
66        Self::parent_on_property_changed(this, change);
67
68        if change.property() == Self::button_spinner_location_property().as_property() {
69            this.update_pseudo_classes(change.get_new_value::<Location>());
70        }
71    }
72}
73
74impl InputElementImpl for ButtonSpinner {
75    fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
76        Self::parent_on_pointer_released(this, e);
77
78        if let Some(increase_button) = this.increase_button() {
79            if !increase_button.is_enabled() {
80                let mouse_position = e.get_position(Some(&increase_button));
81                if mouse_position.x > 0.0
82                    && mouse_position.x < increase_button.width()
83                    && mouse_position.y > 0.0
84                    && mouse_position.y < increase_button.height()
85                {
86                    e.set_handled(true);
87                }
88            }
89        }
90
91        if let Some(decrease_button) = this.decrease_button() {
92            if !decrease_button.is_enabled() {
93                let mouse_position = e.get_position(Some(&decrease_button));
94                if mouse_position.x > 0.0
95                    && mouse_position.x < decrease_button.width()
96                    && mouse_position.y > 0.0
97                    && mouse_position.y < decrease_button.height()
98                {
99                    e.set_handled(true);
100                }
101            }
102        }
103    }
104
105    fn on_key_down(this: &Self, e: &KeyEventArgs) {
106        // If XY navigation is enabled - do not spin with arrow keys, instead
107        // use spinner buttons.
108        if XYFocusHelpers::is_allowed_xy_navigation_mode(this, Some(e.key_device_type)) {
109            return;
110        }
111
112        match e.key {
113            Key::Up => {
114                if this.allow_spin() {
115                    this.on_spin(&SpinEventArgs::with_event(Some(Spinner::spin_event()), SpinDirection::Increase));
116                    e.set_handled(true);
117                }
118            }
119            Key::Down => {
120                if this.allow_spin() {
121                    this.on_spin(&SpinEventArgs::with_event(Some(Spinner::spin_event()), SpinDirection::Decrease));
122                    e.set_handled(true);
123                }
124            }
125            Key::Enter => {
126                // Do not spin on the Enter key when the spinners have focus.
127                if this.increase_button().is_some_and(|button| button.is_focused())
128                    || this.decrease_button().is_some_and(|button| button.is_focused())
129                {
130                    e.set_handled(true);
131                }
132            }
133            _ => {}
134        }
135    }
136
137    fn on_pointer_wheel_changed(this: &Self, e: &PointerWheelEventArgs) {
138        Self::parent_on_pointer_wheel_changed(this, e);
139
140        if this.allow_spin() && this.is_keyboard_focus_within() && e.delta().y != 0.0 {
141            let direction = if e.delta().y < 0.0 { SpinDirection::Decrease } else { SpinDirection::Increase };
142            let spinner_event_args =
143                SpinEventArgs::with_event_and_mouse_wheel(Some(Spinner::spin_event()), direction, true);
144            this.on_spin(&spinner_event_args);
145            e.set_handled(true);
146        }
147    }
148}
149
150impl TemplatedControlImpl for ButtonSpinner {
151    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
152        this.set_increase_button(e.name_scope().find_as::<Button>("PART_IncreaseButton"));
153        this.set_decrease_button(e.name_scope().find_as::<Button>("PART_DecreaseButton"));
154        this.set_button_usage();
155    }
156}
157
158impl SpinnerImpl for ButtonSpinner {
159    fn on_valid_spin_direction_changed(this: &Self, _old_value: ValidSpinDirections, _new_value: ValidSpinDirections) {
160        this.set_button_usage();
161    }
162}
163
164impl ButtonSpinnerImpl for ButtonSpinner {
165    fn on_allow_spin_changed(this: &Self, _old_value: bool, _new_value: bool) {
166        this.set_button_usage();
167    }
168}
169
170impl ButtonSpinner {
171    /// The named parts expected in the control template, in addition to
172    /// those of the base class.
173    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] = &[
174        TemplatePartAttribute::new("PART_DecreaseButton", <Button as StaticType>::TYPE),
175        TemplatePartAttribute::new("PART_IncreaseButton", <Button as StaticType>::TYPE),
176    ];
177
178    /// The pseudoclasses set by the class.
179    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_LEFT, PC_RIGHT]);
180}
181
182ferroui_base::ferro_properties! { impl ButtonSpinner {
183    ferro_property!(
184        /// Defines the `AllowSpin` property.
185        pub fn allow_spin_property() -> StyledProperty<bool> {
186            FerroProperty::register::<ButtonSpinner, _>("AllowSpin", true)
187        }
188    );
189
190    ferro_property!(
191        /// Defines the `ShowButtonSpinner` property.
192        pub fn show_button_spinner_property() -> StyledProperty<bool> {
193            FerroProperty::register::<ButtonSpinner, _>("ShowButtonSpinner", true)
194        }
195    );
196
197    ferro_property!(
198        /// Defines the `ButtonSpinnerLocation` property.
199        pub fn button_spinner_location_property() -> StyledProperty<Location> {
200            FerroProperty::register::<ButtonSpinner, _>("ButtonSpinnerLocation", Location::Right)
201        }
202    );
203} }
204
205impl ButtonSpinner {
206    fn static_constructor() {
207        Self::allow_spin_property().changed().add_class_handler::<ButtonSpinner>(|spinner, e| {
208            let (old_value, new_value) = e.get_old_and_new_value::<bool>();
209            spinner.on_allow_spin_changed(old_value, new_value);
210        });
211    }
212
213    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
214    pub fn construct() -> Self {
215        Self { base: Spinner::construct(), decrease_button: RefCell::new(None), increase_button: RefCell::new(None) }
216    }
217
218    /// Creates a button spinner.
219    pub fn new() -> Ref<Self> {
220        instantiate(Self::construct())
221    }
222
223    fn decrease_button(&self) -> Option<Ref<Button>> {
224        self.decrease_button.borrow().as_ref().map(|(button, _)| button.clone())
225    }
226
227    fn set_decrease_button(&self, value: Option<Ref<Button>>) {
228        let value = value.map(|button| self.attach_click(button));
229        let old = self.decrease_button.replace(value);
230        Self::detach_click(old);
231    }
232
233    fn increase_button(&self) -> Option<Ref<Button>> {
234        self.increase_button.borrow().as_ref().map(|(button, _)| button.clone())
235    }
236
237    fn set_increase_button(&self, value: Option<Ref<Button>>) {
238        let value = value.map(|button| self.attach_click(button));
239        let old = self.increase_button.replace(value);
240        Self::detach_click(old);
241    }
242
243    fn attach_click(&self, button: Ref<Button>) -> SpinButton {
244        let weak = self.to_ref().downgrade();
245        let token = button.click(move |sender, _| {
246            if let Some(this) = weak.upgrade() {
247                this.on_button_click(sender);
248            }
249        });
250        (button, token)
251    }
252
253    fn detach_click(button: Option<SpinButton>) {
254        if let Some((button, token)) = button {
255            button.remove_handler(Button::click_event(), token);
256        }
257    }
258
259    /// Whether the spinner should allow to spin.
260    pub fn allow_spin(&self) -> bool {
261        self.get_value(Self::allow_spin_property())
262    }
263
264    pub fn set_allow_spin(&self, value: bool) {
265        self.set_value(Self::allow_spin_property(), value)
266    }
267
268    /// Whether the spin buttons should be shown.
269    pub fn show_button_spinner(&self) -> bool {
270        self.get_value(Self::show_button_spinner_property())
271    }
272
273    pub fn set_show_button_spinner(&self, value: bool) {
274        self.set_value(Self::show_button_spinner_property(), value)
275    }
276
277    /// The current location of the spin buttons.
278    pub fn button_spinner_location(&self) -> Location {
279        self.get_value(Self::button_spinner_location_property())
280    }
281
282    pub fn set_button_spinner_location(&self, value: Location) {
283        self.set_value(Self::button_spinner_location_property(), value)
284    }
285
286
287    /// Disables or enables the buttons based on the valid spin direction.
288    fn set_button_usage(&self) {
289        if let Some(increase_button) = self.increase_button() {
290            increase_button.set_is_enabled(
291                self.allow_spin() && self.valid_spin_direction().contains(ValidSpinDirections::INCREASE),
292            );
293        }
294
295        if let Some(decrease_button) = self.decrease_button() {
296            decrease_button.set_is_enabled(
297                self.allow_spin() && self.valid_spin_direction().contains(ValidSpinDirections::DECREASE),
298            );
299        }
300    }
301
302    /// Called when the user clicks one of the spin buttons.
303    fn on_button_click(&self, sender: &Interactive) {
304        if self.allow_spin() {
305            let is_increase = self.increase_button().is_some_and(|button| {
306                let button: &Interactive = &button;
307                std::ptr::eq(button, sender)
308            });
309            let direction = if is_increase { SpinDirection::Increase } else { SpinDirection::Decrease };
310            self.on_spin(&SpinEventArgs::with_event(Some(Spinner::spin_event()), direction));
311        }
312    }
313
314    fn update_pseudo_classes(&self, location: Location) {
315        self.pseudo_classes().set(PC_LEFT, location == Location::Left);
316        self.pseudo_classes().set(PC_RIGHT, location == Location::Right);
317    }
318}