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