Repository: slint-ui/slint (language: rust)

Issue:
Qt backend: Nested PopupWindow is not shown
### Bug Description

Opening a `PopupWindow` from within another `PopupWindow` does work on WASM and winit, but not with the Qt backend. With the Qt backend, only the first popup is opened. When calling `show()` on the second one, nothing happens.

Side note: With the given example, clicking into the yellow popup does not work in the bottom right corner (where the second popup is placed). This is independent of the renderer (reproducible in SlintPad) so I'm not sure if this is expected behavior or not. I'd assume the second popup should not intercept click events in its area while it is not shown...

### Reproducible Code (if applicable)

```slint
export component AppWindow inherits Window {
    width: 100px;
    height: 100px;

    popup1 := PopupWindow {
        Rectangle {
            background: yellow;
        }

        x: 10px;
        y: 10px;
        height: 50px;
        width: 50px;

        TouchArea {
            clicked => {
                popup2.show();
            }
        }
    }

    popup2 := PopupWindow {
        Rectangle {
            background: red;
        }

        x: 40px;
        y: 40px;
        height: 50px;
        width: 50px;
    }

    TouchArea {
        clicked => {
            popup1.show();
        }
    }
}
```


### Environment Details

- Slint Version: 1.9.1
- Platform/OS: Linux
- Backend/Renderer: Qt


### Product Impact

_No response_


Discussion / hints:
Another issue with `PopupWindow` with Qt backend:
- On Wayland, popups stay at their absolute screen position even when moving the application window around. With winit the popup keeps its relative window position, i.e. moves with the application window as desired.
- On X11, a popup is somehow globally modal, not just application modal. While a popup is open, no other application receives click events anymore. Instead, any click event outside the application window will just close the popup.

Slint 1.9.1 on Ubuntu 22.04.
The problem is that the yellow PopupWindow's close policy is set to close on click, so the release event is supposed to close the yellow popup.
But the thing is that the release event also open the red popup.

So while delivering the release event this happens
 1. check that there is currently a popup (yes, the yellow popup)
 2. clicked callback open the yellow popup
 3. attempt to close the yellow popup by closing the top popup. Ooops, this closes the red popup instead

The thing is that with winit, there is an additional check that the mouse cursor is inside the popup in step 3, and this fails because it uses the red instead of the yellow. Which is why your testcase "works" when not clicking in the bottom-right corner.

So two possible fix: either
 - Fix the step 3 to close the right popup always (the yellow popup, which will also result of the closing of the red one) 
 - Or if a popup is getting open while processing the event, do not close any popup (which i think is what you expect, but is probably technically wrong.


> - Fix the step 3 to close the right popup always (the yellow popup, which will also result of the closing of the red one)
> - Or if a popup is getting open while processing the event, do not close any popup (which i think is what you expect, but is probably technically wrong.

Actually in my particular case I expect the yellow popup to close when clicked, leaving only the red one open. Not sure if everyone would expect this behavior but at least the yellow popups close policy is set to close if there's a click. That the click also opens a new popup is a quite specific case but IMHO (by default) it should not have an impact on the close behavior of the yellow popup.

EDIT: Btw my yellow popup is in the real use-case a menu item and the red popup a normal popup (like a message box). This is a very common use-case and it is expected that the menu closes when an item is triggered, no matter what action it executes.

Fix patch:
diff --git a/internal/backends/qt/qt_window.rs b/internal/backends/qt/qt_window.rs
index dc6e52d5424..08e8ffe4c3b 100644
--- a/internal/backends/qt/qt_window.rs
+++ b/internal/backends/qt/qt_window.rs
@@ -158,17 +158,23 @@ cpp! {{
             isMouseButtonDown = false;
 
             void *parent_of_popup_to_close = nullptr;
+            int popup_id_to_close = 0;
             if (auto p = dynamic_cast<const SlintWidget*>(parent())) {
                 while (auto pp = dynamic_cast<const SlintWidget*>(p->parent())) {
                     p = pp;
                 }
                 void *parent_window = p->rust_window;
                 bool inside = rect().contains(event->pos());
-                bool close_on_click = rust!(Slint_mouseReleaseEventPopup [parent_window: &QtWindow as "void*", inside: bool as "bool"] -> bool as "bool" {
-                    let close_policy = parent_window.top_close_policy();
-                    close_policy == PopupClosePolicy::CloseOnClick || (close_policy == PopupClosePolicy::CloseOnClickOutside && !inside)
+                popup_id_to_close = rust!(Slint_mouseReleaseEventPopup [parent_window: &QtWindow as "void*", inside: bool as "bool"] -> u32 as "int" {
+                    let active_popups = WindowInner::from_pub(&parent_window.window).active_popups();
+                    if let Some(popup) = active_popups.last() {
+                        if popup.close_policy == PopupClosePolicy::CloseOnClick || (popup.close_policy == PopupClosePolicy::CloseOnClickOutside && !inside) {
+                            return popup.popup_id.get();
+                        }
+                    }
+                    0
                 });
-                if (close_on_click) {
+                if (popup_id_to_close) {
                     parent_of_popup_to_close = parent_window;
                 }
             }
@@ -180,9 +186,9 @@ cpp! {{
                 let button = from_qt_button(button);
                 rust_window.mouse_event(MouseEvent::Released{ position, button, click_count: 0 })
             });
-            if (parent_of_popup_to_close) {
-                rust!(Slint_mouseReleaseEventClosePopup [parent_of_popup_to_close: &QtWindow as "void*"] {
-                    parent_of_popup_to_close.close_top_popup();
+            if (popup_id_to_close) {
+                rust!(Slint_mouseReleaseEventClosePopup [parent_of_popup_to_close: &QtWindow as "void*", popup_id_to_close: std::num::NonZeroU32 as "int"] {
+                    WindowInner::from_pub(&parent_of_popup_to_close.window).close_popup(popup_id_to_close);
                 });
             }
         }
@@ -1750,14 +1756,6 @@ impl QtWindow {
         timer_event();
     }
 
-    fn close_top_popup(&self) {
-        WindowInner::from_pub(&self.window).close_top_popup();
-    }
-
-    fn top_close_policy(&self) -> PopupClosePolicy {
-        WindowInner::from_pub(&self.window).top_close_policy()
-    }
-
     fn window_state_event(&self) {
         let widget_ptr = self.widget_ptr();
 
diff --git a/internal/core/window.rs b/internal/core/window.rs
index e25a80076e9..2ce23d83c84 100644
--- a/internal/core/window.rs
+++ b/internal/core/window.rs
@@ -393,13 +393,13 @@ pub enum PopupWindowLocation {
 #[derive(Clone)]
 pub struct PopupWindow {
     /// The ID of the associated popup.
-    popup_id: NonZeroU32,
+    pub popup_id: NonZeroU32,
     /// The location defines where the pop up is rendered.
     pub location: PopupWindowLocation,
     /// The component that is responsible for providing the popup content.
     pub component: ItemTreeRc,
     /// Defines the close behaviour of the popup.
-    close_policy: PopupClosePolicy,
+    pub close_policy: PopupClosePolicy,
     /// the item that had the focus in the parent window when the popup was opened
     focus_item_in_parent: ItemWeak,
     /// The item from where the Popup was invoked from
@@ -596,8 +596,30 @@ impl WindowInner {
             self.had_popup_on_press.set(!self.active_popups.borrow().is_empty());
         }
 
-        let close_policy = self.top_close_policy();
-        let mut mouse_inside_popup = false;
+        let popup_to_close = self.active_popups.borrow().last().and_then(|popup| {
+            let mouse_inside_popup = || {
+                if let PopupWindowLocation::ChildWindow(coordinates) = &popup.location {
+                    event.position().map_or(true, |pos| {
+                        ItemTreeRc::borrow_pin(&popup.component)
+                            .as_ref()
+                            .item_geometry(0)
+                            .contains(pos - coordinates.to_vector())
+                    })
+                } else {
+                    false
+                }
+            };
+            match popup.close_policy {
+                PopupClosePolicy::CloseOnClick => {
+                    let mouse_inside_popup = mouse_inside_popup();
+                    (mouse_inside_popup && released_event && self.had_popup_on_press.get())
+                        || (!mouse_inside_popup && pressed_event)
+                }
+                PopupClosePolicy::CloseOnClickOutside => !mouse_inside_popup() && pressed_event,
+                PopupClosePolicy::NoAutoClose => false,
+            }
+            .then_some(popup.popup_id)
+        });
 
         mouse_input_state = if let Some(mut event) =
             crate::input::handle_mouse_grab(event, &window_adapter, &mut mouse_input_state)
@@ -610,7 +632,7 @@ impl WindowInner {
             {
                 let geom = ItemTreeRc::borrow_pin(component).as_ref().item_geometry(0);
 
-                mouse_inside_popup = event
+                let mouse_inside_popup = event
                     .position()
                     .map_or(true, |pos| geom.contains(pos - coordinates.to_vector()));
 
@@ -655,21 +677,9 @@ impl WindowInner {
 
         self.mouse_input_state.set(mouse_input_state);
 
-        match close_policy {
-            PopupClosePolicy::CloseOnClick => {
-                if (mouse_inside_popup && released_event && self.had_popup_on_press.get())
-                    || (!mouse_inside_popup && pressed_event)
-                {
-                    self.close_top_popup();
-                }
-            }
-            PopupClosePolicy::CloseOnClickOutside => {
-                if !mouse_inside_popup && pressed_event {
-                    self.close_top_popup();
-                }
-            }
-            PopupClosePolicy::NoAutoClose => {}
-        };
+        if let Some(popup_id) = popup_to_close {
+            self.close_popup(popup_id);
+        }
 
         crate::properties::ChangeTracker::run_change_handlers();
     }
@@ -1203,14 +1213,6 @@ impl WindowInner {
         }
     }
 
-    /// Returns the close policy of the top-most popup. PopupClosePolicy::NoAutoClose if there is no active popup.
-    pub fn top_close_policy(&self) -> PopupClosePolicy {
-        self.active_popups
-            .borrow()
-            .last()
-            .map_or(PopupClosePolicy::NoAutoClose, |popup| popup.close_policy)
-    }
-
     /// Returns the scale factor set on the window, as provided by the windowing system.
     pub fn scale_factor(&self) -> f32 {
         self.pinned_fields.as_ref().project_ref().scale_factor.get()


Test patch:
diff --git a/tests/cases/elements/popupwindow_nested_close-on-click.slint b/tests/cases/elements/popupwindow_nested_close-on-click.slint
new file mode 100644
index 00000000000..69a3a60ff3f
--- /dev/null
+++ b/tests/cases/elements/popupwindow_nested_close-on-click.slint
@@ -0,0 +1,115 @@
+// Copyright © SixtyFPS GmbH <info@slint.dev>
+// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
+
+// https://github.com/slint-ui/slint/issues/7322
+// A PopupWindow with the default close-on-click policy should close when clicked, even if the click open a nested popup
+
+export component TestCase {
+    width: 300px;
+    height: 300px;
+
+    in-out property <int> popup1-clicked;
+    in-out property <int> popup2-clicked;
+    in-out property <int> root-clicked;
+
+    popup1 := PopupWindow {
+        Rectangle {
+            background: yellow;
+        }
+
+        x: 10px;
+        y: 10px;
+        height: 50px;
+        width: 50px;
+
+        TouchArea {
+            clicked => {
+                popup1-clicked += 1;
+                popup2.show();
+            }
+        }
+    }
+
+    popup2 := PopupWindow {
+        Rectangle {
+            background: red;
+        }
+
+        x: 40px;
+        y: 40px;
+        height: 50px;
+        width: 50px;
+
+        TouchArea {
+            clicked => {
+                popup2-clicked += 1;
+            }
+        }
+    }
+
+    TouchArea {
+        clicked => {
+            root-clicked += 1;
+            popup1.show();
+        }
+    }
+}
+
+/*
+
+```rust
+
+let instance = TestCase::new().unwrap();
+
+
+slint_testing::send_mouse_click(&instance, 15., 15.);
+assert_eq!(instance.get_root_clicked(), 1);
+assert_eq!(instance.get_popup1_clicked(), 0);
+assert_eq!(instance.get_popup2_clicked(), 0);
+
+// popup1 is open
+
+slint_testing::send_mouse_click(&instance, 15., 15.);
+assert_eq!(instance.get_root_clicked(), 1);
+assert_eq!(instance.get_popup1_clicked(), 1);
+assert_eq!(instance.get_popup2_clicked(), 0);
+
+// popup2 is open, popup1 is closed
+
+slint_testing::send_mouse_click(&instance, 45., 45.);
+assert_eq!(instance.get_root_clicked(), 1);
+assert_eq!(instance.get_popup1_clicked(), 1);
+assert_eq!(instance.get_popup2_clicked(), 1);
+
+// all popup closed
+
+slint_testing::send_mouse_click(&instance, 45., 45.);
+assert_eq!(instance.get_root_clicked(), 2);
+assert_eq!(instance.get_popup1_clicked(), 1);
+assert_eq!(instance.get_popup2_clicked(), 1);
+
+// popup1 is open
+// click where popup2 will be
+
+slint_testing::send_mouse_click(&instance, 45., 45.);
+assert_eq!(instance.get_root_clicked(), 2);
+assert_eq!(instance.get_popup1_clicked(), 2);
+assert_eq!(instance.get_popup2_clicked(), 1);
+
+// popup2 is open, popup1 is closed
+
+slint_testing::send_mouse_click(&instance, 45., 45.);
+assert_eq!(instance.get_root_clicked(), 2);
+assert_eq!(instance.get_popup1_clicked(), 2);
+assert_eq!(instance.get_popup2_clicked(), 2);
+
+// all popup closed
+
+slint_testing::send_mouse_click(&instance, 45., 45.);
+assert_eq!(instance.get_root_clicked(), 3);
+assert_eq!(instance.get_popup1_clicked(), 2);
+assert_eq!(instance.get_popup2_clicked(), 2);
+
+```
+
+*/
\ No newline at end of file