Skip to main content

ferroui_controls/primitives/
popup_root.rs

1use super::i_popup_host::subscribe_template_applied;
2use super::popup_positioning::PopupPositionRequest;
3use super::{IPopupHost, Popup, TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl};
4use crate::platform::{IPopupImpl, IWindowBaseImpl};
5use crate::presenters::ContentPresenter;
6use crate::{
7    ContentControlImpl, Control, ControlImpl, PlacementMode, TopLevel, TopLevelImpl, WindowBase, WindowBaseImpl,
8};
9use ferroui_base::input::{IFocusScope, InputElement, InputElementImpl};
10use ferroui_base::interactivity::{Interactive, InteractiveImpl};
11use ferroui_base::layout::{LayoutableImpl, LayoutableImplExt};
12use ferroui_base::media::{Brushes, IBrush, Transform};
13use ferroui_base::reactive::IDisposable;
14use ferroui_base::styling::StyleHostRef;
15use ferroui_base::visual_tree::IHostedVisualTreeRoot;
16use ferroui_base::{
17    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt, FerroProperty,
18    FerroPropertyChangedEventArgs, IFerroDependencyResolver, Nullable, Ref, Size, StyledElementImpl, StyledProperty,
19    Thickness, Visual, VisualImpl, WeakRef,
20};
21use std::cell::{Cell, RefCell};
22use std::rc::Rc;
23
24/// The root window of a [`Popup`].
25#[repr(C)]
26pub struct PopupRoot {
27    base: WindowBase,
28    popup_impl: RefCell<Option<Rc<dyn IPopupImpl>>>,
29    parent_top_level: WeakRef<TopLevel>,
30    popup_position_request: RefCell<Option<PopupPositionRequest>>,
31    popup_size: Cell<Size>,
32    child_margin: Cell<Thickness>,
33    needs_update: Cell<bool>,
34    has_initial_position: Cell<bool>,
35}
36
37ferro_class!(PopupRoot: WindowBase);
38ferro_impl_classes!(PopupRoot: VisualImpl, TemplatedControlImpl, ContentControlImpl);
39
40impl ControlImpl for PopupRoot {
41    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
42        crate::automation::peers::PopupRootAutomationPeer::new(this).upcast()
43    }
44}
45
46impl TopLevelImpl for PopupRoot {
47    /// The popups that are currently open directly inside the popup of
48    /// this root, in the order they were opened.
49    fn opened_popups(this: &Self) -> Vec<Ref<Popup>> {
50        this.parent().and_then(|parent| parent.cast::<Popup>()).map(|popup| popup.opened_popups()).unwrap_or_default()
51    }
52}
53
54impl IFocusScope for PopupRoot {}
55
56impl FerroObjectImpl for PopupRoot {
57    fn constructed(this: &Self) {
58        Self::parent_constructed(this);
59
60        if let Some(platform_impl) = this.platform_impl() {
61            platform_impl.set_window_manager_add_shadow_hint(this.window_manager_add_shadow_hint());
62            platform_impl.set_hit_test_visible(this.is_hit_test_visible());
63        }
64    }
65
66    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
67        Self::parent_on_property_changed(this, change);
68
69        if change.property() == Self::window_manager_add_shadow_hint_property().as_property() {
70            let (_, new_value) = change.get_old_and_new_value::<bool>();
71            if let Some(platform_impl) = this.platform_impl() {
72                platform_impl.set_window_manager_add_shadow_hint(new_value);
73            }
74        } else if change.property() == WindowBase::topmost_property().as_property() {
75            let (_, new_value) = change.get_old_and_new_value::<bool>();
76            if let Some(platform_impl) = this.platform_impl() {
77                platform_impl.set_topmost(new_value);
78            }
79        } else if change.property() == InputElement::is_hit_test_visible_property().as_property() {
80            let (_, new_value) = change.get_old_and_new_value::<bool>();
81            if let Some(platform_impl) = this.platform_impl() {
82                platform_impl.set_hit_test_visible(new_value);
83            }
84        }
85    }
86}
87
88impl StyledElementImpl for PopupRoot {
89    /// The styling parent of the popup root: its logical parent.
90    fn styling_parent(this: &Self) -> Option<StyleHostRef> {
91        this.parent().map(StyleHostRef::Element)
92    }
93}
94
95impl InteractiveImpl for PopupRoot {
96    /// The parent control in the event route.
97    ///
98    /// Popup events are passed to their parent window. This facilitates
99    /// this.
100    fn interactive_parent(this: &Self) -> Option<Ref<Interactive>> {
101        this.parent().and_then(|parent| parent.cast::<Interactive>())
102    }
103}
104
105impl InputElementImpl for PopupRoot {
106    fn is_focus_scope(_this: &Self) -> bool {
107        true
108    }
109
110    fn as_hosted_visual_tree_root(this: &Self) -> Option<&dyn IHostedVisualTreeRoot> {
111        Some(this)
112    }
113}
114
115impl IHostedVisualTreeRoot for PopupRoot {
116    /// The control that is hosting the popup root.
117    fn host(&self) -> Option<Ref<Visual>> {
118        // If the parent is attached to a visual tree, then return that. However the parent
119        // will possibly be a standalone Popup (i.e. a Popup not attached to a visual tree,
120        // created by e.g. a ContextMenu): if this is the case, return the ParentTopLevel
121        // if set. This helps to allow the focus manager to restore the focus to the outer
122        // scope when the popup is closed.
123        let parent_visual = self.parent().and_then(|parent| parent.cast::<Visual>());
124        if let Some(parent_visual) = &parent_visual {
125            if parent_visual.is_attached_to_visual_tree() {
126                return Some(parent_visual.clone());
127            }
128        }
129        self.parent_top_level().map(Ref::upcast).or(parent_visual)
130    }
131}
132
133impl LayoutableImpl for PopupRoot {
134    fn measure_override(this: &Self, available_size: Size) -> Size {
135        let max_auto_size =
136            this.platform_impl().map(|platform_impl| platform_impl.max_auto_size_hint()).unwrap_or(Size::INFINITY);
137        let mut constraint = available_size;
138
139        if constraint.width.is_infinite() {
140            constraint = constraint.with_width(max_auto_size.width);
141        }
142
143        if constraint.height.is_infinite() {
144            constraint = constraint.with_height(max_auto_size.height);
145        }
146
147        let measured = Self::parent_measure_override(this, constraint);
148        let mut width = measured.width;
149        let mut height = measured.height;
150        let width_cache = this.width();
151        let height_cache = this.height();
152
153        if !width_cache.is_nan() {
154            width = width_cache;
155        }
156
157        width = width.min(this.max_width());
158        width = width.max(this.min_width());
159
160        if !height_cache.is_nan() {
161            height = height_cache;
162        }
163
164        height = height.min(this.max_height());
165        height = height.max(this.min_height());
166
167        Size::new(width, height)
168    }
169}
170
171impl WindowBaseImpl for PopupRoot {
172    fn arrange_set_bounds(this: &Self, size: Size) -> Size {
173        let mut reposition = false;
174
175        let new_margin = this
176            .presenter()
177            .and_then(|presenter| presenter.child())
178            .map(|child| child.margin())
179            .unwrap_or_default();
180        if new_margin != this.child_margin.get() {
181            this.child_margin.set(new_margin);
182            reposition = true;
183        }
184
185        if this.popup_size.get() != size {
186            this.popup_size.set(size);
187            reposition = true;
188        }
189
190        if this.has_initial_position.get()
191            && this.popup_position_request.borrow().as_ref().is_some_and(|request| request.placement() == PlacementMode::Pointer)
192        {
193            reposition = false;
194        }
195
196        if reposition {
197            if this.popup_position_request.borrow().is_some() {
198                this.has_initial_position.set(true);
199            }
200
201            this.needs_update.set(true);
202            this.update_position();
203        }
204
205        this.client_size()
206    }
207}
208
209ferroui_base::ferro_properties! { impl PopupRoot {
210    ferro_property!(
211        /// Defines the `Transform` property.
212        pub fn transform_property() -> StyledProperty<Option<Ref<Transform>>> {
213            FerroProperty::register::<PopupRoot, _>("Transform", None)
214        }
215    );
216
217    ferro_property!(
218        /// Defines the `WindowManagerAddShadowHint` property.
219        pub fn window_manager_add_shadow_hint_property() -> StyledProperty<bool> {
220            Popup::window_manager_add_shadow_hint_property().add_owner::<PopupRoot>()
221        }
222    );
223} }
224
225impl PopupRoot {
226    fn static_constructor() {
227        let white: Rc<dyn IBrush> = Brushes::white();
228        TemplatedControl::background_property().override_default_value::<PopupRoot>(Some(white));
229    }
230
231    /// Creates a popup root: `parent` is the popup parent and
232    /// `platform_impl` the popup implementation.
233    pub fn new(parent: &Ref<TopLevel>, platform_impl: Rc<dyn IPopupImpl>) -> Ref<Self> {
234        Self::new_with_resolver(parent, platform_impl, None)
235    }
236
237    /// Creates a popup root: `parent` is the popup parent, `platform_impl`
238    /// the popup implementation and `dependency_resolver` the dependency
239    /// resolver to use; if `None` the default dependency resolver will be
240    /// used.
241    pub fn new_with_resolver(
242        parent: &Ref<TopLevel>,
243        platform_impl: Rc<dyn IPopupImpl>,
244        dependency_resolver: Option<Rc<dyn IFerroDependencyResolver>>,
245    ) -> Ref<Self> {
246        let window_base_impl: Rc<dyn IWindowBaseImpl> = platform_impl.clone();
247        instantiate(Self {
248            base: WindowBase::construct_with_resolver(window_base_impl, dependency_resolver),
249            popup_impl: RefCell::new(Some(platform_impl)),
250            parent_top_level: parent.downgrade(),
251            popup_position_request: RefCell::new(None),
252            popup_size: Cell::new(Size::default()),
253            child_margin: Cell::new(Thickness::default()),
254            needs_update: Cell::new(false),
255            has_initial_position: Cell::new(false),
256        })
257    }
258
259    /// The platform-specific window implementation; `None` once the popup
260    /// root has closed.
261    pub fn platform_impl(&self) -> Option<Rc<dyn IPopupImpl>> {
262        TopLevel::platform_impl(self)?;
263        self.popup_impl.borrow().clone()
264    }
265
266    /// A transform that will be applied to the popup.
267    pub fn transform(&self) -> Option<Ref<Transform>> {
268        self.get_value(Self::transform_property())
269    }
270
271    pub fn set_transform(&self, value: impl Into<Nullable<Transform>>) {
272        self.set_value(Self::transform_property(), value.into().0)
273    }
274
275    /// A hint to the window manager that a shadow should be added to the
276    /// popup.
277    pub fn window_manager_add_shadow_hint(&self) -> bool {
278        self.get_value(Self::window_manager_add_shadow_hint_property())
279    }
280
281    pub fn set_window_manager_add_shadow_hint(&self, value: bool) {
282        self.set_value(Self::window_manager_add_shadow_hint_property(), value)
283    }
284
285    /// The top-level the popup belongs to; `None` once that top-level is
286    /// gone (the popup root does not keep its parent alive).
287    pub fn parent_top_level(&self) -> Option<Ref<TopLevel>> {
288        self.parent_top_level.upgrade()
289    }
290
291    /// Releases the platform implementation and closes the popup root.
292    pub fn dispose(&self) {
293        if let Some(platform_impl) = self.platform_impl() {
294            platform_impl.dispose();
295        }
296        self.has_initial_position.set(false);
297        self.ensure_closed();
298    }
299
300    fn update_position(&self) {
301        if !self.needs_update.get() {
302            return;
303        }
304        let Some(popup_position_request) = self.popup_position_request.borrow().clone() else { return };
305
306        self.needs_update.set(false);
307        let positioner = self.platform_impl().and_then(|platform_impl| platform_impl.popup_positioner());
308        if let (Some(positioner), Some(parent_top_level)) = (positioner, self.parent_top_level()) {
309            positioner.update_request(
310                &parent_top_level,
311                &popup_position_request,
312                self.popup_size.get(),
313                self.child_margin.get(),
314                self.flow_direction(),
315            );
316        }
317    }
318
319    /// Configures the position of the popup according to a target control
320    /// and a set of placement parameters.
321    pub fn configure_position(&self, request: PopupPositionRequest) {
322        *self.popup_position_request.borrow_mut() = Some(request);
323        self.needs_update.set(true);
324        self.update_position();
325    }
326
327    /// Sets the control to display in the popup.
328    pub fn set_child(&self, control: impl Into<Nullable<Control>>) {
329        self.set_content(control.into().0.map(Control::boxed));
330    }
331
332    /// Takes focus from any currently focused native control.
333    pub fn take_focus(&self) {
334        if let Some(platform_impl) = self.platform_impl() {
335            platform_impl.take_focus();
336        }
337    }
338
339    /// The popup root as a popup host (the popup host contract it
340    /// implements upstream).
341    pub fn to_popup_host(&self) -> Rc<dyn IPopupHost> {
342        Rc::new(PopupRootHost(self.to_ref()))
343    }
344}
345
346/// The popup host contract of a popup root.
347struct PopupRootHost(Ref<PopupRoot>);
348
349impl IFocusScope for PopupRootHost {}
350
351impl IDisposable for PopupRootHost {
352    fn dispose(&self) {
353        self.0.dispose();
354    }
355}
356
357impl IPopupHost for PopupRootHost {
358    fn width(&self) -> f64 {
359        self.0.width()
360    }
361
362    fn set_width(&self, value: f64) {
363        self.0.set_width(value)
364    }
365
366    fn min_width(&self) -> f64 {
367        self.0.min_width()
368    }
369
370    fn set_min_width(&self, value: f64) {
371        self.0.set_min_width(value)
372    }
373
374    fn max_width(&self) -> f64 {
375        self.0.max_width()
376    }
377
378    fn set_max_width(&self, value: f64) {
379        self.0.set_max_width(value)
380    }
381
382    fn height(&self) -> f64 {
383        self.0.height()
384    }
385
386    fn set_height(&self, value: f64) {
387        self.0.set_height(value)
388    }
389
390    fn min_height(&self) -> f64 {
391        self.0.min_height()
392    }
393
394    fn set_min_height(&self, value: f64) {
395        self.0.set_min_height(value)
396    }
397
398    fn max_height(&self) -> f64 {
399        self.0.max_height()
400    }
401
402    fn set_max_height(&self, value: f64) {
403        self.0.set_max_height(value)
404    }
405
406    fn presenter(&self) -> Option<Ref<ContentPresenter>> {
407        self.0.presenter()
408    }
409
410    fn topmost(&self) -> bool {
411        self.0.topmost()
412    }
413
414    fn set_topmost(&self, value: bool) {
415        self.0.set_topmost(value)
416    }
417
418    fn is_hit_test_visible(&self) -> bool {
419        self.0.is_hit_test_visible()
420    }
421
422    fn set_is_hit_test_visible(&self, value: bool) {
423        self.0.set_is_hit_test_visible(value)
424    }
425
426    fn transform(&self) -> Option<Ref<Transform>> {
427        self.0.transform()
428    }
429
430    fn set_transform(&self, value: Option<Ref<Transform>>) {
431        self.0.set_transform(Nullable(value))
432    }
433
434    fn hosted_visual_tree_root(&self) -> Option<Ref<Visual>> {
435        Some(self.0.clone().upcast())
436    }
437
438    fn template_applied(&self, handler: Rc<dyn Fn(&TemplateAppliedEventArgs)>) -> Rc<dyn IDisposable> {
439        subscribe_template_applied(&self.0.clone().upcast(), handler)
440    }
441
442    fn configure_position(&self, position_request: PopupPositionRequest) {
443        self.0.configure_position(position_request)
444    }
445
446    fn set_child(&self, control: Option<Ref<Control>>) {
447        self.0.set_child(Nullable(control))
448    }
449
450    fn show(&self) {
451        self.0.show()
452    }
453
454    fn hide(&self) {
455        self.0.hide()
456    }
457
458    fn take_focus(&self) {
459        self.0.take_focus()
460    }
461
462    fn as_control(&self) -> Ref<Control> {
463        self.0.clone().upcast()
464    }
465
466    fn as_popup_root(&self) -> Option<Ref<PopupRoot>> {
467        Some(self.0.clone())
468    }
469}