Skip to content

NOT FUNCTIONAL: muda context menu support #8803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion api/rs/slint/private_unstable_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ pub mod re_exports {
pub use i_slint_core::lengths::{
logical_position_to_api, LogicalLength, LogicalPoint, LogicalRect,
};
pub use i_slint_core::menus::{Menu, MenuFromItemTree, MenuVTable};
pub use i_slint_core::menus::{ContextMenuFromItemTree, Menu, MenuFromItemTree, MenuVTable};
pub use i_slint_core::model::*;
pub use i_slint_core::properties::{
set_state_binding, ChangeTracker, Property, PropertyTracker, StateInfo,
Expand Down
15 changes: 11 additions & 4 deletions internal/backends/winit/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub enum CustomEvent {
#[cfg(enable_accesskit)]
Accesskit(accesskit_winit::Event),
#[cfg(muda)]
Muda(muda::MenuEvent),
Muda(muda::MenuEvent, MudaType),
}

impl std::fmt::Debug for CustomEvent {
Expand All @@ -52,11 +52,18 @@ impl std::fmt::Debug for CustomEvent {
#[cfg(enable_accesskit)]
Self::Accesskit(a) => write!(f, "AccessKit({a:?})"),
#[cfg(muda)]
Self::Muda(e) => write!(f, "Muda({e:?})"),
Self::Muda(e, mt) => write!(f, "Muda({e:?},{mt:?})"),
}
}
}

#[cfg(muda)]
#[derive(Clone, Copy, Debug)]
pub enum MudaType {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably best to move that to muda.rs

Menubar,
Context,
}

pub struct EventLoopState {
shared_backend_data: Rc<SharedBackendData>,
// last seen cursor position
Expand Down Expand Up @@ -427,15 +434,15 @@ impl winit::application::ApplicationHandler<SlintEvent> for EventLoopState {
event_loop.set_control_flow(ControlFlow::Poll);
}
#[cfg(muda)]
CustomEvent::Muda(event) => {
CustomEvent::Muda(event, muda_type) => {
if let Some((window, eid)) = event.id().0.split_once('|').and_then(|(w, e)| {
Some((
self.shared_backend_data
.window_by_id(winit::window::WindowId::from(w.parse::<u64>().ok()?))?,
e.parse::<usize>().ok()?,
))
}) {
window.muda_event(eid);
window.muda_event(eid, muda_type);
};
}
}
Expand Down
41 changes: 40 additions & 1 deletion internal/backends/winit/muda.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
// 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

use super::event_loop::MudaType;
use super::CustomEvent;
use super::WinitWindowAdapter;
use crate::SlintEvent;
use core::pin::Pin;
use i_slint_core::api::LogicalPosition;
use i_slint_core::items::MenuEntry;
use i_slint_core::menus::MenuVTable;
use i_slint_core::properties::{PropertyDirtyHandler, PropertyTracker};
use muda::ContextMenu;
use std::rc::Weak;
use winit::event_loop::EventLoopProxy;
use winit::window::Window;
Expand Down Expand Up @@ -42,7 +46,7 @@ impl MudaAdapter {
let menu = muda::Menu::new();

muda::MenuEvent::set_event_handler(Some(move |e| {
let _ = proxy.send_event(SlintEvent(crate::event_loop::CustomEvent::Muda(e)));
let _ = proxy.send_event(SlintEvent(CustomEvent::Muda(e, MudaType::Menubar)));
}));

#[cfg(target_os = "windows")]
Expand All @@ -67,6 +71,41 @@ impl MudaAdapter {
s
}

pub fn show_context_menu(
context_menu: &vtable::VBox<MenuVTable>,
winit_window: &Window,
position: LogicalPosition,
proxy: EventLoopProxy<SlintEvent>,
) -> Self {
let menu = muda::Menu::new();

muda::MenuEvent::set_event_handler(Some(move |e| {
let _ = proxy.send_event(SlintEvent(CustomEvent::Muda(e, MudaType::Context)));
}));

let mut s = Self { entries: Default::default(), tracker: None, menu };
s.rebuild_menu(winit_window, Some(context_menu));

let position = i_slint_core::api::WindowPosition::Logical(position);
let position = Some(crate::winitwindowadapter::position_to_winit(&position));

#[cfg(target_os = "windows")]
{
use winit::raw_window_handle::*;
if let RawWindowHandle::Win32(handle) = winit_window.window_handle().unwrap().as_raw() {
unsafe {
s.menu.show_context_menu_for_hwnd(handle.hwnd.get(), position);
}
}
}
#[cfg(target_os = "macos")]
{
todo!(); // unsafe { menu.show_context_menu_for_nsview(nsview, position) };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this function returned an option, you could return None there. And WindowAdatper::show_context_menu could return false to tell the runtime to fallback to the Slint based menu.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose I was assuming (perhaps incorrectly) that the macOS version of the call was a relatively simple call, and that it in practice it might be easier to just identify the right call than it would be to build a bunch of scaffolding?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm - saw your next comment

}

s
}

pub fn rebuild_menu(
&mut self,
winit_window: &Window,
Expand Down
61 changes: 53 additions & 8 deletions internal/backends/winit/winitwindowadapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ use std::rc::Rc;
use std::rc::Weak;
use std::sync::Arc;

#[cfg(muda)]
use i_slint_core::api::LogicalPosition;
use i_slint_core::lengths::{PhysicalPx, ScaleFactor};
use winit::event_loop::ActiveEventLoop;
#[cfg(target_arch = "wasm32")]
use winit::platform::web::WindowExtWebSys;
#[cfg(target_family = "windows")]
use winit::platform::windows::WindowExtWindows;

#[cfg(muda)]
use crate::event_loop::MudaType;
use crate::renderer::WinitCompatibleRenderer;

use corelib::item_tree::ItemTreeRc;
Expand All @@ -43,7 +47,7 @@ use std::cell::OnceCell;
use winit::event_loop::EventLoopProxy;
use winit::window::{WindowAttributes, WindowButtons};

fn position_to_winit(pos: &corelib::api::WindowPosition) -> winit::dpi::Position {
pub(crate) fn position_to_winit(pos: &corelib::api::WindowPosition) -> winit::dpi::Position {
match pos {
corelib::api::WindowPosition::Logical(pos) => {
winit::dpi::Position::new(winit::dpi::LogicalPosition::new(pos.x, pos.y))
Expand Down Expand Up @@ -149,6 +153,8 @@ enum WinitWindowOrNone {
accesskit_adapter: RefCell<crate::accesskit::AccessKitAdapter>,
#[cfg(muda)]
muda_adapter: RefCell<Option<crate::muda::MudaAdapter>>,
#[cfg(muda)]
context_menu_muda_adapter: RefCell<Option<crate::muda::MudaAdapter>>,
},
None(RefCell<WindowAttributes>),
}
Expand Down Expand Up @@ -340,6 +346,9 @@ pub struct WinitWindowAdapter {
#[cfg(muda)]
menubar: RefCell<Option<vtable::VBox<i_slint_core::menus::MenuVTable>>>,

#[cfg(muda)]
context_menu: RefCell<Option<vtable::VBox<i_slint_core::menus::MenuVTable>>>,

#[cfg(all(muda, target_os = "macos"))]
muda_enable_default_menu_bar: bool,

Expand Down Expand Up @@ -386,6 +395,8 @@ impl WinitWindowAdapter {
xdg_settings_watcher: Default::default(),
#[cfg(muda)]
menubar: Default::default(),
#[cfg(muda)]
context_menu: Default::default(),
#[cfg(all(muda, target_os = "macos"))]
muda_enable_default_menu_bar,
window_icon_cache_key: Default::default(),
Expand Down Expand Up @@ -477,6 +488,8 @@ impl WinitWindowAdapter {
)
})
.into(),
#[cfg(muda)]
context_menu_muda_adapter: None.into(),
};

drop(winit_window_or_none);
Expand Down Expand Up @@ -605,21 +618,31 @@ impl WinitWindowAdapter {
}

#[cfg(muda)]
pub fn muda_event(&self, entry_id: usize) {
pub fn muda_event(&self, entry_id: usize, muda_type: MudaType) {
let Ok(maybe_muda_adapter) = std::cell::Ref::filter_map(
self.winit_window_or_none.borrow(),
|winit_window_or_none| match winit_window_or_none {
WinitWindowOrNone::HasWindow { muda_adapter, .. } => Some(muda_adapter),
WinitWindowOrNone::None(..) => None,
|winit_window_or_none| match (winit_window_or_none, muda_type) {
(WinitWindowOrNone::HasWindow { muda_adapter, .. }, MudaType::Menubar) => {
Some(muda_adapter)
}
(
WinitWindowOrNone::HasWindow { context_menu_muda_adapter, .. },
MudaType::Context,
) => Some(context_menu_muda_adapter),
(WinitWindowOrNone::None(..), _) => None,
},
) else {
return;
};
let maybe_muda_adapter = maybe_muda_adapter.borrow();
let Some(muda_adapter) = maybe_muda_adapter.as_ref() else { return };
let menubar = self.menubar.borrow();
let Some(menubar) = menubar.as_ref() else { return };
muda_adapter.invoke(menubar, entry_id);
let menu = match muda_type {
MudaType::Menubar => &self.menubar,
MudaType::Context => &self.context_menu,
};
let menu = menu.borrow();
let Some(menu) = menu.as_ref() else { return };
muda_adapter.invoke(menu, entry_id);
}

#[cfg(target_arch = "wasm32")]
Expand Down Expand Up @@ -1300,6 +1323,28 @@ impl WindowAdapterInternal for WinitWindowAdapter {
}
}

#[cfg(muda)]
fn show_context_menu(
&self,
context_menu: vtable::VBox<i_slint_core::menus::MenuVTable>,
position: LogicalPosition,
) {
self.context_menu.replace(Some(context_menu));

if let WinitWindowOrNone::HasWindow { context_menu_muda_adapter, .. } =
&*self.winit_window_or_none.borrow()
{
// On Windows, we must destroy the muda menu before re-creating a new one
drop(context_menu_muda_adapter.borrow_mut().take());
context_menu_muda_adapter.replace(Some(crate::muda::MudaAdapter::show_context_menu(
self.context_menu.borrow().as_ref().unwrap(),
&self.winit_window().unwrap(),
position,
self.event_loop_proxy.clone(),
)));
}
}

#[cfg(enable_accesskit)]
fn handle_focus_change(&self, _old: Option<ItemRc>, _new: Option<ItemRc>) {
let Some(accesskit_adapter_cell) = self.accesskit_adapter() else { return };
Expand Down
46 changes: 31 additions & 15 deletions internal/compiler/generator/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2764,7 +2764,8 @@ fn compile_builtin_function_call(
}
}
BuiltinFunction::ShowPopupMenu => {
let [Expression::PropertyReference(context_menu_ref), entries, position] = arguments
let [Expression::PropertyReference(context_menu_ref), entries, position, Expression::BoolLiteral(no_native)] =
arguments
else {
panic!("internal error: invalid args to ShowPopupMenu {arguments:?}")
};
Expand Down Expand Up @@ -2855,7 +2856,7 @@ fn compile_builtin_function_call(
let entries = #entries;
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we only need this part when not using the native context menu (fallback)

let _self = popup_instance_vrc.as_pin_ref();
#access_entries.set(entries);
#access_entries.set(entries.clone());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to clone here?

#fw_sub_menu
#fw_activated
let self_weak = parent_weak.clone();
Expand All @@ -2868,22 +2869,37 @@ fn compile_builtin_function_call(
}
};
let context_menu = context_menu.unwrap();

let native_impl = if *no_native {
quote!()
} else {
quote!(if sp::WindowInner::from_pub(#window_adapter_tokens.window()).supports_native_menu_bar() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-using the supports_native_menu_bar for the context menu might not be needed. I can imagine platform having a native context menu but no native menubar or the opposite.
I'd prefer if the show_context_menu would return false for a fallback.

sp::WindowInner::from_pub(#window_adapter_tokens.window()).show_context_menu(sp::VBox::new(popup_instance_menu), #position);
} else)
};

quote!({
let position = #position;
let popup_instance = #popup_id::new(_self.globals.get().unwrap().clone()).unwrap();
let popup_instance_vrc = sp::VRc::map(popup_instance.clone(), |x| x);
let parent_weak = _self.self_weak.get().unwrap().clone();
#init_popup
#close_popup
let id = sp::WindowInner::from_pub(#window_adapter_tokens.window()).show_popup(
&sp::VRc::into_dyn(popup_instance.into()),
position,
sp::PopupClosePolicy::CloseOnClickOutside,
#context_menu_rc,
true, // is_menu
);
#context_menu.popup_id.set(Some(id));
#popup_id::user_init(popup_instance_vrc);
{
let parent_weak = _self.self_weak.get().unwrap().clone();
#init_popup
let popup_instance_menu = sp::ContextMenuFromItemTree::new(sp::VRc::into_dyn(popup_instance.clone()), entries.clone());
#native_impl
/* else */ {
#close_popup
let id = sp::WindowInner::from_pub(#window_adapter_tokens.window()).show_popup(
&sp::VRc::into_dyn(popup_instance.into()),
position,
sp::PopupClosePolicy::CloseOnClickOutside,
#context_menu_rc,
true, // is_menu
);
#context_menu.popup_id.set(Some(id));
#popup_id::user_init(popup_instance_vrc);
}
}
})
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note that you only take care of the else branch of the if that ends here. But the other branch is for when the menu contains if _: MenuItem{} or for _ in _ : MenuItem{}

There, the context_menu_item_tree is in a Rc instead of a VBox. It might be worth putting them in a VRc in both branches.

BuiltinFunction::SetSelectionOffsets => {
Expand Down Expand Up @@ -3107,7 +3123,7 @@ fn compile_builtin_function_call(
};

quote!({
let menu_item_tree_instance = #item_tree_id::new(_self.self_weak.get().unwrap().clone()).unwrap();
let menu_item_tree_instance = #item_tree_id::new(_self.self_weak.get().unwrap().clone()).unwrap(); // BLGAG
let menu_item_tree = sp::MenuFromItemTree::new(sp::VRc::into_dyn(menu_item_tree_instance));
#native_impl
/*else*/ {
Expand Down
10 changes: 7 additions & 3 deletions internal/compiler/passes/lower_menus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,15 @@ pub async fn lower_menus(

let mut has_menu = false;
let mut has_menubar = false;
let no_native_menu = type_loader.compiler_config.no_native_menu;

doc.visit_all_used_components(|component| {
recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == "Window") {
has_menubar |= process_window(elem, &useful_menu_component, type_loader.compiler_config.no_native_menu, diag);
}
if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal")) {
has_menu |= process_context_menu(elem, &useful_menu_component, diag);
has_menu |= process_context_menu(elem, &useful_menu_component, diag, no_native_menu);
}
})
});
Expand All @@ -169,7 +170,8 @@ pub async fn lower_menus(
recurse_elem_including_sub_components_no_borrow(&menubar_impl, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
{
has_menu |= process_context_menu(elem, &useful_menu_component, diag);
has_menu |=
process_context_menu(elem, &useful_menu_component, diag, no_native_menu);
}
});
}
Expand Down Expand Up @@ -197,7 +199,7 @@ pub async fn lower_menus(
recurse_elem_including_sub_components_no_borrow(&popup_menu_impl, &(), &mut |elem, _| {
if matches!(&elem.borrow().builtin_type(), Some(b) if matches!(b.name.as_str(), "ContextMenuArea" | "ContextMenuInternal"))
{
process_context_menu(elem, &useful_menu_component, diag);
process_context_menu(elem, &useful_menu_component, diag, no_native_menu);
}
});
doc.popup_menu_impl = popup_menu_impl.into();
Expand All @@ -208,6 +210,7 @@ fn process_context_menu(
context_menu_elem: &ElementRc,
components: &UsefulMenuComponents,
diag: &mut BuildDiagnostics,
no_native_menu: bool,
) -> bool {
let is_internal = matches!(&context_menu_elem.borrow().base_type, ElementType::Builtin(b) if b.name == "ContextMenuInternal");

Expand Down Expand Up @@ -303,6 +306,7 @@ fn process_context_menu(
index: 0,
ty: crate::typeregister::logical_point_type(),
},
Expression::BoolLiteral(no_native_menu),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The no_native_menu was done for the live preview because we don't want to replace the live previews's menubar by the previewed menubar.
I think it's fine, and even desirable, if the live preview show the native context menu.

],
source_location,
};
Expand Down
Loading
Loading