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