Skip to main content

i_slint_core/
menus.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// for MenuVTable_static
5#![allow(unsafe_code)]
6
7use crate::graphics::Image;
8use crate::input::{InternalKeyEvent, Keys};
9use crate::item_rendering::CachedRenderingData;
10use crate::item_tree::{ItemTreeRc, ItemWeak, VisitChildrenResult};
11use crate::items::{ItemRc, ItemRef, MenuEntry, VoidArg};
12use crate::properties::PropertyTracker;
13#[cfg(feature = "rtti")]
14use crate::rtti::*;
15use crate::string::ToSharedString;
16use crate::window::WindowAdapter;
17use crate::{Callback, Property, SharedString, SharedVector};
18use alloc::boxed::Box;
19use alloc::collections::BTreeMap;
20use alloc::rc::Rc;
21use core::cell::{Cell, RefCell};
22use core::pin::Pin;
23use i_slint_core_macros::SlintElement;
24use vtable::{VRef, VRefMut};
25
26/// Interface for native menu and menubar
27#[cfg_attr(not(feature = "ffi"), i_slint_core_macros::remove_extern)]
28#[vtable::vtable]
29#[repr(C)]
30pub struct MenuVTable {
31    /// Return the list of items for the sub menu (or the main menu of parent is None)
32    sub_menu: extern "C" fn(VRef<MenuVTable>, Option<&MenuEntry>, &mut SharedVector<MenuEntry>),
33    /// Handler when the menu entry is activated
34    activate: extern "C" fn(VRef<MenuVTable>, &MenuEntry),
35    /// Handler to return whether the menu is visible or not
36    visible: extern "C" fn(VRef<MenuVTable>) -> bool,
37    /// drop_in_place handler
38    drop_in_place: extern "C" fn(VRefMut<MenuVTable>) -> Layout,
39    /// dealloc handler
40    dealloc: extern "C" fn(&MenuVTable, ptr: *mut u8, layout: Layout),
41}
42
43struct ShadowTreeNode {
44    item: ItemWeak,
45    children: SharedVector<MenuEntry>,
46}
47
48pub struct MenuFromItemTree {
49    item_tree: ItemTreeRc,
50    item_cache: RefCell<BTreeMap<SharedString, ShadowTreeNode>>,
51    root: RefCell<SharedVector<MenuEntry>>,
52    next_id: Cell<usize>,
53    tracker: Pin<Box<PropertyTracker>>,
54    condition: Option<Pin<Box<Property<bool>>>>,
55    visible: Option<Pin<Box<Property<bool>>>>,
56}
57
58impl MenuFromItemTree {
59    pub fn new(item_tree: ItemTreeRc) -> Self {
60        Self::new_internal(item_tree, None, None)
61    }
62
63    pub fn new_with_condition_and_visible(
64        item_tree: ItemTreeRc,
65        condition: impl Fn() -> bool + 'static,
66        visible: impl Fn() -> bool + 'static,
67    ) -> Self {
68        fn make_prop(
69            f: impl Fn() -> bool + 'static,
70            name: &'static str,
71        ) -> Option<Pin<Box<Property<bool>>>> {
72            let prop = Box::pin(Property::new_named(true, name));
73            prop.as_ref().set_binding(f);
74            Some(prop)
75        }
76        let condition = make_prop(condition, "MenuFromItemTree::condition");
77        let visible = make_prop(visible, "MenuFromItemTree::visible");
78        Self::new_internal(item_tree, condition, visible)
79    }
80
81    fn new_internal(
82        item_tree: ItemTreeRc,
83        condition: Option<Pin<Box<Property<bool>>>>,
84        visible: Option<Pin<Box<Property<bool>>>>,
85    ) -> Self {
86        Self {
87            item_tree,
88            item_cache: Default::default(),
89            root: Default::default(),
90            tracker: Box::pin(PropertyTracker::default()),
91            next_id: 0.into(),
92            condition,
93            visible,
94        }
95    }
96
97    fn update_shadow_tree(&self) {
98        self.tracker.as_ref().evaluate_if_dirty(|| {
99            self.item_cache.replace(Default::default());
100            if let Some(condition) = &self.condition
101                && !condition.as_ref().get()
102            {
103                self.root.replace(SharedVector::default());
104                return;
105            }
106            crate::item_tree::ensure_item_tree_instantiated(&self.item_tree);
107            self.root.replace(
108                self.update_shadow_tree_recursive(&ItemRc::new_root(self.item_tree.clone())),
109            );
110        });
111    }
112
113    fn update_shadow_tree_recursive(&self, parent: &ItemRc) -> SharedVector<MenuEntry> {
114        let mut result = SharedVector::default();
115        let mut last_is_separator = false;
116
117        let mut actual_visitor =
118            |item_tree: &ItemTreeRc, index: u32, item_pin: Pin<ItemRef>| -> VisitChildrenResult {
119                if let Some(menu_item) = ItemRef::downcast_pin::<MenuItem>(item_pin) {
120                    let title = menu_item.title();
121                    let is_separator =
122                        title.as_str() == i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
123                    if is_separator && (last_is_separator || result.is_empty()) {
124                        return VisitChildrenResult::CONTINUE;
125                    }
126                    last_is_separator = is_separator;
127                    let next_id = self.next_id.get();
128                    self.next_id.set(next_id + 1);
129                    let id = next_id.to_shared_string();
130                    let item = ItemRc::new(item_tree.clone(), index);
131                    let children = self.update_shadow_tree_recursive(&item);
132                    let has_sub_menu = !children.is_empty();
133                    let enabled = menu_item.enabled();
134                    let checkable = menu_item.checkable();
135                    let checked = menu_item.checked();
136                    let icon = menu_item.icon();
137                    let shortcut = menu_item.shortcut();
138                    self.item_cache.borrow_mut().insert(
139                        id.clone(),
140                        ShadowTreeNode { item: ItemRc::downgrade(&item), children },
141                    );
142                    result.push(MenuEntry {
143                        title,
144                        id,
145                        has_sub_menu,
146                        is_separator,
147                        enabled,
148                        checkable,
149                        checked,
150                        icon,
151                        shortcut,
152                    });
153                }
154                VisitChildrenResult::CONTINUE
155            };
156        vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
157
158        vtable::VRc::borrow_pin(parent.item_tree()).as_ref().visit_children_item(
159            parent.index() as isize,
160            crate::item_tree::TraversalOrder::BackToFront,
161            actual_visitor,
162        );
163        if last_is_separator {
164            result.pop();
165        }
166        result
167    }
168}
169
170impl Menu for MenuFromItemTree {
171    fn sub_menu(&self, parent: Option<&MenuEntry>, result: &mut SharedVector<MenuEntry>) {
172        self.update_shadow_tree();
173        match parent {
174            Some(parent) => {
175                if let Some(r) = self.item_cache.borrow().get(parent.id.as_str()) {
176                    *result = r.children.clone();
177                }
178            }
179            None => {
180                *result = self.root.borrow().clone();
181            }
182        }
183    }
184
185    fn activate(&self, entry: &MenuEntry) {
186        if let Some(menu_item) =
187            self.item_cache.borrow().get(entry.id.as_str()).and_then(|e| e.item.upgrade())
188            && let Some(menu_item) = menu_item.downcast::<MenuItem>()
189        {
190            if menu_item.as_pin_ref().checkable() {
191                menu_item.checked.set(!menu_item.as_pin_ref().checked());
192            }
193
194            menu_item.activated.call(&());
195        }
196    }
197
198    fn visible(&self) -> bool {
199        self.visible.as_ref().is_none_or(|v| v.as_ref().get())
200    }
201}
202
203MenuVTable_static!(static MENU_FROM_ITEM_TREE_VT for MenuFromItemTree);
204
205#[repr(C)]
206#[derive(const_field_offset::FieldOffsets, Default, SlintElement)]
207#[pin]
208/// The implementation of an MenuItem items that does nothing
209pub struct MenuItem {
210    pub cached_rendering_data: CachedRenderingData,
211    pub title: Property<SharedString>,
212    pub activated: Callback<VoidArg>,
213    pub enabled: Property<bool>,
214    pub checkable: Property<bool>,
215    pub checked: Property<bool>,
216    pub icon: Property<Image>,
217    pub shortcut: Property<Keys>,
218}
219
220impl crate::items::Item for MenuItem {
221    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
222
223    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
224
225    fn layout_info(
226        self: Pin<&Self>,
227        _orientation: crate::items::Orientation,
228        _cross_axis_constraint: crate::Coord,
229        _window_adapter: &Rc<dyn WindowAdapter>,
230        _self_rc: &ItemRc,
231    ) -> crate::layout::LayoutInfo {
232        Default::default()
233    }
234
235    fn input_event_filter_before_children(
236        self: Pin<&Self>,
237        _: &crate::input::MouseEvent,
238        _window_adapter: &Rc<dyn WindowAdapter>,
239        _self_rc: &ItemRc,
240        _: &mut crate::items::MouseCursor,
241    ) -> crate::input::InputEventFilterResult {
242        Default::default()
243    }
244
245    fn input_event(
246        self: Pin<&Self>,
247        _: &crate::input::MouseEvent,
248        _window_adapter: &Rc<dyn WindowAdapter>,
249        _self_rc: &ItemRc,
250        _: &mut crate::items::MouseCursor,
251    ) -> crate::input::InputEventResult {
252        Default::default()
253    }
254
255    fn capture_key_event(
256        self: Pin<&Self>,
257        _: &InternalKeyEvent,
258        _window_adapter: &Rc<dyn WindowAdapter>,
259        _self_rc: &ItemRc,
260    ) -> crate::input::KeyEventResult {
261        crate::input::KeyEventResult::EventIgnored
262    }
263
264    fn key_event(
265        self: Pin<&Self>,
266        _: &InternalKeyEvent,
267        _window_adapter: &Rc<dyn WindowAdapter>,
268        _self_rc: &ItemRc,
269    ) -> crate::input::KeyEventResult {
270        Default::default()
271    }
272
273    fn focus_event(
274        self: Pin<&Self>,
275        _: &crate::input::FocusEvent,
276        _window_adapter: &Rc<dyn WindowAdapter>,
277        _self_rc: &ItemRc,
278    ) -> crate::input::FocusEventResult {
279        Default::default()
280    }
281
282    fn render(
283        self: Pin<&Self>,
284        _backend: &mut &mut dyn crate::item_rendering::ItemRenderer,
285        _self_rc: &ItemRc,
286        _size: crate::lengths::LogicalSize,
287    ) -> crate::items::RenderingResult {
288        Default::default()
289    }
290
291    fn bounding_rect(
292        self: Pin<&Self>,
293        _window_adapter: &Rc<dyn WindowAdapter>,
294        _self_rc: &ItemRc,
295        geometry: crate::lengths::LogicalRect,
296    ) -> crate::lengths::LogicalRect {
297        geometry
298    }
299
300    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
301        false
302    }
303}
304
305impl crate::items::ItemConsts for MenuItem {
306    const cached_rendering_data_offset: const_field_offset::FieldOffset<
307        MenuItem,
308        CachedRenderingData,
309    > = MenuItem::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
310}
311
312#[cfg(feature = "ffi")]
313pub mod ffi {
314    use super::*;
315
316    /// Create a `VRc::<MenuVTable>`` that wraps the [`ItemTreeRc`]
317    ///
318    /// Put the created VRc into the result pointer with std::ptr::write
319    #[unsafe(no_mangle)]
320    pub unsafe extern "C" fn slint_menus_create_wrapper(
321        menu_tree: &ItemTreeRc,
322        result: *mut vtable::VRc<MenuVTable>,
323        condition: Option<extern "C" fn(menu_tree: &ItemTreeRc) -> bool>,
324        visible: Option<extern "C" fn(menu_tree: &ItemTreeRc) -> bool>,
325    ) {
326        let menu = match (condition, visible) {
327            (None, None) => MenuFromItemTree::new(menu_tree.clone()),
328            (condition, visible) => {
329                let menu_weak = ItemTreeRc::downgrade(menu_tree);
330                let condition = move || {
331                    menu_weak
332                        .upgrade()
333                        .map(|menu_rc| condition.map(|x| x(&menu_rc)).unwrap_or(true))
334                        .unwrap_or(false)
335                };
336
337                let menu_weak = ItemTreeRc::downgrade(menu_tree);
338                let visible = move || {
339                    menu_weak
340                        .upgrade()
341                        .map(|menu_rc| visible.map(|x| x(&menu_rc)).unwrap_or(true))
342                        .unwrap_or(false)
343                };
344
345                MenuFromItemTree::new_with_condition_and_visible(
346                    menu_tree.clone(),
347                    condition,
348                    visible,
349                )
350            }
351        };
352
353        let vrc = vtable::VRc::into_dyn(vtable::VRc::new(menu));
354        unsafe { core::ptr::write(result, vrc) };
355    }
356}