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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum Location {
25 Left,
27 Right,
29}
30
31type SpinButton = (Ref<Button>, RoutedEventHandlerToken);
33
34#[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 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 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 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 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 pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_LEFT, PC_RIGHT]);
180}
181
182ferroui_base::ferro_properties! { impl ButtonSpinner {
183 ferro_property!(
184 pub fn allow_spin_property() -> StyledProperty<bool> {
186 FerroProperty::register::<ButtonSpinner, _>("AllowSpin", true)
187 }
188 );
189
190 ferro_property!(
191 pub fn show_button_spinner_property() -> StyledProperty<bool> {
193 FerroProperty::register::<ButtonSpinner, _>("ShowButtonSpinner", true)
194 }
195 );
196
197 ferro_property!(
198 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 pub fn construct() -> Self {
215 Self { base: Spinner::construct(), decrease_button: RefCell::new(None), increase_button: RefCell::new(None) }
216 }
217
218 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 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 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 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 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 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}