mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-16 13:30:45 +00:00

Added `checkable` property to `MenuItem` and `MenuEntry` ChangeLog: Added `checkable` and `checked` property to MenuEntry
313 lines
10 KiB
Rust
313 lines
10 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
// for MenuVTable_static
|
|
#![allow(unsafe_code)]
|
|
|
|
use crate::graphics::Image;
|
|
use crate::item_rendering::CachedRenderingData;
|
|
use crate::item_tree::{ItemTreeRc, ItemWeak, VisitChildrenResult};
|
|
use crate::items::{ItemRc, ItemRef, MenuEntry, VoidArg};
|
|
use crate::properties::PropertyTracker;
|
|
#[cfg(feature = "rtti")]
|
|
use crate::rtti::*;
|
|
use crate::string::ToSharedString;
|
|
use crate::window::WindowAdapter;
|
|
use crate::{Callback, Property, SharedString, SharedVector};
|
|
use alloc::boxed::Box;
|
|
use alloc::collections::BTreeMap;
|
|
use alloc::rc::Rc;
|
|
use core::cell::{Cell, RefCell};
|
|
use core::pin::Pin;
|
|
use i_slint_core_macros::SlintElement;
|
|
use vtable::{VRef, VRefMut};
|
|
|
|
/// Interface for native menu and menubar
|
|
#[cfg_attr(not(feature = "ffi"), i_slint_core_macros::remove_extern)]
|
|
#[vtable::vtable]
|
|
#[repr(C)]
|
|
pub struct MenuVTable {
|
|
/// Return the list of items for the sub menu (or the main menu of parent is None)
|
|
sub_menu: extern "C" fn(VRef<MenuVTable>, Option<&MenuEntry>, &mut SharedVector<MenuEntry>),
|
|
/// Handler when the menu entry is activated
|
|
activate: extern "C" fn(VRef<MenuVTable>, &MenuEntry),
|
|
/// drop_in_place handler
|
|
drop_in_place: extern "C" fn(VRefMut<MenuVTable>) -> Layout,
|
|
/// dealloc handler
|
|
dealloc: extern "C" fn(&MenuVTable, ptr: *mut u8, layout: Layout),
|
|
}
|
|
|
|
struct ShadowTreeNode {
|
|
item: ItemWeak,
|
|
children: SharedVector<MenuEntry>,
|
|
}
|
|
|
|
pub struct MenuFromItemTree {
|
|
item_tree: ItemTreeRc,
|
|
item_cache: RefCell<BTreeMap<SharedString, ShadowTreeNode>>,
|
|
root: RefCell<SharedVector<MenuEntry>>,
|
|
next_id: Cell<usize>,
|
|
tracker: Pin<Box<PropertyTracker>>,
|
|
condition: Option<Pin<Box<Property<bool>>>>,
|
|
}
|
|
|
|
impl MenuFromItemTree {
|
|
pub fn new(item_tree: ItemTreeRc) -> Self {
|
|
Self {
|
|
item_tree,
|
|
item_cache: Default::default(),
|
|
root: Default::default(),
|
|
tracker: Box::pin(PropertyTracker::default()),
|
|
next_id: 0.into(),
|
|
condition: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_with_condition(
|
|
item_tree: ItemTreeRc,
|
|
condition: impl Fn() -> bool + 'static,
|
|
) -> Self {
|
|
let cond_prop = Box::pin(Property::new_named(true, "MenuFromItemTree::condition"));
|
|
cond_prop.as_ref().set_binding(condition);
|
|
|
|
Self {
|
|
item_tree,
|
|
item_cache: Default::default(),
|
|
root: Default::default(),
|
|
tracker: Box::pin(PropertyTracker::default()),
|
|
next_id: 0.into(),
|
|
condition: Some(cond_prop),
|
|
}
|
|
}
|
|
|
|
fn update_shadow_tree(&self) {
|
|
self.tracker.as_ref().evaluate_if_dirty(|| {
|
|
self.item_cache.replace(Default::default());
|
|
if let Some(condition) = &self.condition {
|
|
if !condition.as_ref().get() {
|
|
self.root.replace(SharedVector::default());
|
|
return;
|
|
}
|
|
}
|
|
self.root.replace(
|
|
self.update_shadow_tree_recursive(&ItemRc::new(self.item_tree.clone(), 0)),
|
|
);
|
|
});
|
|
}
|
|
|
|
fn update_shadow_tree_recursive(&self, parent: &ItemRc) -> SharedVector<MenuEntry> {
|
|
let mut result = SharedVector::default();
|
|
let mut last_is_separator = false;
|
|
|
|
let mut actual_visitor =
|
|
|item_tree: &ItemTreeRc, index: u32, item_pin: Pin<ItemRef>| -> VisitChildrenResult {
|
|
if let Some(menu_item) = ItemRef::downcast_pin::<MenuItem>(item_pin) {
|
|
let title = menu_item.title();
|
|
let is_separator =
|
|
title.as_str() == i_slint_common::MENU_SEPARATOR_PLACEHOLDER_TITLE;
|
|
if is_separator && (last_is_separator || result.is_empty()) {
|
|
return VisitChildrenResult::CONTINUE;
|
|
}
|
|
last_is_separator = is_separator;
|
|
let next_id = self.next_id.get();
|
|
self.next_id.set(next_id + 1);
|
|
let id = next_id.to_shared_string();
|
|
let item = ItemRc::new(item_tree.clone(), index);
|
|
let children = self.update_shadow_tree_recursive(&item);
|
|
let has_sub_menu = !children.is_empty();
|
|
let enabled = menu_item.enabled();
|
|
let checkable = menu_item.checkable();
|
|
let checked = menu_item.checked();
|
|
let icon = menu_item.icon();
|
|
self.item_cache.borrow_mut().insert(
|
|
id.clone(),
|
|
ShadowTreeNode { item: ItemRc::downgrade(&item), children },
|
|
);
|
|
result.push(MenuEntry {
|
|
title,
|
|
id,
|
|
has_sub_menu,
|
|
is_separator,
|
|
enabled,
|
|
checkable,
|
|
checked,
|
|
icon,
|
|
});
|
|
}
|
|
VisitChildrenResult::CONTINUE
|
|
};
|
|
vtable::new_vref!(let mut actual_visitor : VRefMut<crate::item_tree::ItemVisitorVTable> for crate::item_tree::ItemVisitor = &mut actual_visitor);
|
|
|
|
vtable::VRc::borrow_pin(parent.item_tree()).as_ref().visit_children_item(
|
|
parent.index() as isize,
|
|
crate::item_tree::TraversalOrder::BackToFront,
|
|
actual_visitor,
|
|
);
|
|
if last_is_separator {
|
|
result.pop();
|
|
}
|
|
result
|
|
}
|
|
}
|
|
|
|
impl Menu for MenuFromItemTree {
|
|
fn sub_menu(&self, parent: Option<&MenuEntry>, result: &mut SharedVector<MenuEntry>) {
|
|
self.update_shadow_tree();
|
|
match parent {
|
|
Some(parent) => {
|
|
if let Some(r) = self.item_cache.borrow().get(parent.id.as_str()) {
|
|
*result = r.children.clone();
|
|
}
|
|
}
|
|
None => {
|
|
*result = self.root.borrow().clone();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn activate(&self, entry: &MenuEntry) {
|
|
if let Some(menu_item) =
|
|
self.item_cache.borrow().get(entry.id.as_str()).and_then(|e| e.item.upgrade())
|
|
{
|
|
if let Some(menu_item) = menu_item.downcast::<MenuItem>() {
|
|
if menu_item.as_pin_ref().checkable() {
|
|
menu_item.checked.set(!menu_item.as_pin_ref().checked());
|
|
}
|
|
|
|
menu_item.activated.call(&());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MenuVTable_static!(static MENU_FROM_ITEM_TREE_VT for MenuFromItemTree);
|
|
|
|
#[repr(C)]
|
|
#[derive(const_field_offset::FieldOffsets, Default, SlintElement)]
|
|
#[pin]
|
|
/// The implementation of an MenuItem items that does nothing
|
|
pub struct MenuItem {
|
|
pub cached_rendering_data: CachedRenderingData,
|
|
pub title: Property<SharedString>,
|
|
pub activated: Callback<VoidArg>,
|
|
pub enabled: Property<bool>,
|
|
pub checkable: Property<bool>,
|
|
pub checked: Property<bool>,
|
|
pub icon: Property<Image>,
|
|
}
|
|
|
|
impl crate::items::Item for MenuItem {
|
|
fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
|
|
|
|
fn layout_info(
|
|
self: Pin<&Self>,
|
|
_orientation: crate::items::Orientation,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::layout::LayoutInfo {
|
|
Default::default()
|
|
}
|
|
|
|
fn input_event_filter_before_children(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::InputEventFilterResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn input_event(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::MouseEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::InputEventResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn capture_key_event(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::KeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::KeyEventResult {
|
|
crate::input::KeyEventResult::EventIgnored
|
|
}
|
|
|
|
fn key_event(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::KeyEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::KeyEventResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn focus_event(
|
|
self: Pin<&Self>,
|
|
_: &crate::input::FocusEvent,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
) -> crate::input::FocusEventResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn render(
|
|
self: Pin<&Self>,
|
|
_backend: &mut &mut dyn crate::item_rendering::ItemRenderer,
|
|
_self_rc: &ItemRc,
|
|
_size: crate::lengths::LogicalSize,
|
|
) -> crate::items::RenderingResult {
|
|
Default::default()
|
|
}
|
|
|
|
fn bounding_rect(
|
|
self: Pin<&Self>,
|
|
_window_adapter: &Rc<dyn WindowAdapter>,
|
|
_self_rc: &ItemRc,
|
|
geometry: crate::lengths::LogicalRect,
|
|
) -> crate::lengths::LogicalRect {
|
|
geometry
|
|
}
|
|
|
|
fn clips_children(self: core::pin::Pin<&Self>) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
impl crate::items::ItemConsts for MenuItem {
|
|
const cached_rendering_data_offset: const_field_offset::FieldOffset<
|
|
MenuItem,
|
|
CachedRenderingData,
|
|
> = MenuItem::FIELD_OFFSETS.cached_rendering_data.as_unpinned_projection();
|
|
}
|
|
|
|
#[cfg(feature = "ffi")]
|
|
pub mod ffi {
|
|
use super::*;
|
|
|
|
/// Create a `VRc::<MenuVTable>`` that wraps the [`ItemTreeRc`]
|
|
///
|
|
/// Put the created VRc into the result pointer with std::ptr::write
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn slint_menus_create_wrapper(
|
|
menu_tree: &ItemTreeRc,
|
|
result: *mut vtable::VRc<MenuVTable>,
|
|
condition: Option<extern "C" fn(menu_tree: &ItemTreeRc) -> bool>,
|
|
) {
|
|
let menu = match condition {
|
|
Some(condition) => {
|
|
let menu_weak = ItemTreeRc::downgrade(menu_tree);
|
|
MenuFromItemTree::new_with_condition(menu_tree.clone(), move || {
|
|
let Some(menu_rc) = menu_weak.upgrade() else { return false };
|
|
condition(&menu_rc)
|
|
})
|
|
}
|
|
None => MenuFromItemTree::new(menu_tree.clone()),
|
|
};
|
|
|
|
let vrc = vtable::VRc::into_dyn(vtable::VRc::new(menu));
|
|
core::ptr::write(result, vrc);
|
|
}
|
|
}
|