1use crate::diagnostics::{BuildDiagnostics, Spanned};
101use crate::expression_tree::{BuiltinFunction, Callable, Expression, NamedReference};
102use crate::langtype::{ElementType, Type};
103use crate::object_tree::*;
104use core::cell::RefCell;
105use i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
106use smol_str::{SmolStr, format_smolstr};
107use std::rc::{Rc, Weak};
108
109const HEIGHT: &str = "height";
110const ENTRIES: &str = "entries";
111const SUB_MENU: &str = "sub-menu";
112const ACTIVATED: &str = "activated";
113const SHOW: &str = "show";
114
115struct UsefulMenuComponents {
116 menubar_impl: ElementType,
117 vertical_layout: ElementType,
118 context_menu_internal: ElementType,
119 empty: ElementType,
120 menu_entry: Type,
121 menu_item_element: ElementType,
122}
123
124pub async fn lower_menus(
125 doc: &mut Document,
126 type_loader: &mut crate::typeloader::TypeLoader,
127 diag: &mut BuildDiagnostics,
128) {
129 let mut has_menubar_or_context_menu = false;
131 doc.visit_all_used_components(|component| {
132 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
133 if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "MenuBar" | "ContextMenuArea" | "ContextMenuInternal" | "SystemTrayIcon")) {
134 has_menubar_or_context_menu = true;
135 }
136 })
137 });
138
139 if !has_menubar_or_context_menu {
140 return;
141 }
142
143 let mut build_diags_to_ignore = BuildDiagnostics::default();
145
146 let menubar_impl = type_loader
147 .import_component("std-widgets-impl.slint", "MenuBarImpl", &mut build_diags_to_ignore)
148 .await
149 .expect("MenuBarImpl should be in std-widgets-impl.slint");
150
151 let menu_item_element = type_loader
152 .global_type_registry
153 .borrow()
154 .lookup_builtin_element("ContextMenuArea")
155 .unwrap()
156 .as_builtin()
157 .additional_accepted_child_types
158 .get("Menu")
159 .expect("ContextMenuArea should accept Menu")
160 .additional_accepted_child_types
161 .get("MenuItem")
162 .expect("Menu should accept MenuItem")
163 .clone()
164 .into();
165
166 let useful_menu_component = UsefulMenuComponents {
167 menubar_impl: menubar_impl.clone().into(),
168 context_menu_internal: type_loader
169 .global_type_registry
170 .borrow()
171 .lookup_builtin_element("ContextMenuInternal")
172 .expect("ContextMenuInternal is a builtin type"),
173 vertical_layout: type_loader
174 .global_type_registry
175 .borrow()
176 .lookup_builtin_element("VerticalLayout")
177 .expect("VerticalLayout is a builtin type"),
178 empty: type_loader.global_type_registry.borrow().empty_type(),
179 menu_entry: type_loader.global_type_registry.borrow().lookup("MenuEntry"),
180 menu_item_element,
181 };
182 assert!(matches!(&useful_menu_component.menu_entry, Type::Struct(..)));
183
184 let mut has_menu = false;
185 let mut has_menubar = false;
186
187 doc.visit_all_used_components(|component| {
188 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
189 if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
190 has_menubar |= process_window(elem, &useful_menu_component, type_loader.compiler_config.no_native_menu, diag);
191 }
192 if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
193 has_menu |= process_context_menu(elem, &useful_menu_component, diag);
194 }
195 if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "SystemTrayIcon")
196 && matches!(&elem.borrow().base_type, ElementType::Builtin(b) if b.name == "SystemTrayIcon")
197 {
198 process_system_tray_icon(elem, &useful_menu_component, diag);
208 }
209 })
210 });
211
212 if has_menubar {
213 recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
214 if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
215 {
216 has_menu |= process_context_menu(elem, &useful_menu_component, diag);
217 }
218 });
219 }
220 if has_menu {
221 let popup_menu_impl = type_loader
222 .import_component("std-widgets-impl.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
223 .await
224 .expect("PopupMenuImpl should be in std-widgets-impl.slint");
225 {
226 let mut root = popup_menu_impl.root_element.borrow_mut();
227
228 for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
229 match root.property_declarations.get_mut(prop) {
230 Some(d) => d.expose_in_public_api = true,
231 None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
232 }
233 }
234 root.property_analysis
235 .borrow_mut()
236 .entry(SmolStr::new_static(ENTRIES))
237 .or_default()
238 .is_set = true;
239 }
240
241 recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
242 if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
243 {
244 process_context_menu(elem, &useful_menu_component, diag);
245 }
246 });
247 doc.popup_menu_impl = popup_menu_impl.into();
248 }
249}
250
251fn process_context_menu(
252 context_menu_elem: &ElementRc,
253 components: &UsefulMenuComponents,
254 diag: &mut BuildDiagnostics,
255) -> bool {
256 let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");
257
258 if is_internal && context_menu_elem.borrow().property_declarations.contains_key(ENTRIES) {
259 return false;
261 }
262
263 let source_location = Some(context_menu_elem.borrow().to_source_location());
265 let position = Expression::FunctionParameterReference {
266 index: 0,
267 ty: crate::typeregister::logical_point_type().into(),
268 };
269 let expr = if !is_internal {
270 let menu_element_type = context_menu_elem
271 .borrow()
272 .base_type
273 .as_builtin()
274 .additional_accepted_child_types
275 .get("Menu")
276 .expect("ContextMenu should accept Menu")
277 .clone()
278 .into();
279
280 let mut menu_elem: Option<Rc<RefCell<Element>>> = None;
281 context_menu_elem.borrow_mut().children.retain(|x| {
282 if x.borrow().base_type == menu_element_type {
283 if let Some(ref existing) = menu_elem {
284 diag.push_error(
285 "Only one Menu is allowed in a ContextMenu".into(),
286 &*x.borrow(),
287 );
288 diag.push_note("First Menu defined here".into(), &*existing.borrow());
289 } else {
290 menu_elem = Some(x.clone());
291 }
292 false
293 } else {
294 true
295 }
296 });
297
298 let Some(menu_elem) = menu_elem else {
299 diag.push_error(
300 "ContextMenuArea should have a Menu".into(),
301 &*context_menu_elem.borrow(),
302 );
303 return false;
304 };
305 if menu_elem.borrow().repeated.is_some() {
306 diag.push_error(
307 "ContextMenuArea's root Menu cannot be in a conditional or repeated element".into(),
308 &*menu_elem.borrow(),
309 );
310 }
311
312 let children = std::mem::take(&mut menu_elem.borrow_mut().children);
313 let c = lower_menu_items(context_menu_elem, children, components, diag);
314 let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
315
316 context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
317 for (name, _) in &components.context_menu_internal.property_list() {
318 if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
319 diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
320 }
321 }
322
323 Expression::FunctionCall {
324 function: BuiltinFunction::ShowPopupMenu.into(),
325 arguments: vec![
326 Expression::ElementReference(Rc::downgrade(context_menu_elem)),
327 item_tree_root,
328 position,
329 ],
330 source_location,
331 }
332 } else {
333 context_menu_elem.borrow_mut().property_declarations.insert(
337 SmolStr::new_static(ENTRIES),
338 Type::Array(components.menu_entry.clone().into()).into(),
339 );
340 let entries = Expression::PropertyReference(NamedReference::new(
341 context_menu_elem,
342 SmolStr::new_static(ENTRIES),
343 ));
344
345 Expression::FunctionCall {
346 function: BuiltinFunction::ShowPopupMenuInternal.into(),
347 arguments: vec![
348 Expression::ElementReference(Rc::downgrade(context_menu_elem)),
349 entries,
350 position,
351 ],
352 source_location,
353 }
354 };
355
356 let old = context_menu_elem
357 .borrow_mut()
358 .bindings
359 .insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
360 if let Some(old) = old {
361 diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
362 }
363
364 true
365}
366
367fn process_system_tray_icon(
368 system_tray_elem: &ElementRc,
369 components: &UsefulMenuComponents,
370 diag: &mut BuildDiagnostics,
371) {
372 let menu_element_type: ElementType = system_tray_elem
374 .borrow()
375 .base_type
376 .as_builtin()
377 .additional_accepted_child_types
378 .get("Menu")
379 .expect("SystemTrayIcon should accept Menu")
380 .clone()
381 .into();
382
383 let mut menu_elem: Option<Rc<RefCell<Element>>> = None;
384 system_tray_elem.borrow_mut().children.retain(|x| {
385 if x.borrow().base_type == menu_element_type {
386 if let Some(ref existing) = menu_elem {
387 diag.push_error(
388 "Only one Menu is allowed in a SystemTrayIcon".into(),
389 &*x.borrow(),
390 );
391 diag.push_note("First Menu defined here".into(), &*existing.borrow());
392 } else {
393 menu_elem = Some(x.clone());
394 }
395 false
396 } else {
397 true
398 }
399 });
400
401 let Some(menu_elem) = menu_elem else {
402 return;
404 };
405
406 let repeated = menu_elem.borrow_mut().repeated.take();
410 let condition = repeated.map(|repeated| {
411 if !repeated.is_conditional_element {
412 diag.push_error(
413 "SystemTrayIcon's Menu cannot be in a repeated element".into(),
414 &*menu_elem.borrow(),
415 );
416 }
417 repeated.model
418 });
419
420 let source_location = Some(system_tray_elem.borrow().to_source_location());
421 let children = std::mem::take(&mut menu_elem.borrow_mut().children);
422 let c = lower_menu_items(system_tray_elem, children, components, diag);
423 let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
424
425 let mut arguments =
426 vec![Expression::ElementReference(Rc::downgrade(system_tray_elem)), item_tree_root];
427 if let Some(condition) = condition {
428 arguments.push(condition);
429 }
430
431 let setup = Expression::FunctionCall {
432 function: BuiltinFunction::SetupSystemTrayIcon.into(),
433 arguments,
434 source_location,
435 };
436
437 let component = system_tray_elem.borrow().enclosing_component.upgrade().unwrap();
438 component.init_code.borrow_mut().constructor_code.push(setup);
439}
440
441fn process_window(
442 win: &ElementRc,
443 components: &UsefulMenuComponents,
444 no_native_menu: bool,
445 diag: &mut BuildDiagnostics,
446) -> bool {
447 let mut menu_bar: Option<Rc<RefCell<Element>>> = None;
448 win.borrow_mut().children.retain(|x| {
449 if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
450 if let Some(ref menu_bar) = menu_bar {
451 diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
452 diag.push_note("First MenuBar defined here".into(), &*menu_bar.borrow());
453 } else {
454 menu_bar = Some(x.clone());
455 }
456 false
457 } else {
458 true
459 }
460 });
461
462 let Some(menu_bar) = menu_bar else {
463 return false;
464 };
465 let repeated = menu_bar.borrow_mut().repeated.take();
466 let mut condition = repeated.map(|repeated| {
467 if !repeated.is_conditional_element {
468 diag.push_error("MenuBar cannot be in a repeated element".into(), &*menu_bar.borrow());
469 }
470 repeated.model
471 });
472 let original_cond = condition.clone();
473
474 let children = std::mem::take(&mut menu_bar.borrow_mut().children);
476 let c = lower_menu_items(&menu_bar, children, components, diag);
477 let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
478
479 if !no_native_menu {
480 let supports_native_menu_bar = Expression::UnaryOp {
481 op: '!',
482 sub: Expression::FunctionCall {
483 function: BuiltinFunction::SupportsNativeMenuBar.into(),
484 arguments: Vec::new(),
485 source_location: None,
486 }
487 .into(),
488 };
489 condition = match condition {
490 Some(condition) => Some(Expression::BinaryExpression {
491 lhs: condition.into(),
492 rhs: supports_native_menu_bar.into(),
493 op: '&',
494 }),
495 None => Some(supports_native_menu_bar),
496 };
497 }
498
499 let mut window = win.borrow_mut();
500 let menubar_impl = Element {
501 id: format_smolstr!("{}-menulayout", window.id),
502 base_type: components.menubar_impl.clone(),
503 enclosing_component: window.enclosing_component.clone(),
504 repeated: condition.clone().map(|condition| crate::object_tree::RepeatedElementInfo {
505 model: condition,
506 model_data_id: SmolStr::default(),
507 index_id: SmolStr::default(),
508 is_conditional_element: true,
509 is_listview: None,
510 }),
511 ..Default::default()
512 }
513 .make_rc();
514
515 let child = Element {
517 id: format_smolstr!("{}-child", window.id),
518 base_type: components.empty.clone(),
519 enclosing_component: window.enclosing_component.clone(),
520 children: std::mem::take(&mut window.children),
521 ..Default::default()
522 }
523 .make_rc();
524
525 let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
526
527 let source_location = Some(menu_bar.borrow().to_source_location());
528
529 for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
530 let ty = components.menubar_impl.lookup_property(prop).property_type;
532 assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
533 let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
534 let forward_expr = if let Type::Callback(cb) = &ty {
535 Expression::FunctionCall {
536 function: Callable::Callback(nr),
537 arguments: cb
538 .args
539 .iter()
540 .enumerate()
541 .map(|(index, ty)| Expression::FunctionParameterReference {
542 index,
543 ty: ty.clone(),
544 })
545 .collect(),
546 source_location: source_location.clone(),
547 }
548 } else {
549 Expression::PropertyReference(nr)
550 };
551 menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
552 let old = menu_bar
553 .borrow_mut()
554 .property_declarations
555 .insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
556 if let Some(old) = old {
557 diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
558 }
559 }
560
561 let visible_binding = menu_bar.borrow_mut().bindings.remove("visible");
563 if let Some(visible_binding) = &visible_binding {
564 menubar_impl
565 .borrow_mut()
566 .bindings
567 .insert(SmolStr::new_static("menubar-visible"), visible_binding.clone());
568 }
569
570 menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
572 menu_bar.borrow_mut().children = vec![menubar_impl, child];
573
574 for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
575 menu_bar
576 .borrow()
577 .property_analysis
578 .borrow_mut()
579 .entry(SmolStr::new_static(prop))
580 .or_default()
581 .is_set = true;
582 }
583
584 window.children.push(menu_bar.clone());
585 let component = window.enclosing_component.upgrade().unwrap();
586 drop(window);
587
588 let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
590 crate::object_tree::visit_all_named_references(&component, &mut |nr| {
591 if nr == &win_height {
592 *nr = child_height.clone()
593 }
594 });
595 win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
597
598 let mut arguments = vec![
599 Expression::PropertyReference(NamedReference::new(&menu_bar, SmolStr::new_static(ENTRIES))),
600 Expression::PropertyReference(NamedReference::new(
601 &menu_bar,
602 SmolStr::new_static(SUB_MENU),
603 )),
604 Expression::PropertyReference(NamedReference::new(
605 &menu_bar,
606 SmolStr::new_static(ACTIVATED),
607 )),
608 item_tree_root,
609 Expression::BoolLiteral(no_native_menu),
610 ];
611
612 if let Some(condition) = original_cond {
613 arguments.push(condition);
614 } else {
615 arguments.push(Expression::BoolLiteral(true));
616 }
617
618 if let Some(visible_binding) = visible_binding {
619 arguments.push(visible_binding.borrow().expression.clone());
620 } else {
621 arguments.push(Expression::BoolLiteral(true));
622 }
623
624 let setup_menubar = Expression::FunctionCall {
625 function: BuiltinFunction::SetupMenuBar.into(),
626 arguments,
627 source_location,
628 };
629 component.init_code.borrow_mut().constructor_code.push(setup_menubar);
630
631 true
632}
633
634fn lower_menu_items(
638 parent: &ElementRc,
639 children: Vec<ElementRc>,
640 components: &UsefulMenuComponents,
641 diag: &mut BuildDiagnostics,
642) -> Rc<Component> {
643 let in_menubar = parent.borrow().base_type.type_name() == Some("MenuBar");
644 let component = Rc::new_cyclic(|component_weak| {
645 let root_element = Rc::new(RefCell::new(Element {
646 base_type: components.empty.clone(),
647 children,
648 enclosing_component: component_weak.clone(),
649 ..Default::default()
650 }));
651 recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
652 if !is_root {
653 debug_assert!(Weak::ptr_eq(
654 &element.borrow().enclosing_component,
655 &parent.borrow().enclosing_component
656 ));
657 element.borrow_mut().enclosing_component = component_weak.clone();
658 element.borrow_mut().geometry_props = None;
659
660 if !in_menubar && let Some(binding) = element.borrow().bindings.get("shortcut") {
661 diag.push_error(
662 "MenuItem shortcuts are currently only supported in the MenuBar".into(),
663 &*binding.borrow(),
664 );
665 }
666
667 if element.borrow().base_type.type_name() == Some("MenuSeparator") {
668 element.borrow_mut().bindings.insert(
669 "title".into(),
670 RefCell::new(
671 Expression::StringLiteral(SmolStr::new_static(
672 MENU_SEPARATOR_PLACEHOLDER_TITLE,
673 ))
674 .into(),
675 ),
676 );
677 }
678 element.borrow_mut().base_type = components.menu_item_element.clone();
680 }
681 false
682 });
683 Component {
684 id: SmolStr::default(),
685 root_element,
686 parent_element: RefCell::new(Rc::downgrade(parent)),
687 ..Default::default()
688 }
689 });
690 let enclosing = parent.borrow().enclosing_component.upgrade().unwrap();
691
692 super::lower_popups::check_no_reference_to_popup(
693 parent,
694 &enclosing,
695 &Rc::downgrade(&component),
696 &NamedReference::new(parent, SmolStr::new_static("x")),
697 diag,
698 );
699
700 enclosing.menu_item_tree.borrow_mut().push(component.clone());
701
702 component
703}