Skip to main content

ferroui_controls/split_button/
split_button.rs

1use crate::i_clickable_control::{as_clickable_control, register_clickable_control};
2use crate::i_command_source::register_command_source;
3use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
4use crate::primitives::{
5    FlyoutBase, Popup, PopupFlyoutBase, TemplateAppliedEventArgs, TemplatedControlImpl,
6    TemplatedControlImplExt,
7};
8use crate::{Button, ContentControl, ContentControlImpl, ControlImpl};
9use ferroui_base::input::navigation::XYFocusHelpers;
10use ferroui_base::input::{
11    IClickableControl, ICommand, ICommandSource, InputElement, InputElementImpl, InputElementImplExt, Key,
12    KeyEventArgs, KeyGesture, KeyModifiers, PointerPressedEventArgs,
13};
14use ferroui_base::interactivity::{
15    Interactive, InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
16};
17use ferroui_base::layout::LayoutableImpl;
18use ferroui_base::logical_tree::LogicalTreeAttachmentEventArgs;
19use ferroui_base::utilities::WeakEvents;
20use ferroui_base::reactive::{Disposable, IDisposable};
21use ferroui_base::utilities::HandlerList;
22use ferroui_base::{
23    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, BoxedValue,
24    FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt, FerroPropertyChangedEventArgs, Nullable, Ref, StaticType, StyledElementImpl,
25    StyledElementImplExt, StyledProperty, VisualImpl,
26};
27use std::cell::{Cell, RefCell};
28use std::rc::Rc;
29
30pub(crate) const PC_CHECKED: &str = ":checked";
31pub(crate) const PC_PRESSED: &str = ":pressed";
32pub(crate) const PC_FLYOUT_OPEN: &str = ":flyout-open";
33
34/// The secondary button of the template and the handlers attached to it:
35/// the click handler and the pointer pressed (tunnel) handler.
36type SecondaryButton = (Ref<Button>, RoutedEventHandlerToken, RoutedEventHandlerToken);
37
38/// A button with primary and secondary parts that can each be pressed
39/// separately. The primary part behaves like a [`Button`] and the secondary
40/// part opens a flyout.
41#[repr(C)]
42pub struct SplitButton {
43    base: ContentControl,
44    primary_button: RefCell<Option<(Ref<Button>, RoutedEventHandlerToken)>>,
45    secondary_button: RefCell<Option<SecondaryButton>>,
46    hotkey: Cell<Option<KeyGesture>>,
47    can_execute_change_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
48    command_can_execute: Cell<bool>,
49    is_attached_to_logical_tree: Cell<bool>,
50    is_flyout_open: Cell<bool>,
51    is_keyboard_pressed: Cell<bool>,
52    /// The subscriptions to the `Opened` and `Closed` events of the flyout.
53    flyout_events: RefCell<Option<(Rc<dyn IDisposable>, Rc<dyn IDisposable>)>>,
54    flyout_property_changed_disposable: RefCell<Option<Rc<dyn IDisposable>>>,
55    flyout_state_changed: HandlerList<dyn Fn()>,
56}
57
58ferro_class! {
59    SplitButton: ContentControl, virtuals SplitButtonImpl: ContentControlImpl {
60        /// A value indicating whether the button is currently checked.
61        ///
62        /// This member exists only for the derived toggle split button and
63        /// is unused (false) within the split button. Doing this allows the
64        /// two controls to share a default style.
65        fn internal_is_checked(this) -> bool;
66        /// Invokes the `Click` event when the primary button part is
67        /// clicked. `e` are the event args from the internal click event.
68        fn on_click_primary(this, e: Option<&RoutedEventArgs>);
69        /// Invoked when the secondary button part is clicked. `e` are the
70        /// event args from the internal click event.
71        fn on_click_secondary(this, e: Option<&RoutedEventArgs>);
72        /// Invoked when the split button's flyout is opened.
73        fn on_flyout_opened(this);
74        /// Invoked when the split button's flyout is closed.
75        fn on_flyout_closed(this);
76    }
77}
78ferroui_base::ferro_class_info!(SplitButton { new: SplitButton::new });
79
80ferro_impl_classes!(SplitButton: VisualImpl, LayoutableImpl, InteractiveImpl, ContentControlImpl);
81
82impl ControlImpl for SplitButton {
83    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
84        crate::automation::peers::SplitButtonAutomationPeer::new(this).upcast()
85    }
86}
87
88impl FerroObjectImpl for SplitButton {
89    fn on_property_changed(this: &Self, e: &FerroPropertyChangedEventArgs<'_>) {
90        if e.property() == Self::command_property().as_property() {
91            // Must unregister events here while a reference to the old
92            // command still exists.
93            let (old_value, new_value) = e.get_old_and_new_value::<Option<Rc<dyn ICommand>>>();
94
95            if this.is_attached_to_logical_tree.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
105            this.can_execute_changed(new_value.as_ref(), this.command_parameter().as_ref());
106        } else if e.property() == Self::command_parameter_property().as_property() && this.is_loaded() {
107            let new_value = e.get_new_value::<Option<BoxedValue>>();
108            this.can_execute_changed(this.command().as_ref(), new_value.as_ref());
109        } else if e.property() == Self::flyout_property().as_property() {
110            let (old_flyout, new_flyout) = e.get_old_and_new_value::<Option<Ref<FlyoutBase>>>();
111
112            // If flyout is changed while one is already open, make sure we
113            // close the old one first.
114            // This is the same behavior as the button.
115            if let Some(old_flyout) = &old_flyout {
116                if old_flyout.is_open() {
117                    old_flyout.hide();
118                }
119
120                if let Some(old_flyout) = old_flyout.downcast_ref::<PopupFlyoutBase>() {
121                    old_flyout.set_default_placement_target(None);
122                }
123            }
124
125            // Must unregister events here while a reference to the old flyout still exists
126            this.unregister_flyout_events(old_flyout.as_ref());
127
128            this.register_flyout_events(new_flyout.as_ref());
129            if let Some(new_flyout) = new_flyout.as_ref().and_then(|flyout| flyout.downcast_ref::<PopupFlyoutBase>()) {
130                new_flyout.set_default_placement_target(Some(this));
131            }
132            this.update_pseudo_classes();
133        }
134
135        Self::parent_on_property_changed(this, e);
136    }
137}
138
139impl StyledElementImpl for SplitButton {
140    fn on_attached_to_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
141        Self::parent_on_attached_to_logical_tree(this, e);
142
143        // Control attached again, set the hot key to create a hot key
144        // manager for this control.
145        this.set_current_value(Self::hot_key_property(), this.hotkey.get());
146
147        if let Some(command) = this.command() {
148            this.subscribe_can_execute_changed(&command);
149            this.can_execute_changed_handler();
150        }
151
152        this.is_attached_to_logical_tree.set(true);
153    }
154
155    fn on_detached_from_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
156        Self::parent_on_detached_from_logical_tree(this, e);
157
158        // This will cause the hot key manager to dispose the observer and
159        // the reference to this control.
160        this.hotkey.set(this.hot_key());
161        this.set_current_value(Self::hot_key_property(), None);
162
163        if this.command().is_some() {
164            this.unsubscribe_can_execute_changed();
165        }
166
167        this.is_attached_to_logical_tree.set(false);
168    }
169}
170
171impl InputElementImpl for SplitButton {
172    fn is_enabled_core(this: &Self) -> bool {
173        Self::parent_is_enabled_core(this) && this.command_can_execute.get()
174    }
175
176    fn on_key_down(this: &Self, e: &KeyEventArgs) {
177        let key = e.key;
178
179        if (this.is_focused() && key == Key::Space) || key == Key::Enter {
180            this.is_keyboard_pressed.set(true);
181            this.update_pseudo_classes();
182        }
183
184        Self::parent_on_key_down(this, e);
185    }
186
187    fn on_key_up(this: &Self, e: &KeyEventArgs) {
188        let key = e.key;
189
190        if (this.is_focused() && key == Key::Space) || key == Key::Enter {
191            this.is_keyboard_pressed.set(false);
192            this.update_pseudo_classes();
193
194            // Consider this a click on the primary button.
195            if this.is_effectively_enabled() {
196                this.on_click_primary(None);
197                e.set_handled(true);
198            }
199        } else if (key == Key::Down
200            && e.key_modifiers.contains(KeyModifiers::ALT)
201            && this.is_effectively_enabled()
202            && !XYFocusHelpers::is_allowed_xy_navigation_mode(this, Some(e.key_device_type)))
203            || (key == Key::F4 && this.is_effectively_enabled())
204        {
205            this.open_flyout();
206            e.set_handled(true);
207        } else if e.key == Key::Escape && this.is_flyout_open.get() {
208            // If the flyout doesn't have focusable content, close the
209            // flyout here. This is the same behavior as the button.
210            this.close_flyout();
211            e.set_handled(true);
212        }
213
214        Self::parent_on_key_up(this, e);
215    }
216}
217
218impl TemplatedControlImpl for SplitButton {
219    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
220        Self::parent_on_apply_template(this, e);
221
222        this.unregister_events();
223        let flyout = this.flyout();
224        this.unregister_flyout_events(flyout.as_ref());
225
226        let primary_button = e.name_scope().find_as::<Button>("PART_PrimaryButton");
227        let secondary_button = e.name_scope().find_as::<Button>("PART_SecondaryButton");
228
229        let primary_button = primary_button.map(|primary_button| {
230            let weak = this.to_ref().downgrade();
231            let token = primary_button.click(move |_, e| {
232                if let Some(this) = weak.upgrade() {
233                    this.primary_button_click(e);
234                }
235            });
236            (primary_button, token)
237        });
238        *this.primary_button.borrow_mut() = primary_button;
239
240        let secondary_button = secondary_button.map(|secondary_button| {
241            let weak = this.to_ref().downgrade();
242            let click = secondary_button.click(move |_, e| {
243                if let Some(this) = weak.upgrade() {
244                    this.secondary_button_click(e);
245                }
246            });
247            let weak = this.to_ref().downgrade();
248            let pressed = secondary_button.add_handler_with(
249                InputElement::pointer_pressed_event(),
250                move |_, e: &PointerPressedEventArgs| {
251                    if let Some(this) = weak.upgrade() {
252                        this.secondary_button_preview_pointer_pressed(e);
253                    }
254                },
255                RoutingStrategies::TUNNEL,
256                false,
257            );
258            (secondary_button, click, pressed)
259        });
260        *this.secondary_button.borrow_mut() = secondary_button;
261
262        this.register_flyout_events(flyout.as_ref());
263        this.update_pseudo_classes();
264    }
265}
266
267impl SplitButtonImpl for SplitButton {
268    fn internal_is_checked(_this: &Self) -> bool {
269        false
270    }
271
272    fn on_click_primary(this: &Self, _e: Option<&RoutedEventArgs>) {
273        let (command, parameter) = (this.command(), this.command_parameter());
274        // Note: It is not currently required to check enabled status;
275        // however, this is a failsafe.
276        if this.is_effectively_enabled() {
277            let event_args = RoutedEventArgs::with_event(Self::click_event());
278            this.raise_event(&event_args);
279
280            if let Some(command) = command {
281                if !event_args.handled() && command.can_execute(parameter.as_ref()) {
282                    command.execute(parameter.as_ref());
283                    event_args.set_handled(true);
284                }
285            }
286        }
287    }
288
289    fn on_click_secondary(this: &Self, _e: Option<&RoutedEventArgs>) {
290        // Note: It is not currently required to check enabled status;
291        // however, this is a failsafe.
292        if this.is_effectively_enabled() {
293            if this.is_flyout_open.get() {
294                this.close_flyout();
295            } else {
296                this.open_flyout();
297            }
298        }
299    }
300
301    fn on_flyout_opened(_this: &Self) {
302        // Available for derived types
303    }
304
305    fn on_flyout_closed(_this: &Self) {
306        // Available for derived types
307    }
308}
309
310/// The split button viewed as a command source and as a clickable control.
311struct SplitButtonHandle(Ref<SplitButton>);
312
313impl ICommandSource for SplitButtonHandle {
314    fn command(&self) -> Option<Rc<dyn ICommand>> {
315        self.0.command()
316    }
317
318    fn command_parameter(&self) -> Option<BoxedValue> {
319        self.0.command_parameter()
320    }
321
322    fn can_execute_changed(&self) {
323        self.0.can_execute_changed_handler();
324    }
325
326    fn is_effectively_enabled(&self) -> bool {
327        self.0.is_effectively_enabled()
328    }
329}
330
331impl IClickableControl for SplitButtonHandle {
332    fn click(&self, handler: Rc<dyn Fn(&RoutedEventArgs)>) -> Rc<dyn IDisposable> {
333        let token = self.0.click(move |_, e| handler(e));
334        let button = self.0.downgrade();
335        Disposable::create(move || {
336            if let Some(button) = button.upgrade() {
337                button.remove_handler(SplitButton::click_event(), token);
338            }
339        })
340    }
341
342    fn raise_click(&self) {
343        let primary_button = self.0.primary_button();
344        if let Some(primary_button) = primary_button.and_then(|button| as_clickable_control(&button)) {
345            primary_button.raise_click();
346        }
347    }
348
349    fn is_effectively_enabled(&self) -> bool {
350        self.0.is_effectively_enabled()
351    }
352}
353
354impl SplitButton {
355    /// The named parts expected in the control template, in addition to
356    /// those of the base class.
357    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] = &[
358        TemplatePartAttribute::new("PART_PrimaryButton", <Button as StaticType>::TYPE),
359        TemplatePartAttribute::new("PART_SecondaryButton", <Button as StaticType>::TYPE),
360    ];
361
362    /// The pseudoclasses set by the class.
363    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_FLYOUT_OPEN, PC_PRESSED]);
364
365    ferro_routed_event!(
366        /// Defines the `Click` event.
367        pub fn click_event() -> RoutedEvent<RoutedEventArgs> {
368            RoutedEvent::register::<SplitButton, _>("Click", RoutingStrategies::BUBBLE)
369        }
370    );
371}
372
373ferroui_base::ferro_properties! { impl SplitButton {
374    ferro_property!(
375        /// Defines the `Command` property.
376        pub fn command_property() -> StyledProperty<Option<Rc<dyn ICommand>>> {
377            Button::command_property().add_owner::<SplitButton>()
378        }
379    );
380
381    ferro_property!(
382        /// Defines the `CommandParameter` property.
383        pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
384            Button::command_parameter_property().add_owner::<SplitButton>()
385        }
386    );
387
388    ferro_property!(
389        /// Defines the `Flyout` property.
390        pub fn flyout_property() -> StyledProperty<Option<Ref<FlyoutBase>>> {
391            Button::flyout_property().add_owner::<SplitButton>()
392        }
393    );
394
395    ferro_property!(
396        /// Defines the `HotKey` property.
397        pub fn hot_key_property() -> StyledProperty<Option<KeyGesture>> {
398            Button::hot_key_property().add_owner::<SplitButton>()
399        }
400    );
401} }
402
403impl SplitButton {
404    fn static_constructor() {
405        register_command_source::<SplitButton>(|button| Rc::new(SplitButtonHandle(button)));
406        register_clickable_control::<SplitButton>(|button| Rc::new(SplitButtonHandle(button)));
407    }
408
409    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
410    pub fn construct() -> Self {
411        Self {
412            base: ContentControl::construct(),
413            primary_button: RefCell::new(None),
414            secondary_button: RefCell::new(None),
415            hotkey: Cell::new(None),
416            can_execute_change_subscription: RefCell::new(None),
417            command_can_execute: Cell::new(true),
418            is_attached_to_logical_tree: Cell::new(false),
419            is_flyout_open: Cell::new(false),
420            is_keyboard_pressed: Cell::new(false),
421            flyout_events: RefCell::new(None),
422            flyout_property_changed_disposable: RefCell::new(None),
423            flyout_state_changed: HandlerList::new(),
424        }
425    }
426
427    /// Creates a split button.
428    pub fn new() -> Ref<Self> {
429        instantiate(Self::construct())
430    }
431
432    /// Raised when the user presses the primary part of the split button.
433    pub fn click(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
434        self.add_handler(Self::click_event(), handler)
435    }
436
437    /// The command invoked when the primary part is pressed.
438    pub fn command(&self) -> Option<Rc<dyn ICommand>> {
439        self.get_value(Self::command_property())
440    }
441
442    pub fn set_command(&self, value: Option<Rc<dyn ICommand>>) {
443        self.set_value(Self::command_property(), value)
444    }
445
446    /// A parameter to be passed to the `Command`.
447    pub fn command_parameter(&self) -> Option<BoxedValue> {
448        self.get_value(Self::command_parameter_property())
449    }
450
451    pub fn set_command_parameter(&self, value: Option<BoxedValue>) {
452        self.set_value(Self::command_parameter_property(), value)
453    }
454
455    /// The flyout that will be shown when the secondary part is pressed.
456    pub fn flyout(&self) -> Option<Ref<FlyoutBase>> {
457        self.get_value(Self::flyout_property())
458    }
459
460    pub fn set_flyout(&self, value: impl Into<Nullable<FlyoutBase>>) {
461        self.set_value(Self::flyout_property(), value.into().0)
462    }
463
464    /// Raised when the flyout of the split button opens or closes
465    /// (internal in the reference). Disposing the returned handle
466    /// unsubscribes.
467    pub(crate) fn flyout_state_changed(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
468        let token = self.flyout_state_changed.add(Rc::new(handler));
469        let weak = self.to_ref().downgrade();
470        Disposable::create(move || {
471            if let Some(this) = weak.upgrade() {
472                this.flyout_state_changed.remove(token);
473            }
474        })
475    }
476
477    /// Invokes the primary button part (internal in the reference).
478    pub(crate) fn invoke_primary(&self) {
479        self.on_click_primary(None);
480    }
481
482    /// Whether the flyout of the split button is open (internal in the
483    /// reference).
484    pub(crate) fn is_flyout_open(&self) -> bool {
485        self.is_flyout_open.get()
486    }
487
488    /// Opens the flyout on behalf of the automation peer (internal in the
489    /// reference).
490    pub(crate) fn open_flyout_for_automation(&self) {
491        self.open_flyout();
492    }
493
494    /// Closes the flyout on behalf of the automation peer (internal in the
495    /// reference).
496    pub(crate) fn close_flyout_for_automation(&self) {
497        self.close_flyout();
498    }
499
500    /// A key gesture associated with this control.
501    pub fn hot_key(&self) -> Option<KeyGesture> {
502        self.get_value(Self::hot_key_property())
503    }
504
505    pub fn set_hot_key(&self, value: Option<KeyGesture>) {
506        self.set_value(Self::hot_key_property(), value)
507    }
508
509    fn primary_button(&self) -> Option<Ref<Button>> {
510        self.primary_button.borrow().as_ref().map(|(button, _)| button.clone())
511    }
512
513    fn subscribe_can_execute_changed(&self, command: &Rc<dyn ICommand>) {
514        // A weak subscription: it ends with this control, so a command
515        // that outlives the control keeps no handler of it.
516        let subscription = WeakEvents::subscribe_command_can_execute_changed(command, &self.to_ref(), |this| {
517            this.can_execute_changed_handler()
518        });
519        let old = self.can_execute_change_subscription.replace(Some(subscription));
520        if let Some(old) = old {
521            old.dispose();
522        }
523    }
524
525    fn unsubscribe_can_execute_changed(&self) {
526        let subscription = self.can_execute_change_subscription.take();
527        if let Some(subscription) = subscription {
528            subscription.dispose();
529        }
530    }
531
532    /// Called when the "can execute changed" event of the command fires.
533    fn can_execute_changed_handler(&self) {
534        let (command, parameter) = (self.command(), self.command_parameter());
535        self.can_execute_changed(command.as_ref(), parameter.as_ref());
536    }
537
538    fn can_execute_changed(&self, command: Option<&Rc<dyn ICommand>>, parameter: Option<&BoxedValue>) {
539        if !self.is_attached_to_logical_tree() {
540            return;
541        }
542
543        let can_execute = command.is_none_or(|command| command.can_execute(parameter));
544
545        if can_execute != self.command_can_execute.get() {
546            self.command_can_execute.set(can_execute);
547            self.update_is_effectively_enabled();
548        }
549    }
550
551    /// Updates the visual state of the control by applying the latest
552    /// pseudoclasses.
553    pub fn update_pseudo_classes(&self) {
554        self.pseudo_classes().set(PC_FLYOUT_OPEN, self.is_flyout_open.get());
555        self.pseudo_classes().set(PC_PRESSED, self.is_keyboard_pressed.get());
556        self.pseudo_classes().set(PC_CHECKED, self.internal_is_checked());
557    }
558
559    /// Opens the secondary button's flyout.
560    pub fn open_flyout(&self) {
561        if let Some(flyout) = self.flyout() {
562            flyout.show_at(self);
563        }
564    }
565
566    /// Closes the secondary button's flyout.
567    pub fn close_flyout(&self) {
568        if let Some(flyout) = self.flyout() {
569            flyout.hide();
570        }
571    }
572
573    /// Registers all flyout events.
574    fn register_flyout_events(&self, flyout: Option<&Ref<FlyoutBase>>) {
575        if let Some(flyout) = flyout {
576            let weak = self.to_ref().downgrade();
577            let weak_flyout = flyout.downgrade();
578            let opened = flyout.opened({
579                let (weak, weak_flyout) = (weak.clone(), weak_flyout.clone());
580                move || {
581                    if let (Some(this), Some(flyout)) = (weak.upgrade(), weak_flyout.upgrade()) {
582                        this.flyout_opened(&flyout);
583                    }
584                }
585            });
586            let closed = flyout.closed({
587                let weak = weak.clone();
588                move || {
589                    if let (Some(this), Some(flyout)) = (weak.upgrade(), weak_flyout.upgrade()) {
590                        this.flyout_closed(&flyout);
591                    }
592                }
593            });
594            *self.flyout_events.borrow_mut() = Some((opened, closed));
595
596            let subscription =
597                flyout.get_property_changed_observable(Popup::placement_property().as_property()).subscribe(move |_| {
598                    if let Some(this) = weak.upgrade() {
599                        this.flyout_placement_property_changed();
600                    }
601                });
602            *self.flyout_property_changed_disposable.borrow_mut() = Some(subscription);
603        }
604    }
605
606    /// Explicitly unregisters all flyout events.
607    fn unregister_flyout_events(&self, flyout: Option<&Ref<FlyoutBase>>) {
608        if flyout.is_some() {
609            let flyout_events = self.flyout_events.take();
610            if let Some((opened, closed)) = flyout_events {
611                opened.dispose();
612                closed.dispose();
613            }
614
615            let subscription = self.flyout_property_changed_disposable.take();
616            if let Some(subscription) = subscription {
617                subscription.dispose();
618            }
619        }
620    }
621
622    /// Explicitly unregisters all events related to the two buttons of the
623    /// template.
624    fn unregister_events(&self) {
625        let primary_button = self.primary_button.borrow_mut().take();
626        if let Some((primary_button, click)) = primary_button {
627            primary_button.remove_handler(Button::click_event(), click);
628        }
629
630        let secondary_button = self.secondary_button.borrow_mut().take();
631        if let Some((secondary_button, click, pressed)) = secondary_button {
632            secondary_button.remove_handler(Button::click_event(), click);
633            secondary_button.remove_handler(InputElement::pointer_pressed_event(), pressed);
634        }
635    }
636
637
638    /// Called when the placement of the flyout changes.
639    fn flyout_placement_property_changed(&self) {
640        self.update_pseudo_classes();
641    }
642
643    /// Whether `flyout` is currently shown at this split button.
644    fn is_flyout_target(&self, flyout: &FlyoutBase) -> bool {
645        let this: Ref<crate::Control> = self.to_ref().upcast();
646        flyout.target() == Some(this)
647    }
648
649    /// Event handler for when the split button's flyout is opened.
650    fn flyout_opened(&self, flyout: &FlyoutBase) {
651        // It is possible to share flyouts among multiple controls including
652        // split buttons. This can cause a problem here since all controls
653        // that share a flyout receive the same Opened/Closed events at the
654        // same time. For split buttons that means they all would be
655        // updating their pseudoclasses accordingly. In other words, all
656        // split buttons with a shared flyout would have the backgrounds
657        // changed together. To fix this, only continue here if the flyout
658        // target matches this split button instance.
659        if self.is_flyout_target(flyout) {
660            self.is_flyout_open.set(true);
661            self.update_pseudo_classes();
662            for (_, handler) in self.flyout_state_changed.snapshot().iter() {
663                handler();
664            }
665
666            self.on_flyout_opened();
667        }
668    }
669
670    /// Event handler for when the split button's flyout is closed.
671    fn flyout_closed(&self, flyout: &FlyoutBase) {
672        // See comments in `flyout_opened`
673        if self.is_flyout_target(flyout) {
674            self.is_flyout_open.set(false);
675            self.update_pseudo_classes();
676            for (_, handler) in self.flyout_state_changed.snapshot().iter() {
677                handler();
678            }
679
680            self.on_flyout_closed();
681        }
682    }
683
684    /// Event handler for when the internal primary button part is clicked.
685    fn primary_button_click(&self, e: &RoutedEventArgs) {
686        // Handle the internal button click, so it won't bubble outside
687        // together with the click event of the split button.
688        e.set_handled(true);
689        self.on_click_primary(Some(e));
690    }
691
692    /// Event handler for when the internal secondary button part is
693    /// clicked.
694    fn secondary_button_click(&self, e: &RoutedEventArgs) {
695        // Handle the internal button click, so it won't bubble outside.
696        e.set_handled(true);
697        self.on_click_secondary(Some(e));
698    }
699
700    /// Event handler for when the internal secondary button part is
701    /// pressed.
702    fn secondary_button_preview_pointer_pressed(&self, e: &PointerPressedEventArgs) {
703        let secondary_button = self.secondary_button.borrow().as_ref().map(|(button, _, _)| button.clone());
704        let Some(secondary_button) = secondary_button else { return };
705
706        if self.is_flyout_open.get()
707            && secondary_button.is_effectively_enabled()
708            && e.get_current_point(Some(&secondary_button)).properties.is_left_button_pressed
709        {
710            // When a flyout is open with pass-through of the overlay
711            // dismiss event enabled and the secondary button is pressed,
712            // close the flyout.
713            e.set_handled(true);
714            self.on_click_secondary(Some(e));
715        }
716    }
717}