Menu API changes

`ContextMenu` -> `ContextMenuArea`

`ContextMenu` must have a `Menu` child.
`MenuItem` can no longer be put dirrectly in `MenuBar` and can no longer
have children
`Menu` is used now for sub menus
This commit is contained in:
Olivier Goffart 2025-02-21 16:03:47 +01:00 committed by GitHub
parent 2b6938bce8
commit 39191e5acd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 352 additions and 238 deletions

View file

@ -183,13 +183,16 @@ export component SwipeGestureHandler {
//-default_size_binding:expands_to_parent_geometry
}
// Fake
component MenuItem {
in property <string> title;
callback activated();
//-disallow_global_types_as_child_elements
//-is_non_item_type
}
component Menu {
in property <string> title;
MenuItem {}
Menu {}
//-disallow_global_types_as_child_elements
//-is_non_item_type
}
@ -198,14 +201,9 @@ component MenuItem {
component MenuBar {
//-is_non_item_type
//-disallow_global_types_as_child_elements
MenuItem {}
// Currently experimental
Menu {}
}
// The NativeItem, exported as ContextMenuInternal for the style
component ContextMenu inherits Empty {
callback activated(entry: MenuEntry);
@ -225,15 +223,14 @@ export component ContextMenuInternal inherits ContextMenu {
// The public ContextMenu which is lowered in the lower_menus pass. See that pass documentation for more info
// Note that this element cannot be named `ContextMenu` because that's the same name as a native item,
// and the load_builtins code doesn't allow that. So use a placeholder name and re-export under `ContextMenu`
component ActualContextMenuElement inherits Empty {
export component ContextMenuArea inherits Empty {
// This is actually function as part of out interface, but a callback as much is the runtime concerned
callback show(position: Point);
function close() {}
//-default_size_binding:expands_to_parent_geometry
MenuItem {}
Menu {}
}
export { ActualContextMenuElement as ContextMenu }
component WindowItem {
in-out property <length> width;

View file

@ -1,7 +1,7 @@
// 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
//! Passe lower the `MenuBar` and `ContextMenu` as well as all their contents
//! Passe lower the `MenuBar` and `ContextMenuArea` as well as all their contents
//!
//! We can't have properties of type model because that is not binary compatible with C++,
//! so all the code that handle model of MenuEntry need to be handle by code in the generated code
@ -12,14 +12,17 @@
//! ```slint
//! Window {
//! menu-bar := MenuBar {
//! Menu {
//! title: "File";
//! MenuItem {
//! title: "A";
//! activated => { ... }
//! }
//! MenuItem {
//! Menu {
//! title: "B";
//! MenuItem { title: "C"; }
//! }
//! }
//! }
//! content := ...
//! }
@ -28,8 +31,11 @@
//! ```slint
//! Window {
//! menu-bar := VerticalLayout {
//! property <[MenuEntry]> entries : [ { id: "1", title: "A" }, { id: "2", title: "B", has-sub-menu: true } ];
//! callback sub-menu(entry: MenuEntry) => { if(entry.id == "2") { return [ { id: "3", title: "C" } ]; } else { return []; } }
//! property <[MenuEntry]> entries : [ { id: "0", title: "File", has-sub-menu: true } ];
//! callback sub-menu(entry: MenuEntry) => {
//! if(entry.id == "0") { return [ { id: "1", title: "A" }, { id: "2", title: "B", has-sub-menu: true } ]; }
//! else if(entry.id == "2") { return [ { id: "3", title: "C" } ]; } else { return []; }
//! }
//! callback activated() => { if (entry.id == "2") { ... } }
//! if !Builtin.supports_native_menu_bar() : MenuBarImpl {
//! entries: parent.entries
@ -50,7 +56,7 @@
//! ## ContextMenuInternal
//!
//! ```slint
//! menu := ContextMenu {
//! menu := ContextMenuInternal {
//! entries: [...]
//! sub-menu => ...
//! activated => ...
@ -69,7 +75,7 @@
//! callback show(point) => { Builtin.show_context_menu(entries, sub-menu, activated, point) }
//! }
//!
//! ## ContextMenu
//! ## ContextMenuArea
//!
//! This is the same as ContextMenuInternal, but entries, sub-menu, and activated are generated
//! from the MenuItem similar to MenuBar
@ -95,6 +101,7 @@ struct UsefulMenuComponents {
context_menu_internal: ElementType,
empty: ElementType,
menu_entry: Type,
menu_item_element: ElementType,
}
pub async fn lower_menus(
@ -109,13 +116,29 @@ pub async fn lower_menus(
.import_component("std-widgets.slint", "MenuBarImpl", &mut build_diags_to_ignore)
.await
.expect("MenuBarImpl should be in std-widgets.slint");
let menu_item_element = type_loader
.global_type_registry
.borrow()
.lookup_builtin_element("ContextMenuArea")
.unwrap()
.as_builtin()
.additional_accepted_child_types
.get("Menu")
.expect("ContextMenuArea should accept Menu")
.additional_accepted_child_types
.get("MenuItem")
.expect("Menu should accept MenuItem")
.clone()
.into();
let useful_menu_component = UsefulMenuComponents {
menubar_impl: menubar_impl.clone().into(),
context_menu_internal: type_loader
.global_type_registry
.borrow()
.lookup_builtin_element("ContextMenuInternal")
.expect("ContextMenu is a builtin type"),
.expect("ContextMenuInternal is a builtin type"),
vertical_layout: type_loader
.global_type_registry
.borrow()
@ -123,6 +146,7 @@ pub async fn lower_menus(
.expect("VerticalLayout is a builtin type"),
empty: type_loader.global_type_registry.borrow().empty_type(),
menu_entry: type_loader.global_type_registry.borrow().lookup("MenuEntry"),
menu_item_element,
};
assert!(matches!(&useful_menu_component.menu_entry, Type::Struct(..)));
@ -133,7 +157,7 @@ pub async fn lower_menus(
if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
has_menu |= process_window(elem, &useful_menu_component, diag);
}
if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenu" | "ContextMenuInternal")) {
if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
has_menu |= process_context_menu(elem, &useful_menu_component, diag);
}
})
@ -161,7 +185,7 @@ pub async fn lower_menus(
}
recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenu" | "ContextMenuInternal"))
if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
{
process_context_menu(elem, &useful_menu_component, diag);
}
@ -178,33 +202,53 @@ fn process_context_menu(
let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");
let item_tree_root = if !is_internal {
// Lower MenuItem's into entries
let menu_item = context_menu_elem
// Lower Menu into entries
let menu_element_type = context_menu_elem
.borrow()
.base_type
.as_builtin()
.additional_accepted_child_types
.get("MenuItem")
.expect("ContextMenu should accept MenuItem")
.get("Menu")
.expect("ContextMenu should accept Menu")
.clone()
.into();
context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
let mut items = vec![];
let mut menu_elem = None;
context_menu_elem.borrow_mut().children.retain(|x| {
if x.borrow().base_type == menu_item {
items.push(x.clone());
if x.borrow().base_type == menu_element_type {
if menu_elem.is_some() {
diag.push_error(
"Only one Menu is allowed in a ContextMenu".into(),
&*x.borrow(),
);
} else {
menu_elem = Some(x.clone());
}
false
} else {
true
}
});
let item_tree_root = if !items.is_empty() {
lower_menu_items(context_menu_elem, items, components)
let item_tree_root = if let Some(menu_elem) = menu_elem {
if menu_elem.borrow().repeated.is_some() {
diag.push_error(
"ContextMenuArea's root Menu cannot be in a conditional or repeated element"
.into(),
&*menu_elem.borrow(),
);
}
let children = std::mem::take(&mut menu_elem.borrow_mut().children);
lower_menu_items(context_menu_elem, children, components)
.map(|c| Expression::ElementReference(Rc::downgrade(&c.root_element)))
} else {
diag.push_error(
"ContextMenuArea should have a Menu".into(),
&*context_menu_elem.borrow(),
);
None
};
@ -252,7 +296,7 @@ fn process_context_menu(
.bindings
.insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
if let Some(old) = old {
diag.push_error("'show' is not a callback in ContextMenu".into(), &old.borrow().span);
diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
}
true
@ -420,7 +464,7 @@ fn process_window(
true
}
/// Lower the MenuItem to either
/// Lower the MenuItem's and Menu's to either
/// - `entries` and `activated` and `sub-menu` properties/callback, in which cases it returns None
/// - or a Component which is a tree of MenuItem, in which case returns the component that is within the enclosing component's menu_item_trees
fn lower_menu_items(
@ -487,6 +531,8 @@ fn lower_menu_items(
));
element.borrow_mut().enclosing_component = component_weak.clone();
element.borrow_mut().geometry_props = None;
// Menu -> MenuItem
element.borrow_mut().base_type = components.menu_item_element.clone();
}
false
});
@ -550,7 +596,11 @@ fn generate_menu_entries(
for item in menu_items {
let mut borrow_mut = item.borrow_mut();
assert_eq!(borrow_mut.base_type.type_name(), Some("MenuItem"));
let base_name = borrow_mut.base_type.type_name().unwrap();
let is_sub_menu = base_name == "Menu";
if !is_sub_menu {
assert_eq!(base_name, "MenuItem");
}
borrow_mut
.enclosing_component
@ -577,9 +627,10 @@ fn generate_menu_entries(
state.activate.push((id_str.clone(), callback.into_inner().expression));
}
let sub_entries =
generate_menu_entries(std::mem::take(&mut borrow_mut.children).into_iter(), state);
if !sub_entries.is_empty() {
if is_sub_menu {
let sub_entries =
generate_menu_entries(std::mem::take(&mut borrow_mut.children).into_iter(), state);
state.sub_menu.push((
id_str,
Expression::Array { element_ty: state.menu_entry.clone(), values: sub_entries },

View file

@ -2,15 +2,16 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component A {
ContextMenu {
ContextMenuArea {
show => {
// ^error{'show' is not a callback in ContextMenu}
// ^error{'show' is not a callback in ContextMenuArea}
debug("hello");
}
MenuItem {
Menu {
}
property <int> entries: 45;
// ^error{Cannot re-define internal property 'entries'}
property <int> sub-menu: 45;
@ -21,5 +22,27 @@ export component A {
property <string> xyz: "me";
}
ContextMenuArea { Rectangle {} }
// ^error{ContextMenuArea should have a Menu}
ContextMenuArea {
Menu {
MenuItem { title: "ok"; }
}
Menu {
// ^error{Only one Menu is allowed in a ContextMenu}
MenuItem { title: "hello"; }
}
}
ContextMenuArea {
if false : Menu {}
// ^error{ContextMenuArea's root Menu cannot be in a conditional or repeated element}
}
ContextMenuArea {
for _ in [1,2,3] : Menu {}
// ^error{ContextMenuArea's root Menu cannot be in a conditional or repeated element}
}
}

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
export component A {
cb := ContextMenu {
cb := ContextMenuArea {
entries: [];
// ^error{Unknown property entries in ContextMenu}
sub-menu => {
@ -10,10 +10,15 @@ export component A {
debug("hello");
}
MenuItem {
Menu {
preferred-height: 45px;
// ^error{Unknown property preferred-height in Menu}
entries: [];
// ^error{Unknown property entries in MenuItem}
// ^error{Unknown property entries in Menu}
MenuItem {
entries: [];
// ^error{Unknown property entries in MenuItem}
title: "ok";
sub-menu => {}
// ^error{'sub-menu' is not a callback in MenuItem}
@ -23,14 +28,16 @@ export component A {
// ^error{Unknown property col in MenuItem}
}
}
MenuItem {}
// ^error{Unknown element 'MenuItem'}
}
TouchArea {
clicked => {
cb.activated({});
// ^error{Element 'ContextMenu' does not have a property 'activated'}
// ^error{Element 'ContextMenuArea' does not have a property 'activated'}
debug(cb.entries);
// ^error{Element 'ContextMenu' does not have a property 'entries'}
// ^error{Element 'ContextMenuArea' does not have a property 'entries'}
}
}
@ -38,5 +45,7 @@ export component A {
ContextMenuInternal {
// ^error{Unknown element 'ContextMenuInternal'}
}
ContextMenu {}
// ^error{Unknown element 'ContextMenu'}
}

View file

@ -9,7 +9,7 @@ export component MyMenu inherits MenuBar {
export component A inherits Window {
mb := MenuBar {
Rectangle {}
// ^error{Rectangle is not allowed within MenuBar. Only MenuItem are valid children}
// ^error{Rectangle is not allowed within MenuBar. Only Menu are valid children}
x: 45px;
// ^error{Unknown property x in MenuBar}
width: 45px;
@ -19,16 +19,16 @@ export component A inherits Window {
init => {}
// ^error{'init' is not a callback in MenuBar}
MenuItem {
Menu {
title: "hello";
x: 0px;
// ^error{Unknown property x in MenuItem}
// ^error{Unknown property x in Menu}
max-height: 13px;
// ^error{Unknown property max-height in MenuItem}
// ^error{Unknown property max-height in Menu}
opacity: 0.4;
// ^error{Unknown property opacity in MenuItem}
// ^error{Unknown property opacity in Menu}
Rectangle {
// ^error{Rectangle is not allowed within MenuItem. Only MenuItem are valid children}
// ^error{Rectangle is not allowed within Menu. Only Menu MenuItem are valid children}
}
MenuItem {
@ -36,9 +36,15 @@ export component A inherits Window {
mb.activated({});
// ^error{Element 'MenuBar' does not have a property 'activated'}
}
TouchArea {}
// ^error{MenuItem cannot have children elements}
}
}
MenuItem {}
// ^error{MenuItem is not allowed within MenuBar. Only Menu are valid children}
}
Rectangle {
@ -46,6 +52,15 @@ export component A inherits Window {
MenuBar {
// ^error{MenuBar can only be within a Window element}
}
MenuItem {
// ^error{Unknown element 'MenuItem'}
Menu {}
// ^error{Menu can only be within a ContextMenuArea element}
}
Menu {}
// ^error{Menu can only be within a ContextMenuArea element}
}
}

View file

@ -67,22 +67,24 @@ export component LineEditBase inherits Rectangle {
accessible-role: none;
}
ContextMenu {
MenuItem {
title: @tr("Cut");
activated => { text-input.cut(); }
}
MenuItem {
title: @tr("Copy");
activated => { text-input.copy(); }
}
MenuItem {
title: @tr("Paste");
activated => { text-input.paste(); }
}
MenuItem {
title: @tr("Select All");
activated => { text-input.select-all(); }
ContextMenuArea {
Menu {
MenuItem {
title: @tr("Cut");
activated => { text-input.cut(); }
}
MenuItem {
title: @tr("Copy");
activated => { text-input.copy(); }
}
MenuItem {
title: @tr("Paste");
activated => { text-input.paste(); }
}
MenuItem {
title: @tr("Select All");
activated => { text-input.select-all(); }
}
}
text-input := TextInput {

View file

@ -58,22 +58,24 @@ export component TextEditBase inherits Rectangle {
forward-focus: text-input;
ContextMenu {
MenuItem {
title: @tr("Cut");
activated => { text-input.cut(); }
}
MenuItem {
title: @tr("Copy");
activated => { text-input.copy(); }
}
MenuItem {
title: @tr("Paste");
activated => { text-input.paste(); }
}
MenuItem {
title: @tr("Select All");
activated => { text-input.select-all(); }
ContextMenuArea {
Menu {
MenuItem {
title: @tr("Cut");
activated => { text-input.cut(); }
}
MenuItem {
title: @tr("Copy");
activated => { text-input.copy(); }
}
MenuItem {
title: @tr("Paste");
activated => { text-input.paste(); }
}
MenuItem {
title: @tr("Select All");
activated => { text-input.select-all(); }
}
}
scroll-view := ScrollView {

View file

@ -139,22 +139,24 @@ export component TextEdit {
border-width: 1px;
}
ContextMenu {
MenuItem {
title: @tr("Cut");
activated => { text-input.cut(); }
}
MenuItem {
title: @tr("Copy");
activated => { text-input.copy(); }
}
MenuItem {
title: @tr("Paste");
activated => { text-input.paste(); }
}
MenuItem {
title: @tr("Select All");
activated => { text-input.select-all(); }
ContextMenuArea {
Menu {
MenuItem {
title: @tr("Cut");
activated => { text-input.cut(); }
}
MenuItem {
title: @tr("Copy");
activated => { text-input.copy(); }
}
MenuItem {
title: @tr("Paste");
activated => { text-input.paste(); }
}
MenuItem {
title: @tr("Select All");
activated => { text-input.select-all(); }
}
}
scroll-view := ScrollView {