Skip to main content

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::{SmolStr, format_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    // First check if any MenuBar or ContextMenuArea is used - avoid loading std-widgets.slint if not needed
129    let mut has_menubar_or_context_menu = false;
130    doc.visit_all_used_components(|component| {
131        recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
132            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "MenuBar" | "ContextMenuArea" | "ContextMenuInternal")) {
133                has_menubar_or_context_menu = true;
134            }
135        })
136    });
137
138    if !has_menubar_or_context_menu {
139        return;
140    }
141
142    // Ignore import errors
143    let mut build_diags_to_ignore = BuildDiagnostics::default();
144
145    let menubar_impl = type_loader
146        .import_component("std-widgets.slint", "MenuBarImpl", &mut build_diags_to_ignore)
147        .await
148        .expect("MenuBarImpl should be in std-widgets.slint");
149
150    let menu_item_element = type_loader
151        .global_type_registry
152        .borrow()
153        .lookup_builtin_element("ContextMenuArea")
154        .unwrap()
155        .as_builtin()
156        .additional_accepted_child_types
157        .get("Menu")
158        .expect("ContextMenuArea should accept Menu")
159        .additional_accepted_child_types
160        .get("MenuItem")
161        .expect("Menu should accept MenuItem")
162        .clone()
163        .into();
164
165    let useful_menu_component = UsefulMenuComponents {
166        menubar_impl: menubar_impl.clone().into(),
167        context_menu_internal: type_loader
168            .global_type_registry
169            .borrow()
170            .lookup_builtin_element("ContextMenuInternal")
171            .expect("ContextMenuInternal is a builtin type"),
172        vertical_layout: type_loader
173            .global_type_registry
174            .borrow()
175            .lookup_builtin_element("VerticalLayout")
176            .expect("VerticalLayout is a builtin type"),
177        empty: type_loader.global_type_registry.borrow().empty_type(),
178        menu_entry: type_loader.global_type_registry.borrow().lookup("MenuEntry"),
179        menu_item_element,
180    };
181    assert!(matches!(&useful_menu_component.menu_entry, Type::Struct(..)));
182
183    let mut has_menu = false;
184    let mut has_menubar = false;
185
186    doc.visit_all_used_components(|component| {
187        recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
188            if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
189                has_menubar |= process_window(elem, &useful_menu_component, type_loader.compiler_config.no_native_menu, diag);
190            }
191            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
192                has_menu |= process_context_menu(elem, &useful_menu_component, diag);
193            }
194        })
195    });
196
197    if has_menubar {
198        recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
199            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
200            {
201                has_menu |= process_context_menu(elem, &useful_menu_component, diag);
202            }
203        });
204    }
205    if has_menu {
206        let popup_menu_impl = type_loader
207            .import_component("std-widgets.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
208            .await
209            .expect("PopupMenuImpl should be in std-widgets.slint");
210        {
211            let mut root = popup_menu_impl.root_element.borrow_mut();
212
213            for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
214                match root.property_declarations.get_mut(prop) {
215                    Some(d) => d.expose_in_public_api = true,
216                    None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
217                }
218            }
219            root.property_analysis
220                .borrow_mut()
221                .entry(SmolStr::new_static(ENTRIES))
222                .or_default()
223                .is_set = true;
224        }
225
226        recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
227            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
228            {
229                process_context_menu(elem, &useful_menu_component, diag);
230            }
231        });
232        doc.popup_menu_impl = popup_menu_impl.into();
233    }
234}
235
236fn process_context_menu(
237    context_menu_elem: &ElementRc,
238    components: &UsefulMenuComponents,
239    diag: &mut BuildDiagnostics,
240) -> bool {
241    let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");
242
243    if is_internal && context_menu_elem.borrow().property_declarations.contains_key(ENTRIES) {
244        // Already processed;
245        return false;
246    }
247
248    // generate the show callback
249    let source_location = Some(context_menu_elem.borrow().to_source_location());
250    let position = Expression::FunctionParameterReference {
251        index: 0,
252        ty: crate::typeregister::logical_point_type().into(),
253    };
254    let expr = if !is_internal {
255        let menu_element_type = context_menu_elem
256            .borrow()
257            .base_type
258            .as_builtin()
259            .additional_accepted_child_types
260            .get("Menu")
261            .expect("ContextMenu should accept Menu")
262            .clone()
263            .into();
264
265        let mut menu_elem: Option<Rc<RefCell<Element>>> = None;
266        context_menu_elem.borrow_mut().children.retain(|x| {
267            if x.borrow().base_type == menu_element_type {
268                if let Some(ref existing) = menu_elem {
269                    diag.push_error(
270                        "Only one Menu is allowed in a ContextMenu".into(),
271                        &*x.borrow(),
272                    );
273                    diag.push_note("First Menu defined here".into(), &*existing.borrow());
274                } else {
275                    menu_elem = Some(x.clone());
276                }
277                false
278            } else {
279                true
280            }
281        });
282
283        let Some(menu_elem) = menu_elem else {
284            diag.push_error(
285                "ContextMenuArea should have a Menu".into(),
286                &*context_menu_elem.borrow(),
287            );
288            return false;
289        };
290        if menu_elem.borrow().repeated.is_some() {
291            diag.push_error(
292                "ContextMenuArea's root Menu cannot be in a conditional or repeated element".into(),
293                &*menu_elem.borrow(),
294            );
295        }
296
297        let children = std::mem::take(&mut menu_elem.borrow_mut().children);
298        let c = lower_menu_items(context_menu_elem, children, components, diag);
299        let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
300
301        context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
302        for (name, _) in &components.context_menu_internal.property_list() {
303            if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
304                diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
305            }
306        }
307
308        Expression::FunctionCall {
309            function: BuiltinFunction::ShowPopupMenu.into(),
310            arguments: vec![
311                Expression::ElementReference(Rc::downgrade(context_menu_elem)),
312                item_tree_root,
313                position,
314            ],
315            source_location,
316        }
317    } else {
318        // `ContextMenuInternal`
319
320        // Materialize the entries property
321        context_menu_elem.borrow_mut().property_declarations.insert(
322            SmolStr::new_static(ENTRIES),
323            Type::Array(components.menu_entry.clone().into()).into(),
324        );
325        let entries = Expression::PropertyReference(NamedReference::new(
326            context_menu_elem,
327            SmolStr::new_static(ENTRIES),
328        ));
329
330        Expression::FunctionCall {
331            function: BuiltinFunction::ShowPopupMenuInternal.into(),
332            arguments: vec![
333                Expression::ElementReference(Rc::downgrade(context_menu_elem)),
334                entries,
335                position,
336            ],
337            source_location,
338        }
339    };
340
341    let old = context_menu_elem
342        .borrow_mut()
343        .bindings
344        .insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
345    if let Some(old) = old {
346        diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
347    }
348
349    true
350}
351
352fn process_window(
353    win: &ElementRc,
354    components: &UsefulMenuComponents,
355    no_native_menu: bool,
356    diag: &mut BuildDiagnostics,
357) -> bool {
358    let mut menu_bar: Option<Rc<RefCell<Element>>> = None;
359    win.borrow_mut().children.retain(|x| {
360        if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
361            if let Some(ref menu_bar) = menu_bar {
362                diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
363                diag.push_note("First MenuBar defined here".into(), &*menu_bar.borrow());
364            } else {
365                menu_bar = Some(x.clone());
366            }
367            false
368        } else {
369            true
370        }
371    });
372
373    let Some(menu_bar) = menu_bar else {
374        return false;
375    };
376    let repeated = menu_bar.borrow_mut().repeated.take();
377    let mut condition = repeated.map(|repeated| {
378        if !repeated.is_conditional_element {
379            diag.push_error("MenuBar cannot be in a repeated element".into(), &*menu_bar.borrow());
380        }
381        repeated.model
382    });
383    let original_cond = condition.clone();
384
385    // Lower MenuItem's into a tree root
386    let children = std::mem::take(&mut menu_bar.borrow_mut().children);
387    let c = lower_menu_items(&menu_bar, children, components, diag);
388    let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
389
390    if !no_native_menu {
391        let supportes_native_menu_bar = Expression::UnaryOp {
392            op: '!',
393            sub: Expression::FunctionCall {
394                function: BuiltinFunction::SupportsNativeMenuBar.into(),
395                arguments: Vec::new(),
396                source_location: None,
397            }
398            .into(),
399        };
400        condition = match condition {
401            Some(condition) => Some(Expression::BinaryExpression {
402                lhs: condition.into(),
403                rhs: supportes_native_menu_bar.into(),
404                op: '&',
405            }),
406            None => Some(supportes_native_menu_bar),
407        };
408    }
409
410    let mut window = win.borrow_mut();
411    let menubar_impl = Element {
412        id: format_smolstr!("{}-menulayout", window.id),
413        base_type: components.menubar_impl.clone(),
414        enclosing_component: window.enclosing_component.clone(),
415        repeated: condition.clone().map(|condition| crate::object_tree::RepeatedElementInfo {
416            model: condition,
417            model_data_id: SmolStr::default(),
418            index_id: SmolStr::default(),
419            is_conditional_element: true,
420            is_listview: None,
421        }),
422        ..Default::default()
423    }
424    .make_rc();
425
426    // Create a child that contains all the children of the window but the menubar
427    let child = Element {
428        id: format_smolstr!("{}-child", window.id),
429        base_type: components.empty.clone(),
430        enclosing_component: window.enclosing_component.clone(),
431        children: std::mem::take(&mut window.children),
432        ..Default::default()
433    }
434    .make_rc();
435
436    let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
437
438    let source_location = Some(menu_bar.borrow().to_source_location());
439
440    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
441        // materialize the properties and callbacks
442        let ty = components.menubar_impl.lookup_property(prop).property_type;
443        assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
444        let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
445        let forward_expr = if let Type::Callback(cb) = &ty {
446            Expression::FunctionCall {
447                function: Callable::Callback(nr),
448                arguments: cb
449                    .args
450                    .iter()
451                    .enumerate()
452                    .map(|(index, ty)| Expression::FunctionParameterReference {
453                        index,
454                        ty: ty.clone(),
455                    })
456                    .collect(),
457                source_location: source_location.clone(),
458            }
459        } else {
460            Expression::PropertyReference(nr)
461        };
462        menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
463        let old = menu_bar
464            .borrow_mut()
465            .property_declarations
466            .insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
467        if let Some(old) = old {
468            diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
469        }
470    }
471
472    // Transform the MenuBar in a layout
473    menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
474    menu_bar.borrow_mut().children = vec![menubar_impl, child];
475
476    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
477        menu_bar
478            .borrow()
479            .property_analysis
480            .borrow_mut()
481            .entry(SmolStr::new_static(prop))
482            .or_default()
483            .is_set = true;
484    }
485
486    window.children.push(menu_bar.clone());
487    let component = window.enclosing_component.upgrade().unwrap();
488    drop(window);
489
490    // Rename every access to `root.height` into `child.height`
491    let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
492    crate::object_tree::visit_all_named_references(&component, &mut |nr| {
493        if nr == &win_height {
494            *nr = child_height.clone()
495        }
496    });
497    // except for the actual geometry
498    win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
499
500    let mut arguments = vec![
501        Expression::PropertyReference(NamedReference::new(&menu_bar, SmolStr::new_static(ENTRIES))),
502        Expression::PropertyReference(NamedReference::new(
503            &menu_bar,
504            SmolStr::new_static(SUB_MENU),
505        )),
506        Expression::PropertyReference(NamedReference::new(
507            &menu_bar,
508            SmolStr::new_static(ACTIVATED),
509        )),
510        item_tree_root,
511        Expression::BoolLiteral(no_native_menu),
512    ];
513
514    if let Some(condition) = original_cond {
515        arguments.push(condition);
516    }
517
518    let setup_menubar = Expression::FunctionCall {
519        function: BuiltinFunction::SetupMenuBar.into(),
520        arguments,
521        source_location,
522    };
523    component.init_code.borrow_mut().constructor_code.push(setup_menubar);
524
525    true
526}
527
528/// Lower the MenuItem's and Menu's to either
529///  - `entries` and `activated` and `sub-menu` properties/callback, in which cases it returns None
530///  - 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
531fn lower_menu_items(
532    parent: &ElementRc,
533    children: Vec<ElementRc>,
534    components: &UsefulMenuComponents,
535    diag: &mut BuildDiagnostics,
536) -> Rc<Component> {
537    let in_menubar = parent.borrow().base_type.type_name() == Some("MenuBar");
538    let component = Rc::new_cyclic(|component_weak| {
539        let root_element = Rc::new(RefCell::new(Element {
540            base_type: components.empty.clone(),
541            children,
542            enclosing_component: component_weak.clone(),
543            ..Default::default()
544        }));
545        recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
546            if !is_root {
547                debug_assert!(Weak::ptr_eq(
548                    &element.borrow().enclosing_component,
549                    &parent.borrow().enclosing_component
550                ));
551                element.borrow_mut().enclosing_component = component_weak.clone();
552                element.borrow_mut().geometry_props = None;
553
554                if !in_menubar && let Some(binding) = element.borrow().bindings.get("shortcut") {
555                    diag.push_error(
556                        "MenuItem shortcuts are currently only supported in the MenuBar".into(),
557                        &*binding.borrow(),
558                    );
559                }
560
561                if element.borrow().base_type.type_name() == Some("MenuSeparator") {
562                    element.borrow_mut().bindings.insert(
563                        "title".into(),
564                        RefCell::new(
565                            Expression::StringLiteral(SmolStr::new_static(
566                                MENU_SEPARATOR_PLACEHOLDER_TITLE,
567                            ))
568                            .into(),
569                        ),
570                    );
571                }
572                // Menu/MenuSeparator -> MenuItem
573                element.borrow_mut().base_type = components.menu_item_element.clone();
574            }
575            false
576        });
577        Component {
578            id: SmolStr::default(),
579            root_element,
580            parent_element: RefCell::new(Rc::downgrade(parent)),
581            ..Default::default()
582        }
583    });
584    let enclosing = parent.borrow().enclosing_component.upgrade().unwrap();
585
586    super::lower_popups::check_no_reference_to_popup(
587        parent,
588        &enclosing,
589        &Rc::downgrade(&component),
590        &NamedReference::new(parent, SmolStr::new_static("x")),
591        diag,
592    );
593
594    enclosing.menu_item_tree.borrow_mut().push(component.clone());
595
596    component
597}