1use 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 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 return false;
217 }
218
219 let item_tree_root = if !is_internal {
220 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 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 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 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 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 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 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 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 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
487fn 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 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 activate: Vec<(SmolStr, Expression)>,
615 sub_menu: Vec<(SmolStr, Expression)>,
617
618 menu_entry: Type,
619}
620
621fn 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}