Skip to main content

ferroui_controls/flyouts/
popup_flyout_base.rs

1use super::{FlyoutBase, FlyoutBaseImpl, FlyoutShowMode};
2use crate::diagnostics::IPopupHostProvider;
3use crate::platform::{FeedbackAction, PlatformFeedbackExtensions};
4use crate::primitives::popup_positioning::{
5    CustomPopupPlacementCallback, PopupAnchor, PopupGravity, PopupPositionerConstraintAdjustment,
6};
7use crate::primitives::{CustomPopupPlacementCallbackValue, IPopupHost, Popup};
8use crate::{Application, Control, PlacementMode};
9use ferroui_base::controls::Classes;
10use ferroui_base::input::raw::{IRawInputEventArgs, RawPointerEventArgs, RawPointerEventType};
11use ferroui_base::input::{
12    ContextRequestedEventArgs, InputElement, InputManager, KeyEventArgs, KeyboardNavigationHandler,
13    NavigationDirection,
14};
15use ferroui_base::interactivity::{Interactive, RoutedEventArgs, RoutedEventHandlerToken};
16use ferroui_base::layout::{LayoutHelper, Layoutable};
17use ferroui_base::logging::{LogEventLevel, Logger};
18use ferroui_base::reactive::{AnonymousObserver, Disposable, IDisposable};
19use ferroui_base::utilities::{CancelEventArgs, HandlerList};
20use ferroui_base::{
21    ferro_class, ferro_property, FerroObjectImpl, FerroProperty, FerroPropertyChangedEventArgs,
22    Nullable, PixelRect, Rect, Ref, Size, StyledProperty, Thickness, VisualTreeAttachmentEventArgs, WeakRef,
23};
24use std::cell::{Cell, OnceCell, RefCell};
25use std::rc::Rc;
26
27/// The handlers attached to the target of an open flyout: the subscription
28/// to its detachment from the visual tree and the key up handler.
29type TargetHandlers = (Rc<dyn IDisposable>, RoutedEventHandlerToken);
30
31/// The base class of flyouts that are shown in a [`Popup`].
32///
33/// This class is abstract: only its subclasses can be created.
34#[repr(C)]
35pub struct PopupFlyoutBase {
36    base: FlyoutBase,
37    popup_lazy: OnceCell<Ref<Popup>>,
38    enlarged_popup_rect: Cell<Option<Rect>>,
39    enlarge_popup_rect_screen_pixel_rect: Cell<Option<PixelRect>>,
40    transient_disposable: RefCell<Option<Rc<dyn IDisposable>>>,
41    popup_host_changed_handler: HandlerList<dyn Fn(Option<&Rc<dyn IPopupHost>>)>,
42    is_open: Cell<bool>,
43    ignore_is_open_changed: Cell<bool>,
44    /// The flyout does not own its placement targets: an owner (a button)
45    /// holds the flyout, so the reference back is weak.
46    last_placement_target: RefCell<Option<WeakRef<Control>>>,
47    target_handlers: RefCell<Option<TargetHandlers>>,
48    closing: HandlerList<dyn Fn(&CancelEventArgs)>,
49    opening: HandlerList<dyn Fn(&CancelEventArgs)>,
50}
51
52ferro_class! {
53    PopupFlyoutBase: FlyoutBase, virtuals PopupFlyoutBaseImpl: FlyoutBaseImpl {
54        /// Hides the flyout; `can_cancel` tells whether the `Closing` event
55        /// is raised and may cancel. Returns true if the action was handled.
56        fn hide_core(this, can_cancel: bool) -> bool;
57        /// Shows the flyout at the given control, at the pointer location
58        /// when `show_at_pointer`. Returns true if the action was handled.
59        fn show_at_core(this, placement_target: &Control, show_at_pointer: bool) -> bool;
60        /// Raises the `Opening` event.
61        fn on_opening(this, args: &CancelEventArgs);
62        /// Raises the `Closing` event.
63        fn on_closing(this, args: &CancelEventArgs);
64        /// Used to create the content the flyout displays.
65        fn create_presenter(this) -> Ref<Control>;
66    }
67}
68
69ferroui_base::ferro_impl_classes!(PopupFlyoutBase: FerroObjectImpl);
70
71impl FlyoutBaseImpl for PopupFlyoutBase {
72    /// Shows the flyout at the given control. Sealed in the reference:
73    /// derived classes override `show_at_core`.
74    fn show_at(this: &Self, placement_target: &Control) {
75        this.show_at_core(placement_target, false);
76    }
77
78    /// Hides the flyout. Sealed in the reference: derived classes override
79    /// `hide_core`.
80    fn hide(this: &Self) {
81        this.hide_core(true);
82    }
83}
84
85impl PopupFlyoutBaseImpl for PopupFlyoutBase {
86    fn hide_core(this: &Self, can_cancel: bool) -> bool {
87        if !this.is_open.get() {
88            return false;
89        }
90
91        if can_cancel && this.cancel_closing() {
92            return false;
93        }
94
95        this.is_open.set(false);
96        {
97            let _ignore = this.begin_ignoring_is_open();
98            this.set_current_value(FlyoutBase::is_open_property(), false);
99        }
100        let popup = this.popup();
101        popup.set_is_open(false);
102
103        popup.set_placement_target(None);
104        popup.set_popup_parent(None);
105
106        // Ensure this isn't active
107        let transient_disposable = this.transient_disposable.take();
108        if let Some(transient_disposable) = transient_disposable {
109            transient_disposable.dispose();
110        }
111        this.enlarged_popup_rect.set(None);
112        this.enlarge_popup_rect_screen_pixel_rect.set(None);
113
114        let target_handlers = this.target_handlers.take();
115        if let (Some(target), Some((detached, key_up))) = (this.target(), target_handlers) {
116            detached.dispose();
117            target.remove_handler(InputElement::key_up_event(), key_up);
118        }
119
120        this.on_closed();
121
122        this.set_target(None);
123
124        true
125    }
126
127    fn show_at_core(this: &Self, placement_target: &Control, show_at_pointer: bool) -> bool {
128        let placement_target = placement_target.to_ref();
129
130        *this.last_placement_target.borrow_mut() = Some(placement_target.downgrade());
131
132        if this.is_open.get() {
133            if this.target().as_ref() == Some(&placement_target) {
134                return false;
135            } else {
136                // Close before opening a new one
137                let _ = this.hide_core(false);
138            }
139        }
140
141        let popup = this.popup();
142        this.set_target(&placement_target);
143        popup.set_placement_target(&placement_target);
144        popup.set_popup_parent(Some(&placement_target));
145
146        if popup.child().is_none() {
147            popup.set_child(this.create_presenter());
148        }
149
150        popup.set_overlay_dismiss_event_pass_through(this.overlay_dismiss_event_pass_through());
151        popup.set_overlay_input_pass_through_element(this.overlay_input_pass_through_element());
152
153        if this.cancel_opening() {
154            return false;
155        }
156
157        this.position_popup(show_at_pointer);
158        this.is_open.set(true);
159        {
160            let _ignore = this.begin_ignoring_is_open();
161            this.set_current_value(FlyoutBase::is_open_property(), true);
162        }
163        popup.set_is_open(true);
164        this.on_opened();
165
166        let weak = this.to_ref().downgrade();
167        let detached = placement_target.detached_from_visual_tree({
168            let weak = weak.clone();
169            move |e| {
170                if let Some(this) = weak.upgrade() {
171                    this.placement_target_detached_from_visual_tree(e);
172                }
173            }
174        });
175        let key_up = placement_target.add_handler(InputElement::key_up_event(), move |_, e: &KeyEventArgs| {
176            if let Some(this) = weak.upgrade() {
177                this.on_placement_target_or_popup_key_up(e);
178            }
179        });
180        *this.target_handlers.borrow_mut() = Some((detached, key_up));
181
182        match this.show_mode() {
183            FlyoutShowMode::Standard => {
184                // Try and focus content inside Flyout
185                if let Some(child) = popup.child() {
186                    if child.focusable() {
187                        child.focus();
188                    } else {
189                        let next_focus =
190                            KeyboardNavigationHandler::get_next(&child.upcast(), NavigationDirection::Next);
191                        if let Some(next_focus) = next_focus {
192                            next_focus.focus();
193                        }
194                    }
195                }
196            }
197            FlyoutShowMode::TransientWithDismissOnPointerMoveAway => {
198                if let Some(input_manager) = InputManager::instance() {
199                    let weak = this.to_ref().downgrade();
200                    let subscription = input_manager.process().subscribe(Rc::new(AnonymousObserver::new(
201                        move |e: Rc<dyn IRawInputEventArgs>| {
202                            if let Some(this) = weak.upgrade() {
203                                this.handle_transient_dismiss(&*e);
204                            }
205                        },
206                    )));
207                    *this.transient_disposable.borrow_mut() = Some(subscription);
208                }
209            }
210            FlyoutShowMode::Transient => {}
211        }
212
213        true
214    }
215
216    fn on_opening(this: &Self, args: &CancelEventArgs) {
217        for (_, handler) in this.opening.snapshot().iter() {
218            handler(args);
219        }
220    }
221
222    fn on_closing(this: &Self, args: &CancelEventArgs) {
223        for (_, handler) in this.closing.snapshot().iter() {
224            handler(args);
225        }
226    }
227
228    fn create_presenter(_this: &Self) -> Ref<Control> {
229        panic!("PopupFlyoutBase is abstract.")
230    }
231}
232
233/// Resets the flag that makes the flyout ignore changes of `IsOpen`.
234struct IgnoreIsOpenScope<'a> {
235    owner: &'a PopupFlyoutBase,
236}
237
238impl Drop for IgnoreIsOpenScope<'_> {
239    fn drop(&mut self) {
240        self.owner.ignore_is_open_changed.set(false);
241    }
242}
243
244ferroui_base::ferro_properties! { impl PopupFlyoutBase {
245    ferro_property!(
246        /// Defines the `Placement` property.
247        pub fn placement_property() -> StyledProperty<PlacementMode> {
248            Popup::placement_property().add_owner::<PopupFlyoutBase>()
249        }
250    );
251
252    ferro_property!(
253        /// Defines the `HorizontalOffset` property.
254        pub fn horizontal_offset_property() -> StyledProperty<f64> {
255            Popup::horizontal_offset_property().add_owner::<PopupFlyoutBase>()
256        }
257    );
258
259    ferro_property!(
260        /// Defines the `VerticalOffset` property.
261        pub fn vertical_offset_property() -> StyledProperty<f64> {
262            Popup::vertical_offset_property().add_owner::<PopupFlyoutBase>()
263        }
264    );
265
266    ferro_property!(
267        /// Defines the `PlacementAnchor` property.
268        pub fn placement_anchor_property() -> StyledProperty<PopupAnchor> {
269            Popup::placement_anchor_property().add_owner::<PopupFlyoutBase>()
270        }
271    );
272
273    ferro_property!(
274        /// Defines the `PlacementGravity` property.
275        pub fn placement_gravity_property() -> StyledProperty<PopupGravity> {
276            Popup::placement_gravity_property().add_owner::<PopupFlyoutBase>()
277        }
278    );
279
280    ferro_property!(
281        /// Defines the `CustomPopupPlacementCallback` property.
282        pub fn custom_popup_placement_callback_property() -> StyledProperty<Option<CustomPopupPlacementCallbackValue>> {
283            Popup::custom_popup_placement_callback_property().add_owner::<PopupFlyoutBase>()
284        }
285    );
286
287    ferro_property!(
288        /// Defines the `ShowMode` property.
289        pub fn show_mode_property() -> StyledProperty<FlyoutShowMode> {
290            FerroProperty::register::<PopupFlyoutBase, _>("ShowMode", FlyoutShowMode::Standard)
291        }
292    );
293
294    ferro_property!(
295        /// Defines the `OverlayDismissEventPassThrough` property.
296        pub fn overlay_dismiss_event_pass_through_property() -> StyledProperty<bool> {
297            Popup::overlay_dismiss_event_pass_through_property().add_owner::<PopupFlyoutBase>()
298        }
299    );
300
301    ferro_property!(
302        /// Defines the `OverlayInputPassThroughElement` property.
303        pub fn overlay_input_pass_through_element_property() -> StyledProperty<Option<ferroui_base::ElementRef<InputElement>>> {
304            Popup::overlay_input_pass_through_element_property().add_owner::<PopupFlyoutBase>()
305        }
306    );
307
308    ferro_property!(
309        /// Defines the `PlacementConstraintAdjustment` property.
310        pub fn placement_constraint_adjustment_property() -> StyledProperty<PopupPositionerConstraintAdjustment> {
311            Popup::placement_constraint_adjustment_property().add_owner::<PopupFlyoutBase>()
312        }
313    );
314} }
315
316impl PopupFlyoutBase {
317    fn static_constructor() {
318        FlyoutBase::is_open_property().changed().add_class_handler::<PopupFlyoutBase>(|x, e| x.is_open_changed(e));
319        Control::context_flyout_property()
320            .changed()
321            .add_class_handler::<Control>(Self::on_context_flyout_property_changed);
322    }
323
324    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
325    pub fn construct() -> Self {
326        Self {
327            base: FlyoutBase::construct(),
328            popup_lazy: OnceCell::new(),
329            enlarged_popup_rect: Cell::new(None),
330            enlarge_popup_rect_screen_pixel_rect: Cell::new(None),
331            transient_disposable: RefCell::new(None),
332            popup_host_changed_handler: HandlerList::new(),
333            is_open: Cell::new(false),
334            ignore_is_open_changed: Cell::new(false),
335            last_placement_target: RefCell::new(None),
336            target_handlers: RefCell::new(None),
337            closing: HandlerList::new(),
338            opening: HandlerList::new(),
339        }
340    }
341
342    /// The popup the flyout is shown in, created on first use. For derived
343    /// classes.
344    pub fn popup(&self) -> Ref<Popup> {
345        self.popup_lazy.get_or_init(|| self.create_popup()).clone()
346    }
347
348    /// The desired placement of the flyout in relation to its target.
349    pub fn placement(&self) -> PlacementMode {
350        self.get_value(Self::placement_property())
351    }
352
353    pub fn set_placement(&self, value: PlacementMode) {
354        self.set_value(Self::placement_property(), value)
355    }
356
357    /// Defines in what direction the flyout should open when the placement
358    /// is [`PlacementMode::AnchorAndGravity`].
359    pub fn placement_gravity(&self) -> PopupGravity {
360        self.get_value(Self::placement_gravity_property())
361    }
362
363    pub fn set_placement_gravity(&self, value: PopupGravity) {
364        self.set_value(Self::placement_gravity_property(), value)
365    }
366
367    /// The anchor point on the placement rect when the placement is
368    /// [`PlacementMode::AnchorAndGravity`].
369    pub fn placement_anchor(&self) -> PopupAnchor {
370        self.get_value(Self::placement_anchor_property())
371    }
372
373    pub fn set_placement_anchor(&self, value: PopupAnchor) {
374        self.set_value(Self::placement_anchor_property(), value)
375    }
376
377    /// The horizontal offset of the flyout in relation to its target.
378    pub fn horizontal_offset(&self) -> f64 {
379        self.get_value(Self::horizontal_offset_property())
380    }
381
382    pub fn set_horizontal_offset(&self, value: f64) {
383        self.set_value(Self::horizontal_offset_property(), value)
384    }
385
386    /// The vertical offset of the flyout in relation to its target.
387    pub fn vertical_offset(&self) -> f64 {
388        self.get_value(Self::vertical_offset_property())
389    }
390
391    pub fn set_vertical_offset(&self, value: f64) {
392        self.set_value(Self::vertical_offset_property(), value)
393    }
394
395    /// The callback that positions the flyout when the placement is
396    /// [`PlacementMode::Custom`].
397    pub fn custom_popup_placement_callback(&self) -> Option<CustomPopupPlacementCallback> {
398        self.get_value(Self::custom_popup_placement_callback_property()).map(|value| value.0)
399    }
400
401    pub fn set_custom_popup_placement_callback(&self, value: Option<CustomPopupPlacementCallback>) {
402        self.set_value(
403            Self::custom_popup_placement_callback_property(),
404            value.map(CustomPopupPlacementCallbackValue),
405        )
406    }
407
408    /// The desired show mode.
409    pub fn show_mode(&self) -> FlyoutShowMode {
410        self.get_value(Self::show_mode_property())
411    }
412
413    pub fn set_show_mode(&self, value: FlyoutShowMode) {
414        self.set_value(Self::show_mode_property(), value)
415    }
416
417    /// Whether the event that closes the flyout is passed through to the
418    /// parent window.
419    ///
420    /// Clicks outside the popup cause the popup to close. When this is
421    /// false, these clicks will be handled by the popup and not be
422    /// registered by the parent window. When true, the events will be passed
423    /// through to the parent window.
424    pub fn overlay_dismiss_event_pass_through(&self) -> bool {
425        self.get_value(Self::overlay_dismiss_event_pass_through_property())
426    }
427
428    pub fn set_overlay_dismiss_event_pass_through(&self, value: bool) {
429        self.set_value(Self::overlay_dismiss_event_pass_through_property(), value)
430    }
431
432    /// An element that should receive pointer input events even when
433    /// underneath the flyout's overlay.
434    pub fn overlay_input_pass_through_element(&self) -> Option<Ref<InputElement>> {
435        ferroui_base::ElementRef::resolve(&self.get_value(Self::overlay_input_pass_through_element_property()))
436    }
437
438    pub fn set_overlay_input_pass_through_element(&self, value: impl Into<Nullable<InputElement>>) {
439        self.set_value(
440            Self::overlay_input_pass_through_element_property(),
441            ferroui_base::ElementRef::from_nullable(value.into().0),
442        )
443    }
444
445    /// Describes how the flyout position will be adjusted if the unadjusted
446    /// position would result in the flyout being partly constrained.
447    pub fn placement_constraint_adjustment(&self) -> PopupPositionerConstraintAdjustment {
448        self.get_value(Self::placement_constraint_adjustment_property())
449    }
450
451    pub fn set_placement_constraint_adjustment(&self, value: PopupPositionerConstraintAdjustment) {
452        self.set_value(Self::placement_constraint_adjustment_property(), value)
453    }
454
455    /// The flyout as a popup host provider (the diagnostics contract it
456    /// implements in the reference).
457    pub fn to_popup_host_provider(&self) -> Rc<dyn IPopupHostProvider> {
458        Rc::new(PopupFlyoutHostProvider(self.to_ref()))
459    }
460
461    fn subscribe<F: ?Sized + 'static>(
462        &self,
463        select: fn(&PopupFlyoutBase) -> &HandlerList<F>,
464        handler: Rc<F>,
465    ) -> Rc<dyn IDisposable> {
466        let token = select(self).add(handler);
467        let weak = self.to_ref().downgrade();
468        Disposable::create(move || {
469            if let Some(this) = weak.upgrade() {
470                select(&this).remove(token);
471            }
472        })
473    }
474
475    /// Raised before the flyout closes; the close can be cancelled.
476    /// Disposing the returned handle unsubscribes.
477    pub fn closing(&self, handler: impl Fn(&CancelEventArgs) + 'static) -> Rc<dyn IDisposable> {
478        self.subscribe::<dyn Fn(&CancelEventArgs)>(|flyout| &flyout.closing, Rc::new(handler))
479    }
480
481    /// Raised before the flyout opens; the opening can be cancelled.
482    /// Disposing the returned handle unsubscribes.
483    pub fn opening(&self, handler: impl Fn(&CancelEventArgs) + 'static) -> Rc<dyn IDisposable> {
484        self.subscribe::<dyn Fn(&CancelEventArgs)>(|flyout| &flyout.opening, Rc::new(handler))
485    }
486
487    /// Pre-registers a control as the default placement target for this
488    /// flyout. Used by owning controls (e.g. the button) so that setting
489    /// `IsOpen` to `true` works on first use.
490    pub(crate) fn set_default_placement_target(&self, target: Option<&Control>) {
491        *self.last_placement_target.borrow_mut() = target.map(|target| target.to_ref().downgrade());
492    }
493
494    /// Shows the flyout for the given control at the current pointer
495    /// location when `show_at_pointer`, as in a context flyout.
496    pub fn show_at_with(&self, placement_target: &Control, show_at_pointer: bool) {
497        self.show_at_core(placement_target, show_at_pointer);
498    }
499
500    fn placement_target_detached_from_visual_tree(&self, _e: &VisualTreeAttachmentEventArgs) {
501        let _ = self.hide_core(false);
502        *self.last_placement_target.borrow_mut() = None;
503    }
504
505    fn handle_transient_dismiss(&self, args: &dyn IRawInputEventArgs) {
506        let Some(p_args) = args.downcast_ref::<RawPointerEventArgs>() else { return };
507        if p_args.type_() != RawPointerEventType::Move {
508            return;
509        }
510
511        // In the show mode that dismisses on pointer move away, the flyout
512        // is kept shown as long as the pointer is within a certain px
513        // distance from the flyout itself: 100px, which seems about right.
514        // The enlarged popup rect is the flyout bounds enlarged 100px.
515        // For windowed popups, the enlarged rect is in screen coordinates,
516        // for overlay popups, it is in overlay layer coordinates.
517        let host = self.popup().host();
518        let popup_root = host.as_ref().and_then(|host| host.as_popup_root());
519        let overlay_popup_host = host.as_ref().and_then(|host| host.as_overlay_popup_host());
520
521        if self.enlarged_popup_rect.get().is_none() && self.enlarge_popup_rect_screen_pixel_rect.get().is_none() {
522            // Only do this once when the flyout opens & cache the result
523            if let Some(root) = popup_root {
524                // Get the popup root bounds and convert to screen coordinates
525                let tmp = root.bounds().inflate(100.0);
526                self.enlarge_popup_rect_screen_pixel_rect.set(Some(PixelRect::from_points(
527                    root.point_to_screen(tmp.top_left()),
528                    root.point_to_screen(tmp.bottom_right()),
529                )));
530            } else if let Some(host) = overlay_popup_host {
531                // Overlay popups are in overlay layer coordinates, just use that
532                self.enlarged_popup_rect.set(Some(host.bounds().inflate(100.0)));
533            }
534
535            return;
536        }
537
538        if popup_root.is_some() {
539            // As long as the pointer stays within the enlarged popup rect
540            // the flyout stays open. If it leaves, close it.
541            // Despite working in screen coordinates, leaving the top-level
542            // window will not close this (as pointer events stop).
543            let event_root = p_args.root().root_element();
544            let pt = event_root.point_to_screen(p_args.position());
545            if self.enlarge_popup_rect_screen_pixel_rect.get().is_some_and(|rect| !rect.contains(pt)) {
546                self.hide_core(false);
547            }
548        } else if overlay_popup_host.is_some() {
549            // Same as above here, but just different coordinate space
550            // so we don't need to translate
551            if self.enlarged_popup_rect.get().is_some_and(|rect| !rect.contains(p_args.position())) {
552                self.hide_core(false);
553            }
554        }
555    }
556
557    fn create_popup(&self) -> Ref<Popup> {
558        let popup = Popup::new();
559        popup.set_window_manager_add_shadow_hint(false);
560        popup.set_is_light_dismiss_enabled(true);
561
562        // The flyout owns the popup and lives as long as it: the handlers
563        // are never removed.
564        let weak = self.to_ref().downgrade();
565        let _ = popup.opened({
566            let weak = weak.clone();
567            move || {
568                if let Some(this) = weak.upgrade() {
569                    this.on_popup_opened();
570                }
571            }
572        });
573        let _ = popup.closed({
574            let weak = weak.clone();
575            move || {
576                if let Some(this) = weak.upgrade() {
577                    this.on_popup_closed();
578                }
579            }
580        });
581        let _ = popup.closing({
582            let weak = weak.clone();
583            move |e| {
584                if let Some(this) = weak.upgrade() {
585                    this.on_popup_closing(e);
586                }
587            }
588        });
589        popup.add_handler(InputElement::key_up_event(), move |_, e: &KeyEventArgs| {
590            if let Some(this) = weak.upgrade() {
591                this.on_placement_target_or_popup_key_up(e);
592            }
593        });
594        popup
595    }
596
597    fn on_popup_opened(&self) {
598        self.is_open.set(true);
599        {
600            let _ignore = self.begin_ignoring_is_open();
601            self.set_current_value(FlyoutBase::is_open_property(), true);
602        }
603
604        let host = self.popup().host();
605        for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
606            handler(host.as_ref());
607        }
608    }
609
610    fn on_popup_closing(&self, e: &CancelEventArgs) {
611        if self.is_open.get() {
612            e.set_cancel(self.cancel_closing());
613        }
614    }
615
616    fn on_popup_closed(&self) {
617        self.hide_core(false);
618
619        for (_, handler) in self.popup_host_changed_handler.snapshot().iter() {
620            handler(None);
621        }
622    }
623
624    // This method is handling both popup logical tree and target logical tree.
625    fn on_placement_target_or_popup_key_up(&self, e: &KeyEventArgs) {
626        if e.handled() || !self.is_open.get() {
627            return;
628        }
629
630        let this: Ref<FlyoutBase> = self.to_ref().upcast();
631        if self.target().and_then(|target| target.context_flyout()) != Some(this) {
632            return;
633        }
634
635        let keymap = Application::current()
636            .and_then(|application| application.platform_settings())
637            .map(|settings| settings.hotkey_configuration());
638
639        if keymap.is_some_and(|keymap| keymap.open_context_menu.iter().any(|k| k.matches(Some(e)))) {
640            e.set_handled(self.hide_core(true));
641        }
642    }
643
644    fn is_open_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
645        if self.ignore_is_open_changed.get() {
646            return;
647        }
648
649        if e.get_new_value::<bool>() {
650            let last_placement_target = self.last_placement_target.borrow().as_ref().and_then(WeakRef::upgrade);
651            if let Some(last_placement_target) = last_placement_target {
652                if self.show_at_core(&last_placement_target, false) {
653                    return;
654                }
655            }
656
657            // No target, or opening was cancelled: revert so IsOpen stays honest
658            let _ignore = self.begin_ignoring_is_open();
659            self.set_current_value(FlyoutBase::is_open_property(), false);
660        } else if !self.hide_core(true) {
661            // Closing was cancelled: revert so IsOpen stays honest
662            let _ignore = self.begin_ignoring_is_open();
663            self.set_current_value(FlyoutBase::is_open_property(), true);
664        }
665    }
666
667    fn begin_ignoring_is_open(&self) -> IgnoreIsOpenScope<'_> {
668        self.ignore_is_open_changed.set(true);
669        IgnoreIsOpenScope { owner: self }
670    }
671
672    fn position_popup(&self, show_at_pointer: bool) {
673        let popup = self.popup();
674
675        // The child of the popup can't be missing here, it was set in
676        // `show_at_core`.
677        let child = popup.child().expect("the popup has a child");
678        if child.desired_size() == Size::default() {
679            // Popup may not have been shown yet. Measure content
680            let child: &Layoutable = &child;
681            LayoutHelper::measure_child(Some(child), Size::INFINITY, Thickness::default());
682        }
683
684        popup.set_vertical_offset(self.vertical_offset());
685        popup.set_horizontal_offset(self.horizontal_offset());
686        popup.set_placement_anchor(self.placement_anchor());
687        popup.set_placement_gravity(self.placement_gravity());
688        popup.set_custom_popup_placement_callback(self.custom_popup_placement_callback());
689        if show_at_pointer {
690            popup.set_placement(PlacementMode::Pointer);
691        } else {
692            popup.set_placement(self.placement());
693            popup.set_placement_constraint_adjustment(self.placement_constraint_adjustment());
694        }
695    }
696
697    fn on_context_flyout_property_changed(c: &Control, args: &FerroPropertyChangedEventArgs<'_>) {
698        let (old_value, new_value) = args.get_old_and_new_value::<Option<Ref<FlyoutBase>>>();
699
700        if old_value.is_some() {
701            if let Some((context_requested, context_canceled)) = c.context_flyout_handlers().take() {
702                c.remove_handler(InputElement::context_requested_event(), context_requested);
703                c.remove_handler(InputElement::context_canceled_event(), context_canceled);
704            }
705        }
706        if new_value.is_some() {
707            let context_requested =
708                c.add_handler(InputElement::context_requested_event(), Self::on_control_context_requested);
709            let context_canceled =
710                c.add_handler(InputElement::context_canceled_event(), Self::on_control_context_canceled);
711            c.context_flyout_handlers().set(Some((context_requested, context_canceled)));
712        }
713    }
714
715    fn on_control_context_canceled(sender: &Interactive, e: &RoutedEventArgs) {
716        if e.handled() {
717            return;
718        }
719        let Some(control) = sender.downcast_ref::<Control>() else { return };
720        let Some(flyout) = control.context_flyout() else { return };
721
722        if flyout.is_open() {
723            flyout.hide();
724        }
725    }
726
727    fn on_control_context_requested(sender: &Interactive, e: &ContextRequestedEventArgs) {
728        if e.handled() {
729            return;
730        }
731        let Some(control) = sender.downcast_ref::<Control>() else { return };
732        let Some(flyout) = control.context_flyout() else { return };
733
734        if control.context_menu().is_some() {
735            if let Some(logger) = Logger::try_get(LogEventLevel::Verbose, "FlyoutBase") {
736                let source: &dyn std::any::Any = control;
737                logger.log(Some(source), "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
738            }
739            return;
740        }
741
742        if let Some(popup_flyout) = flyout.cast::<PopupFlyoutBase>() {
743            // Absolute popup positioning is not supported yet, so the point
744            // is ignored at this moment.
745            let triggered_by_pointer_input = e.try_get_position(None).is_some();
746            e.set_handled(popup_flyout.show_at_core(control, triggered_by_pointer_input));
747
748            if e.handled() && e.is_holding() {
749                control.perform_feedback(FeedbackAction::hold());
750            }
751        } else {
752            flyout.show_at(control);
753            e.set_handled(true);
754
755            if e.is_holding() {
756                control.perform_feedback(FeedbackAction::hold());
757            }
758        }
759    }
760
761    fn cancel_closing(&self) -> bool {
762        let event_args = CancelEventArgs::new();
763        self.on_closing(&event_args);
764        event_args.cancel()
765    }
766
767    fn cancel_opening(&self) -> bool {
768        let event_args = CancelEventArgs::new();
769        self.on_opening(&event_args);
770        event_args.cancel()
771    }
772
773    /// Makes the classes of `presenter` the given ones, leaving its
774    /// pseudoclasses alone.
775    pub(crate) fn set_presenter_classes(presenter: Option<&Control>, classes: &Classes) {
776        let Some(presenter) = presenter else { return };
777
778        // Remove any classes no longer in use, ignoring pseudo classes
779        let presenter_classes = presenter.classes();
780        for i in (0..presenter_classes.count()).rev() {
781            let name = presenter_classes.get(i);
782            if !classes.contains(&name) && !name.contains(':') {
783                presenter_classes.remove_at(i);
784            }
785        }
786
787        // Add new classes
788        presenter_classes.add_range(classes.snapshot().iter());
789    }
790}
791
792/// The popup host provider contract of a flyout.
793struct PopupFlyoutHostProvider(Ref<PopupFlyoutBase>);
794
795impl IPopupHostProvider for PopupFlyoutHostProvider {
796    fn popup_host(&self) -> Option<Rc<dyn IPopupHost>> {
797        self.0.popup().host()
798    }
799
800    fn popup_host_changed(&self, handler: Rc<dyn Fn(Option<&Rc<dyn IPopupHost>>)>) -> Rc<dyn IDisposable> {
801        self.0.subscribe::<dyn Fn(Option<&Rc<dyn IPopupHost>>)>(|flyout| &flyout.popup_host_changed_handler, handler)
802    }
803}