i_slint_compiler/passes/
lower_menus.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! This pass lowers the `MenuBar` and `ContextMenuArea` as well as all their contents
5//!
6//! We can't have properties of type Model because that is not binary compatible with C++,
7//! so all the code that handle model of MenuEntry need to be handle by code in the generated code
8//! and transformed into a `SharedVector<MenuEntry>` that is passed to Slint runtime.
9//!
10//! ## MenuBar
11//!
12//! ```slint
13//! Window {
14//!      menu-bar := MenuBar {
15//!        Menu {
16//!           title: "File";
17//!           if cond1 : MenuItem {
18//!             title: "A";
19//!             activated => { debug("A") }
20//!           }
21//!           Menu {
22//!               title: "B";
23//!               for x in 42 : MenuItem { title: "C" + x; }
24//!           }
25//!        }
26//!      }
27//!      content := ...
28//! }
29//! ```
30//! Is transformed to
31//! ```slint
32//! Window {
33//!     menu-bar := VerticalLayout {
34//!        // these callbacks are connected by the setup_native_menu_bar call to an adapter from the menu tree
35//!        callback sub-menu(entry: MenuEntry);
36//!        callback activated();
37//!        if !Builtin.supports_native_menu_bar() : MenuBarImpl {
38//!           entries: parent.entries
39//!           sub-menu(..) => { parent.sub-menu(..) }
40//!           activated(..) => { parent.activated(..) }
41//!        }
42//!        Empty {
43//!           content := ...
44//!        }
45//!    }
46//!    init => {
47//!        // ... rest of init ...
48//!        // that function will always be called even for non-native.
49//!        // the menu-index is the index of the `Menu` element moved in the `object_tree::Component::menu_item_trees`
50//!        Builtin.setup_native_menu_bar(menu-bar.entries, menu-bar.sub-menu, menu-bar.activated, menu-index, no_native_menu)
51//!    }
52//! }
53//! ```
54//!
55//! ## ContextMenuInternal
56//!
57//! ```slint
58//! menu := ContextMenuInternal {
59//!     entries: [...]
60//!     sub-menu => ...
61//!     activated => ...
62//! }
63//! Button { clicked => {menu.show({x: 0, y: 0;})} }
64//! ```
65//! Is transformed to
66//!
67//! ```slint
68//! menu := ContextMenu {
69//!    property <[MenuEntry]> entries : ...
70//!    sub-menu => { ... }
71//!    activated => { ... }
72//!
73//!    // show is actually a callback called by the native code when right clicking
74//!    show(point) => { Builtin.show_popup_menu(self, self.entries, &self.sub-menu, &self.activated, point) }
75//! }
76//! ```
77//!
78//! ## ContextMenuArea
79//!
80//! This is the same as ContextMenuInternal, but entries, sub-menu, and activated are generated
81//! from the MenuItem similar to MenuBar
82//!
83//! We get a extra item tree in [`Component::menu_item_trees`]
84//! and the call to `show_popup_menu` will be responsible to set the callback handler to the
85//! `ContextMenu` item callbacks.
86//!
87//! ```slint
88//! // A `ContextMenuArea` with a complex Menu with `if` and `for` will be lowered to:
89//! menu := ContextMenu {
90//!    show(point) => {
91//!       // menu-index is an index in `Component::menu_item_trees`
92//!       // that function will set the handler to self.sub-menu and self.activated
93//!       Builtin.show_popup_menu(self, menu-index, &self.sub-menu, &self.activated, point)
94//!    }
95//! }
96//! ```
97//!
98
99use crate::diagnostics::{BuildDiagnostics, Spanned};
100use crate::expression_tree::{BuiltinFunction, Callable, Expression, NamedReference};
101use crate::langtype::{ElementType, Type};
102use crate::object_tree::*;
103use core::cell::RefCell;
104use i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
105use smol_str::{format_smolstr, SmolStr};
106use std::rc::{Rc, Weak};
107
108const HEIGHT: &str = "height";
109const ENTRIES: &str = "entries";
110const SUB_MENU: &str = "sub-menu";
111const ACTIVATED: &str = "activated";
112const SHOW: &str = "show";
113
114struct UsefulMenuComponents {
115    menubar_impl: ElementType,
116    vertical_layout: ElementType,
117    context_menu_internal: ElementType,
118    empty: ElementType,
119    menu_entry: Type,
120    menu_item_element: ElementType,
121}
122
123pub async fn lower_menus(
124    doc: &mut Document,
125    type_loader: &mut crate::typeloader::TypeLoader,
126    diag: &mut BuildDiagnostics,
127) {
128    // Ignore import errors
129    let mut build_diags_to_ignore = BuildDiagnostics::default();
130
131    let menubar_impl = type_loader
132        .import_component("std-widgets.slint", "MenuBarImpl", &mut build_diags_to_ignore)
133        .await
134        .expect("MenuBarImpl should be in std-widgets.slint");
135
136    let menu_item_element = type_loader
137        .global_type_registry
138        .borrow()
139        .lookup_builtin_element("ContextMenuArea")
140        .unwrap()
141        .as_builtin()
142        .additional_accepted_child_types
143        .get("Menu")
144        .expect("ContextMenuArea should accept Menu")
145        .additional_accepted_child_types
146        .get("MenuItem")
147        .expect("Menu should accept MenuItem")
148        .clone()
149        .into();
150
151    let useful_menu_component = UsefulMenuComponents {
152        menubar_impl: menubar_impl.clone().into(),
153        context_menu_internal: type_loader
154            .global_type_registry
155            .borrow()
156            .lookup_builtin_element("ContextMenuInternal")
157            .expect("ContextMenuInternal is a builtin type"),
158        vertical_layout: type_loader
159            .global_type_registry
160            .borrow()
161            .lookup_builtin_element("VerticalLayout")
162            .expect("VerticalLayout is a builtin type"),
163        empty: type_loader.global_type_registry.borrow().empty_type(),
164        menu_entry: type_loader.global_type_registry.borrow().lookup("MenuEntry"),
165        menu_item_element,
166    };
167    assert!(matches!(&useful_menu_component.menu_entry, Type::Struct(..)));
168
169    let mut has_menu = false;
170    let mut has_menubar = false;
171
172    doc.visit_all_used_components(|component| {
173        recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
174            if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
175                has_menubar |= process_window(elem, &useful_menu_component, type_loader.compiler_config.no_native_menu, diag);
176            }
177            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
178                has_menu |= process_context_menu(elem, &useful_menu_component, diag);
179            }
180        })
181    });
182
183    if has_menubar {
184        recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
185            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
186            {
187                has_menu |= process_context_menu(elem, &useful_menu_component, diag);
188            }
189        });
190    }
191    if has_menu {
192        let popup_menu_impl = type_loader
193            .import_component("std-widgets.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
194            .await
195            .expect("PopupMenuImpl should be in std-widgets.slint");
196        {
197            let mut root = popup_menu_impl.root_element.borrow_mut();
198
199            for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
200                match root.property_declarations.get_mut(prop) {
201                    Some(d) => d.expose_in_public_api = true,
202                    None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
203                }
204            }
205            root.property_analysis
206                .borrow_mut()
207                .entry(SmolStr::new_static(ENTRIES))
208                .or_default()
209                .is_set = true;
210        }
211
212        recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
213            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
214            {
215                process_context_menu(elem, &useful_menu_component, diag);
216            }
217        });
218        doc.popup_menu_impl = popup_menu_impl.into();
219    }
220}
221
222fn process_context_menu(
223    context_menu_elem: &ElementRc,
224    components: &UsefulMenuComponents,
225    diag: &mut BuildDiagnostics,
226) -> bool {
227    let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");
228
229    if is_internal && context_menu_elem.borrow().property_declarations.contains_key(ENTRIES) {
230        // Already processed;
231        return false;
232    }
233
234    // generate the show callback
235    let source_location = Some(context_menu_elem.borrow().to_source_location());
236    let position = Expression::FunctionParameterReference {
237        index: 0,
238        ty: crate::typeregister::logical_point_type().into(),
239    };
240    let expr = if !is_internal {
241        let menu_element_type = context_menu_elem
242            .borrow()
243            .base_type
244            .as_builtin()
245            .additional_accepted_child_types
246            .get("Menu")
247            .expect("ContextMenu should accept Menu")
248            .clone()
249            .into();
250
251        context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
252
253        let mut menu_elem = None;
254        context_menu_elem.borrow_mut().children.retain(|x| {
255            if x.borrow().base_type == menu_element_type {
256                if menu_elem.is_some() {
257                    diag.push_error(
258                        "Only one Menu is allowed in a ContextMenu".into(),
259                        &*x.borrow(),
260                    );
261                } else {
262                    menu_elem = Some(x.clone());
263                }
264                false
265            } else {
266                true
267            }
268        });
269
270        let Some(menu_elem) = menu_elem else {
271            diag.push_error(
272                "ContextMenuArea should have a Menu".into(),
273                &*context_menu_elem.borrow(),
274            );
275            return false;
276        };
277        if menu_elem.borrow().repeated.is_some() {
278            diag.push_error(
279                "ContextMenuArea's root Menu cannot be in a conditional or repeated element".into(),
280                &*menu_elem.borrow(),
281            );
282        }
283
284        let children = std::mem::take(&mut menu_elem.borrow_mut().children);
285        let c = lower_menu_items(context_menu_elem, children, components);
286        let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
287
288        for (name, _) in &components.context_menu_internal.property_list() {
289            if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
290                diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
291            }
292        }
293
294        Expression::FunctionCall {
295            function: BuiltinFunction::ShowPopupMenu.into(),
296            arguments: vec![
297                Expression::ElementReference(Rc::downgrade(context_menu_elem)),
298                item_tree_root,
299                position,
300            ],
301            source_location,
302        }
303    } else {
304        // `ContextMenuInternal`
305
306        // Materialize the entries property
307        context_menu_elem.borrow_mut().property_declarations.insert(
308            SmolStr::new_static(ENTRIES),
309            Type::Array(components.menu_entry.clone().into()).into(),
310        );
311        let entries = Expression::PropertyReference(NamedReference::new(
312            context_menu_elem,
313            SmolStr::new_static(ENTRIES),
314        ));
315
316        Expression::FunctionCall {
317            function: BuiltinFunction::ShowPopupMenuInternal.into(),
318            arguments: vec![
319                Expression::ElementReference(Rc::downgrade(context_menu_elem)),
320                entries,
321                position,
322            ],
323            source_location,
324        }
325    };
326
327    let old = context_menu_elem
328        .borrow_mut()
329        .bindings
330        .insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
331    if let Some(old) = old {
332        diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
333    }
334
335    true
336}
337
338fn process_window(
339    win: &ElementRc,
340    components: &UsefulMenuComponents,
341    no_native_menu: bool,
342    diag: &mut BuildDiagnostics,
343) -> bool {
344    let mut window = win.borrow_mut();
345    let mut menu_bar = None;
346    window.children.retain(|x| {
347        if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
348            if menu_bar.is_some() {
349                diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
350            } else {
351                menu_bar = Some(x.clone());
352            }
353            false
354        } else {
355            true
356        }
357    });
358
359    let Some(menu_bar) = menu_bar else {
360        return false;
361    };
362    let repeated = menu_bar.borrow_mut().repeated.take();
363    let mut condition = repeated.map(|repeated| {
364        if !repeated.is_conditional_element {
365            diag.push_error("MenuBar cannot be in a repeated element".into(), &*menu_bar.borrow());
366        }
367        repeated.model
368    });
369    let original_cond = condition.clone();
370
371    // Lower MenuItem's into a tree root
372    let children = std::mem::take(&mut menu_bar.borrow_mut().children);
373    let c = lower_menu_items(&menu_bar, children, components);
374    let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
375
376    if !no_native_menu {
377        let supportes_native_menu_bar = Expression::UnaryOp {
378            op: '!',
379            sub: Expression::FunctionCall {
380                function: BuiltinFunction::SupportsNativeMenuBar.into(),
381                arguments: vec![],
382                source_location: None,
383            }
384            .into(),
385        };
386        condition = match condition {
387            Some(condition) => Some(
388                Expression::BinaryExpression {
389                    lhs: condition.into(),
390                    rhs: supportes_native_menu_bar.into(),
391                    op: '&',
392                }
393                .into(),
394            ),
395            None => Some(supportes_native_menu_bar.into()),
396        };
397    }
398
399    let menubar_impl = Element {
400        id: format_smolstr!("{}-menulayout", window.id),
401        base_type: components.menubar_impl.clone(),
402        enclosing_component: window.enclosing_component.clone(),
403        repeated: condition.clone().map(|condition| crate::object_tree::RepeatedElementInfo {
404            model: condition,
405            model_data_id: SmolStr::default(),
406            index_id: SmolStr::default(),
407            is_conditional_element: true,
408            is_listview: None,
409        }),
410        ..Default::default()
411    }
412    .make_rc();
413
414    // Create a child that contains all the children of the window but the menubar
415    let child = Element {
416        id: format_smolstr!("{}-child", window.id),
417        base_type: components.empty.clone(),
418        enclosing_component: window.enclosing_component.clone(),
419        children: std::mem::take(&mut window.children),
420        ..Default::default()
421    }
422    .make_rc();
423
424    let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
425
426    let source_location = Some(menu_bar.borrow().to_source_location());
427
428    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
429        // materialize the properties and callbacks
430        let ty = components.menubar_impl.lookup_property(prop).property_type;
431        assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
432        let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
433        let forward_expr = if let Type::Callback(cb) = &ty {
434            Expression::FunctionCall {
435                function: Callable::Callback(nr),
436                arguments: cb
437                    .args
438                    .iter()
439                    .enumerate()
440                    .map(|(index, ty)| Expression::FunctionParameterReference {
441                        index,
442                        ty: ty.clone(),
443                    })
444                    .collect(),
445                source_location: source_location.clone(),
446            }
447        } else {
448            Expression::PropertyReference(nr)
449        };
450        menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
451        let old = menu_bar
452            .borrow_mut()
453            .property_declarations
454            .insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
455        if let Some(old) = old {
456            diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
457        }
458    }
459
460    // Transform the MenuBar in a layout
461    menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
462    menu_bar.borrow_mut().children = vec![menubar_impl, child];
463
464    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
465        menu_bar
466            .borrow()
467            .property_analysis
468            .borrow_mut()
469            .entry(SmolStr::new_static(prop))
470            .or_default()
471            .is_set = true;
472    }
473
474    window.children.push(menu_bar.clone());
475    let component = window.enclosing_component.upgrade().unwrap();
476    drop(window);
477
478    // Rename every access to `root.height` into `child.height`
479    let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
480    crate::object_tree::visit_all_named_references(&component, &mut |nr| {
481        if nr == &win_height {
482            *nr = child_height.clone()
483        }
484    });
485    // except for the actual geometry
486    win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
487
488    let mut arguments = vec![
489        Expression::PropertyReference(NamedReference::new(&menu_bar, SmolStr::new_static(ENTRIES))),
490        Expression::PropertyReference(NamedReference::new(
491            &menu_bar,
492            SmolStr::new_static(SUB_MENU),
493        )),
494        Expression::PropertyReference(NamedReference::new(
495            &menu_bar,
496            SmolStr::new_static(ACTIVATED),
497        )),
498        item_tree_root.into(),
499        Expression::BoolLiteral(no_native_menu),
500    ];
501
502    if let Some(condition) = original_cond {
503        arguments.push(condition);
504    }
505
506    let setup_menubar = Expression::FunctionCall {
507        function: BuiltinFunction::SetupMenuBar.into(),
508        arguments,
509        source_location,
510    };
511    component.init_code.borrow_mut().constructor_code.push(setup_menubar.into());
512
513    true
514}
515
516/// Lower the MenuItem's and Menu's to either
517///  - `entries` and `activated` and `sub-menu` properties/callback, in which cases it returns None
518///  - 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
519fn lower_menu_items(
520    parent: &ElementRc,
521    children: Vec<ElementRc>,
522    components: &UsefulMenuComponents,
523) -> Rc<Component> {
524    let component = Rc::new_cyclic(|component_weak| {
525        let root_element = Rc::new(RefCell::new(Element {
526            base_type: components.empty.clone(),
527            children,
528            enclosing_component: component_weak.clone(),
529            ..Default::default()
530        }));
531        recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
532            if !is_root {
533                debug_assert!(Weak::ptr_eq(
534                    &element.borrow().enclosing_component,
535                    &parent.borrow().enclosing_component
536                ));
537                element.borrow_mut().enclosing_component = component_weak.clone();
538                element.borrow_mut().geometry_props = None;
539                if element.borrow().base_type.type_name() == Some("MenuSeparator") {
540                    element.borrow_mut().bindings.insert(
541                        "title".into(),
542                        RefCell::new(
543                            Expression::StringLiteral(SmolStr::new_static(
544                                MENU_SEPARATOR_PLACEHOLDER_TITLE,
545                            ))
546                            .into(),
547                        ),
548                    );
549                }
550                // Menu/MenuSeparator -> MenuItem
551                element.borrow_mut().base_type = components.menu_item_element.clone();
552            }
553            false
554        });
555        Component {
556            id: SmolStr::default(),
557            root_element,
558            parent_element: Rc::downgrade(parent),
559            ..Default::default()
560        }
561    });
562    parent
563        .borrow()
564        .enclosing_component
565        .upgrade()
566        .unwrap()
567        .menu_item_tree
568        .borrow_mut()
569        .push(component.clone());
570    component
571}