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
34type SecondaryButton = (Ref<Button>, RoutedEventHandlerToken, RoutedEventHandlerToken);
37
38#[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 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 fn internal_is_checked(this) -> bool;
66 fn on_click_primary(this, e: Option<&RoutedEventArgs>);
69 fn on_click_secondary(this, e: Option<&RoutedEventArgs>);
72 fn on_flyout_opened(this);
74 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 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 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 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 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.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 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 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 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 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 }
304
305 fn on_flyout_closed(_this: &Self) {
306 }
308}
309
310struct 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 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 pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[PC_FLYOUT_OPEN, PC_PRESSED]);
364
365 ferro_routed_event!(
366 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 pub fn command_property() -> StyledProperty<Option<Rc<dyn ICommand>>> {
377 Button::command_property().add_owner::<SplitButton>()
378 }
379 );
380
381 ferro_property!(
382 pub fn command_parameter_property() -> StyledProperty<Option<BoxedValue>> {
384 Button::command_parameter_property().add_owner::<SplitButton>()
385 }
386 );
387
388 ferro_property!(
389 pub fn flyout_property() -> StyledProperty<Option<Ref<FlyoutBase>>> {
391 Button::flyout_property().add_owner::<SplitButton>()
392 }
393 );
394
395 ferro_property!(
396 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 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 pub fn new() -> Ref<Self> {
429 instantiate(Self::construct())
430 }
431
432 pub fn click(&self, handler: impl Fn(&Interactive, &RoutedEventArgs) + 'static) -> RoutedEventHandlerToken {
434 self.add_handler(Self::click_event(), handler)
435 }
436
437 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 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 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 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 pub(crate) fn invoke_primary(&self) {
479 self.on_click_primary(None);
480 }
481
482 pub(crate) fn is_flyout_open(&self) -> bool {
485 self.is_flyout_open.get()
486 }
487
488 pub(crate) fn open_flyout_for_automation(&self) {
491 self.open_flyout();
492 }
493
494 pub(crate) fn close_flyout_for_automation(&self) {
497 self.close_flyout();
498 }
499
500 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 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 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 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 pub fn open_flyout(&self) {
561 if let Some(flyout) = self.flyout() {
562 flyout.show_at(self);
563 }
564 }
565
566 pub fn close_flyout(&self) {
568 if let Some(flyout) = self.flyout() {
569 flyout.hide();
570 }
571 }
572
573 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 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 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 fn flyout_placement_property_changed(&self) {
640 self.update_pseudo_classes();
641 }
642
643 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 fn flyout_opened(&self, flyout: &FlyoutBase) {
651 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 fn flyout_closed(&self, flyout: &FlyoutBase) {
672 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 fn primary_button_click(&self, e: &RoutedEventArgs) {
686 e.set_handled(true);
689 self.on_click_primary(Some(e));
690 }
691
692 fn secondary_button_click(&self, e: &RoutedEventArgs) {
695 e.set_handled(true);
697 self.on_click_secondary(Some(e));
698 }
699
700 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 e.set_handled(true);
714 self.on_click_secondary(Some(e));
715 }
716 }
717}