Add support for manually closing PopupWindows

This patch adds a `close()` function that can be called to close a popup
window, and a `close-to-click` boolean that can be set to false to
disable the default behavior.
This commit is contained in:
Simon Hausmann 2023-06-02 08:12:39 +02:00 committed by Simon Hausmann
parent a0a5294e49
commit 0f54b9599b
20 changed files with 349 additions and 24 deletions

View file

@ -20,6 +20,7 @@ All notable changes to this project are documented in this file.
- Added boolean `font-italic` property to `Text` and `TextInput`.
- Added `select-all()`, `cut()`, `copy()`, and `paste() to `TextInput`, `LineEdit`, and `TextEdit`.
- Added functions on color: `transparentize`, `mix`, and `with-alpha`.
- Added a `close()` function and a `close-on-click` boolean property to `PopupWindow`.
### Rust

View file

@ -331,6 +331,7 @@ fn gen_corelib(
"slint_windowrc_set_focus_item",
"slint_windowrc_set_component",
"slint_windowrc_show_popup",
"slint_windowrc_close_popup",
"slint_windowrc_set_rendering_notifier",
"slint_windowrc_request_redraw",
"slint_windowrc_on_close_requested",

View file

@ -161,13 +161,16 @@ public:
}
template<typename Component, typename Parent>
void show_popup(const Parent *parent_component, cbindgen_private::Point p,
void show_popup(const Parent *parent_component, cbindgen_private::Point p, bool close_on_click,
cbindgen_private::ItemRc parent_item) const
{
auto popup = Component::create(parent_component).into_dyn();
cbindgen_private::slint_windowrc_show_popup(&inner, &popup, p, &parent_item);
cbindgen_private::slint_windowrc_show_popup(&inner, &popup, p, close_on_click,
&parent_item);
}
void close_popup() const { cbindgen_private::slint_windowrc_close_popup(&inner); }
template<std::invocable<RenderingState, GraphicsAPI> F>
std::optional<SetRenderingNotifierError> set_rendering_notifier(F callback) const
{

View file

@ -495,9 +495,15 @@ Use this element to show a popup window like a tooltip or a popup menu.
Note: It isn't allowed to access properties of elements within the popup from outside of the `PopupWindow`.
### Properties
- **`close-on-click`** (_in_ _bool_): By default, a PopupWindow closes when the user clicks. Set this
to false to prevent that behavior and close it manually using the `close()` function. (default value: true)
### Functions
- **`show()`** Show the popup on the screen.
- **`close()`** Closes the popup. Use this if you set the `close-on-click` property to false.
### Example

View file

@ -138,6 +138,17 @@ cpp! {{
}
isMouseButtonDown = false;
void *parent_of_popup_to_close = nullptr;
if (auto p = dynamic_cast<const SlintWidget*>(parent())) {
void *parent_window = p->rust_window;
bool close_popup = rust!(Slint_mouseReleaseEventPopup [parent_window: &QtWindow as "void*"] -> bool as "bool" {
parent_window.close_popup_after_click()
});
if (close_popup) {
parent_of_popup_to_close = parent_window;
}
}
QPoint pos = event->pos();
int button = event->button();
rust!(Slint_mouseReleaseEvent [rust_window: &QtWindow as "void*", pos: qttypes::QPoint as "QPoint", button: u32 as "int" ] {
@ -145,11 +156,9 @@ cpp! {{
let button = from_qt_button(button);
rust_window.mouse_event(MouseEvent::Released{ position, button, click_count: 0 })
});
if (auto p = dynamic_cast<const SlintWidget*>(parent())) {
// FIXME: better way to close the popup
void *parent_window = p->rust_window;
rust!(Slint_mouseReleaseEventPopup [parent_window: &QtWindow as "void*", pos: qttypes::QPoint as "QPoint"] {
parent_window.close_popup();
if (parent_of_popup_to_close) {
rust!(Slint_mouseReleaseEventClosePopup [parent_of_popup_to_close: &QtWindow as "void*"] {
parent_of_popup_to_close.close_popup();
});
}
}
@ -1497,6 +1506,10 @@ impl QtWindow {
fn close_popup(&self) {
WindowInner::from_pub(&self.window).close_popup();
}
fn close_popup_after_click(&self) -> bool {
WindowInner::from_pub(&self.window).close_popup_after_click()
}
}
impl WindowAdapter for QtWindow {

View file

@ -387,6 +387,7 @@ export component PopupWindow {
in property <length> anchor_y;
in property <length> anchor_height;
in property <length> anchor_width;*/
in property <bool> close-on-click: true;
//show() is hardcoded in typeregister.rs
}

View file

@ -39,6 +39,7 @@ pub enum BuiltinFunction {
Pow,
SetFocusItem,
ShowPopupWindow,
ClosePopupWindow,
/// A function that belongs to an item (such as TextInput's select-all function).
ItemMemberFunction(String),
/// the "42".to_float()
@ -123,10 +124,12 @@ impl BuiltinFunction {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
},
BuiltinFunction::ShowPopupWindow => Type::Function {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
},
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => {
Type::Function {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
}
}
BuiltinFunction::ItemMemberFunction(..) => Type::Function {
return_type: Box::new(Type::Void),
args: vec![Type::ElementReference],
@ -225,7 +228,7 @@ impl BuiltinFunction {
| BuiltinFunction::Pow
| BuiltinFunction::ATan => true,
BuiltinFunction::SetFocusItem => false,
BuiltinFunction::ShowPopupWindow => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
BuiltinFunction::ColorBrighter
@ -276,7 +279,7 @@ impl BuiltinFunction {
| BuiltinFunction::Pow
| BuiltinFunction::ATan => true,
BuiltinFunction::SetFocusItem => false,
BuiltinFunction::ShowPopupWindow => false,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => false,
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
BuiltinFunction::ColorBrighter

View file

@ -2744,7 +2744,7 @@ fn compile_builtin_function_call(
format!("{}.text_input_focused()", access_window_field(ctx))
}
BuiltinFunction::ShowPopupWindow => {
if let [llr::Expression::NumberLiteral(popup_index), x, y, llr::Expression::PropertyReference(parent_ref)] =
if let [llr::Expression::NumberLiteral(popup_index), x, y, close_on_click, llr::Expression::PropertyReference(parent_ref)] =
arguments
{
let mut parent_ctx = ctx;
@ -2764,13 +2764,18 @@ fn compile_builtin_function_call(
let parent_component = access_item_rc(parent_ref, ctx);
let x = compile_expression(x, ctx);
let y = compile_expression(y, ctx);
let close_on_click = compile_expression(close_on_click, ctx);
format!(
"{window}.show_popup<{popup_window_id}>({component_access}, {{ static_cast<float>({x}), static_cast<float>({y}) }}, {{ {parent_component} }})"
"{window}.show_popup<{popup_window_id}>({component_access}, {{ static_cast<float>({x}), static_cast<float>({y}) }}, {close_on_click}, {{ {parent_component} }})"
)
} else {
panic!("internal error: invalid args to ShowPopupWindow {:?}", arguments)
}
}
BuiltinFunction::ClosePopupWindow => {
let window = access_window_field(ctx);
format!("{window}.close_popup()")
}
BuiltinFunction::ItemMemberFunction(name) => {
if let [llr::Expression::PropertyReference(pr)] = arguments {
let item = access_member(pr, ctx);

View file

@ -2233,7 +2233,7 @@ fn compile_builtin_function_call(
}
}
BuiltinFunction::ShowPopupWindow => {
if let [Expression::NumberLiteral(popup_index), x, y, Expression::PropertyReference(parent_ref)] =
if let [Expression::NumberLiteral(popup_index), x, y, close_on_click, Expression::PropertyReference(parent_ref)] =
arguments
{
let mut parent_ctx = ctx;
@ -2252,6 +2252,7 @@ fn compile_builtin_function_call(
let parent_component = access_item_rc(parent_ref, ctx);
let x = compile_expression(x, ctx);
let y = compile_expression(y, ctx);
let close_on_click = compile_expression(close_on_click, ctx);
let window_adapter_tokens = access_window_adapter_field(ctx);
quote!(
slint::private_unstable_api::re_exports::WindowInner::from_pub(#window_adapter_tokens.window()).show_popup(
@ -2261,6 +2262,7 @@ fn compile_builtin_function_call(
instance.into()
}),
Point::new(#x as slint::private_unstable_api::re_exports::Coord, #y as slint::private_unstable_api::re_exports::Coord),
#close_on_click,
#parent_component
)
)
@ -2268,6 +2270,12 @@ fn compile_builtin_function_call(
panic!("internal error: invalid args to ShowPopupWindow {:?}", arguments)
}
}
BuiltinFunction::ClosePopupWindow => {
let window_adapter_tokens = access_window_adapter_field(ctx);
quote!(
slint::private_unstable_api::re_exports::WindowInner::from_pub(#window_adapter_tokens.window()).close_popup()
)
}
BuiltinFunction::ItemMemberFunction(name) => {
if let [Expression::PropertyReference(pr)] = arguments {
let item = access_member(pr, ctx);

View file

@ -355,7 +355,13 @@ fn lower_show_popup(args: &[tree_Expression], ctx: &ExpressionContext) -> llr_Ex
);
llr_Expression::BuiltinFunctionCall {
function: BuiltinFunction::ShowPopupWindow,
arguments: vec![llr_Expression::NumberLiteral(popup_index as _), x, y, item_ref],
arguments: vec![
llr_Expression::NumberLiteral(popup_index as _),
x,
y,
llr_Expression::BoolLiteral(popup.close_on_click),
item_ref,
],
}
} else {
panic!("invalid arguments to ShowPopupWindow");

View file

@ -87,7 +87,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::Log => 10,
BuiltinFunction::Pow => 10,
BuiltinFunction::SetFocusItem => isize::MAX,
BuiltinFunction::ShowPopupWindow => isize::MAX,
BuiltinFunction::ShowPopupWindow | BuiltinFunction::ClosePopupWindow => isize::MAX,
BuiltinFunction::ItemMemberFunction(..) => isize::MAX,
BuiltinFunction::StringToFloat => 50,
BuiltinFunction::StringIsFloat => 50,

View file

@ -203,6 +203,7 @@ pub struct PopupWindow {
pub component: Rc<Component>,
pub x: NamedReference,
pub y: NamedReference,
pub close_on_click: bool,
pub parent_element: ElementRc,
}

View file

@ -344,6 +344,7 @@ fn duplicate_popup(
PopupWindow {
x: p.x.clone(),
y: p.y.clone(),
close_on_click: p.close_on_click.clone(),
component: duplicate_sub_component(&p.component, &parent, mapping, priority_delta),
parent_element: mapping
.get(&element_key(p.parent_element.clone()))

View file

@ -68,6 +68,28 @@ fn lower_popup_window(
parent_element.borrow_mut().has_popup_child = true;
popup_window_element.borrow_mut().base_type = window_type.clone();
popup_window_element.borrow_mut().property_declarations.insert(
"close-on-click".into(),
PropertyDeclaration { property_type: Type::Bool, ..PropertyDeclaration::default() },
);
let close_on_click =
match popup_window_element.borrow_mut().bindings.remove("close-on-click").map_or_else(
|| Ok(true),
|binding| match binding.borrow().expression {
Expression::BoolLiteral(value) => Ok(value),
_ => Err(binding.borrow().span.clone()),
},
) {
Ok(coc) => coc,
Err(location) => {
diag.push_error(
"The close-on-click property only supports constants at the moment".into(),
&location,
);
return;
}
};
let popup_comp = Rc::new(Component {
root_element: popup_window_element.clone(),
@ -105,6 +127,7 @@ fn lower_popup_window(
component: popup_comp,
x: coord_x,
y: coord_y,
close_on_click,
parent_element: parent_element.clone(),
});
}

View file

@ -0,0 +1,14 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
export Bar := Rectangle {
in property <bool> external;
PopupWindow {
close-on-click: true;
}
PopupWindow {
close-on-click: root.external;
// ^error{The close-on-click property only supports constants at the moment}
}
}

View file

@ -259,7 +259,16 @@ impl TypeRegister {
.unwrap()
.member_functions
.insert("show".into(), BuiltinFunction::ShowPopupWindow);
Rc::get_mut(b).unwrap().properties.insert(
"close".into(),
BuiltinPropertyInfo::new(BuiltinFunction::ClosePopupWindow.ty()),
);
Rc::get_mut(b)
.unwrap()
.member_functions
.insert("close".into(), BuiltinFunction::ClosePopupWindow);
}
_ => unreachable!(),
};

View file

@ -257,6 +257,9 @@ struct PopupWindow {
location: PopupWindowLocation,
/// The component that is responsible for providing the popup content.
component: ComponentRc,
/// If true, Slint will close the popup after any mouse click within the popup.
/// Set to false and call close() on the PopupWindow to close it manually.
close_on_click: bool,
}
#[pin_project::pin_project]
@ -397,6 +400,8 @@ impl WindowInner {
// handle multiple press release
event = self.click_state.check_repeat(event);
let close_popup_after_click = self.close_popup_after_click();
let embedded_popup_component =
self.active_popup.borrow().as_ref().and_then(|popup| match popup.location {
PopupWindowLocation::TopLevel(_) => None,
@ -439,9 +444,7 @@ impl WindowInner {
self.mouse_input_state.take(),
));
if embedded_popup_component.is_some() {
//FIXME: currently the ComboBox is the only thing that uses the popup, and it should close automatically
// on release. But ideally, there would be API to close the popup rather than always closing it on release
if embedded_popup_component.is_some() && close_popup_after_click {
if matches!(event, MouseEvent::Released { .. }) {
self.close_popup();
}
@ -721,6 +724,7 @@ impl WindowInner {
&self,
popup_componentrc: &ComponentRc,
position: Point,
close_on_click: bool,
parent_item: &ItemRc,
) {
let position = parent_item.map_to_window(
@ -774,11 +778,15 @@ impl WindowInner {
}
};
self.active_popup
.replace(Some(PopupWindow { location, component: popup_componentrc.clone() }));
self.active_popup.replace(Some(PopupWindow {
location,
component: popup_componentrc.clone(),
close_on_click,
}));
}
/// Removes any active popup.
/// TODO: this function should take a component ref as parameter, to close a specific popup - i.e. when popup menus create a hierarchy of popups.
pub fn close_popup(&self) {
if let Some(current_popup) = self.active_popup.replace(None) {
if let PopupWindowLocation::ChildWindow(offset) = current_popup.location {
@ -798,6 +806,11 @@ impl WindowInner {
}
}
/// Returns true if the currently active popup is configured to close on click. None if there is no active popup.
pub fn close_popup_after_click(&self) -> bool {
self.active_popup.borrow().as_ref().map_or(false, |popup| popup.close_on_click)
}
/// 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()
@ -1015,12 +1028,19 @@ pub mod ffi {
handle: *const WindowAdapterRcOpaque,
popup: &ComponentRc,
position: crate::graphics::Point,
close_on_click: bool,
parent_item: &ItemRc,
) {
let window_adapter = &*(handle as *const Rc<dyn WindowAdapter>);
WindowInner::from_pub(window_adapter.window()).show_popup(popup, position, parent_item);
WindowInner::from_pub(window_adapter.window()).show_popup(
popup,
position,
close_on_click,
parent_item,
);
}
/// Close the current popup
#[no_mangle]
pub unsafe extern "C" fn slint_windowrc_close_popup(handle: *const WindowAdapterRcOpaque) {
let window_adapter = &*(handle as *const Rc<dyn WindowAdapter>);
WindowInner::from_pub(window_adapter.window()).close_popup();

View file

@ -1745,6 +1745,7 @@ impl<'a, 'id> InstanceRef<'a, 'id> {
pub fn show_popup(
popup: &object_tree::PopupWindow,
pos: i_slint_core::graphics::Point,
close_on_click: bool,
parent_comp: ComponentRefPin,
parent_window_adapter: &Rc<dyn WindowAdapter>,
parent_item: &ItemRc,
@ -1757,6 +1758,7 @@ pub fn show_popup(
WindowInner::from_pub(parent_window_adapter.window()).show_popup(
&vtable::VRc::into_dyn(inst),
pos,
close_on_click,
parent_item,
);
}

View file

@ -572,6 +572,7 @@ fn call_builtin_function(
x.try_into().unwrap(),
y.try_into().unwrap(),
),
popup.close_on_click,
component.borrow(),
window_adapter_ref(component).unwrap(),
&parent_item,
@ -581,6 +582,18 @@ fn call_builtin_function(
panic!("internal error: argument to SetFocusItem must be an element")
}
}
BuiltinFunction::ClosePopupWindow => {
let component = match local_context.component_instance {
ComponentInstance::InstanceRef(c) => c,
ComponentInstance::GlobalComponent(_) => {
panic!("Cannot show popup from a global component")
}
};
window_ref(component).unwrap().close_popup();
Value::Void
}
BuiltinFunction::ItemMemberFunction(name) => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to item member function call")

View file

@ -0,0 +1,195 @@
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
export component TestCase {
width: 300px;
height: 300px;
in-out property <bool> popup-created;
in-out property <int> click-count;
in-out property <int> popup-selector: 0;
default-popup := PopupWindow {
width: parent.width;
height: parent.height;
Text {
text: "I'm a default";
}
init => {
root.popup-created = true;
}
}
self-closing-popup := PopupWindow {
close-on-click: false;
width: parent.width;
height: parent.height;
Text {
text: "I'm a self-closing popup";
}
TouchArea {
clicked => {
self-closing-popup.close();
}
}
init => {
root.popup-created = true;
}
}
never-closing-popup := PopupWindow {
close-on-click: false;
width: parent.width;
height: parent.height;
Text {
text: "I'm a popup that never closes";
}
TouchArea {
clicked => {
}
}
init => {
root.popup-created = true;
}
}
TouchArea {
clicked => {
root.click-count = root.click-count + 1;
if (root.popup-selector == 0) {
root.popup-selector = 3;
default-popup.show();
} else if (root.popup-selector == 1) {
root.popup-selector = 3;
self-closing-popup.show();
} else if (root.popup-selector == 2) {
root.popup-selector = 3;
never-closing-popup.show();
}
}
}
}
/*
```rust
let instance = TestCase::new().unwrap();
assert_eq!(instance.get_click_count(), 0);
assert_eq!(instance.get_popup_created(), false);
instance.set_popup_selector(0);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 2);
instance.set_popup_selector(1);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 2);
instance.set_popup_selector(2);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
assert_eq!(instance.get_popup_created(), true);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq!(instance.get_click_count(), 1);
```
```cpp
auto handle = TestCase::create();
const TestCase &instance = *handle;
assert_eq(instance.get_click_count(), 0);
assert_eq(instance.get_popup_created(), false);
instance.set_popup_selector(0);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_created(), true);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 1);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 2);
instance.set_popup_selector(1);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_created(), true);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 1);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 2);
instance.set_popup_selector(2);
instance.set_popup_created(false);
instance.set_click_count(0);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 1);
assert_eq(instance.get_popup_created(), true);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 1);
slint_testing::send_mouse_click(&instance, 5., 5.);
assert_eq(instance.get_click_count(), 1);
```
```js
var instance = new slint.TestCase({});
assert.equal(instance.click_count, 0);
assert.equal(instance.popup_created, false);
instance.popup_selector = 0;
instance.popup_created = false;
instance.click_count = 0;
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 1);
assert.equal(instance.popup_created, true);
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 1);
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 2);
instance.popup_selector = 1;
instance.popup_created = false;
instance.click_count = 0;
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 1);
assert.equal(instance.popup_created, true);
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 1);
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 2);
instance.popup_selector = 2;
instance.popup_created = false;
instance.click_count = 0;
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 1);
assert.equal(instance.popup_created, true);
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 1);
instance.send_mouse_click(5., 5.);
assert.equal(instance.click_count, 1);
```
*/