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 = None;
266        context_menu_elem.borrow_mut().children.retain(|x| {
267            if x.borrow().base_type == menu_element_type {
268                if menu_elem.is_some() {
269                    diag.push_error(
270                        "Only one Menu is allowed in a ContextMenu".into(),
271                        &*x.borrow(),
272                    );
273                } else {
274                    menu_elem = Some(x.clone());
275                }
276                false
277            } else {
278                true
279            }
280        });
281
282        let Some(menu_elem) = menu_elem else {
283            diag.push_error(
284                "ContextMenuArea should have a Menu".into(),
285                &*context_menu_elem.borrow(),
286            );
287            return false;
288        };
289        if menu_elem.borrow().repeated.is_some() {
290            diag.push_error(
291                "ContextMenuArea's root Menu cannot be in a conditional or repeated element".into(),
292                &*menu_elem.borrow(),
293            );
294        }
295
296        let children = std::mem::take(&mut menu_elem.borrow_mut().children);
297        let c = lower_menu_items(context_menu_elem, children, components, diag);
298        let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
299
300        context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
301        for (name, _) in &components.context_menu_internal.property_list() {
302            if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
303                diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
304            }
305        }
306
307        Expression::FunctionCall {
308            function: BuiltinFunction::ShowPopupMenu.into(),
309            arguments: vec![
310                Expression::ElementReference(Rc::downgrade(context_menu_elem)),
311                item_tree_root,
312                position,
313            ],
314            source_location,
315        }
316    } else {
317        // `ContextMenuInternal`
318
319        // Materialize the entries property
320        context_menu_elem.borrow_mut().property_declarations.insert(
321            SmolStr::new_static(ENTRIES),
322            Type::Array(components.menu_entry.clone().into()).into(),
323        );
324        let entries = Expression::PropertyReference(NamedReference::new(
325            context_menu_elem,
326            SmolStr::new_static(ENTRIES),
327        ));
328
329        Expression::FunctionCall {
330            function: BuiltinFunction::ShowPopupMenuInternal.into(),
331            arguments: vec![
332                Expression::ElementReference(Rc::downgrade(context_menu_elem)),
333                entries,
334                position,
335            ],
336            source_location,
337        }
338    };
339
340    let old = context_menu_elem
341        .borrow_mut()
342        .bindings
343        .insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
344    if let Some(old) = old {
345        diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
346    }
347
348    true
349}
350
351fn process_window(
352    win: &ElementRc,
353    components: &UsefulMenuComponents,
354    no_native_menu: bool,
355    diag: &mut BuildDiagnostics,
356) -> bool {
357    let mut menu_bar = None;
358    win.borrow_mut().children.retain(|x| {
359        if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
360            if menu_bar.is_some() {
361                diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
362            } else {
363                menu_bar = Some(x.clone());
364            }
365            false
366        } else {
367            true
368        }
369    });
370
371    let Some(menu_bar) = menu_bar else {
372        return false;
373    };
374    let repeated = menu_bar.borrow_mut().repeated.take();
375    let mut condition = repeated.map(|repeated| {
376        if !repeated.is_conditional_element {
377            diag.push_error("MenuBar cannot be in a repeated element".into(), &*menu_bar.borrow());
378        }
379        repeated.model
380    });
381    let original_cond = condition.clone();
382
383    // Lower MenuItem's into a tree root
384    let children = std::mem::take(&mut menu_bar.borrow_mut().children);
385    let c = lower_menu_items(&menu_bar, children, components, diag);
386    let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
387
388    if !no_native_menu {
389        let supportes_native_menu_bar = Expression::UnaryOp {
390            op: '!',
391            sub: Expression::FunctionCall {
392                function: BuiltinFunction::SupportsNativeMenuBar.into(),
393                arguments: Vec::new(),
394                source_location: None,
395            }
396            .into(),
397        };
398        condition = match condition {
399            Some(condition) => Some(Expression::BinaryExpression {
400                lhs: condition.into(),
401                rhs: supportes_native_menu_bar.into(),
402                op: '&',
403            }),
404            None => Some(supportes_native_menu_bar),
405        };
406    }
407
408    let mut window = win.borrow_mut();
409    let menubar_impl = Element {
410        id: format_smolstr!("{}-menulayout", window.id),
411        base_type: components.menubar_impl.clone(),
412        enclosing_component: window.enclosing_component.clone(),
413        repeated: condition.clone().map(|condition| crate::object_tree::RepeatedElementInfo {
414            model: condition,
415            model_data_id: SmolStr::default(),
416            index_id: SmolStr::default(),
417            is_conditional_element: true,
418            is_listview: None,
419        }),
420        ..Default::default()
421    }
422    .make_rc();
423
424    // Create a child that contains all the children of the window but the menubar
425    let child = Element {
426        id: format_smolstr!("{}-child", window.id),
427        base_type: components.empty.clone(),
428        enclosing_component: window.enclosing_component.clone(),
429        children: std::mem::take(&mut window.children),
430        ..Default::default()
431    }
432    .make_rc();
433
434    let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
435
436    let source_location = Some(menu_bar.borrow().to_source_location());
437
438    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
439        // materialize the properties and callbacks
440        let ty = components.menubar_impl.lookup_property(prop).property_type;
441        assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
442        let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
443        let forward_expr = if let Type::Callback(cb) = &ty {
444            Expression::FunctionCall {
445                function: Callable::Callback(nr),
446                arguments: cb
447                    .args
448                    .iter()
449                    .enumerate()
450                    .map(|(index, ty)| Expression::FunctionParameterReference {
451                        index,
452                        ty: ty.clone(),
453                    })
454                    .collect(),
455                source_location: source_location.clone(),
456            }
457        } else {
458            Expression::PropertyReference(nr)
459        };
460        menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
461        let old = menu_bar
462            .borrow_mut()
463            .property_declarations
464            .insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
465        if let Some(old) = old {
466            diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
467        }
468    }
469
470    // Transform the MenuBar in a layout
471    menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
472    menu_bar.borrow_mut().children = vec![menubar_impl, child];
473
474    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
475        menu_bar
476            .borrow()
477            .property_analysis
478            .borrow_mut()
479            .entry(SmolStr::new_static(prop))
480            .or_default()
481            .is_set = true;
482    }
483
484    window.children.push(menu_bar.clone());
485    let component = window.enclosing_component.upgrade().unwrap();
486    drop(window);
487
488    // Rename every access to `root.height` into `child.height`
489    let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
490    crate::object_tree::visit_all_named_references(&component, &mut |nr| {
491        if nr == &win_height {
492            *nr = child_height.clone()
493        }
494    });
495    // except for the actual geometry
496    win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
497
498    let mut arguments = vec![
499        Expression::PropertyReference(NamedReference::new(&menu_bar, SmolStr::new_static(ENTRIES))),
500        Expression::PropertyReference(NamedReference::new(
501            &menu_bar,
502            SmolStr::new_static(SUB_MENU),
503        )),
504        Expression::PropertyReference(NamedReference::new(
505            &menu_bar,
506            SmolStr::new_static(ACTIVATED),
507        )),
508        item_tree_root,
509        Expression::BoolLiteral(no_native_menu),
510    ];
511
512    if let Some(condition) = original_cond {
513        arguments.push(condition);
514    }
515
516    let setup_menubar = Expression::FunctionCall {
517        function: BuiltinFunction::SetupMenuBar.into(),
518        arguments,
519        source_location,
520    };
521    component.init_code.borrow_mut().constructor_code.push(setup_menubar);
522
523    true
524}
525
526/// Lower the MenuItem's and Menu's to either
527///  - `entries` and `activated` and `sub-menu` properties/callback, in which cases it returns None
528///  - 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
529fn lower_menu_items(
530    parent: &ElementRc,
531    children: Vec<ElementRc>,
532    components: &UsefulMenuComponents,
533    diag: &mut BuildDiagnostics,
534) -> Rc<Component> {
535    let component = Rc::new_cyclic(|component_weak| {
536        let root_element = Rc::new(RefCell::new(Element {
537            base_type: components.empty.clone(),
538            children,
539            enclosing_component: component_weak.clone(),
540            ..Default::default()
541        }));
542        recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
543            if !is_root {
544                debug_assert!(Weak::ptr_eq(
545                    &element.borrow().enclosing_component,
546                    &parent.borrow().enclosing_component
547                ));
548                element.borrow_mut().enclosing_component = component_weak.clone();
549                element.borrow_mut().geometry_props = None;
550                if element.borrow().base_type.type_name() == Some("MenuSeparator") {
551                    element.borrow_mut().bindings.insert(
552                        "title".into(),
553                        RefCell::new(
554                            Expression::StringLiteral(SmolStr::new_static(
555                                MENU_SEPARATOR_PLACEHOLDER_TITLE,
556                            ))
557                            .into(),
558                        ),
559                    );
560                }
561                // Menu/MenuSeparator -> MenuItem
562                element.borrow_mut().base_type = components.menu_item_element.clone();
563            }
564            false
565        });
566        Component {
567            id: SmolStr::default(),
568            root_element,
569            parent_element: Rc::downgrade(parent),
570            ..Default::default()
571        }
572    });
573    let enclosing = parent.borrow().enclosing_component.upgrade().unwrap();
574
575    super::lower_popups::check_no_reference_to_popup(
576        parent,
577        &enclosing,
578        &Rc::downgrade(&component),
579        &NamedReference::new(parent, SmolStr::new_static("x")),
580        diag,
581    );
582
583    enclosing.menu_item_tree.borrow_mut().push(component.clone());
584
585    component
586}