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