slint/internal/compiler/passes/lower_menus.rs
2025-08-18 11:17:57 +02:00

571 lines
20 KiB
Rust

// 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
//! This pass lowers 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
//! and transformed into a `SharedVector<MenuEntry>` that is passed to Slint runtime.
//!
//! ## MenuBar
//!
//! ```slint
//! Window {
//! menu-bar := MenuBar {
//! Menu {
//! title: "File";
//! if cond1 : MenuItem {
//! title: "A";
//! activated => { debug("A") }
//! }
//! Menu {
//! title: "B";
//! for x in 42 : MenuItem { title: "C" + x; }
//! }
//! }
//! }
//! content := ...
//! }
//! ```
//! Is transformed to
//! ```slint
//! Window {
//! menu-bar := VerticalLayout {
//! // these callbacks are connected by the setup_native_menu_bar call to an adapter from the menu tree
//! callback sub-menu(entry: MenuEntry);
//! callback activated();
//! if !Builtin.supports_native_menu_bar() : MenuBarImpl {
//! entries: parent.entries
//! sub-menu(..) => { parent.sub-menu(..) }
//! activated(..) => { parent.activated(..) }
//! }
//! Empty {
//! content := ...
//! }
//! }
//! init => {
//! // ... rest of init ...
//! // that function will always be called even for non-native.
//! // the menu-index is the index of the `Menu` element moved in the `object_tree::Component::menu_item_trees`
//! Builtin.setup_native_menu_bar(menu-bar.entries, menu-bar.sub-menu, menu-bar.activated, menu-index, no_native_menu)
//! }
//! }
//! ```
//!
//! ## ContextMenuInternal
//!
//! ```slint
//! menu := ContextMenuInternal {
//! entries: [...]
//! sub-menu => ...
//! activated => ...
//! }
//! Button { clicked => {menu.show({x: 0, y: 0;})} }
//! ```
//! Is transformed to
//!
//! ```slint
//! menu := ContextMenu {
//! property <[MenuEntry]> entries : ...
//! sub-menu => { ... }
//! activated => { ... }
//!
//! // show is actually a callback called by the native code when right clicking
//! show(point) => { Builtin.show_popup_menu(self, self.entries, &self.sub-menu, &self.activated, point) }
//! }
//! ```
//!
//! ## ContextMenuArea
//!
//! This is the same as ContextMenuInternal, but entries, sub-menu, and activated are generated
//! from the MenuItem similar to MenuBar
//!
//! We get a extra item tree in [`Component::menu_item_trees`]
//! and the call to `show_popup_menu` will be responsible to set the callback handler to the
//! `ContextMenu` item callbacks.
//!
//! ```slint
//! // A `ContextMenuArea` with a complex Menu with `if` and `for` will be lowered to:
//! menu := ContextMenu {
//! show(point) => {
//! // menu-index is an index in `Component::menu_item_trees`
//! // that function will set the handler to self.sub-menu and self.activated
//! Builtin.show_popup_menu(self, menu-index, &self.sub-menu, &self.activated, point)
//! }
//! }
//! ```
//!
use crate::diagnostics::{BuildDiagnostics, Spanned};
use crate::expression_tree::{BuiltinFunction, Callable, Expression, NamedReference};
use crate::langtype::{ElementType, Type};
use crate::object_tree::*;
use core::cell::RefCell;
use i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
use smol_str::{format_smolstr, SmolStr};
use std::rc::{Rc, Weak};
const HEIGHT: &str = "height";
const ENTRIES: &str = "entries";
const SUB_MENU: &str = "sub-menu";
const ACTIVATED: &str = "activated";
const SHOW: &str = "show";
struct UsefulMenuComponents {
menubar_impl: ElementType,
vertical_layout: ElementType,
context_menu_internal: ElementType,
empty: ElementType,
menu_entry: Type,
menu_item_element: ElementType,
}
pub async fn lower_menus(
doc: &mut Document,
type_loader: &mut crate::typeloader::TypeLoader,
diag: &mut BuildDiagnostics,
) {
// Ignore import errors
let mut build_diags_to_ignore = BuildDiagnostics::default();
let menubar_impl = type_loader
.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("ContextMenuInternal is a builtin type"),
vertical_layout: type_loader
.global_type_registry
.borrow()
.lookup_builtin_element("VerticalLayout")
.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(..)));
let mut has_menu = false;
let mut has_menubar = false;
doc.visit_all_used_components(|component| {
recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
has_menubar |= process_window(elem, &useful_menu_component, type_loader.compiler_config.no_native_menu, diag);
}
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);
}
})
});
if has_menubar {
recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
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);
}
});
}
if has_menu {
let popup_menu_impl = type_loader
.import_component("std-widgets.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
.await
.expect("PopupMenuImpl should be in std-widgets.slint");
{
let mut root = popup_menu_impl.root_element.borrow_mut();
for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
match root.property_declarations.get_mut(prop) {
Some(d) => d.expose_in_public_api = true,
None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
}
}
root.property_analysis
.borrow_mut()
.entry(SmolStr::new_static(ENTRIES))
.or_default()
.is_set = true;
}
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(), "ContextMenuArea" | "ContextMenuInternal"))
{
process_context_menu(elem, &useful_menu_component, diag);
}
});
doc.popup_menu_impl = popup_menu_impl.into();
}
}
fn process_context_menu(
context_menu_elem: &ElementRc,
components: &UsefulMenuComponents,
diag: &mut BuildDiagnostics,
) -> bool {
let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");
if is_internal && context_menu_elem.borrow().property_declarations.contains_key(ENTRIES) {
// Already processed;
return false;
}
// generate the show callback
let source_location = Some(context_menu_elem.borrow().to_source_location());
let position = Expression::FunctionParameterReference {
index: 0,
ty: crate::typeregister::logical_point_type(),
};
let expr = if !is_internal {
let menu_element_type = context_menu_elem
.borrow()
.base_type
.as_builtin()
.additional_accepted_child_types
.get("Menu")
.expect("ContextMenu should accept Menu")
.clone()
.into();
context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
let mut menu_elem = None;
context_menu_elem.borrow_mut().children.retain(|x| {
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 Some(menu_elem) = menu_elem else {
diag.push_error(
"ContextMenuArea should have a Menu".into(),
&*context_menu_elem.borrow(),
);
return false;
};
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);
let c = lower_menu_items(context_menu_elem, children, components);
let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
for (name, _) in &components.context_menu_internal.property_list() {
if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
}
}
Expression::FunctionCall {
function: BuiltinFunction::ShowPopupMenu.into(),
arguments: vec![
Expression::ElementReference(Rc::downgrade(context_menu_elem)),
item_tree_root,
position,
],
source_location,
}
} else {
// `ContextMenuInternal`
// Materialize the entries property
context_menu_elem.borrow_mut().property_declarations.insert(
SmolStr::new_static(ENTRIES),
Type::Array(components.menu_entry.clone().into()).into(),
);
let entries = Expression::PropertyReference(NamedReference::new(
context_menu_elem,
SmolStr::new_static(ENTRIES),
));
Expression::FunctionCall {
function: BuiltinFunction::ShowPopupMenuInternal.into(),
arguments: vec![
Expression::ElementReference(Rc::downgrade(context_menu_elem)),
entries,
position,
],
source_location,
}
};
let old = context_menu_elem
.borrow_mut()
.bindings
.insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
if let Some(old) = old {
diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
}
true
}
fn process_window(
win: &ElementRc,
components: &UsefulMenuComponents,
no_native_menu: bool,
diag: &mut BuildDiagnostics,
) -> bool {
let mut window = win.borrow_mut();
let mut menu_bar = None;
window.children.retain(|x| {
if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
if menu_bar.is_some() {
diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
} else {
menu_bar = Some(x.clone());
}
false
} else {
true
}
});
let Some(menu_bar) = menu_bar else {
return false;
};
let repeated = menu_bar.borrow_mut().repeated.take();
let mut condition = repeated.map(|repeated| {
if !repeated.is_conditional_element {
diag.push_error("MenuBar cannot be in a repeated element".into(), &*menu_bar.borrow());
}
repeated.model
});
let original_cond = condition.clone();
// Lower MenuItem's into a tree root
let children = std::mem::take(&mut menu_bar.borrow_mut().children);
let c = lower_menu_items(&menu_bar, children, components);
let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
if !no_native_menu {
let supportes_native_menu_bar = Expression::UnaryOp {
op: '!',
sub: Expression::FunctionCall {
function: BuiltinFunction::SupportsNativeMenuBar.into(),
arguments: vec![],
source_location: None,
}
.into(),
};
condition = match condition {
Some(condition) => Some(
Expression::BinaryExpression {
lhs: condition.into(),
rhs: supportes_native_menu_bar.into(),
op: '&',
}
.into(),
),
None => Some(supportes_native_menu_bar.into()),
};
}
let menubar_impl = Element {
id: format_smolstr!("{}-menulayout", window.id),
base_type: components.menubar_impl.clone(),
enclosing_component: window.enclosing_component.clone(),
repeated: condition.clone().map(|condition| crate::object_tree::RepeatedElementInfo {
model: condition,
model_data_id: SmolStr::default(),
index_id: SmolStr::default(),
is_conditional_element: true,
is_listview: None,
}),
..Default::default()
}
.make_rc();
// Create a child that contains all the children of the window but the menubar
let child = Element {
id: format_smolstr!("{}-child", window.id),
base_type: components.empty.clone(),
enclosing_component: window.enclosing_component.clone(),
children: std::mem::take(&mut window.children),
..Default::default()
}
.make_rc();
let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
let source_location = Some(menu_bar.borrow().to_source_location());
for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
// materialize the properties and callbacks
let ty = components.menubar_impl.lookup_property(prop).property_type;
assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
let forward_expr = if let Type::Callback(cb) = &ty {
Expression::FunctionCall {
function: Callable::Callback(nr),
arguments: cb
.args
.iter()
.enumerate()
.map(|(index, ty)| Expression::FunctionParameterReference {
index,
ty: ty.clone(),
})
.collect(),
source_location: source_location.clone(),
}
} else {
Expression::PropertyReference(nr)
};
menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
let old = menu_bar
.borrow_mut()
.property_declarations
.insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
if let Some(old) = old {
diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
}
}
// Transform the MenuBar in a layout
menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
menu_bar.borrow_mut().children = vec![menubar_impl, child];
for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
menu_bar
.borrow()
.property_analysis
.borrow_mut()
.entry(SmolStr::new_static(prop))
.or_default()
.is_set = true;
}
window.children.push(menu_bar.clone());
let component = window.enclosing_component.upgrade().unwrap();
drop(window);
// Rename every access to `root.height` into `child.height`
let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
crate::object_tree::visit_all_named_references(&component, &mut |nr| {
if nr == &win_height {
*nr = child_height.clone()
}
});
// except for the actual geometry
win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
let mut arguments = vec![
Expression::PropertyReference(NamedReference::new(&menu_bar, SmolStr::new_static(ENTRIES))),
Expression::PropertyReference(NamedReference::new(
&menu_bar,
SmolStr::new_static(SUB_MENU),
)),
Expression::PropertyReference(NamedReference::new(
&menu_bar,
SmolStr::new_static(ACTIVATED),
)),
item_tree_root.into(),
Expression::BoolLiteral(no_native_menu),
];
if let Some(condition) = original_cond {
arguments.push(condition);
}
let setup_menubar = Expression::FunctionCall {
function: BuiltinFunction::SetupMenuBar.into(),
arguments,
source_location,
};
component.init_code.borrow_mut().constructor_code.push(setup_menubar.into());
true
}
/// 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(
parent: &ElementRc,
children: Vec<ElementRc>,
components: &UsefulMenuComponents,
) -> Rc<Component> {
let component = Rc::new_cyclic(|component_weak| {
let root_element = Rc::new(RefCell::new(Element {
base_type: components.empty.clone(),
children,
enclosing_component: component_weak.clone(),
..Default::default()
}));
recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
if !is_root {
debug_assert!(Weak::ptr_eq(
&element.borrow().enclosing_component,
&parent.borrow().enclosing_component
));
element.borrow_mut().enclosing_component = component_weak.clone();
element.borrow_mut().geometry_props = None;
if element.borrow().base_type.type_name() == Some("MenuSeparator") {
element.borrow_mut().bindings.insert(
"title".into(),
RefCell::new(
Expression::StringLiteral(SmolStr::new_static(
MENU_SEPARATOR_PLACEHOLDER_TITLE,
))
.into(),
),
);
}
// Menu/MenuSeparator -> MenuItem
element.borrow_mut().base_type = components.menu_item_element.clone();
}
false
});
Component {
id: SmolStr::default(),
root_element,
parent_element: Rc::downgrade(parent),
..Default::default()
}
});
parent
.borrow()
.enclosing_component
.upgrade()
.unwrap()
.menu_item_tree
.borrow_mut()
.push(component.clone());
component
}