Skip to main content

slint_interpreter/
popup.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//! Runtime side of the `ShowPopupWindow` / `ClosePopupWindow` /
5//! `SetupMenuBar` / `ShowPopupMenu` builtins: build the popup `Instance`,
6//! register it with the window, and wire the menu callbacks.
7
8use crate::Value;
9use crate::eval::{
10    EvalContext, eval_expression, find_window_adapter, resolve_item_rc_from_ref, store_property,
11    walk_to,
12};
13use crate::instance::SubComponentInstance;
14use i_slint_compiler::llr::{Expression, LocalMemberIndex, MemberReference};
15use i_slint_core::model::Model;
16use std::pin::Pin;
17use std::rc::Rc;
18
19pub(crate) fn show_popup_window(ctx: &mut EvalContext, arguments: &[Expression]) -> Value {
20    // Expected args (from `BuiltinFunction::ShowPopupWindow`):
21    //   0: NumberLiteral(popup_index) into the declaring sub-component's popup_windows
22    //   1: close_policy expression
23    //   2: PropertyReference to the declaring component's root (`owner_ref`): its
24    //      `popup_id` and the popup's scope live here. Resolved through the full
25    //      reference (parent level *and* sub-component path) so a popup shown from
26    //      a nested sub-component or an inlined function resolves correctly.
27    //   3: PropertyReference to the parent item used for positioning (`anchor_ref`),
28    //      which may be a different (nested) item than the declaring component.
29    //   4 (optional): PropertyReference to the synthesized `is-open` property,
30    //      resolving in this call's own frame (see lower_show_popup_window)
31    let [
32        Expression::NumberLiteral(popup_index),
33        close_policy_expr,
34        Expression::PropertyReference(owner_ref),
35        Expression::PropertyReference(anchor_ref),
36        is_open_args @ ..,
37    ] = arguments
38    else {
39        return Value::Void;
40    };
41    let MemberReference::Relative { parent_level, local_reference } = owner_ref else {
42        return Value::Void;
43    };
44
45    let current = match ctx.current.as_ref() {
46        Some(c) => c.clone(),
47        None => return Value::Void,
48    };
49    let owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
50    let cu = owner.compilation_unit.clone();
51    let sc = &cu.sub_components[owner.sub_component_idx];
52    let popup = match sc.popup_windows.get(*popup_index as usize) {
53        Some(p) => p,
54        None => return Value::Void,
55    };
56
57    let popup_vrc = new_popup_for(&owner, &popup.item_tree);
58
59    let close_policy: i_slint_core::items::PopupClosePolicy =
60        eval_expression(ctx, close_policy_expr).try_into().unwrap_or_default();
61
62    let Some((parent_instance, parent_flat)) = resolve_item_rc_from_ref(ctx, anchor_ref) else {
63        return Value::Void;
64    };
65    let parent_item_rc = i_slint_core::items::ItemRc::new(
66        vtable::VRc::into_dyn(parent_instance.clone()),
67        parent_flat as u32,
68    );
69    // The owner may itself be a popup without its own window adapter; walk
70    // the parent chain so a popup-in-popup registers with the root window's
71    // adapter instead of creating a fresh, headless one.
72    let Some(adapter) = find_window_adapter(ctx) else {
73        return Value::Void;
74    };
75
76    let window_kind = || {
77        if popup.is_tooltip {
78            i_slint_core::window::WindowKind::ToolTip
79        } else {
80            i_slint_core::window::WindowKind::Popup
81        }
82    };
83    // Give the popup its own window adapter when the backend can create
84    // one — `WindowInner::show_popup` then shows it as a native top-level
85    // popup instead of a child window rendered into the parent surface.
86    if let Some(child_adapter) = i_slint_core::window::WindowInner::from_pub(adapter.window())
87        .create_child_window_adapter(window_kind())
88    {
89        let _ = popup_vrc.window_adapter.set(child_adapter);
90    }
91
92    // Install bindings now but defer `init_code` until after `show_popup`,
93    // so `forward-focus` calls reach a popup the window adapter already
94    // considers active.
95    crate::instance::install_bindings_for_repeated_row(&popup_vrc);
96
97    // The position expression is evaluated lazily so the window can
98    // re-query it after measuring the popup.
99    let access_position: Box<dyn Fn() -> i_slint_core::api::LogicalPosition> = {
100        let pos_expr = popup.position.borrow().clone();
101        let popup_root = popup_vrc.root_sub_component.clone();
102        Box::new(move || {
103            let mut popup_ctx = EvalContext::new(popup_root.clone());
104            eval_expression(&mut popup_ctx, &pos_expr).try_into().unwrap_or_default()
105        })
106    };
107    // Keeps the caller's synthesized `is-open` property in sync:
108    // `show_popup` invokes this setter with `true` on show and `false` from
109    // every close path. The reference resolves in the show call's own frame.
110    let is_open_setter: Box<dyn Fn(bool)> = match is_open_args.first() {
111        Some(Expression::PropertyReference(is_open_ref)) => {
112            let is_open_ref = is_open_ref.clone();
113            let current_weak = std::rc::Rc::downgrade(&Pin::into_inner(current.clone()));
114            Box::new(move |value: bool| {
115                if let Some(current) = current_weak.upgrade() {
116                    let ctx = EvalContext::new(Pin::new(current));
117                    store_property(&ctx, &is_open_ref, Value::Bool(value));
118                }
119            })
120        }
121        _ => Box::new(|_| {}),
122    };
123    let popup_dyn = vtable::VRc::into_dyn(popup_vrc.clone());
124    let popup_id = i_slint_core::window::WindowInner::from_pub(adapter.window()).show_popup(
125        &popup_dyn,
126        access_position,
127        close_policy,
128        &parent_item_rc,
129        window_kind(),
130        is_open_setter,
131    );
132    // Remember the id so `close_popup_window` can find it by popup index.
133    if let Some(slot) = owner.popup_ids.get(*popup_index as usize) {
134        slot.set(Some(popup_id));
135    }
136    // `init_code` runs now that the popup is registered, so `forward-focus`
137    // calls can target items in the popup.
138    crate::instance::finalize_instance(&popup_vrc);
139    Value::Void
140}
141
142pub(crate) fn close_popup_window(ctx: &mut EvalContext, arguments: &[Expression]) -> Value {
143    // Expected args (from `BuiltinFunction::ClosePopupWindow`):
144    //   0: NumberLiteral(popup_index) into the declaring sub-component's popup_windows
145    //   1: PropertyReference to the declaring component's root, where `popup_id`
146    //      lives — resolved through the full reference like `show_popup_window`.
147    let [Expression::NumberLiteral(popup_index), Expression::PropertyReference(parent_ref)] =
148        arguments
149    else {
150        return Value::Void;
151    };
152    let MemberReference::Relative { parent_level, local_reference } = parent_ref else {
153        return Value::Void;
154    };
155    let Some(_current) = ctx.current.as_ref() else { return Value::Void };
156    // Resolve the declaring component through the full parent item reference,
157    // matching `show_popup_window` so the popup id stored there is found again.
158    let owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
159    let id = owner.popup_ids.get(*popup_index as usize).and_then(|slot| slot.take());
160    if let Some(id) = id
161        && let Some(root_inst) = owner.root.get().and_then(|w| w.upgrade())
162        && let Some(adapter) = root_inst.window_adapter_or_default()
163    {
164        i_slint_core::window::WindowInner::from_pub(adapter.window()).close_popup(id);
165    }
166    Value::Void
167}
168
169pub(crate) fn setup_menubar(ctx: &mut EvalContext, arguments: &[Expression]) -> Value {
170    let [
171        Expression::PropertyReference(entries_ref),
172        Expression::PropertyReference(sub_menu_ref),
173        Expression::PropertyReference(activated_ref),
174        Expression::NumberLiteral(tree_index),
175        Expression::BoolLiteral(no_native),
176        condition,
177        visible,
178        ..,
179    ] = arguments
180    else {
181        return Value::Void;
182    };
183    let (tree_index, no_native) = (*tree_index as usize, *no_native);
184
185    let current = match ctx.current.as_ref() {
186        Some(c) => c.clone(),
187        None => return Value::Void,
188    };
189    let cu = current.compilation_unit.clone();
190    let sc = &cu.sub_components[current.sub_component_idx];
191    let Some(menu_tree) = sc.menu_item_trees.get(tree_index) else {
192        return Value::Void;
193    };
194
195    let menu_vrc = new_popup_for(&current, menu_tree);
196    crate::instance::finalize_instance(&menu_vrc);
197
198    let menu_dyn = vtable::VRc::into_dyn(menu_vrc);
199    let menu_item_tree =
200        vtable::VRc::new(i_slint_core::menus::MenuFromItemTree::new_with_condition_and_visible(
201            menu_dyn,
202            bool_binding(&current, condition),
203            bool_binding(&current, visible),
204        ));
205
206    let Some(adapter) = find_window_adapter(ctx) else { return Value::Void };
207    let window_inner = i_slint_core::window::WindowInner::from_pub(adapter.window());
208    let menubar = vtable::VRc::into_dyn(vtable::VRc::clone(&menu_item_tree));
209    window_inner.setup_menubar_shortcuts(vtable::VRc::clone(&menubar));
210
211    if !no_native && window_inner.supports_native_menu_bar() {
212        window_inner.setup_menubar(menubar);
213        return Value::Void;
214    }
215
216    // Keep the menubar alive on the owning sub-component.
217    *current.menubar.borrow_mut() = Some(menubar);
218
219    // Wire up entries/sub_menu/activated for the fallback menu bar widget.
220    wire_menu_from_item_tree(ctx, entries_ref, sub_menu_ref, activated_ref, menu_item_tree);
221
222    Value::Void
223}
224
225pub(crate) fn setup_system_tray_icon(ctx: &mut EvalContext, arguments: &[Expression]) -> Value {
226    // Expected args (from `lower_menus`):
227    //   0: PropertyReference to the native `SystemTrayIcon` item
228    //   1: NumberLiteral(tree_index) into the owning sub-component's menu_item_trees
229    //   2 (optional): condition expression from `if cond : Menu { ... }`
230    let [
231        Expression::PropertyReference(system_tray_ref),
232        Expression::NumberLiteral(tree_index),
233        rest @ ..,
234    ] = arguments
235    else {
236        return Value::Void;
237    };
238
239    let current = match ctx.current.as_ref() {
240        Some(c) => c.clone(),
241        None => return Value::Void,
242    };
243    let cu = current.compilation_unit.clone();
244    let sc = &cu.sub_components[current.sub_component_idx];
245    let Some(menu_tree) = sc.menu_item_trees.get(*tree_index as usize) else {
246        return Value::Void;
247    };
248
249    let menu_vrc = new_popup_for(&current, menu_tree);
250    crate::instance::finalize_instance(&menu_vrc);
251
252    // `if cond : Menu { ... }` lowers the condition into a closure that
253    // gates the menu's shadow tree.
254    let condition: Box<dyn Fn() -> bool> = match rest.first() {
255        Some(expr) => Box::new(bool_binding(&current, expr)),
256        None => Box::new(|| true),
257    };
258    let menu_item_tree =
259        vtable::VRc::new(i_slint_core::menus::MenuFromItemTree::new_with_condition_and_visible(
260            vtable::VRc::into_dyn(menu_vrc),
261            condition,
262            || true,
263        ));
264
265    let Some((parent_inst, flat_idx)) = resolve_item_rc_from_ref(ctx, system_tray_ref) else {
266        return Value::Void;
267    };
268    let item_rc =
269        i_slint_core::items::ItemRc::new(vtable::VRc::into_dyn(parent_inst), flat_idx as u32);
270    if let Some(tray) = item_rc.downcast::<i_slint_core::items::SystemTrayIcon>() {
271        tray.as_pin_ref().set_menu(&item_rc, vtable::VRc::into_dyn(menu_item_tree));
272    }
273    Value::Void
274}
275
276pub(crate) fn show_popup_menu(ctx: &mut EvalContext, arguments: &[Expression]) -> Value {
277    let [Expression::PropertyReference(context_menu_ref), entries_expr, position_expr] = arguments
278    else {
279        return Value::Void;
280    };
281
282    let position: i_slint_core::api::LogicalPosition =
283        eval_expression(ctx, position_expr).try_into().unwrap_or_default();
284
285    let Some((parent_inst, context_flat_idx)) = resolve_item_rc_from_ref(ctx, context_menu_ref)
286    else {
287        return Value::Void;
288    };
289    let context_item_rc = i_slint_core::items::ItemRc::new(
290        vtable::VRc::into_dyn(parent_inst.clone()),
291        context_flat_idx as u32,
292    );
293    let context_menu_item_weak = context_item_rc.downgrade();
294    let Some(adapter) = find_window_adapter(ctx) else {
295        return Value::Void;
296    };
297
298    let cu = ctx.current.as_ref().map(|c| c.compilation_unit.clone()).unwrap();
299    let Some(popup_menu) = cu.popup_menu.as_ref() else {
300        return Value::Void;
301    };
302
303    let current = ctx.current.as_ref().unwrap();
304    let popup_vrc = new_popup_for(current, &popup_menu.item_tree);
305    // Install bindings now but defer `init_code` until after `show_popup`,
306    // so `forward-focus` calls reach a popup the window adapter already
307    // considers active.
308    crate::instance::install_bindings_for_repeated_row(&popup_vrc);
309
310    // Wire entries/sub_menu/activated on the popup. Two flavors:
311    //
312    //   - `ShowPopupMenu` (regular `ContextMenuArea`): the entries come
313    //     from a `MenuItem` tree we walk via `MenuFromItemTree`.
314    //   - `ShowPopupMenuInternal` (`ContextMenuInternal`): the entries
315    //     come from an array property on the user's `ContextMenu` item,
316    //     and `sub_menu` / `activated` forward back to the user's
317    //     callbacks rather than a shadow tree.
318    let popup_ctx = crate::eval::EvalContext::new(popup_vrc.root_sub_component.clone());
319
320    if let Expression::NumberLiteral(tree_index) = entries_expr {
321        let sc = &cu.sub_components[current.sub_component_idx];
322        let Some(menu_tree) = sc.menu_item_trees.get(*tree_index as usize) else {
323            return Value::Void;
324        };
325        let menu_vrc = crate::instance::Instance::new_popup(
326            cu.clone(),
327            menu_tree,
328            std::rc::Rc::downgrade(&Pin::into_inner(current.clone())),
329            popup_vrc.globals.clone(),
330        );
331        crate::instance::finalize_instance(&menu_vrc);
332        let menu_item_tree = vtable::VRc::new(i_slint_core::menus::MenuFromItemTree::new(
333            vtable::VRc::into_dyn(menu_vrc),
334        ));
335
336        // Prefer the platform's native context menu; fall back to the
337        // Slint-rendered popup below when the backend doesn't provide one.
338        if i_slint_core::window::WindowInner::from_pub(adapter.window()).show_native_popup_menu(
339            vtable::VRc::into_dyn(vtable::VRc::clone(&menu_item_tree)),
340            position,
341            &context_item_rc,
342        ) {
343            return Value::Void;
344        }
345
346        wire_menu_from_item_tree(
347            &popup_ctx,
348            &popup_menu.entries,
349            &popup_menu.sub_menu,
350            &popup_menu.activated,
351            menu_item_tree,
352        );
353    } else {
354        // ShowPopupMenuInternal: entries are an array property of the
355        // `ContextMenuInternal` item; sub_menu/activated forward to the
356        // user-defined callbacks on the same item.
357        let entries_value = eval_expression(ctx, entries_expr);
358        wire_popup_menu_prop(&popup_ctx, &popup_menu.entries, move || entries_value.clone());
359
360        if let MemberReference::Relative { parent_level, local_reference } = context_menu_ref {
361            let sub_menu_owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
362            let LocalMemberIndex::Native { item_index, .. } = &local_reference.reference else {
363                return Value::Void;
364            };
365            let item_index_for_sub = *item_index;
366            let sub_menu_owner_weak =
367                std::rc::Rc::downgrade(&Pin::into_inner(sub_menu_owner.clone()));
368            wire_popup_menu_cb(&popup_ctx, &popup_menu.sub_menu, move |args| {
369                let Some(owner) = sub_menu_owner_weak.upgrade() else { return Value::Void };
370                let item = Pin::as_ref(&owner.items[item_index_for_sub]);
371                let entry: i_slint_core::items::MenuEntry =
372                    args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
373                let raw = item.as_item_ref();
374                use i_slint_core::items::ContextMenu;
375                let Some(cm) = vtable::VRef::downcast_pin::<ContextMenu>(raw) else {
376                    return Value::Void;
377                };
378                let mut out = i_slint_core::SharedVector::default();
379                cm.sub_menu.call(&(entry,)).iter().for_each(|e| out.push(e));
380                Value::Model(i_slint_core::model::ModelRc::new(
381                    i_slint_core::model::VecModel::from(
382                        out.into_iter().map(Value::from).collect::<Vec<_>>(),
383                    ),
384                ))
385            });
386
387            let activated_owner_weak =
388                std::rc::Rc::downgrade(&Pin::into_inner(sub_menu_owner.clone()));
389            let item_index_for_activated = *item_index;
390            wire_popup_menu_cb(&popup_ctx, &popup_menu.activated, move |args| {
391                let Some(owner) = activated_owner_weak.upgrade() else { return Value::Void };
392                let item = Pin::as_ref(&owner.items[item_index_for_activated]);
393                let entry: i_slint_core::items::MenuEntry =
394                    args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
395                let raw = item.as_item_ref();
396                use i_slint_core::items::ContextMenu;
397                let Some(cm) = vtable::VRef::downcast_pin::<ContextMenu>(raw) else {
398                    return Value::Void;
399                };
400                cm.activated.call(&(entry,));
401                Value::Void
402            });
403        }
404    }
405
406    // Wire the popup menu's `close` callback so a menu item that calls
407    // `root.close()` dismisses the popup. The shared `Cell` bridges the
408    // popup id (only known after `show_popup`) into the close handler
409    // installed before the show.
410    let popup_id_cell: std::rc::Rc<std::cell::Cell<Option<core::num::NonZeroU32>>> =
411        std::rc::Rc::new(std::cell::Cell::new(None));
412    let id_cell_for_close = popup_id_cell.clone();
413    let adapter_for_close = adapter.clone();
414    wire_popup_menu_cb(&popup_ctx, &popup_menu.close, move |_args| {
415        if let Some(id) = id_cell_for_close.take() {
416            i_slint_core::window::WindowInner::from_pub(adapter_for_close.window()).close_popup(id);
417        }
418        Value::Void
419    });
420
421    let popup_dyn = vtable::VRc::into_dyn(popup_vrc.clone());
422    let window_inner = i_slint_core::window::WindowInner::from_pub(adapter.window());
423    let popup_id = window_inner.show_popup(
424        &popup_dyn,
425        Box::new(move || position),
426        i_slint_core::items::PopupClosePolicy::CloseOnClickOutside,
427        &context_item_rc,
428        i_slint_core::window::WindowKind::Menu,
429        Box::new(|_| {}),
430    );
431    popup_id_cell.set(Some(popup_id));
432    // Store the popup id on the native `ContextMenu` item, so its
433    // `is-open()` and `close()` member functions see this popup as the
434    // active one.
435    if let Some(item_rc) = context_menu_item_weak.upgrade()
436        && let Some(cm) = item_rc.downcast::<i_slint_core::items::ContextMenu>()
437    {
438        cm.as_pin_ref().popup_id.set(Some(popup_id));
439    }
440
441    // `init_code` runs now that the popup is registered, so `forward-focus`
442    // calls can target items in the popup.
443    crate::instance::finalize_instance(&popup_vrc);
444
445    Value::Void
446}
447
448/// Build a popup / menu `Instance` owned by `owner`, sharing the root
449/// instance's global storage (or fresh storage when the owner is already
450/// detached from a root).
451fn new_popup_for(
452    owner: &Pin<Rc<SubComponentInstance>>,
453    item_tree: &i_slint_compiler::llr::ItemTree,
454) -> vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, crate::instance::Instance> {
455    let cu = owner.compilation_unit.clone();
456    let parent_weak = std::rc::Rc::downgrade(&Pin::into_inner(owner.clone()));
457    let globals = owner
458        .root
459        .get()
460        .and_then(|w| w.upgrade())
461        .map(|inst| inst.globals.clone())
462        .unwrap_or_else(|| std::rc::Rc::new(crate::globals::GlobalStorage::new(&cu)));
463    crate::instance::Instance::new_popup(cu, item_tree, parent_weak, globals)
464}
465
466/// A closure that evaluates `expr` in `owner`'s scope on each call;
467/// `false` once the owner is gone.
468fn bool_binding(
469    owner: &Pin<Rc<SubComponentInstance>>,
470    expr: &Expression,
471) -> impl Fn() -> bool + 'static {
472    let expr = expr.clone();
473    let weak = std::rc::Rc::downgrade(&Pin::into_inner(owner.clone()));
474    move || {
475        let Some(owner) = weak.upgrade() else { return false };
476        let mut ctx = EvalContext::new(Pin::new(owner));
477        matches!(eval_expression(&mut ctx, &expr), Value::Bool(true))
478    }
479}
480
481/// Wire the `entries` property and the `sub-menu` / `activated` callbacks
482/// to a `MenuFromItemTree`. Shared by the menu bar fallback widget and the
483/// Slint-rendered context menu popup.
484fn wire_menu_from_item_tree(
485    ctx: &EvalContext,
486    entries: &MemberReference,
487    sub_menu: &MemberReference,
488    activated: &MemberReference,
489    menu_item_tree: vtable::VRc<
490        i_slint_core::menus::MenuVTable,
491        i_slint_core::menus::MenuFromItemTree,
492    >,
493) {
494    fn entries_model(
495        mt: &i_slint_core::menus::MenuFromItemTree,
496        parent: Option<&i_slint_core::items::MenuEntry>,
497    ) -> Value {
498        let mut entries = i_slint_core::SharedVector::default();
499        i_slint_core::menus::Menu::sub_menu(mt, parent, &mut entries);
500        Value::Model(i_slint_core::model::ModelRc::new(i_slint_core::model::VecModel::from(
501            entries.into_iter().map(Value::from).collect::<Vec<_>>(),
502        )))
503    }
504    let mt = vtable::VRc::clone(&menu_item_tree);
505    wire_popup_menu_prop(ctx, entries, move || entries_model(&mt, None));
506    let mt = vtable::VRc::clone(&menu_item_tree);
507    wire_popup_menu_cb(ctx, sub_menu, move |args| {
508        let entry = args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
509        entries_model(&mt, Some(&entry))
510    });
511    wire_popup_menu_cb(ctx, activated, move |args| {
512        let entry = args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
513        i_slint_core::menus::Menu::activate(&*menu_item_tree, &entry);
514        Value::Void
515    });
516}
517
518fn wire_popup_menu_prop(
519    ctx: &EvalContext,
520    mr: &MemberReference,
521    binding: impl Fn() -> Value + 'static,
522) {
523    if let MemberReference::Relative { parent_level, local_reference } = mr {
524        let owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
525        if let LocalMemberIndex::Property(idx) = &local_reference.reference {
526            Pin::as_ref(&owner.properties[*idx]).set_binding(binding);
527        }
528    }
529}
530
531fn wire_popup_menu_cb(
532    ctx: &EvalContext,
533    mr: &MemberReference,
534    handler: impl Fn(&[Value]) -> Value + 'static,
535) {
536    if let MemberReference::Relative { parent_level, local_reference } = mr {
537        let owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
538        if let LocalMemberIndex::Callback(idx) = &local_reference.reference {
539            Pin::as_ref(&owner.callbacks[*idx]).set_handler(handler);
540        }
541    }
542}