Skip to main content

ferroui_controls/primitives/
overlay_popup_host.rs

1use super::i_popup_host::subscribe_template_applied;
2use super::popup_positioning::{
3    IManagedPopupPositionerPopup, IPopupPositioner, ManagedPopupPositioner, ManagedPopupPositionerScreenInfo,
4    PopupPositionRequest,
5};
6use super::{IPopupHost, PopupOverlayLayer, PopupRoot, TemplateAppliedEventArgs, TemplatedControlImpl};
7use crate::presenters::ContentPresenter;
8use crate::{Canvas, ContentControl, ContentControlImpl, Control, ControlImpl, TopLevel};
9use ferroui_base::input::{
10    IFocusScope, IKeyboardNavigationHandler, InputElementImpl, KeyboardNavigation, KeyboardNavigationMode,
11};
12use ferroui_base::interactivity::{Interactive, InteractiveImpl};
13use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
14use ferroui_base::media::{MediaContext, Transform};
15use ferroui_base::reactive::IDisposable;
16use ferroui_base::{
17    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroLocator, FerroObjectImpl, FerroObjectImplExt,
18    IFerroDependencyResolver, LocatorExtensions, Nullable, Point, Rect, Ref, Size, StyledElementImpl, StyledProperty,
19    Thickness, Visual, VisualImpl, WeakRef,
20};
21use std::cell::{Cell, OnceCell, RefCell};
22use std::rc::Rc;
23
24/// The popup host that shows a popup inside its top-level, on the popup
25/// overlay layer.
26#[repr(C)]
27pub struct OverlayPopupHost {
28    base: ContentControl,
29    overlay_layer: WeakRef<PopupOverlayLayer>,
30    positioner: OnceCell<Rc<dyn IPopupPositioner>>,
31    keyboard_navigation_handler: RefCell<Option<Rc<dyn IKeyboardNavigationHandler>>>,
32    last_requested_position: Cell<Point>,
33    popup_position_request: RefCell<Option<PopupPositionRequest>>,
34    popup_size: Cell<Size>,
35    child_margin: Cell<Thickness>,
36    needs_update: Cell<bool>,
37}
38
39ferro_class!(OverlayPopupHost: ContentControl);
40ferro_impl_classes!(
41    OverlayPopupHost: StyledElementImpl,
42    VisualImpl,
43    ControlImpl,
44    TemplatedControlImpl,
45    ContentControlImpl
46);
47
48impl IFocusScope for OverlayPopupHost {}
49
50impl FerroObjectImpl for OverlayPopupHost {
51    fn constructed(this: &Self) {
52        Self::parent_constructed(this);
53
54        let popup: Rc<dyn IManagedPopupPositionerPopup> =
55            Rc::new(OverlayPopupHostPositionerPopup(this.to_ref().downgrade()));
56        let positioner: Rc<dyn IPopupPositioner> = Rc::new(ManagedPopupPositioner::new(popup));
57        if this.positioner.set(positioner).is_err() {
58            unreachable!("the host is constructed once");
59        }
60
61        let keyboard_navigation_handler = FerroLocator::current().get_service::<dyn IKeyboardNavigationHandler>();
62        if let Some(handler) = &keyboard_navigation_handler {
63            handler.set_owner(&this.to_ref().upcast());
64        }
65        *this.keyboard_navigation_handler.borrow_mut() = keyboard_navigation_handler;
66    }
67}
68
69impl InteractiveImpl for OverlayPopupHost {
70    fn interactive_parent(this: &Self) -> Option<Ref<Interactive>> {
71        this.parent().and_then(|parent| parent.cast::<Interactive>())
72    }
73}
74
75impl InputElementImpl for OverlayPopupHost {
76    fn is_focus_scope(_this: &Self) -> bool {
77        true
78    }
79}
80
81impl LayoutableImpl for OverlayPopupHost {
82    fn arrange_override(this: &Self, final_size: Size) -> Size {
83        let mut reposition = false;
84
85        let new_margin = this
86            .presenter()
87            .and_then(|presenter| presenter.child())
88            .map(|child| child.margin())
89            .unwrap_or_default();
90        if new_margin != this.child_margin.get() {
91            this.child_margin.set(new_margin);
92            reposition = true;
93        }
94
95        if this.popup_size.get() != final_size {
96            this.popup_size.set(final_size);
97            reposition = true;
98        }
99
100        if reposition {
101            this.needs_update.set(true);
102            this.update_position();
103        }
104        Self::parent_arrange_override(this, final_size)
105    }
106}
107
108ferroui_base::ferro_properties! { impl OverlayPopupHost {
109    ferro_property!(
110        /// Defines the `Transform` property.
111        pub fn transform_property() -> StyledProperty<Option<Ref<Transform>>> {
112            PopupRoot::transform_property().add_owner::<OverlayPopupHost>()
113        }
114    );
115} }
116
117impl OverlayPopupHost {
118    fn static_constructor() {
119        KeyboardNavigation::tab_navigation_property()
120            .override_default_value::<OverlayPopupHost>(KeyboardNavigationMode::Cycle);
121    }
122
123    /// Creates a host that is shown on `overlay_layer` (internal upstream).
124    pub fn new(overlay_layer: &Ref<PopupOverlayLayer>) -> Ref<Self> {
125        instantiate(Self {
126            base: ContentControl::construct(),
127            overlay_layer: overlay_layer.downgrade(),
128            positioner: OnceCell::new(),
129            keyboard_navigation_handler: RefCell::new(None),
130            last_requested_position: Cell::new(Point::default()),
131            popup_position_request: RefCell::new(None),
132            popup_size: Cell::new(Size::default()),
133            child_margin: Cell::new(Thickness::default()),
134            needs_update: Cell::new(false),
135        })
136    }
137
138    /// The keyboard navigation handler of the host (for unit tests).
139    pub fn tests_keyboard_navigation_handler(&self) -> Option<Rc<dyn IKeyboardNavigationHandler>> {
140        self.keyboard_navigation_handler.borrow().clone()
141    }
142
143    /// Sets the control to display in the popup.
144    pub fn set_child(&self, control: impl Into<Nullable<Control>>) {
145        self.set_content(control.into().0.map(Control::boxed));
146    }
147
148    /// The root of the separate visual tree of the popup: none, an overlay
149    /// popup lives in the visual tree of its top-level.
150    pub fn hosted_visual_tree_root(&self) -> Option<Ref<Visual>> {
151        None
152    }
153
154    /// A transform that will be applied to the popup.
155    pub fn transform(&self) -> Option<Ref<Transform>> {
156        self.get_value(Self::transform_property())
157    }
158
159    pub fn set_transform(&self, value: impl Into<Nullable<Transform>>) {
160        self.set_value(Self::transform_property(), value.into().0)
161    }
162
163    /// Hides the popup.
164    pub fn dispose(&self) {
165        self.hide();
166    }
167
168    /// Shows the popup: adds the host to its overlay layer.
169    pub fn show(&self) {
170        if let Some(overlay_layer) = self.overlay_layer.upgrade() {
171            overlay_layer.children().add(self.to_ref());
172        }
173
174        let content = self.content().and_then(|content| Control::from_boxed(&content));
175        if content.is_some_and(|content| !content.is_attached_to_visual_tree()) {
176            // We need to force a measure pass so any descendants are built, for focus to work.
177            self.update_layout();
178        }
179    }
180
181    /// Hides the popup: removes the host from its overlay layer.
182    pub fn hide(&self) {
183        if let Some(overlay_layer) = self.overlay_layer.upgrade() {
184            overlay_layer.children().remove(self.to_ref());
185        }
186    }
187
188    /// Takes focus from any currently focused native control.
189    pub fn take_focus(&self) {
190        // Nothing to do here: overlay popups are implemented inside the window.
191    }
192
193    /// Configures the position of the popup according to a target control
194    /// and a set of placement parameters.
195    pub fn configure_position(&self, position_request: PopupPositionRequest) {
196        *self.popup_position_request.borrow_mut() = Some(position_request);
197        self.needs_update.set(true);
198        self.update_position();
199    }
200
201    fn update_position(&self) {
202        if !self.needs_update.get() {
203            return;
204        }
205        let Some(popup_position_request) = self.popup_position_request.borrow().clone() else { return };
206
207        self.needs_update.set(false);
208        let overlay_layer = self.overlay_layer.upgrade();
209        let top_level = overlay_layer
210            .and_then(|overlay_layer| TopLevel::get_top_level(Some(&overlay_layer)))
211            .expect("the popup overlay layer is in a top-level");
212        let positioner = self.positioner.get().expect("the positioner is created with the host");
213        positioner.update_request(
214            &top_level,
215            &popup_position_request,
216            self.popup_size.get(),
217            self.child_margin.get(),
218            self.flow_direction(),
219        );
220    }
221
222    /// The host as a popup host (the popup host contract it implements
223    /// upstream).
224    pub fn to_popup_host(&self) -> Rc<dyn IPopupHost> {
225        Rc::new(OverlayPopupHostHost(self.to_ref()))
226    }
227
228    /// Creates the popup host for a popup placed relative to `target`: a
229    /// native popup root when the platform of the top-level of `target`
230    /// creates popups and `should_use_overlay_layer` is false, otherwise an
231    /// overlay popup host on the popup overlay layer of `target` (internal
232    /// upstream).
233    ///
234    /// # Panics
235    ///
236    /// Panics when neither can be created.
237    pub fn create_popup_host(
238        target: &Visual,
239        dependency_resolver: Option<Rc<dyn IFerroDependencyResolver>>,
240        should_use_overlay_layer: bool,
241    ) -> Rc<dyn IPopupHost> {
242        if !should_use_overlay_layer {
243            if let Some(top_level) = TopLevel::get_top_level(Some(target)) {
244                let popup_impl = top_level.platform_impl().and_then(|platform_impl| platform_impl.create_popup());
245                if let Some(popup_impl) = popup_impl {
246                    return PopupRoot::new_with_resolver(&top_level, popup_impl, dependency_resolver).to_popup_host();
247                }
248            }
249        }
250
251        if let Some(overlay_layer) = PopupOverlayLayer::get_popup_overlay_layer(target) {
252            return OverlayPopupHost::new(&overlay_layer).to_popup_host();
253        }
254
255        panic!("Unable to create IPopupImpl and no overlay layer is found for the target control");
256    }
257}
258
259/// The popup the managed positioner of an overlay popup host positions:
260/// the host itself, within its overlay layer.
261struct OverlayPopupHostPositionerPopup(WeakRef<OverlayPopupHost>);
262
263impl IManagedPopupPositionerPopup for OverlayPopupHostPositionerPopup {
264    fn screens(&self) -> Vec<ManagedPopupPositionerScreenInfo> {
265        let Some(host) = self.0.upgrade() else { return Vec::new() };
266        let available_size =
267            host.overlay_layer.upgrade().map(|overlay_layer| overlay_layer.available_size()).unwrap_or_default();
268        let mut rc = Rect::from_size(available_size);
269        if let Some(top_level) = TopLevel::get_top_level(Some(&host)) {
270            let padding =
271                top_level.insets_manager().map(|insets_manager| insets_manager.safe_area_padding()).unwrap_or_default();
272            rc = rc.deflate_thickness(padding);
273        }
274
275        vec![ManagedPopupPositionerScreenInfo::new(rc, rc)]
276    }
277
278    fn parent_client_area_screen_geometry(&self) -> Rect {
279        let size = self
280            .0
281            .upgrade()
282            .and_then(|host| host.overlay_layer.upgrade())
283            .map(|overlay_layer| overlay_layer.bounds().size())
284            .unwrap_or_default();
285        Rect::from_size(size)
286    }
287
288    fn move_and_resize(&self, device_point: Point, _virtual_size: Size) {
289        let Some(host) = self.0.upgrade() else { return };
290        host.last_requested_position.set(device_point);
291        let weak = self.0.clone();
292        MediaContext::instance().begin_invoke_on_render(Rc::new(move || {
293            if let Some(host) = weak.upgrade() {
294                let position = host.last_requested_position.get();
295                Canvas::set_left(&host, position.x);
296                Canvas::set_top(&host, position.y);
297            }
298        }));
299    }
300
301    fn scaling(&self) -> f64 {
302        1.0
303    }
304}
305
306/// The popup host contract of an overlay popup host.
307struct OverlayPopupHostHost(Ref<OverlayPopupHost>);
308
309impl IFocusScope for OverlayPopupHostHost {}
310
311impl IDisposable for OverlayPopupHostHost {
312    fn dispose(&self) {
313        self.0.dispose();
314    }
315}
316
317impl IPopupHost for OverlayPopupHostHost {
318    fn width(&self) -> f64 {
319        self.0.width()
320    }
321
322    fn set_width(&self, value: f64) {
323        self.0.set_width(value)
324    }
325
326    fn min_width(&self) -> f64 {
327        self.0.min_width()
328    }
329
330    fn set_min_width(&self, value: f64) {
331        self.0.set_min_width(value)
332    }
333
334    fn max_width(&self) -> f64 {
335        self.0.max_width()
336    }
337
338    fn set_max_width(&self, value: f64) {
339        self.0.set_max_width(value)
340    }
341
342    fn height(&self) -> f64 {
343        self.0.height()
344    }
345
346    fn set_height(&self, value: f64) {
347        self.0.set_height(value)
348    }
349
350    fn min_height(&self) -> f64 {
351        self.0.min_height()
352    }
353
354    fn set_min_height(&self, value: f64) {
355        self.0.set_min_height(value)
356    }
357
358    fn max_height(&self) -> f64 {
359        self.0.max_height()
360    }
361
362    fn set_max_height(&self, value: f64) {
363        self.0.set_max_height(value)
364    }
365
366    fn presenter(&self) -> Option<Ref<ContentPresenter>> {
367        self.0.presenter()
368    }
369
370    fn topmost(&self) -> bool {
371        false
372    }
373
374    fn set_topmost(&self, _value: bool) {
375        // Not currently supported in overlay popups
376    }
377
378    fn is_hit_test_visible(&self) -> bool {
379        self.0.is_hit_test_visible()
380    }
381
382    fn set_is_hit_test_visible(&self, value: bool) {
383        self.0.set_is_hit_test_visible(value)
384    }
385
386    fn transform(&self) -> Option<Ref<Transform>> {
387        self.0.transform()
388    }
389
390    fn set_transform(&self, value: Option<Ref<Transform>>) {
391        self.0.set_transform(Nullable(value))
392    }
393
394    fn hosted_visual_tree_root(&self) -> Option<Ref<Visual>> {
395        self.0.hosted_visual_tree_root()
396    }
397
398    fn template_applied(&self, handler: Rc<dyn Fn(&TemplateAppliedEventArgs)>) -> Rc<dyn IDisposable> {
399        subscribe_template_applied(&self.0.clone().upcast(), handler)
400    }
401
402    fn configure_position(&self, position_request: PopupPositionRequest) {
403        self.0.configure_position(position_request)
404    }
405
406    fn set_child(&self, control: Option<Ref<Control>>) {
407        self.0.set_child(Nullable(control))
408    }
409
410    fn show(&self) {
411        self.0.show()
412    }
413
414    fn hide(&self) {
415        self.0.hide()
416    }
417
418    fn take_focus(&self) {
419        self.0.take_focus()
420    }
421
422    fn as_control(&self) -> Ref<Control> {
423        self.0.clone().upcast()
424    }
425
426    fn as_overlay_popup_host(&self) -> Option<Ref<OverlayPopupHost>> {
427        Some(self.0.clone())
428    }
429}