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    // Keep the menubar alive on the owning sub-component: the native menu bar only holds a weak
212    // reference to it.
213    *current.menubar.borrow_mut() = Some(vtable::VRc::clone(&menubar));
214
215    if !no_native && window_inner.supports_native_menu_bar() {
216        window_inner.setup_menubar(menubar);
217        return Value::Void;
218    }
219
220    // Wire up entries/sub_menu/activated for the fallback menu bar widget.
221    wire_menu_from_item_tree(ctx, entries_ref, sub_menu_ref, activated_ref, menu_item_tree);
222
223    Value::Void
224}
225
226pub(crate) fn setup_system_tray_icon(ctx: &mut EvalContext, arguments: &[Expression]) -> Value {
227    // Expected args (from `lower_menus`):
228    //   0: PropertyReference to the native `SystemTrayIcon` item
229    //   1: NumberLiteral(tree_index) into the owning sub-component's menu_item_trees
230    //   2 (optional): condition expression from `if cond : Menu { ... }`
231    let [
232        Expression::PropertyReference(system_tray_ref),
233        Expression::NumberLiteral(tree_index),
234        rest @ ..,
235    ] = arguments
236    else {
237        return Value::Void;
238    };
239
240    let current = match ctx.current.as_ref() {
241        Some(c) => c.clone(),
242        None => return Value::Void,
243    };
244    let cu = current.compilation_unit.clone();
245    let sc = &cu.sub_components[current.sub_component_idx];
246    let Some(menu_tree) = sc.menu_item_trees.get(*tree_index as usize) else {
247        return Value::Void;
248    };
249
250    let menu_vrc = new_popup_for(&current, menu_tree);
251    crate::instance::finalize_instance(&menu_vrc);
252
253    // `if cond : Menu { ... }` lowers the condition into a closure that
254    // gates the menu's shadow tree.
255    let condition: Box<dyn Fn() -> bool> = match rest.first() {
256        Some(expr) => Box::new(bool_binding(&current, expr)),
257        None => Box::new(|| true),
258    };
259    let menu_item_tree =
260        vtable::VRc::new(i_slint_core::menus::MenuFromItemTree::new_with_condition_and_visible(
261            vtable::VRc::into_dyn(menu_vrc),
262            condition,
263            || true,
264        ));
265
266    let Some((parent_inst, flat_idx)) = resolve_item_rc_from_ref(ctx, system_tray_ref) else {
267        return Value::Void;
268    };
269    let item_rc =
270        i_slint_core::items::ItemRc::new(vtable::VRc::into_dyn(parent_inst), flat_idx as u32);
271    if let Some(tray) = item_rc.downcast::<i_slint_core::items::SystemTrayIcon>() {
272        tray.as_pin_ref().set_menu(&item_rc, vtable::VRc::into_dyn(menu_item_tree));
273    }
274    Value::Void
275}
276
277pub(crate) fn show_popup_menu(ctx: &mut EvalContext, arguments: &[Expression]) -> Value {
278    let [Expression::PropertyReference(context_menu_ref), entries_expr, position_expr] = arguments
279    else {
280        return Value::Void;
281    };
282
283    let position: i_slint_core::api::LogicalPosition =
284        eval_expression(ctx, position_expr).try_into().unwrap_or_default();
285
286    let Some((parent_inst, context_flat_idx)) = resolve_item_rc_from_ref(ctx, context_menu_ref)
287    else {
288        return Value::Void;
289    };
290    let context_item_rc = i_slint_core::items::ItemRc::new(
291        vtable::VRc::into_dyn(parent_inst.clone()),
292        context_flat_idx as u32,
293    );
294    let context_menu_item_weak = context_item_rc.downgrade();
295    let Some(adapter) = find_window_adapter(ctx) else {
296        return Value::Void;
297    };
298
299    let cu = ctx.current.as_ref().map(|c| c.compilation_unit.clone()).unwrap();
300    let Some(popup_menu) = cu.popup_menu.as_ref() else {
301        return Value::Void;
302    };
303
304    let current = ctx.current.as_ref().unwrap();
305    let popup_vrc = new_popup_for(current, &popup_menu.item_tree);
306    // Install bindings now but defer `init_code` until after `show_popup`,
307    // so `forward-focus` calls reach a popup the window adapter already
308    // considers active.
309    crate::instance::install_bindings_for_repeated_row(&popup_vrc);
310
311    // Wire entries/sub_menu/activated on the popup. Two flavors:
312    //
313    //   - `ShowPopupMenu` (regular `ContextMenuArea`): the entries come
314    //     from a `MenuItem` tree we walk via `MenuFromItemTree`.
315    //   - `ShowPopupMenuInternal` (`ContextMenuInternal`): the entries
316    //     come from an array property on the user's `ContextMenu` item,
317    //     and `sub_menu` / `activated` forward back to the user's
318    //     callbacks rather than a shadow tree.
319    let popup_ctx = crate::eval::EvalContext::new(popup_vrc.root_sub_component.clone());
320
321    if let Expression::NumberLiteral(tree_index) = entries_expr {
322        let sc = &cu.sub_components[current.sub_component_idx];
323        let Some(menu_tree) = sc.menu_item_trees.get(*tree_index as usize) else {
324            return Value::Void;
325        };
326        let menu_vrc = crate::instance::Instance::new_popup(
327            cu.clone(),
328            menu_tree,
329            std::rc::Rc::downgrade(&Pin::into_inner(current.clone())),
330            popup_vrc.globals.clone(),
331        );
332        crate::instance::finalize_instance(&menu_vrc);
333        let menu_item_tree = vtable::VRc::new(i_slint_core::menus::MenuFromItemTree::new(
334            vtable::VRc::into_dyn(menu_vrc),
335        ));
336
337        // Prefer the platform's native context menu; fall back to the
338        // Slint-rendered popup below when the backend doesn't provide one.
339        if i_slint_core::window::WindowInner::from_pub(adapter.window()).show_native_popup_menu(
340            vtable::VRc::into_dyn(vtable::VRc::clone(&menu_item_tree)),
341            position,
342            &context_item_rc,
343        ) {
344            return Value::Void;
345        }
346
347        wire_menu_from_item_tree(
348            &popup_ctx,
349            &popup_menu.entries,
350            &popup_menu.sub_menu,
351            &popup_menu.activated,
352            menu_item_tree,
353        );
354    } else {
355        // ShowPopupMenuInternal: entries are an array property of the
356        // `ContextMenuInternal` item; sub_menu/activated forward to the
357        // user-defined callbacks on the same item.
358        let entries_value = eval_expression(ctx, entries_expr);
359        wire_popup_menu_prop(&popup_ctx, &popup_menu.entries, move || entries_value.clone());
360
361        if let MemberReference::Relative { parent_level, local_reference } = context_menu_ref {
362            let sub_menu_owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
363            let LocalMemberIndex::Native { item_index, .. } = &local_reference.reference else {
364                return Value::Void;
365            };
366            let item_index_for_sub = *item_index;
367            let sub_menu_owner_weak =
368                std::rc::Rc::downgrade(&Pin::into_inner(sub_menu_owner.clone()));
369            wire_popup_menu_cb(&popup_ctx, &popup_menu.sub_menu, move |args| {
370                let Some(owner) = sub_menu_owner_weak.upgrade() else { return Value::Void };
371                let item = Pin::as_ref(&owner.items[item_index_for_sub]);
372                let entry: i_slint_core::items::MenuEntry =
373                    args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
374                let raw = item.as_item_ref();
375                use i_slint_core::items::ContextMenu;
376                let Some(cm) = vtable::VRef::downcast_pin::<ContextMenu>(raw) else {
377                    return Value::Void;
378                };
379                let mut out = i_slint_core::SharedVector::default();
380                cm.sub_menu.call(&(entry,)).iter().for_each(|e| out.push(e));
381                Value::Model(i_slint_core::model::ModelRc::new(
382                    i_slint_core::model::VecModel::from(
383                        out.into_iter().map(Value::from).collect::<Vec<_>>(),
384                    ),
385                ))
386            });
387
388            let activated_owner_weak =
389                std::rc::Rc::downgrade(&Pin::into_inner(sub_menu_owner.clone()));
390            let item_index_for_activated = *item_index;
391            wire_popup_menu_cb(&popup_ctx, &popup_menu.activated, move |args| {
392                let Some(owner) = activated_owner_weak.upgrade() else { return Value::Void };
393                let item = Pin::as_ref(&owner.items[item_index_for_activated]);
394                let entry: i_slint_core::items::MenuEntry =
395                    args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
396                let raw = item.as_item_ref();
397                use i_slint_core::items::ContextMenu;
398                let Some(cm) = vtable::VRef::downcast_pin::<ContextMenu>(raw) else {
399                    return Value::Void;
400                };
401                cm.activated.call(&(entry,));
402                Value::Void
403            });
404        }
405    }
406
407    // Wire the popup menu's `close` callback so a menu item that calls
408    // `root.close()` dismisses the popup. The shared `Cell` bridges the
409    // popup id (only known after `show_popup`) into the close handler
410    // installed before the show.
411    let popup_id_cell: std::rc::Rc<std::cell::Cell<Option<core::num::NonZeroU32>>> =
412        std::rc::Rc::new(std::cell::Cell::new(None));
413    let id_cell_for_close = popup_id_cell.clone();
414    let adapter_for_close = adapter.clone();
415    wire_popup_menu_cb(&popup_ctx, &popup_menu.close, move |_args| {
416        if let Some(id) = id_cell_for_close.take() {
417            i_slint_core::window::WindowInner::from_pub(adapter_for_close.window()).close_popup(id);
418        }
419        Value::Void
420    });
421
422    let popup_dyn = vtable::VRc::into_dyn(popup_vrc.clone());
423    let window_inner = i_slint_core::window::WindowInner::from_pub(adapter.window());
424    let popup_id = window_inner.show_popup(
425        &popup_dyn,
426        Box::new(move || position),
427        i_slint_core::items::PopupClosePolicy::CloseOnClickOutside,
428        &context_item_rc,
429        i_slint_core::window::WindowKind::Menu,
430        Box::new(|_| {}),
431    );
432    popup_id_cell.set(Some(popup_id));
433    // Store the popup id on the native `ContextMenu` item, so its
434    // `is-open()` and `close()` member functions see this popup as the
435    // active one.
436    if let Some(item_rc) = context_menu_item_weak.upgrade()
437        && let Some(cm) = item_rc.downcast::<i_slint_core::items::ContextMenu>()
438    {
439        cm.as_pin_ref().popup_id.set(Some(popup_id));
440    }
441
442    // `init_code` runs now that the popup is registered, so `forward-focus`
443    // calls can target items in the popup.
444    crate::instance::finalize_instance(&popup_vrc);
445
446    Value::Void
447}
448
449/// Build a popup / menu `Instance` owned by `owner`, sharing the root
450/// instance's global storage (or fresh storage when the owner is already
451/// detached from a root).
452fn new_popup_for(
453    owner: &Pin<Rc<SubComponentInstance>>,
454    item_tree: &i_slint_compiler::llr::ItemTree,
455) -> vtable::VRc<i_slint_core::item_tree::ItemTreeVTable, crate::instance::Instance> {
456    let cu = owner.compilation_unit.clone();
457    let parent_weak = std::rc::Rc::downgrade(&Pin::into_inner(owner.clone()));
458    let globals = owner
459        .root
460        .get()
461        .and_then(|w| w.upgrade())
462        .map(|inst| inst.globals.clone())
463        .unwrap_or_else(|| std::rc::Rc::new(crate::globals::GlobalStorage::new(&cu)));
464    crate::instance::Instance::new_popup(cu, item_tree, parent_weak, globals)
465}
466
467/// A closure that evaluates `expr` in `owner`'s scope on each call;
468/// `false` once the owner is gone.
469fn bool_binding(
470    owner: &Pin<Rc<SubComponentInstance>>,
471    expr: &Expression,
472) -> impl Fn() -> bool + 'static {
473    let expr = expr.clone();
474    let weak = std::rc::Rc::downgrade(&Pin::into_inner(owner.clone()));
475    move || {
476        let Some(owner) = weak.upgrade() else { return false };
477        let mut ctx = EvalContext::new(Pin::new(owner));
478        matches!(eval_expression(&mut ctx, &expr), Value::Bool(true))
479    }
480}
481
482/// Wire the `entries` property and the `sub-menu` / `activated` callbacks
483/// to a `MenuFromItemTree`. Shared by the menu bar fallback widget and the
484/// Slint-rendered context menu popup.
485fn wire_menu_from_item_tree(
486    ctx: &EvalContext,
487    entries: &MemberReference,
488    sub_menu: &MemberReference,
489    activated: &MemberReference,
490    menu_item_tree: vtable::VRc<
491        i_slint_core::menus::MenuVTable,
492        i_slint_core::menus::MenuFromItemTree,
493    >,
494) {
495    fn entries_model(
496        mt: &i_slint_core::menus::MenuFromItemTree,
497        parent: Option<&i_slint_core::items::MenuEntry>,
498    ) -> Value {
499        let mut entries = i_slint_core::SharedVector::default();
500        i_slint_core::menus::Menu::sub_menu(mt, parent, &mut entries);
501        Value::Model(i_slint_core::model::ModelRc::new(i_slint_core::model::VecModel::from(
502            entries.into_iter().map(Value::from).collect::<Vec<_>>(),
503        )))
504    }
505    let mt = vtable::VRc::clone(&menu_item_tree);
506    wire_popup_menu_prop(ctx, entries, move || entries_model(&mt, None));
507    let mt = vtable::VRc::clone(&menu_item_tree);
508    wire_popup_menu_cb(ctx, sub_menu, move |args| {
509        let entry = args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
510        entries_model(&mt, Some(&entry))
511    });
512    wire_popup_menu_cb(ctx, activated, move |args| {
513        let entry = args.first().cloned().unwrap_or_default().try_into().unwrap_or_default();
514        i_slint_core::menus::Menu::activate(&*menu_item_tree, &entry);
515        Value::Void
516    });
517}
518
519fn wire_popup_menu_prop(
520    ctx: &EvalContext,
521    mr: &MemberReference,
522    binding: impl Fn() -> Value + 'static,
523) {
524    if let MemberReference::Relative { parent_level, local_reference } = mr {
525        let owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
526        if let LocalMemberIndex::Property(idx) = &local_reference.reference {
527            Pin::as_ref(&owner.properties[*idx]).set_binding(binding);
528        }
529    }
530}
531
532fn wire_popup_menu_cb(
533    ctx: &EvalContext,
534    mr: &MemberReference,
535    handler: impl Fn(&[Value]) -> Value + 'static,
536) {
537    if let MemberReference::Relative { parent_level, local_reference } = mr {
538        let owner = walk_to(ctx, *parent_level, &local_reference.sub_component_path);
539        if let LocalMemberIndex::Callback(idx) = &local_reference.reference {
540            Pin::as_ref(&owner.callbacks[*idx]).set_handler(handler);
541        }
542    }
543}