Skip to main content

ferroui_controls/
button.rs

1use crate::i_clickable_control::register_clickable_control;
2use crate::i_command_source::register_command_source;
3use crate::metadata::PseudoClassesAttribute;
4use crate::platform::{FeedbackAction, FeedbackType, PlatformFeedback, PlatformFeedbackExtensions};
5use crate::primitives::{
6    FlyoutBase, PopupFlyoutBase, TemplateAppliedEventArgs, TemplatedControlImpl,
7    TemplatedControlImplExt,
8};
9use crate::{ContentControl, ContentControlImpl, ControlImpl, HotKeyManager};
10use ferroui_base::data::{BindingError, BindingValueType};
11use ferroui_base::input::{
12    AccessKeyEventArgs, AccessKeyHandler, AccessKeyPressedEventArgs, FocusChangedEventArgs, IClickableControl,
13    ICommand, ICommandSource, InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyGesture, MouseButton, PointerCaptureLostEventArgs,
14    PointerPressedEventArgs, PointerReleasedEventArgs,
15};
16use ferroui_base::interactivity::{
17    IRoutedEventArgs, Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken,
18    RoutingStrategies,
19};
20use ferroui_base::layout::LayoutableImpl;
21use ferroui_base::logical_tree::LogicalTreeAttachmentEventArgs;
22use ferroui_base::utilities::WeakEvents;
23use ferroui_base::reactive::{Disposable, IDisposable};
24use ferroui_base::{
25    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, BoxedValue, DirectProperty,
26    FerroObjectImpl, FerroObjectImplExt, FerroProperty, FerroPropertyChangedEventArgs, Nullable, Ref, StyledElementImpl,
27    StyledElementImplExt, StyledProperty, StyledPropertyOptions, Visual, VisualImpl, VisualImplExt,
28    VisualTreeAttachmentEventArgs,
29};
30use std::cell::{Cell, RefCell};
31use std::rc::Rc;
32
33/// Defines how a [`Button`] reacts to clicks.
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
35pub enum ClickMode {
36    /// The `Click` event is raised when the pointer is released.
37    #[default]
38    Release,
39
40    /// The `Click` event is raised when the pointer is pressed.
41    Press,
42}
43
44const PC_PRESSED: &str = ":pressed";
45const PC_FLYOUT_OPEN: &str = ":flyout-open";
46
47/// A standard button control.
48#[repr(C)]
49pub struct Button {
50    base: ContentControl,
51    can_execute_change_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
52    is_subscribed_to_command: Cell<bool>,
53    default_key_down_handlers: RefCell<Vec<RoutedEventHandlerToken>>,
54    cancel_key_down_handlers: RefCell<Vec<RoutedEventHandlerToken>>,
55    command_can_execute: Cell<bool>,
56    hotkey: Cell<Option<KeyGesture>>,
57    is_flyout_open: Cell<bool>,
58    is_pressed: Cell<bool>,
59    /// The subscriptions to the `Opened` and `Closed` events of the flyout.
60    flyout_events: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
61}
62
63ferro_class! {
64    Button: ContentControl, virtuals ButtonImpl: ContentControlImpl {
65        /// Invokes the `Click` event.
66        fn on_click(this);
67        /// Opens the button's flyout.
68        fn open_flyout(this);
69        /// Closes the button's flyout.
70        fn close_flyout(this);
71        /// Invoked when the button's flyout is opened.
72        fn on_flyout_opened(this);
73        /// Invoked when the button's flyout is closed.
74        fn on_flyout_closed(this);
75    }
76}
77ferroui_base::ferro_class_info!(Button { new: Button::new });
78
79ferro_impl_classes!(Button: LayoutableImpl, InteractiveImpl, ContentControlImpl);
80
81impl ControlImpl for Button {
82    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
83        crate::automation::peers::ButtonAutomationPeer::new(this).upcast()
84    }
85}
86
87impl FerroObjectImpl for Button {
88    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
89        Self::parent_on_property_changed(this, change);
90
91        let property = change.property();
92
93        if property == Self::command_property().as_property() {
94            let (old_value, new_value) = change.get_old_and_new_value::<Option<Rc<dyn ICommand>>>();
95            if this.is_subscribed_to_command.get() {
96                if old_value.is_some() {
97                    this.unsubscribe_can_execute_changed();
98                }
99
100                if let Some(new_command) = &new_value {
101                    this.subscribe_can_execute_changed(new_command);
102                }
103            }
104            this.can_execute_changed(new_value.as_ref(), this.command_parameter().as_ref());
105        } else if property == Self::command_parameter_property().as_property() {
106            let new_value = change.get_new_value::<Option<BoxedValue>>();
107            this.can_execute_changed(this.command().as_ref(), new_value.as_ref());
108        } else if property == Self::is_cancel_property().as_property() {
109            let is_cancel = change.get_new_value::<bool>();
110
111            if let Some(input_root) = this.visual_root().and_then(|root| root.downcast::<InputElement>().ok()) {
112                if is_cancel {
113                    this.listen_for_cancel(&input_root);
114                } else {
115                    this.stop_listening_for_cancel(&input_root);
116                }
117            }
118        } else if property == Self::is_default_property().as_property() {
119            let is_default = change.get_new_value::<bool>();
120
121            if let Some(input_root) = this.visual_root().and_then(|root| root.downcast::<InputElement>().ok()) {
122                if is_default {
123                    this.listen_for_default(&input_root);
124                } else {
125                    this.stop_listening_for_default(&input_root);
126                }
127            }
128        } else if property == Self::is_pressed_property().as_property() {
129            this.update_pseudo_classes();
130        } else if property == Self::flyout_property().as_property() {
131            let (old_flyout, new_flyout) = change.get_old_and_new_value::<Option<Ref<FlyoutBase>>>();
132
133            // If flyout is changed while one is already open, make sure we
134            // close the old one first
135            if let Some(old_flyout) = &old_flyout {
136                if old_flyout.is_open() {
137                    old_flyout.hide();
138                }
139
140                if let Some(old_flyout) = old_flyout.downcast_ref::<PopupFlyoutBase>() {
141                    old_flyout.set_default_placement_target(None);
142                }
143            }
144
145            // Must unregister events here while a reference to the old flyout still exists
146            this.unregister_flyout_events(old_flyout.as_ref());
147
148            this.register_flyout_events(new_flyout.as_ref());
149            if let Some(new_flyout) = new_flyout.as_ref().and_then(|flyout| flyout.downcast_ref::<PopupFlyoutBase>()) {
150                new_flyout.set_default_placement_target(Some(this));
151            }
152            this.update_pseudo_classes();
153        } else if property == InputElement::is_effectively_enabled_property().as_property()
154            && !change.get_new_value::<bool>()
155        {
156            this.set_is_pressed(false);
157        }
158    }
159
160    fn update_data_validation(
161        this: &Self,
162        property: &'static FerroProperty,
163        state: BindingValueType,
164        error: Option<&BindingError>,
165    ) {
166        Self::parent_update_data_validation(this, property, state, error);
167
168        if property == Self::command_property().as_property()
169            && state == BindingValueType::BINDING_ERROR
170            && this.command_can_execute.get()
171        {
172            this.command_can_execute.set(false);
173            this.update_is_effectively_enabled();
174        }
175    }
176}
177
178impl StyledElementImpl for Button {
179    fn on_attached_to_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
180        // Control attached again, set the hot key to create a hot key
181        // manager for this control.
182        if let Some(hotkey) = this.hotkey.get() {
183            this.set_current_value(Self::hot_key_property(), Some(hotkey));
184        }
185
186        Self::parent_on_attached_to_logical_tree(this, e);
187
188        let (command, parameter) = (this.command(), this.command_parameter());
189        if let Some(command) = &command {
190            this.subscribe_can_execute_changed(command);
191            this.can_execute_changed(Some(command), parameter.as_ref());
192        }
193
194        this.is_subscribed_to_command.set(true);
195    }
196
197    fn on_detached_from_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
198        // This will cause the hot key manager to dispose the observer and
199        // the reference to this control.
200        if let Some(hotkey) = this.hot_key() {
201            this.hotkey.set(Some(hotkey));
202            this.set_current_value(Self::hot_key_property(), None);
203        }
204
205        Self::parent_on_detached_from_logical_tree(this, e);
206
207        if this.command().is_some() {
208            this.unsubscribe_can_execute_changed();
209        }
210
211        this.is_subscribed_to_command.set(false);
212    }
213}
214
215impl VisualImpl for Button {
216    fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
217        Self::parent_on_attached_to_visual_tree(this, e);
218
219        if this.is_default() {
220            if let Some(input_element) = e.root_visual().cast::<InputElement>() {
221                this.listen_for_default(&input_element);
222            }
223        }
224        if this.is_cancel() {
225            if let Some(input_element) = e.root_visual().cast::<InputElement>() {
226                this.listen_for_cancel(&input_element);
227            }
228        }
229    }
230
231    fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
232        Self::parent_on_detached_from_visual_tree(this, e);
233
234        if this.is_default() {
235            if let Some(input_element) = e.root_visual().cast::<InputElement>() {
236                this.stop_listening_for_default(&input_element);
237            }
238        }
239        if this.is_cancel() {
240            if let Some(input_element) = e.root_visual().cast::<InputElement>() {
241                this.stop_listening_for_cancel(&input_element);
242            }
243        }
244    }
245}
246
247impl InputElementImpl for Button {
248    fn is_enabled_core(this: &Self) -> bool {
249        Self::parent_is_enabled_core(this) && this.command_can_execute.get()
250    }
251
252    fn on_access_key(this: &Self, e: &dyn IRoutedEventArgs) {
253        if e.downcast_ref::<AccessKeyEventArgs>().is_some_and(AccessKeyEventArgs::is_multiple) {
254            Self::parent_on_access_key(this, e);
255        } else {
256            this.on_click();
257        }
258    }
259
260    fn on_key_down(this: &Self, e: &KeyEventArgs) {
261        match e.key {
262            Key::Enter => {
263                this.on_click();
264                e.set_handled(true);
265            }
266            Key::Space => {
267                // Avoid handling Space if the button isn't focused: a child
268                // text box might need it for text input.
269                if this.is_focused() {
270                    if this.click_mode() == ClickMode::Press {
271                        this.on_click();
272                    }
273
274                    this.set_is_pressed(true);
275                    e.set_handled(true);
276                }
277            }
278            Key::Escape if this.flyout().is_some() => {
279                // If Flyout doesn't have focusable content, close the flyout here
280                this.close_flyout();
281            }
282            _ => {}
283        }
284
285        Self::parent_on_key_down(this, e);
286    }
287
288    fn on_key_up(this: &Self, e: &KeyEventArgs) {
289        // Avoid handling Space if the button isn't focused: a child text
290        // box might need it for text input.
291        if e.key == Key::Space && this.is_focused() {
292            if this.click_mode() == ClickMode::Release {
293                this.on_click();
294            }
295            this.set_is_pressed(false);
296            e.set_handled(true);
297        }
298
299        Self::parent_on_key_up(this, e);
300    }
301
302    fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
303        Self::parent_on_pointer_pressed(this, e);
304
305        if e.get_current_point(Some(this)).properties.is_left_button_pressed {
306            if this.is_flyout_open.get() && this.is_effectively_enabled() {
307                // When a flyout is open with pass-through of the overlay
308                // dismiss event enabled and the button is pressed, close
309                // the flyout, but do not transition to a pressed state.
310                e.set_handled(true);
311                this.on_click();
312            } else {
313                this.set_is_pressed(true);
314                e.set_handled(true);
315
316                if this.click_mode() == ClickMode::Press {
317                    this.on_click();
318                }
319            }
320        }
321    }
322
323    fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
324        Self::parent_on_pointer_released(this, e);
325
326        if this.is_pressed() && e.initial_press_mouse_button() == MouseButton::Left {
327            this.set_is_pressed(false);
328            e.set_handled(true);
329
330            if this.click_mode() == ClickMode::Release {
331                let this_visual: &Visual = this;
332                let is_over = this.get_visuals_at(e.get_position(Some(this))).iter().any(|c| {
333                    std::ptr::eq::<Visual>(&**c, this_visual) || this.is_visual_ancestor_of(c)
334                });
335
336                if is_over {
337                    this.on_click();
338                }
339            }
340        }
341    }
342
343    fn on_pointer_capture_lost(this: &Self, e: &PointerCaptureLostEventArgs) {
344        Self::parent_on_pointer_capture_lost(this, e);
345
346        this.set_is_pressed(false);
347    }
348
349    fn on_lost_focus(this: &Self, e: &FocusChangedEventArgs) {
350        Self::parent_on_lost_focus(this, e);
351
352        this.set_is_pressed(false);
353    }
354}
355
356impl TemplatedControlImpl for Button {
357    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
358        Self::parent_on_apply_template(this, e);
359
360        let flyout = this.flyout();
361        this.unregister_flyout_events(flyout.as_ref());
362        this.register_flyout_events(flyout.as_ref());
363        this.update_pseudo_classes();
364    }
365}
366
367impl ButtonImpl for Button {
368    fn on_click(this: &Self) {
369        if this.is_effectively_enabled() {
370            if this.is_flyout_open.get() {
371                this.close_flyout();
372            } else {
373                this.open_flyout();
374            }
375
376            this.perform_feedback(FeedbackAction::click());
377
378            let e = RoutedEventArgs::with_event(Self::click_event());
379            this.raise_event(&e);
380
381            let (command, parameter) = (this.command(), this.command_parameter());
382            if let Some(command) = command {
383                if !e.handled() && command.can_execute(parameter.as_ref()) {
384                    command.execute(parameter.as_ref());
385                    e.set_handled(true);
386                }
387            }
388        }
389    }
390
391    fn open_flyout(this: &Self) {
392        if let Some(flyout) = this.flyout() {
393            flyout.show_at(this);
394        }
395    }
396
397    fn close_flyout(this: &Self) {
398        if let Some(flyout) = this.flyout() {
399            flyout.hide();
400        }
401    }
402
403    fn on_flyout_opened(_this: &Self) {
404        // Available for derived types
405    }
406
407    fn on_flyout_closed(_this: &Self) {
408        // Available for derived types
409    }
410}
411
412/// The button viewed as a command source and as a clickable control.
413struct ButtonHandle(Ref<Button>);
414
415impl ICommandSource for ButtonHandle {
416    fn command(&self) -> Option<Rc<dyn ICommand>> {
417        self.0.command()
418    }
419
420    fn command_parameter(&self) -> Option<BoxedValue> {
421        self.0.command_parameter()
422    }
423
424    fn can_execute_changed(&self) {
425        self.0.can_execute_changed_handler();
426    }
427
428    fn is_effectively_enabled(&self) -> bool {
429        self.0.is_effectively_enabled()
430    }
431}
432
433impl IClickableControl for ButtonHandle {
434    fn click(&self, handler: Rc<dyn Fn(&RoutedEventArgs)>) -> Rc<dyn IDisposable> {
435        let token = self.0.click(move |_, e| handler(e));
436        let button = self.0.downgrade();
437        Disposable::create(move || {
438            if let Some(button) = button.upgrade() {
439                button.remove_handler(Button::click_event(), token);
440            }
441        })
442    }
443
444    fn raise_click(&self) {
445        self.0.on_click();
446    }
447
448    fn is_effectively_enabled(&self) -> bool {
449        self.0.is_effectively_enabled()
450    }
451}
452
453impl Button {
454    /// The pseudoclasses set by the class.
455    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_FLYOUT_OPEN, PC_PRESSED]);
456}
457
458ferroui_base::ferro_properties! { impl Button, also [
459    Button::is_pressed_property,
460    Button::flyout_property,
461] {
462    ferro_property!(
463        /// Defines the `ClickMode` property.
464        pub fn click_mode_property() -> StyledProperty<ClickMode> {
465            FerroProperty::register::<Button, _>("ClickMode", ClickMode::Release)
466        }
467    );
468
469    ferro_property!(
470        /// Defines the `Command` property.
471        pub fn command_property() -> StyledProperty<Option<Rc<dyn ICommand>>> {
472            FerroProperty::register_with::<Button, _>(
473                "Command",
474                StyledPropertyOptions::new(None).enable_data_validation(true),
475            )
476        }
477    );
478
479    ferro_property!(
480        /// Defines the `HotKey` property.
481        pub fn hot_key_property() -> StyledProperty<Option<KeyGesture>> {
482            HotKeyManager::hot_key_property().add_owner::<Button>()
483        }
484    );
485
486    ferro_property!(
487        /// Defines the `CommandParameter` property.
488        pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
489            FerroProperty::register::<Button, _>("CommandParameter", None)
490        }
491    );
492
493    ferro_property!(
494        /// Defines the `IsDefault` property.
495        pub fn is_default_property() -> StyledProperty<bool> {
496            FerroProperty::register::<Button, _>("IsDefault", false)
497        }
498    );
499
500    ferro_property!(
501        /// Defines the `IsCancel` property.
502        pub fn is_cancel_property() -> StyledProperty<bool> {
503            FerroProperty::register::<Button, _>("IsCancel", false)
504        }
505    );
506} }
507
508impl Button {
509    ferro_routed_event!(
510        /// Defines the `Click` event.
511        pub fn click_event() -> RoutedEvent<RoutedEventArgs> {
512            RoutedEvent::register::<Button, _>("Click", RoutingStrategies::BUBBLE)
513        }
514    );
515
516    ferro_property!(for Button;
517        /// Defines the `IsPressed` property.
518        pub fn is_pressed_property() -> DirectProperty<Button, bool> {
519            FerroProperty::register_direct::<Button, bool>("IsPressed", |b| b.is_pressed(), None, false)
520        }
521    );
522
523    ferro_property!(for Button;
524        /// Defines the `Flyout` property.
525        pub fn flyout_property() -> StyledProperty<Option<Ref<FlyoutBase>>> {
526            FerroProperty::register::<Button, _>("Flyout", None)
527        }
528    );
529
530    fn static_constructor() {
531        register_command_source::<Button>(|button| Rc::new(ButtonHandle(button)));
532        register_clickable_control::<Button>(|button| Rc::new(ButtonHandle(button)));
533
534        InputElement::focusable_property().override_default_value::<Button>(true);
535        PlatformFeedback::feedback_type_property().override_default_value::<Button>(FeedbackType::Auto);
536        AccessKeyHandler::access_key_pressed_event().add_class_handler::<Button>(Self::on_access_key_pressed);
537    }
538
539    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
540    pub fn construct() -> Self {
541        Self {
542            base: ContentControl::construct(),
543            can_execute_change_subscription: RefCell::new(None),
544            is_subscribed_to_command: Cell::new(false),
545            default_key_down_handlers: RefCell::new(Vec::new()),
546            cancel_key_down_handlers: RefCell::new(Vec::new()),
547            command_can_execute: Cell::new(true),
548            hotkey: Cell::new(None),
549            is_flyout_open: Cell::new(false),
550            is_pressed: Cell::new(false),
551            flyout_events: RefCell::new(None),
552        }
553    }
554
555    /// Creates a button.
556    pub fn new() -> Ref<Self> {
557        instantiate(Self::construct())
558    }
559
560    /// Raised when the user clicks the button.
561    pub fn click(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
562        self.add_handler(Self::click_event(), handler)
563    }
564
565    /// A value indicating how the button should react to clicks.
566    pub fn click_mode(&self) -> ClickMode {
567        self.get_value(Self::click_mode_property())
568    }
569
570    pub fn set_click_mode(&self, value: ClickMode) {
571        self.set_value(Self::click_mode_property(), value)
572    }
573
574    /// A command to be invoked when the button is clicked.
575    pub fn command(&self) -> Option<Rc<dyn ICommand>> {
576        self.get_value(Self::command_property())
577    }
578
579    pub fn set_command(&self, value: Option<Rc<dyn ICommand>>) {
580        self.set_value(Self::command_property(), value)
581    }
582
583    /// A key gesture associated with this control.
584    pub fn hot_key(&self) -> Option<KeyGesture> {
585        self.get_value(Self::hot_key_property())
586    }
587
588    pub fn set_hot_key(&self, value: Option<KeyGesture>) {
589        self.set_value(Self::hot_key_property(), value)
590    }
591
592    /// A parameter to be passed to the `Command`.
593    pub fn command_parameter(&self) -> Option<BoxedValue> {
594        self.get_value(Self::command_parameter_property())
595    }
596
597    pub fn set_command_parameter(&self, value: Option<BoxedValue>) {
598        self.set_value(Self::command_parameter_property(), value)
599    }
600
601    /// A value indicating whether the button is the default button for the
602    /// root it is attached to.
603    pub fn is_default(&self) -> bool {
604        self.get_value(Self::is_default_property())
605    }
606
607    pub fn set_is_default(&self, value: bool) {
608        self.set_value(Self::is_default_property(), value)
609    }
610
611    /// A value indicating whether the button is the Cancel button for the
612    /// root it is attached to.
613    pub fn is_cancel(&self) -> bool {
614        self.get_value(Self::is_cancel_property())
615    }
616
617    pub fn set_is_cancel(&self, value: bool) {
618        self.set_value(Self::is_cancel_property(), value)
619    }
620
621    /// A value indicating whether the button is currently pressed.
622    #[inline]
623    pub fn is_pressed(&self) -> bool {
624        self.is_pressed.get()
625    }
626
627    fn set_is_pressed(&self, value: bool) {
628        self.set_and_raise_cell(Self::is_pressed_property(), &self.is_pressed, value);
629    }
630
631    /// The flyout that should be shown with this button.
632    pub fn flyout(&self) -> Option<Ref<FlyoutBase>> {
633        self.get_value(Self::flyout_property())
634    }
635
636    pub fn set_flyout(&self, value: impl Into<Nullable<FlyoutBase>>) {
637        self.set_value(Self::flyout_property(), value.into().0)
638    }
639
640    /// Clicks the button.
641    #[allow(dead_code)]
642    pub(crate) fn perform_click(&self) {
643        self.on_click();
644    }
645
646    fn on_access_key_pressed(sender: &Button, e: &AccessKeyPressedEventArgs) {
647        if e.handled() || e.target().is_some() {
648            return;
649        }
650        e.set_target(Some(sender.to_ref().upcast()));
651        e.set_handled(true);
652    }
653
654    fn subscribe_can_execute_changed(&self, command: &Rc<dyn ICommand>) {
655        // A weak subscription: it ends with this control, so a command
656        // that outlives the control keeps no handler of it.
657        let subscription = WeakEvents::subscribe_command_can_execute_changed(command, &self.to_ref(), |this| {
658            this.can_execute_changed_handler()
659        });
660        let old = self.can_execute_change_subscription.replace(Some(subscription));
661        if let Some(old) = old {
662            old.dispose();
663        }
664    }
665
666    fn unsubscribe_can_execute_changed(&self) {
667        let subscription = self.can_execute_change_subscription.take();
668        if let Some(subscription) = subscription {
669            subscription.dispose();
670        }
671    }
672
673    /// Called when the "can execute changed" event of the command fires.
674    fn can_execute_changed_handler(&self) {
675        self.can_execute_changed(self.command().as_ref(), self.command_parameter().as_ref());
676    }
677
678    fn can_execute_changed(&self, command: Option<&Rc<dyn ICommand>>, parameter: Option<&BoxedValue>) {
679        if !self.is_attached_to_logical_tree() {
680            return;
681        }
682
683        let can_execute = command.is_none_or(|command| command.can_execute(parameter));
684
685        if can_execute != self.command_can_execute.get() {
686            self.command_can_execute.set(can_execute);
687            self.update_is_effectively_enabled();
688        }
689    }
690
691    /// Starts listening for the Enter key when the button `is_default`.
692    fn listen_for_default(&self, root: &InputElement) {
693        let weak = self.to_ref().downgrade();
694        let token = root.add_handler(InputElement::key_down_event(), move |_, e| {
695            if let Some(this) = weak.upgrade() {
696                this.root_default_key_down(e);
697            }
698        });
699        self.default_key_down_handlers.borrow_mut().push(token);
700    }
701
702    /// Starts listening for the Escape key when the button `is_cancel`.
703    fn listen_for_cancel(&self, root: &InputElement) {
704        let weak = self.to_ref().downgrade();
705        let token = root.add_handler(InputElement::key_down_event(), move |_, e| {
706            if let Some(this) = weak.upgrade() {
707                this.root_cancel_key_down(e);
708            }
709        });
710        self.cancel_key_down_handlers.borrow_mut().push(token);
711    }
712
713    /// Stops listening for the Enter key when the button is no longer
714    /// `is_default`.
715    fn stop_listening_for_default(&self, root: &InputElement) {
716        let token = self.default_key_down_handlers.borrow_mut().pop();
717        if let Some(token) = token {
718            root.remove_handler(InputElement::key_down_event(), token);
719        }
720    }
721
722    /// Stops listening for the Escape key when the button is no longer
723    /// `is_cancel`.
724    fn stop_listening_for_cancel(&self, root: &InputElement) {
725        let token = self.cancel_key_down_handlers.borrow_mut().pop();
726        if let Some(token) = token {
727            root.remove_handler(InputElement::key_down_event(), token);
728        }
729    }
730
731    /// Called when a key is pressed on the input root and the button
732    /// `is_default`.
733    fn root_default_key_down(&self, e: &KeyEventArgs) {
734        if e.key == Key::Enter && self.is_effectively_visible() && self.is_effectively_enabled() {
735            self.on_click();
736            e.set_handled(true);
737        }
738    }
739
740    /// Called when a key is pressed on the input root and the button
741    /// `is_cancel`.
742    fn root_cancel_key_down(&self, e: &KeyEventArgs) {
743        if e.key == Key::Escape && self.is_effectively_visible() && self.is_effectively_enabled() {
744            self.on_click();
745            e.set_handled(true);
746        }
747    }
748
749    /// Registers all flyout events.
750    fn register_flyout_events(&self, flyout: Option<&Ref<FlyoutBase>>) {
751        if let Some(flyout) = flyout {
752            let weak = self.to_ref().downgrade();
753            let weak_flyout = flyout.downgrade();
754            let opened = flyout.opened({
755                let (weak, weak_flyout) = (weak.clone(), weak_flyout.clone());
756                move || {
757                    if let (Some(this), Some(flyout)) = (weak.upgrade(), weak_flyout.upgrade()) {
758                        this.flyout_opened(&flyout);
759                    }
760                }
761            });
762            let closed = flyout.closed(move || {
763                if let (Some(this), Some(flyout)) = (weak.upgrade(), weak_flyout.upgrade()) {
764                    this.flyout_closed(&flyout);
765                }
766            });
767            *self.flyout_events.borrow_mut() = Some((opened, closed));
768        }
769    }
770
771    /// Explicitly unregisters all flyout events.
772    fn unregister_flyout_events(&self, flyout: Option<&Ref<FlyoutBase>>) {
773        if flyout.is_some() {
774            let flyout_events = self.flyout_events.take();
775            if let Some((opened, closed)) = flyout_events {
776                opened.dispose();
777                closed.dispose();
778            }
779        }
780    }
781
782    /// Whether `flyout` is currently shown at this button.
783    fn is_flyout_target(&self, flyout: &FlyoutBase) -> bool {
784        let this: Ref<crate::Control> = self.to_ref().upcast();
785        flyout.target() == Some(this)
786    }
787
788    /// Event handler for when the button's flyout is opened.
789    fn flyout_opened(&self, flyout: &FlyoutBase) {
790        // It is possible to share flyouts among multiple controls including
791        // buttons. This can cause a problem here since all controls that
792        // share a flyout receive the same Opened/Closed events at the same
793        // time. For buttons that means they all would be updating their
794        // pseudoclasses accordingly. In other words, all buttons with a
795        // shared flyout would have the backgrounds changed together. To fix
796        // this, only continue here if the flyout target matches this button
797        // instance.
798        if self.is_flyout_target(flyout) {
799            self.is_flyout_open.set(true);
800            self.update_pseudo_classes();
801
802            self.on_flyout_opened();
803        }
804    }
805
806    /// Event handler for when the button's flyout is closed.
807    fn flyout_closed(&self, flyout: &FlyoutBase) {
808        // See comments in `flyout_opened`
809        if self.is_flyout_target(flyout) {
810            self.is_flyout_open.set(false);
811            self.update_pseudo_classes();
812
813            self.on_flyout_closed();
814        }
815    }
816
817    /// Updates the visual state of the control by applying the latest
818    /// pseudoclasses.
819    fn update_pseudo_classes(&self) {
820        self.pseudo_classes().set(PC_FLYOUT_OPEN, self.is_flyout_open.get());
821        self.pseudo_classes().set(PC_PRESSED, self.is_pressed());
822    }
823}