1use 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 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 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 return false;
246 }
247
248 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 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 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 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 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 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 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 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
528fn 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 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}