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::{format_smolstr, 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 build_diags_to_ignore = BuildDiagnostics::default();
130
131 let menubar_impl = type_loader
132 .import_component("std-widgets.slint", "MenuBarImpl", &mut build_diags_to_ignore)
133 .await
134 .expect("MenuBarImpl should be in std-widgets.slint");
135
136 let menu_item_element = type_loader
137 .global_type_registry
138 .borrow()
139 .lookup_builtin_element("ContextMenuArea")
140 .unwrap()
141 .as_builtin()
142 .additional_accepted_child_types
143 .get("Menu")
144 .expect("ContextMenuArea should accept Menu")
145 .additional_accepted_child_types
146 .get("MenuItem")
147 .expect("Menu should accept MenuItem")
148 .clone()
149 .into();
150
151 let useful_menu_component = UsefulMenuComponents {
152 menubar_impl: menubar_impl.clone().into(),
153 context_menu_internal: type_loader
154 .global_type_registry
155 .borrow()
156 .lookup_builtin_element("ContextMenuInternal")
157 .expect("ContextMenuInternal is a builtin type"),
158 vertical_layout: type_loader
159 .global_type_registry
160 .borrow()
161 .lookup_builtin_element("VerticalLayout")
162 .expect("VerticalLayout is a builtin type"),
163 empty: type_loader.global_type_registry.borrow().empty_type(),
164 menu_entry: type_loader.global_type_registry.borrow().lookup("MenuEntry"),
165 menu_item_element,
166 };
167 assert!(matches!(&useful_menu_component.menu_entry, Type::Struct(..)));
168
169 let mut has_menu = false;
170 let mut has_menubar = false;
171
172 doc.visit_all_used_components(|component| {
173 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
174 if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
175 has_menubar |= process_window(elem, &useful_menu_component, type_loader.compiler_config.no_native_menu, diag);
176 }
177 if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
178 has_menu |= process_context_menu(elem, &useful_menu_component, diag);
179 }
180 })
181 });
182
183 if has_menubar {
184 recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
185 if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
186 {
187 has_menu |= process_context_menu(elem, &useful_menu_component, diag);
188 }
189 });
190 }
191 if has_menu {
192 let popup_menu_impl = type_loader
193 .import_component("std-widgets.slint", "PopupMenuImpl", &mut build_diags_to_ignore)
194 .await
195 .expect("PopupMenuImpl should be in std-widgets.slint");
196 {
197 let mut root = popup_menu_impl.root_element.borrow_mut();
198
199 for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
200 match root.property_declarations.get_mut(prop) {
201 Some(d) => d.expose_in_public_api = true,
202 None => diag.push_error(format!("PopupMenuImpl doesn't have {prop}"), &*root),
203 }
204 }
205 root.property_analysis
206 .borrow_mut()
207 .entry(SmolStr::new_static(ENTRIES))
208 .or_default()
209 .is_set = true;
210 }
211
212 recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
213 if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
214 {
215 process_context_menu(elem, &useful_menu_component, diag);
216 }
217 });
218 doc.popup_menu_impl = popup_menu_impl.into();
219 }
220}
221
222fn process_context_menu(
223 context_menu_elem: &ElementRc,
224 components: &UsefulMenuComponents,
225 diag: &mut BuildDiagnostics,
226) -> bool {
227 let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");
228
229 if is_internal && context_menu_elem.borrow().property_declarations.contains_key(ENTRIES) {
230 return false;
232 }
233
234 let source_location = Some(context_menu_elem.borrow().to_source_location());
236 let position = Expression::FunctionParameterReference {
237 index: 0,
238 ty: crate::typeregister::logical_point_type().into(),
239 };
240 let expr = if !is_internal {
241 let menu_element_type = context_menu_elem
242 .borrow()
243 .base_type
244 .as_builtin()
245 .additional_accepted_child_types
246 .get("Menu")
247 .expect("ContextMenu should accept Menu")
248 .clone()
249 .into();
250
251 context_menu_elem.borrow_mut().base_type = components.context_menu_internal.clone();
252
253 let mut menu_elem = None;
254 context_menu_elem.borrow_mut().children.retain(|x| {
255 if x.borrow().base_type == menu_element_type {
256 if menu_elem.is_some() {
257 diag.push_error(
258 "Only one Menu is allowed in a ContextMenu".into(),
259 &*x.borrow(),
260 );
261 } else {
262 menu_elem = Some(x.clone());
263 }
264 false
265 } else {
266 true
267 }
268 });
269
270 let Some(menu_elem) = menu_elem else {
271 diag.push_error(
272 "ContextMenuArea should have a Menu".into(),
273 &*context_menu_elem.borrow(),
274 );
275 return false;
276 };
277 if menu_elem.borrow().repeated.is_some() {
278 diag.push_error(
279 "ContextMenuArea's root Menu cannot be in a conditional or repeated element".into(),
280 &*menu_elem.borrow(),
281 );
282 }
283
284 let children = std::mem::take(&mut menu_elem.borrow_mut().children);
285 let c = lower_menu_items(context_menu_elem, children, components);
286 let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
287
288 for (name, _) in &components.context_menu_internal.property_list() {
289 if let Some(decl) = context_menu_elem.borrow().property_declarations.get(name) {
290 diag.push_error(format!("Cannot re-define internal property '{name}'"), &decl.node);
291 }
292 }
293
294 Expression::FunctionCall {
295 function: BuiltinFunction::ShowPopupMenu.into(),
296 arguments: vec![
297 Expression::ElementReference(Rc::downgrade(context_menu_elem)),
298 item_tree_root,
299 position,
300 ],
301 source_location,
302 }
303 } else {
304 context_menu_elem.borrow_mut().property_declarations.insert(
308 SmolStr::new_static(ENTRIES),
309 Type::Array(components.menu_entry.clone().into()).into(),
310 );
311 let entries = Expression::PropertyReference(NamedReference::new(
312 context_menu_elem,
313 SmolStr::new_static(ENTRIES),
314 ));
315
316 Expression::FunctionCall {
317 function: BuiltinFunction::ShowPopupMenuInternal.into(),
318 arguments: vec![
319 Expression::ElementReference(Rc::downgrade(context_menu_elem)),
320 entries,
321 position,
322 ],
323 source_location,
324 }
325 };
326
327 let old = context_menu_elem
328 .borrow_mut()
329 .bindings
330 .insert(SmolStr::new_static(SHOW), RefCell::new(expr.into()));
331 if let Some(old) = old {
332 diag.push_error("'show' is not a callback in ContextMenuArea".into(), &old.borrow().span);
333 }
334
335 true
336}
337
338fn process_window(
339 win: &ElementRc,
340 components: &UsefulMenuComponents,
341 no_native_menu: bool,
342 diag: &mut BuildDiagnostics,
343) -> bool {
344 let mut window = win.borrow_mut();
345 let mut menu_bar = None;
346 window.children.retain(|x| {
347 if matches!(&x.borrow().base_type, ElementType::Builtin(b) if b.name == "MenuBar") {
348 if menu_bar.is_some() {
349 diag.push_error("Only one MenuBar is allowed in a Window".into(), &*x.borrow());
350 } else {
351 menu_bar = Some(x.clone());
352 }
353 false
354 } else {
355 true
356 }
357 });
358
359 let Some(menu_bar) = menu_bar else {
360 return false;
361 };
362 let repeated = menu_bar.borrow_mut().repeated.take();
363 let mut condition = repeated.map(|repeated| {
364 if !repeated.is_conditional_element {
365 diag.push_error("MenuBar cannot be in a repeated element".into(), &*menu_bar.borrow());
366 }
367 repeated.model
368 });
369 let original_cond = condition.clone();
370
371 let children = std::mem::take(&mut menu_bar.borrow_mut().children);
373 let c = lower_menu_items(&menu_bar, children, components);
374 let item_tree_root = Expression::ElementReference(Rc::downgrade(&c.root_element));
375
376 if !no_native_menu {
377 let supportes_native_menu_bar = Expression::UnaryOp {
378 op: '!',
379 sub: Expression::FunctionCall {
380 function: BuiltinFunction::SupportsNativeMenuBar.into(),
381 arguments: vec![],
382 source_location: None,
383 }
384 .into(),
385 };
386 condition = match condition {
387 Some(condition) => Some(
388 Expression::BinaryExpression {
389 lhs: condition.into(),
390 rhs: supportes_native_menu_bar.into(),
391 op: '&',
392 }
393 .into(),
394 ),
395 None => Some(supportes_native_menu_bar.into()),
396 };
397 }
398
399 let menubar_impl = Element {
400 id: format_smolstr!("{}-menulayout", window.id),
401 base_type: components.menubar_impl.clone(),
402 enclosing_component: window.enclosing_component.clone(),
403 repeated: condition.clone().map(|condition| crate::object_tree::RepeatedElementInfo {
404 model: condition,
405 model_data_id: SmolStr::default(),
406 index_id: SmolStr::default(),
407 is_conditional_element: true,
408 is_listview: None,
409 }),
410 ..Default::default()
411 }
412 .make_rc();
413
414 let child = Element {
416 id: format_smolstr!("{}-child", window.id),
417 base_type: components.empty.clone(),
418 enclosing_component: window.enclosing_component.clone(),
419 children: std::mem::take(&mut window.children),
420 ..Default::default()
421 }
422 .make_rc();
423
424 let child_height = NamedReference::new(&child, SmolStr::new_static(HEIGHT));
425
426 let source_location = Some(menu_bar.borrow().to_source_location());
427
428 for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
429 let ty = components.menubar_impl.lookup_property(prop).property_type;
431 assert_ne!(ty, Type::Invalid, "Can't lookup type for {prop}");
432 let nr = NamedReference::new(&menu_bar, SmolStr::new_static(prop));
433 let forward_expr = if let Type::Callback(cb) = &ty {
434 Expression::FunctionCall {
435 function: Callable::Callback(nr),
436 arguments: cb
437 .args
438 .iter()
439 .enumerate()
440 .map(|(index, ty)| Expression::FunctionParameterReference {
441 index,
442 ty: ty.clone(),
443 })
444 .collect(),
445 source_location: source_location.clone(),
446 }
447 } else {
448 Expression::PropertyReference(nr)
449 };
450 menubar_impl.borrow_mut().bindings.insert(prop.into(), RefCell::new(forward_expr.into()));
451 let old = menu_bar
452 .borrow_mut()
453 .property_declarations
454 .insert(prop.into(), PropertyDeclaration { property_type: ty, ..Default::default() });
455 if let Some(old) = old {
456 diag.push_error(format!("Cannot re-define internal property '{prop}'"), &old.node);
457 }
458 }
459
460 menu_bar.borrow_mut().base_type = components.vertical_layout.clone();
462 menu_bar.borrow_mut().children = vec![menubar_impl, child];
463
464 for prop in [ENTRIES, SUB_MENU, ACTIVATED] {
465 menu_bar
466 .borrow()
467 .property_analysis
468 .borrow_mut()
469 .entry(SmolStr::new_static(prop))
470 .or_default()
471 .is_set = true;
472 }
473
474 window.children.push(menu_bar.clone());
475 let component = window.enclosing_component.upgrade().unwrap();
476 drop(window);
477
478 let win_height = NamedReference::new(win, SmolStr::new_static(HEIGHT));
480 crate::object_tree::visit_all_named_references(&component, &mut |nr| {
481 if nr == &win_height {
482 *nr = child_height.clone()
483 }
484 });
485 win.borrow_mut().geometry_props.as_mut().unwrap().height = win_height;
487
488 let mut arguments = vec![
489 Expression::PropertyReference(NamedReference::new(&menu_bar, SmolStr::new_static(ENTRIES))),
490 Expression::PropertyReference(NamedReference::new(
491 &menu_bar,
492 SmolStr::new_static(SUB_MENU),
493 )),
494 Expression::PropertyReference(NamedReference::new(
495 &menu_bar,
496 SmolStr::new_static(ACTIVATED),
497 )),
498 item_tree_root.into(),
499 Expression::BoolLiteral(no_native_menu),
500 ];
501
502 if let Some(condition) = original_cond {
503 arguments.push(condition);
504 }
505
506 let setup_menubar = Expression::FunctionCall {
507 function: BuiltinFunction::SetupMenuBar.into(),
508 arguments,
509 source_location,
510 };
511 component.init_code.borrow_mut().constructor_code.push(setup_menubar.into());
512
513 true
514}
515
516fn lower_menu_items(
520 parent: &ElementRc,
521 children: Vec<ElementRc>,
522 components: &UsefulMenuComponents,
523) -> Rc<Component> {
524 let component = Rc::new_cyclic(|component_weak| {
525 let root_element = Rc::new(RefCell::new(Element {
526 base_type: components.empty.clone(),
527 children,
528 enclosing_component: component_weak.clone(),
529 ..Default::default()
530 }));
531 recurse_elem(&root_element, &true, &mut |element: &ElementRc, is_root| {
532 if !is_root {
533 debug_assert!(Weak::ptr_eq(
534 &element.borrow().enclosing_component,
535 &parent.borrow().enclosing_component
536 ));
537 element.borrow_mut().enclosing_component = component_weak.clone();
538 element.borrow_mut().geometry_props = None;
539 if element.borrow().base_type.type_name() == Some("MenuSeparator") {
540 element.borrow_mut().bindings.insert(
541 "title".into(),
542 RefCell::new(
543 Expression::StringLiteral(SmolStr::new_static(
544 MENU_SEPARATOR_PLACEHOLDER_TITLE,
545 ))
546 .into(),
547 ),
548 );
549 }
550 element.borrow_mut().base_type = components.menu_item_element.clone();
552 }
553 false
554 });
555 Component {
556 id: SmolStr::default(),
557 root_element,
558 parent_element: Rc::downgrade(parent),
559 ..Default::default()
560 }
561 });
562 parent
563 .borrow()
564 .enclosing_component
565 .upgrade()
566 .unwrap()
567 .menu_item_tree
568 .borrow_mut()
569 .push(component.clone());
570 component
571}