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//! Passe lower 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//!           MenuItem {
18//!             title: "A";
19//!             activated => { ... }
20//!           }
21//!           Menu {
22//!               title: "B";
23//!               MenuItem { title: "C"; }
24//!           }
25//!        }
26//!      }
27//!      content := ...
28//! }
29//! ```
30//! Is transformed to
31//! ```slint
32//! Window {
33//!     menu-bar := VerticalLayout {
34//!        property <[MenuEntry]> entries : [ { id: "0", title: "File", has-sub-menu: true } ];
35//!        callback sub-menu(entry: MenuEntry) => {
36//!            if(entry.id == "0") { return [ { id: "1", title: "A" }, { id: "2", title: "B", has-sub-menu: true } ]; }
37//!            else if(entry.id == "2") { return [ { id: "3", title: "C" } ]; } else { return []; }
38//!        }
39//!        callback activated() => { if (entry.id == "2") { ... } }
40//!        if !Builtin.supports_native_menu_bar() : MenuBarImpl {
41//!           entries: parent.entries
42//!           sub-menu(..) => { parent.sub-menu(..) }
43//!           activated(..) => { parent.activated(..) }
44//!        }
45//!        Empty {
46//!           content := ...
47//!        }
48//!    }
49//!    init => {
50//!        // ... rest of init ...
51//!        Builtin.setup_native_menu_bar(menu-bar.entries, menu-bar.sub-menu, menu-bar.activated)
52//!    }
53//! }
54//! ```
55//!
56//! ## ContextMenuInternal
57//!
58//! ```slint
59//! menu := ContextMenuInternal {
60//!     entries: [...]
61//!     sub-menu => ...
62//!     activated => ...
63//! }
64//! Button { clicked => {menu.show({x: 0, y: 0;})} }
65//! ```
66//! Is transformed to
67//!
68//! ```slint
69//! menu := ContextMenu {
70//!    property <[MenuEntry]> entries : ...
71//!    sub-menu => { ... }
72//!    activated => { ... }
73//!
74//!    // show is actually a callback called by the native code when right clicking
75//!    callback show(point) => { Builtin.show_context_menu(entries, sub-menu, activated, point) }
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
83use crate::diagnostics::{BuildDiagnostics, Spanned};
84use crate::expression_tree::{BuiltinFunction, Callable, Expression, NamedReference};
85use crate::langtype::{ElementType, Type};
86use crate::object_tree::*;
87use core::cell::RefCell;
88use i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
89use smol_str::{format_smolstr, SmolStr};
90use std::collections::HashMap;
91use std::rc::{Rc, Weak};
92
93const HEIGHT: &str = "height";
94const ENTRIES: &str = "entries";
95const SUB_MENU: &str = "sub-menu";
96const ACTIVATED: &str = "activated";
97const SHOW: &str = "show";
98
99struct UsefulMenuComponents {
100    menubar_impl: ElementType,
101    vertical_layout: ElementType,
102    context_menu_internal: ElementType,
103    empty: ElementType,
104    menu_entry: Type,
105    menu_item_element: ElementType,
106}
107
108pub async fn lower_menus(
109    doc: &mut Document,
110    type_loader: &mut crate::typeloader::TypeLoader,
111    diag: &mut BuildDiagnostics,
112) {
113    // Ignore import errors
114    let mut build_diags_to_ignore = BuildDiagnostics::default();
115
116    let menubar_impl = type_loader
117        .import_component("std-widgets.slint", "MenuBarImpl", &mut build_diags_to_ignore)
118        .await
119        .expect("MenuBarImpl should be in std-widgets.slint");
120
121    let menu_item_element = type_loader
122        .global_type_registry
123        .borrow()
124        .lookup_builtin_element("ContextMenuArea")
125        .unwrap()
126        .as_builtin()
127        .additional_accepted_child_types
128        .get("Menu")
129        .expect("ContextMenuArea should accept Menu")
130        .additional_accepted_child_types
131        .get("MenuItem")
132        .expect("Menu should accept MenuItem")
133        .clone()
134        .into();
135
136    let useful_menu_component = UsefulMenuComponents {
137        menubar_impl: menubar_impl.clone().into(),
138        context_menu_internal: type_loader
139            .global_type_registry
140            .borrow()
141            .lookup_builtin_element("ContextMenuInternal")
142            .expect("ContextMenuInternal is a builtin type"),
143        vertical_layout: type_loader
144            .global_type_registry
145            .borrow()
146            .lookup_builtin_element("VerticalLayout")
147            .expect("VerticalLayout is a builtin type"),
148        empty: type_loader.global_type_registry.borrow().empty_type(),
149        menu_entry: type_loader.global_type_registry.borrow().lookup("MenuEntry"),
150        menu_item_element,
151    };
152    assert!(matches!(&useful_menu_component.menu_entry, Type::Struct(..)));
153
154    let mut has_menu = false;
155    let mut has_menubar = false;
156
157    doc.visit_all_used_components(|component| {
158        recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
159            if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
160                has_menubar |= process_window(elem, &useful_menu_component, type_loader.compiler_config.no_native_menu, diag);
161            }
162            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
163                has_menu |= process_context_menu(elem, &useful_menu_component, diag);
164            }
165        })
166    });
167
168    if has_menubar {
169        recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
170            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
171            {
172                has_menu |= process_context_menu(elem, &useful_menu_component, diag);
173            }
174        });
175    }
176    if has_menu {
177        let popup_menu_impl = type_loader
178            .import_component("std-widgets.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
179            .await
180            .expect("PopupMenuImpl should be in std-widgets.slint");
181        {
182            let mut root = popup_menu_impl.root_element.borrow_mut();
183
184            for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
185                match root.property_declarations.get_mut(prop) {
186                    Some(d) => d.expose_in_public_api = true,
187                    None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
188                }
189            }
190            root.property_analysis
191                .borrow_mut()
192                .entry(SmolStr::new_static(ENTRIES))
193                .or_default()
194                .is_set = true;
195        }
196
197        recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
198            if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
199            {
200                process_context_menu(elem, &useful_menu_component, diag);
201            }
202        });
203        doc.popup_menu_impl = popup_menu_impl.into();
204    }
205}
206
207fn process_context_menu(
208    context_menu_elem: &ElementRc,
209    components: &UsefulMenuComponents,
210    diag: &mut BuildDiagnostics,
211) -> bool {
212    let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");
213
214    if is_internal && context_menu_elem.borrow().property_declarations.contains_key(ENTRIES) {
215        // Already processed;
216        return false;
217    }
218
219    let item_tree_root = if !is_internal {
220        // Lower Menu into entries
221        let menu_element_type = context_menu_elem
222            .borrow()
223            .base_type
224            .as_builtin()
225            .additional_accepted_child_types
226            .get("Menu")
227            .expect("ContextMenu should accept Menu")
228            .clone()
229            .into();
230
231        context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
232
233        let mut menu_elem = None;
234        context_menu_elem.borrow_mut().children.retain(|x| {
235            if x.borrow().base_type == menu_element_type {
236                if menu_elem.is_some() {
237                    diag.push_error(
238                        "Only one Menu is allowed in a ContextMenu".into(),
239                        &*x.borrow(),
240                    );
241                } else {
242                    menu_elem = Some(x.clone());
243                }
244                false
245            } else {
246                true
247            }
248        });
249
250        let item_tree_root = if let Some(menu_elem) = menu_elem {
251            if menu_elem.borrow().repeated.is_some() {
252                diag.push_error(
253                    "ContextMenuArea's root Menu cannot be in a conditional or repeated element"
254                        .into(),
255                    &*menu_elem.borrow(),
256                );
257            }
258
259            let children = std::mem::take(&mut menu_elem.borrow_mut().children);
260            lower_menu_items(context_menu_elem, children, components)
261                .map(|c| Expression::ElementReference(Rc::downgrade(&c.root_element)))
262        } else {
263            diag.push_error(
264                "ContextMenuArea should have a Menu".into(),
265                &*context_menu_elem.borrow(),
266            );
267            None
268        };
269
270        for (name, _) in &components.context_menu_internal.property_list() {
271            if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
272                diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
273            }
274        }
275
276        item_tree_root
277    } else {
278        None
279    };
280
281    let entries = if let Some(item_tree_root) = item_tree_root {
282        item_tree_root
283    } else {
284        // Materialize the entries property
285        context_menu_elem.borrow_mut().property_declarations.insert(
286            SmolStr::new_static(ENTRIES),
287            Type::Array(components.menu_entry.clone().into()).into(),
288        );
289        Expression::PropertyReference(NamedReference::new(
290            context_menu_elem,
291            SmolStr::new_static(ENTRIES),
292        ))
293    };
294
295    // generate the show callback
296    let source_location = Some(context_menu_elem.borrow().to_source_location());
297    let expr = Expression::FunctionCall {
298        function: BuiltinFunction::ShowPopupMenu.into(),
299        arguments: vec![
300            Expression::ElementReference(Rc::downgrade(context_menu_elem)),
301            entries,
302            Expression::FunctionParameterReference {
303                index: 0,
304                ty: crate::typeregister::logical_point_type(),
305            },
306        ],
307        source_location,
308    };
309    let old = context_menu_elem
310        .borrow_mut()
311        .bindings
312        .insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
313    if let Some(old) = old {
314        diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
315    }
316
317    true
318}
319
320fn process_window(
321    win: &ElementRc,
322    components: &UsefulMenuComponents,
323    no_native_menu: bool,
324    diag: &mut BuildDiagnostics,
325) -> bool {
326    let mut window = win.borrow_mut();
327    let mut menu_bar = None;
328    window.children.retain(|x| {
329        if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
330            if menu_bar.is_some() {
331                diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
332            } else {
333                menu_bar = Some(x.clone());
334            }
335            false
336        } else {
337            true
338        }
339    });
340
341    let Some(menu_bar) = menu_bar else {
342        return false;
343    };
344    if menu_bar.borrow().repeated.is_some() {
345        diag.push_error(
346            "MenuBar cannot be in a conditional or repeated element".into(),
347            &*menu_bar.borrow(),
348        );
349    }
350
351    // Lower MenuItem's into entries
352    let children = std::mem::take(&mut menu_bar.borrow_mut().children);
353    let item_tree_root = if !children.is_empty() {
354        lower_menu_items(&menu_bar, children, components)
355            .map(|c| Expression::ElementReference(Rc::downgrade(&c.root_element)))
356    } else {
357        None
358    };
359
360    let menubar_impl = Element {
361        id: format_smolstr!("{}-menulayout", window.id),
362        base_type: components.menubar_impl.clone(),
363        enclosing_component: window.enclosing_component.clone(),
364        repeated: (!no_native_menu).then(|| crate::object_tree::RepeatedElementInfo {
365            model: Expression::UnaryOp {
366                op: '!',
367                sub: Expression::FunctionCall {
368                    function: BuiltinFunction::SupportsNativeMenuBar.into(),
369                    arguments: vec![],
370                    source_location: None,
371                }
372                .into(),
373            },
374            model_data_id: SmolStr::default(),
375            index_id: SmolStr::default(),
376            is_conditional_element: true,
377            is_listview: None,
378        }),
379        ..Default::default()
380    }
381    .make_rc();
382
383    // Create a child that contains all the children of the window but the menubar
384    let child = Element {
385        id: format_smolstr!("{}-child", window.id),
386        base_type: components.empty.clone(),
387        enclosing_component: window.enclosing_component.clone(),
388        children: std::mem::take(&mut window.children),
389        ..Default::default()
390    }
391    .make_rc();
392
393    let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
394
395    let source_location = Some(menu_bar.borrow().to_source_location());
396
397    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
398        // materialize the properties and callbacks
399        let ty = components.menubar_impl.lookup_property(prop).property_type;
400        assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
401        let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
402        let forward_expr = if let Type::Callback(cb) = &ty {
403            Expression::FunctionCall {
404                function: Callable::Callback(nr),
405                arguments: cb
406                    .args
407                    .iter()
408                    .enumerate()
409                    .map(|(index, ty)| Expression::FunctionParameterReference {
410                        index,
411                        ty: ty.clone(),
412                    })
413                    .collect(),
414                source_location: source_location.clone(),
415            }
416        } else {
417            Expression::PropertyReference(nr)
418        };
419        menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
420        let old = menu_bar
421            .borrow_mut()
422            .property_declarations
423            .insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
424        if let Some(old) = old {
425            diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
426        }
427    }
428
429    // Transform the MenuBar in a layout
430    menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
431    menu_bar.borrow_mut().children = vec![menubar_impl, child];
432
433    for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
434        menu_bar
435            .borrow()
436            .property_analysis
437            .borrow_mut()
438            .entry(SmolStr::new_static(prop))
439            .or_default()
440            .is_set = true;
441    }
442
443    window.children.push(menu_bar.clone());
444    let component = window.enclosing_component.upgrade().unwrap();
445    drop(window);
446
447    // Rename every access to `root.height` into `child.height`
448    let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
449    crate::object_tree::visit_all_named_references(&component, &mut |nr| {
450        if nr == &win_height {
451            *nr = child_height.clone()
452        }
453    });
454    // except for the actual geometry
455    win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
456
457    if !no_native_menu || item_tree_root.is_some() {
458        let mut arguments = vec![
459            Expression::PropertyReference(NamedReference::new(
460                &menu_bar,
461                SmolStr::new_static(ENTRIES),
462            )),
463            Expression::PropertyReference(NamedReference::new(
464                &menu_bar,
465                SmolStr::new_static(SUB_MENU),
466            )),
467            Expression::PropertyReference(NamedReference::new(
468                &menu_bar,
469                SmolStr::new_static(ACTIVATED),
470            )),
471        ];
472
473        if let Some(item_tree_root) = item_tree_root {
474            arguments.push(item_tree_root.into());
475            arguments.push(Expression::BoolLiteral(no_native_menu));
476        }
477        let setup_menubar = Expression::FunctionCall {
478            function: BuiltinFunction::SetupNativeMenuBar.into(),
479            arguments,
480            source_location,
481        };
482        component.init_code.borrow_mut().constructor_code.push(setup_menubar.into());
483    }
484    true
485}
486
487/// Lower the MenuItem's and Menu's to either
488///  - `entries` and `activated` and `sub-menu` properties/callback, in which cases it returns None
489///  - 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
490fn lower_menu_items(
491    parent: &ElementRc,
492    children: Vec<ElementRc>,
493    components: &UsefulMenuComponents,
494) -> Option<Rc<Component>> {
495    let mut has_repeated = false;
496    for i in &children {
497        recurse_elem(i, &(), &mut |e, _| {
498            if e.borrow().repeated.is_some() {
499                has_repeated = true;
500            }
501        });
502        if has_repeated {
503            break;
504        }
505    }
506    if !has_repeated {
507        let menu_entry = &components.menu_entry;
508        let mut state = GenMenuState {
509            id: 0,
510            menu_entry: menu_entry.clone(),
511            activate: Vec::new(),
512            sub_menu: Vec::new(),
513        };
514        let entries = generate_menu_entries(children.into_iter(), &mut state);
515        parent.borrow_mut().bindings.insert(
516            ENTRIES.into(),
517            RefCell::new(
518                Expression::Array { element_ty: menu_entry.clone(), values: entries }.into(),
519            ),
520        );
521        let entry_id = Expression::StructFieldAccess {
522            base: Expression::FunctionParameterReference { index: 0, ty: menu_entry.clone() }
523                .into(),
524            name: SmolStr::new_static("id"),
525        };
526
527        let sub_entries = build_cases_function(
528            &entry_id,
529            Expression::Array { element_ty: menu_entry.clone(), values: vec![] },
530            state.sub_menu,
531        );
532        parent.borrow_mut().bindings.insert(SUB_MENU.into(), RefCell::new(sub_entries.into()));
533
534        let activated =
535            build_cases_function(&entry_id, Expression::CodeBlock(vec![]), state.activate);
536        parent.borrow_mut().bindings.insert(ACTIVATED.into(), RefCell::new(activated.into()));
537        None
538    } else {
539        let component = Rc::new_cyclic(|component_weak| {
540            let root_element = Rc::new(RefCell::new(Element {
541                base_type: components.empty.clone(),
542                children,
543                enclosing_component: component_weak.clone(),
544                ..Default::default()
545            }));
546            recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
547                if !is_root {
548                    debug_assert!(Weak::ptr_eq(
549                        &element.borrow().enclosing_component,
550                        &parent.borrow().enclosing_component
551                    ));
552                    element.borrow_mut().enclosing_component = component_weak.clone();
553                    element.borrow_mut().geometry_props = None;
554                    if element.borrow().base_type.type_name() == Some("MenuSeparator") {
555                        element.borrow_mut().bindings.insert(
556                            "title".into(),
557                            RefCell::new(
558                                Expression::StringLiteral(SmolStr::new_static(
559                                    MENU_SEPARATOR_PLACEHOLDER_TITLE,
560                                ))
561                                .into(),
562                            ),
563                        );
564                    }
565                    // Menu/MenuSeparator -> MenuItem
566                    element.borrow_mut().base_type = components.menu_item_element.clone();
567                }
568                false
569            });
570            Component {
571                node: parent.borrow().debug.first().map(|n| n.node.clone().into()),
572                id: SmolStr::default(),
573                root_element,
574                parent_element: Rc::downgrade(parent),
575                ..Default::default()
576            }
577        });
578        parent
579            .borrow()
580            .enclosing_component
581            .upgrade()
582            .unwrap()
583            .menu_item_tree
584            .borrow_mut()
585            .push(component.clone());
586        Some(component)
587    }
588}
589
590fn build_cases_function(
591    entry_id: &Expression,
592    default_case: Expression,
593    cases: Vec<(SmolStr, Expression)>,
594) -> Expression {
595    let mut result = default_case;
596    for (id, expr) in cases.into_iter().rev() {
597        result = Expression::Condition {
598            condition: Expression::BinaryExpression {
599                lhs: entry_id.clone().into(),
600                rhs: Expression::StringLiteral(id).into(),
601                op: '=',
602            }
603            .into(),
604            true_expr: expr.into(),
605            false_expr: result.into(),
606        }
607    }
608    result
609}
610
611struct GenMenuState {
612    id: usize,
613    /// Maps `entry.id` to the callback
614    activate: Vec<(SmolStr, Expression)>,
615    /// Maps `entry.id` to the sub-menu entries
616    sub_menu: Vec<(SmolStr, Expression)>,
617
618    menu_entry: Type,
619}
620
621/// Recursively generate the menu entries for the given menu items
622fn generate_menu_entries(
623    menu_items: impl Iterator<Item = ElementRc>,
624    state: &mut GenMenuState,
625) -> Vec<Expression> {
626    let mut entries = Vec::new();
627    let mut last_is_separator = false;
628
629    for item in menu_items {
630        let mut borrow_mut = item.borrow_mut();
631        let base_name = borrow_mut.base_type.type_name().unwrap();
632        let is_sub_menu = base_name == "Menu";
633        let is_separator = base_name == "MenuSeparator";
634        if !is_sub_menu && !is_separator {
635            assert_eq!(base_name, "MenuItem");
636        }
637
638        if is_separator && (last_is_separator || entries.is_empty()) {
639            continue;
640        }
641        last_is_separator = is_separator;
642
643        borrow_mut
644            .enclosing_component
645            .upgrade()
646            .unwrap()
647            .optimized_elements
648            .borrow_mut()
649            .push(item.clone());
650
651        assert!(borrow_mut.repeated.is_none());
652
653        let mut values = HashMap::<SmolStr, Expression>::new();
654        state.id += 1;
655        let id_str = format_smolstr!("{}", state.id);
656        values.insert(SmolStr::new_static("id"), Expression::StringLiteral(id_str.clone()));
657
658        if let Some(callback) = borrow_mut.bindings.remove(ACTIVATED) {
659            state.activate.push((id_str.clone(), callback.into_inner().expression));
660        }
661
662        if is_sub_menu {
663            let sub_entries =
664                generate_menu_entries(std::mem::take(&mut borrow_mut.children).into_iter(), state);
665
666            state.sub_menu.push((
667                id_str,
668                Expression::Array { element_ty: state.menu_entry.clone(), values: sub_entries },
669            ));
670            values
671                .insert(SmolStr::new_static("has-sub-menu"), Expression::BoolLiteral(true).into());
672        }
673
674        drop(borrow_mut);
675        if !is_separator {
676            for prop in ["title", "enabled"] {
677                if item.borrow().bindings.contains_key(prop) {
678                    let n = SmolStr::new_static(prop);
679                    values.insert(
680                        n.clone(),
681                        Expression::PropertyReference(NamedReference::new(&item, n)),
682                    );
683                }
684            }
685        } else {
686            values.insert(SmolStr::new_static("is_separator"), Expression::BoolLiteral(true));
687        }
688
689        entries.push(mk_struct(state.menu_entry.clone(), values));
690    }
691    if last_is_separator {
692        entries.pop();
693    }
694
695    entries
696}
697
698fn mk_struct(ty: Type, mut values: HashMap<SmolStr, Expression>) -> Expression {
699    let Type::Struct(ty) = ty else { panic!("Not a struct") };
700    for (k, v) in ty.fields.iter() {
701        values.entry(k.clone()).or_insert_with(|| Expression::default_value_for_type(v));
702    }
703    Expression::Struct { ty, values }
704}