From 7b1ec735c27d111fe50da86e982b3904b3eb9336 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 22 Oct 2025 15:41:02 +0200 Subject: [PATCH 1/5] macos native menu bar --- Cargo.lock | 69 ++++++++++--- desktop/Cargo.toml | 1 + desktop/src/app.rs | 14 ++- desktop/src/cef.rs | 2 +- desktop/src/event.rs | 5 +- desktop/src/gpu_context.rs | 2 +- desktop/src/lib.rs | 2 + desktop/src/persist.rs | 2 +- desktop/src/render/graphics_state.rs | 2 +- desktop/src/window.rs | 13 ++- desktop/src/window/linux.rs | 7 +- desktop/src/window/mac.rs | 22 ++++- desktop/src/window/mac/menu.rs | 96 +++++++++++++++++++ desktop/src/window/win.rs | 6 +- desktop/wrapper/Cargo.toml | 1 + .../src/handle_desktop_wrapper_message.rs | 10 ++ .../wrapper/src/intercept_frontend_message.rs | 62 ++++++++++++ desktop/wrapper/src/messages.rs | 13 +++ 18 files changed, 294 insertions(+), 35 deletions(-) create mode 100644 desktop/src/window/mac/menu.rs diff --git a/Cargo.lock b/Cargo.lock index ddb7df6e62..226085de7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1078,6 +1078,15 @@ dependencies = [ "itertools 0.13.0", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1324,6 +1333,12 @@ dependencies = [ "ureq", ] +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + [[package]] name = "dpi" version = "0.1.2" @@ -2272,6 +2287,7 @@ dependencies = [ "graphite-desktop-wrapper", "libc", "metal", + "muda", "objc", "open", "rand 0.9.2", @@ -2337,6 +2353,7 @@ dependencies = [ "image", "ron", "serde", + "serde_json", "thiserror 2.0.16", "tracing", "vello", @@ -3089,6 +3106,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.9.3", + "serde", + "unicode-segmentation", +] + [[package]] name = "keyboard-types" version = "0.8.1" @@ -3398,6 +3426,25 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "keyboard-types 0.7.0", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "thiserror 2.0.16", + "windows-sys 0.60.2", +] + [[package]] name = "naga" version = "25.0.1" @@ -7203,7 +7250,7 @@ dependencies = [ "bitflags 2.9.3", "cfg_aliases", "cursor-icon", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "libc", "raw-window-handle", "rustix", @@ -7229,7 +7276,7 @@ source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec dependencies = [ "android-activity", "bitflags 2.9.3", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "ndk", "raw-window-handle", "smol_str", @@ -7245,7 +7292,7 @@ dependencies = [ "bitflags 2.9.3", "block2", "dispatch2", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -7281,8 +7328,8 @@ source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec dependencies = [ "bitflags 2.9.3", "cursor-icon", - "dpi", - "keyboard-types", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "keyboard-types 0.8.1", "raw-window-handle", "serde", "smol_str", @@ -7295,7 +7342,7 @@ version = "0.30.12" source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" dependencies = [ "bitflags 2.9.3", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "orbclient", "raw-window-handle", "redox_syscall", @@ -7312,7 +7359,7 @@ dependencies = [ "bitflags 2.9.3", "block2", "dispatch2", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -7334,7 +7381,7 @@ dependencies = [ "bitflags 2.9.3", "calloop", "cursor-icon", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "libc", "memmap2", "raw-window-handle", @@ -7360,7 +7407,7 @@ dependencies = [ "bitflags 2.9.3", "concurrent-queue", "cursor-icon", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "js-sys", "pin-project", "raw-window-handle", @@ -7380,7 +7427,7 @@ source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec dependencies = [ "bitflags 2.9.3", "cursor-icon", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "raw-window-handle", "smol_str", "tracing", @@ -7398,7 +7445,7 @@ dependencies = [ "bytemuck", "calloop", "cursor-icon", - "dpi", + "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", "libc", "percent-encoding", "raw-window-handle", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index a73c3c2b71..6c53678a3c 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -66,6 +66,7 @@ windows = { version = "0.58.0", features = [ # macOS-specific dependencies [target.'cfg(target_os = "macos")'.dependencies] +muda = { version = "0.17", default-features = false } metal = { version = "0.31.0", optional = true } objc = { version = "0.2", optional = true } core-foundation = { version = "0.10", optional = true } diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 0ad28d6c34..3aec9d2e3d 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -19,8 +19,8 @@ use crate::event::{AppEvent, AppEventScheduler}; use crate::persist::PersistentData; use crate::render::GraphicsState; use crate::window::Window; -use graphite_desktop_wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform}; -use graphite_desktop_wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; +use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, Platform}; +use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; pub(crate) struct App { cef_context: Box, @@ -258,6 +258,11 @@ impl App { } }); } + DesktopFrontendMessage::UpdateMenu { entries } => { + if let Some(window) = &self.window { + window.update_menu(entries); + } + } } } @@ -338,12 +343,15 @@ impl App { tracing::info!("Exiting main event loop"); event_loop.exit(); } + AppEvent::MenuEvent { id } => { + self.dispatch_desktop_wrapper_message(DesktopWrapperMessage::MenuEvent { id }); + } } } } impl ApplicationHandler for App { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { - let window = Window::new(event_loop); + let window = Window::new(event_loop, self.app_event_scheduler.clone()); self.window = Some(window); let graphics_state = GraphicsState::new(self.window.as_ref().unwrap(), self.wgpu_context.clone()); diff --git a/desktop/src/cef.rs b/desktop/src/cef.rs index aa2a5adbec..666379893b 100644 --- a/desktop/src/cef.rs +++ b/desktop/src/cef.rs @@ -14,7 +14,7 @@ use crate::event::{AppEvent, AppEventScheduler}; use crate::render::FrameBufferRef; -use graphite_desktop_wrapper::{WgpuContext, deserialize_editor_message}; +use crate::wrapper::{WgpuContext, deserialize_editor_message}; use std::fs::File; use std::io::{Cursor, Read}; use std::path::PathBuf; diff --git a/desktop/src/event.rs b/desktop/src/event.rs index b6809f5118..e4eaba58c4 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -1,5 +1,5 @@ -use graphite_desktop_wrapper::NodeGraphExecutionResult; -use graphite_desktop_wrapper::messages::DesktopWrapperMessage; +use crate::wrapper::NodeGraphExecutionResult; +use crate::wrapper::messages::DesktopWrapperMessage; pub(crate) enum AppEvent { UiUpdate(wgpu::Texture), @@ -9,6 +9,7 @@ pub(crate) enum AppEvent { DesktopWrapperMessage(DesktopWrapperMessage), NodeGraphExecutionResult(NodeGraphExecutionResult), CloseWindow, + MenuEvent { id: u64 }, } #[derive(Clone)] diff --git a/desktop/src/gpu_context.rs b/desktop/src/gpu_context.rs index 787d6cda2f..9b25f30fd9 100644 --- a/desktop/src/gpu_context.rs +++ b/desktop/src/gpu_context.rs @@ -1,4 +1,4 @@ -use graphite_desktop_wrapper::{WgpuContext, WgpuContextBuilder, WgpuFeatures}; +use crate::wrapper::{WgpuContext, WgpuContextBuilder, WgpuFeatures}; pub(super) async fn create_wgpu_context() -> WgpuContext { let wgpu_context_builder = WgpuContextBuilder::new().with_features(WgpuFeatures::PUSH_CONSTANTS); diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 9f1591675b..34332555bd 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -16,6 +16,8 @@ mod window; mod gpu_context; +pub(crate) use graphite_desktop_wrapper as wrapper; + use app::App; use cef::CefHandler; use cli::Cli; diff --git a/desktop/src/persist.rs b/desktop/src/persist.rs index e959112ffa..003bb83f83 100644 --- a/desktop/src/persist.rs +++ b/desktop/src/persist.rs @@ -1,4 +1,4 @@ -use graphite_desktop_wrapper::messages::{Document, DocumentId, Preferences}; +use crate::wrapper::messages::{Document, DocumentId, Preferences}; #[derive(Default, serde::Serialize, serde::Deserialize)] pub(crate) struct PersistentData { diff --git a/desktop/src/render/graphics_state.rs b/desktop/src/render/graphics_state.rs index d6797f937e..afe912053c 100644 --- a/desktop/src/render/graphics_state.rs +++ b/desktop/src/render/graphics_state.rs @@ -1,6 +1,6 @@ use crate::window::Window; -use graphite_desktop_wrapper::{Color, WgpuContext, WgpuExecutor}; +use crate::wrapper::{Color, WgpuContext, WgpuExecutor}; #[derive(derivative::Derivative)] #[derivative(Debug)] diff --git a/desktop/src/window.rs b/desktop/src/window.rs index 8445e55f2a..9e43860288 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -3,10 +3,13 @@ use winit::event_loop::ActiveEventLoop; use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; +use crate::event::AppEventScheduler; +use crate::wrapper::messages::MenuItem; pub(crate) trait NativeWindow { fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes; - fn new(window: &dyn WinitWindow) -> Self; + fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self; + fn update_menu(&self, _entries: Vec) {} } #[cfg(target_os = "linux")] @@ -31,7 +34,7 @@ pub(crate) struct Window { } impl Window { - pub(crate) fn new(event_loop: &dyn ActiveEventLoop) -> Self { + pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self { let mut attributes = WindowAttributes::default() .with_title(APP_NAME) .with_min_surface_size(winit::dpi::LogicalSize::new(400, 300)) @@ -42,7 +45,7 @@ impl Window { attributes = native::NativeWindowImpl::configure(attributes, event_loop); let winit_window = event_loop.create_window(attributes).unwrap(); - let native_handle = native::NativeWindowImpl::new(winit_window.as_ref()); + let native_handle = native::NativeWindowImpl::new(winit_window.as_ref(), app_event_scheduler); Self { winit_window: winit_window.into(), native_handle, @@ -84,4 +87,8 @@ impl Window { pub(crate) fn set_cursor(&self, cursor: winit::cursor::Cursor) { self.winit_window.set_cursor(cursor); } + + pub(crate) fn update_menu(&self, entries: Vec) { + self.native_handle.update_menu(entries); + } } diff --git a/desktop/src/window/linux.rs b/desktop/src/window/linux.rs index 5a48385acc..f17928722e 100644 --- a/desktop/src/window/linux.rs +++ b/desktop/src/window/linux.rs @@ -5,12 +5,11 @@ use winit::platform::x11::WindowAttributesX11; use winit::window::{Window, WindowAttributes}; use crate::consts::{APP_ID, APP_NAME}; - -use super::NativeWindow; +use crate::event::AppEventScheduler; pub(super) struct NativeWindowImpl {} -impl NativeWindow for NativeWindowImpl { +impl super::NativeWindow for NativeWindowImpl { fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes { if event_loop.is_wayland() { let wayland_attributes = WindowAttributesWayland::default().with_name(APP_ID, "").with_prefer_csd(true); @@ -21,7 +20,7 @@ impl NativeWindow for NativeWindowImpl { } } - fn new(_window: &dyn Window) -> Self { + fn new(_window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self { NativeWindowImpl {} } } diff --git a/desktop/src/window/mac.rs b/desktop/src/window/mac.rs index 83386909b5..4c4010a8b1 100644 --- a/desktop/src/window/mac.rs +++ b/desktop/src/window/mac.rs @@ -1,11 +1,15 @@ use winit::event_loop::ActiveEventLoop; use winit::window::{Window, WindowAttributes}; -use super::NativeWindow; +use crate::consts::APP_NAME; +use crate::event::AppEventScheduler; +use crate::wrapper::messages::MenuItem; -pub(super) struct NativeWindowImpl {} +pub(super) struct NativeWindowImpl { + menu: menu::Menu, +} -impl NativeWindow for NativeWindowImpl { +impl super::NativeWindow for NativeWindowImpl { fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes { let mac_window = winit::platform::macos::WindowAttributesMacOS::default() .with_titlebar_transparent(true) @@ -14,7 +18,15 @@ impl NativeWindow for NativeWindowImpl { attributes.with_platform_attributes(Box::new(mac_window)) } - fn new(_window: &dyn Window) -> Self { - NativeWindowImpl {} + fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self { + let menu = menu::Menu::new(app_event_scheduler, APP_NAME); + + NativeWindowImpl { menu } + } + + fn update_menu(&self, entries: Vec) { + self.menu.update(entries); } } + +mod menu; diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs new file mode 100644 index 0000000000..0cb7b9c940 --- /dev/null +++ b/desktop/src/window/mac/menu.rs @@ -0,0 +1,96 @@ +use muda::Menu as MudaMenu; +use muda::{AboutMetadataBuilder, CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Submenu}; + +use crate::event::{AppEvent, AppEventScheduler}; +use crate::wrapper::messages::MenuItem as WrapperMenuItem; + +pub(super) struct Menu { + inner: MudaMenu, +} + +impl Menu { + pub(super) fn new(event_scheduler: AppEventScheduler, app_name: &str) -> Self { + let about = PredefinedMenuItem::about(None, Some(AboutMetadataBuilder::new().name(Some(app_name)).build())); + let hide = PredefinedMenuItem::hide(None); + let hide_others = PredefinedMenuItem::hide_others(None); + let show_all = PredefinedMenuItem::show_all(None); + let quit = PredefinedMenuItem::quit(None); + let app_submenu = Submenu::with_items( + "", + true, + &[&about, &PredefinedMenuItem::separator(), &hide, &hide_others, &show_all, &PredefinedMenuItem::separator(), &quit], + ) + .unwrap(); + + let menu = MudaMenu::new(); + menu.prepend(&app_submenu).unwrap(); + + menu.init_for_nsapp(); + + MenuEvent::set_event_handler(Some(move |event: MenuEvent| { + if let Some(id) = menu_id_to_u64(event.id()) { + event_scheduler.schedule(AppEvent::MenuEvent { id }); + } + })); + + Menu { inner: menu } + } + + pub(super) fn update(&self, entries: Vec) { + // remove all items except the first (app menu) + self.inner.items().iter().skip(1).for_each(|item: &muda::MenuItemKind| { + self.inner.remove(menu_item_kind_to_dyn(item)).unwrap(); + }); + + let items = menu_items_from_wrapper(entries); + let items = items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::>(); + self.inner.append_items(items.as_ref()).unwrap(); + } +} + +fn menu_items_from_wrapper(entries: Vec) -> Vec { + let mut menu_items: Vec = Vec::new(); + for entry in entries { + match entry { + WrapperMenuItem::Action { id, text, enabled } => { + let id = u64_to_menu_id(id); + let item = MenuItem::with_id(id, text, enabled, None); + menu_items.push(MenuItemKind::MenuItem(item)); + } + WrapperMenuItem::Checkbox { id, text, enabled, checked } => { + let id = u64_to_menu_id(id); + let check = CheckMenuItem::with_id(id, text, enabled, checked, None); + menu_items.push(MenuItemKind::Check(check)); + } + WrapperMenuItem::SubMenu { text: name, items, .. } => { + let items = menu_items_from_wrapper(items); + let items = items.iter().map(|item| menu_item_kind_to_dyn(item)).collect::>(); + let submenu = Submenu::with_items(name, true, &items).unwrap(); + menu_items.push(MenuItemKind::Submenu(submenu)); + } + WrapperMenuItem::Separator => { + let separator = PredefinedMenuItem::separator(); + menu_items.push(MenuItemKind::Predefined(separator)); + } + } + } + menu_items +} + +fn menu_item_kind_to_dyn(item: &MenuItemKind) -> &dyn IsMenuItem { + match item { + MenuItemKind::MenuItem(i) => i, + MenuItemKind::Submenu(i) => i, + MenuItemKind::Predefined(i) => i, + MenuItemKind::Check(i) => i, + MenuItemKind::Icon(i) => i, + } +} + +fn u64_to_menu_id(id: u64) -> String { + format!("{id:08x}") +} + +fn menu_id_to_u64(id: &MenuId) -> Option { + u64::from_str_radix(&id.0, 16).ok() +} diff --git a/desktop/src/window/win.rs b/desktop/src/window/win.rs index 8f23866f08..24ee0ef452 100644 --- a/desktop/src/window/win.rs +++ b/desktop/src/window/win.rs @@ -1,13 +1,13 @@ use winit::event_loop::ActiveEventLoop; use winit::window::{Window, WindowAttributes}; -use super::NativeWindow; +use crate::event::AppEventScheduler; pub(super) struct NativeWindowImpl { native_handle: native_handle::NativeWindowHandle, } -impl NativeWindow for NativeWindowImpl { +impl super::NativeWindow for NativeWindowImpl { fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes { if let Ok(win_icon) = winit::platform::windows::WinIcon::from_resource(1, None) { let icon = winit::icon::Icon(std::sync::Arc::new(win_icon)); @@ -17,7 +17,7 @@ impl NativeWindow for NativeWindowImpl { } } - fn new(window: &dyn Window) -> Self { + fn new(window: &dyn Window, _app_event_scheduler: AppEventScheduler) -> Self { let native_handle = native_handle::NativeWindowHandle::new(window); NativeWindowImpl { native_handle } } diff --git a/desktop/wrapper/Cargo.toml b/desktop/wrapper/Cargo.toml index 53302baadb..abe446fbde 100644 --- a/desktop/wrapper/Cargo.toml +++ b/desktop/wrapper/Cargo.toml @@ -31,3 +31,4 @@ ron = { workspace = true} vello = { workspace = true } image = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index a833a5cdeb..09f279faa7 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -1,7 +1,9 @@ use graphene_std::Color; use graphene_std::raster::Image; use graphite_editor::messages::app_window::app_window_message_handler::AppWindowPlatform; +use graphite_editor::messages::layout::LayoutMessage; use graphite_editor::messages::prelude::{AppWindowMessage, DocumentMessage, FrontendMessage, PortfolioMessage, PreferencesMessage}; +use graphite_editor::messages::tool::tool_messages::tool_prelude::{LayoutTarget, WidgetId}; use crate::messages::Platform; @@ -148,5 +150,13 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess let message = PreferencesMessage::Load { preferences }; dispatcher.queue_editor_message(message.into()); } + DesktopWrapperMessage::MenuEvent { id } => { + let message = LayoutMessage::WidgetValueUpdate { + layout_target: LayoutTarget::MenuBar, + widget_id: WidgetId(id), + value: serde_json::Value::Bool(true), + }; + dispatcher.queue_editor_message(message.into()); + } } } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 6077626800..9ebe5b1adb 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -1,7 +1,10 @@ use std::path::PathBuf; +use graphite_editor::messages::layout::utility_types::widgets::menu_widgets::MenuBarEntry; use graphite_editor::messages::prelude::FrontendMessage; +use crate::messages::MenuItem; + use super::DesktopWrapperMessageDispatcher; use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; @@ -119,6 +122,65 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD FrontendMessage::TriggerLoadPreferences => { dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences); } + FrontendMessage::UpdateMenuBarLayout { layout_target, layout } => { + fn items_from_children(children: &Vec>) -> Vec { + let mut items = Vec::new(); + for (i, section) in children.iter().enumerate() { + for entry in section.iter() { + if let Some(item) = create_menu_item(entry) { + items.push(item); + } + } + if i != children.len() - 1 { + items.push(MenuItem::Separator); + } + } + items + } + + fn create_menu_item( + MenuBarEntry { + label, + children, + action, + disabled, + icon, + .. + }: &MenuBarEntry, + ) -> Option { + let id = action.widget_id.0; + let text = if label.is_empty() { + return None; + } else { + label.clone() + }; + let enabled = !*disabled; + + if !children.0.is_empty() { + let items = items_from_children(&children.0); + return Some(MenuItem::SubMenu { id, text, enabled, items }); + } + + // TODO: Find a better way to determine if this is a checkbox + match icon.as_deref() { + Some("CheckboxChecked") => { + return Some(MenuItem::Checkbox { id, text, enabled, checked: true }); + } + Some("CheckboxUnchecked") => { + return Some(MenuItem::Checkbox { id, text, enabled, checked: false }); + } + _ => {} + } + + Some(MenuItem::Action { id, text, enabled }) + } + + let entries: Vec = layout.iter().filter_map(|entry| create_menu_item(entry)).collect(); + + dispatcher.respond(DesktopFrontendMessage::UpdateMenu { entries }); + + return Some(FrontendMessage::UpdateMenuBarLayout { layout, layout_target }); + } m => return Some(m), } None diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 33704c2548..257c87e55e 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -56,6 +56,9 @@ pub enum DesktopFrontendMessage { preferences: Preferences, }, PersistenceLoadPreferences, + UpdateMenu { + entries: Vec, + }, } pub enum DesktopWrapperMessage { @@ -106,6 +109,9 @@ pub enum DesktopWrapperMessage { LoadPreferences { preferences: Option, }, + MenuEvent { + id: u64, + }, } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] @@ -136,3 +142,10 @@ pub enum Platform { Mac, Linux, } + +pub enum MenuItem { + Action { id: u64, text: String, enabled: bool }, + Checkbox { id: u64, text: String, enabled: bool, checked: bool }, + SubMenu { id: u64, text: String, enabled: bool, items: Vec }, + Separator, +} From b5ff8717a7c3bc2b70266444fe5b0da76cf5e51c Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 22 Oct 2025 15:41:02 +0200 Subject: [PATCH 2/5] fix nix build --- Cargo.lock | 49 ++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 226085de7f..a4d1268680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,11 +1338,6 @@ name = "dpi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" - -[[package]] -name = "dpi" -version = "0.1.2" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" dependencies = [ "serde", ] @@ -3433,7 +3428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ "crossbeam-channel", - "dpi 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "dpi", "keyboard-types 0.7.0", "objc2", "objc2-app-kit", @@ -7245,12 +7240,12 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winit" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "bitflags 2.9.3", "cfg_aliases", "cursor-icon", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "libc", "raw-window-handle", "rustix", @@ -7272,11 +7267,11 @@ dependencies = [ [[package]] name = "winit-android" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "android-activity", "bitflags 2.9.3", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "ndk", "raw-window-handle", "smol_str", @@ -7287,12 +7282,12 @@ dependencies = [ [[package]] name = "winit-appkit" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "bitflags 2.9.3", "block2", "dispatch2", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -7309,7 +7304,7 @@ dependencies = [ [[package]] name = "winit-common" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "memmap2", "objc2", @@ -7324,11 +7319,11 @@ dependencies = [ [[package]] name = "winit-core" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "bitflags 2.9.3", "cursor-icon", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "keyboard-types 0.8.1", "raw-window-handle", "serde", @@ -7339,10 +7334,10 @@ dependencies = [ [[package]] name = "winit-orbital" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "bitflags 2.9.3", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "orbclient", "raw-window-handle", "redox_syscall", @@ -7354,12 +7349,12 @@ dependencies = [ [[package]] name = "winit-uikit" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "bitflags 2.9.3", "block2", "dispatch2", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -7375,13 +7370,13 @@ dependencies = [ [[package]] name = "winit-wayland" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "ahash", "bitflags 2.9.3", "calloop", "cursor-icon", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "libc", "memmap2", "raw-window-handle", @@ -7401,13 +7396,13 @@ dependencies = [ [[package]] name = "winit-web" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "atomic-waker", "bitflags 2.9.3", "concurrent-queue", "cursor-icon", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "js-sys", "pin-project", "raw-window-handle", @@ -7423,11 +7418,11 @@ dependencies = [ [[package]] name = "winit-win32" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "bitflags 2.9.3", "cursor-icon", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "raw-window-handle", "smol_str", "tracing", @@ -7439,13 +7434,13 @@ dependencies = [ [[package]] name = "winit-x11" version = "0.30.12" -source = "git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad#37221c7192ef1deec022a2278e54429331c01bad" +source = "git+https://github.com/timon-schelling/winit.git?rev=4be17a5ed552c7a39a671bca97880c929334751e#4be17a5ed552c7a39a671bca97880c929334751e" dependencies = [ "bitflags 2.9.3", "bytemuck", "calloop", "cursor-icon", - "dpi 0.1.2 (git+https://github.com/timon-schelling/winit.git?rev=37221c7192ef1deec022a2278e54429331c01bad)", + "dpi", "libc", "percent-encoding", "raw-window-handle", diff --git a/Cargo.toml b/Cargo.toml index c2a361da4d..a1945d18cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,7 +134,7 @@ web-sys = { version = "=0.3.77", features = [ "HtmlImageElement", "ImageBitmapRenderingContext", ] } -winit = { git = "https://github.com/timon-schelling/winit.git", rev = "37221c7192ef1deec022a2278e54429331c01bad" } +winit = { git = "https://github.com/timon-schelling/winit.git", rev = "4be17a5ed552c7a39a671bca97880c929334751e" } url = "2.5" tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] } vello = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made From d260abdeddf0e65c11eed7e1ac9f4a9495b3c68b Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 23 Oct 2025 12:52:05 +0200 Subject: [PATCH 3/5] Add shortcut symbols to menu --- Cargo.lock | 19 +- Cargo.toml | 1 + desktop/Cargo.toml | 2 +- desktop/src/window/mac/menu.rs | 11 +- desktop/wrapper/Cargo.toml | 1 + .../wrapper/src/intercept_frontend_message.rs | 182 ++++++++++++++++-- desktop/wrapper/src/messages.rs | 13 +- .../utility_types/input_keyboard.rs | 11 +- frontend/src/messages.ts | 2 +- 9 files changed, 190 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a4d1268680..3f7f93b6b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2346,6 +2346,7 @@ dependencies = [ "graphene-std", "graphite-editor", "image", + "keyboard-types", "ron", "serde", "serde_json", @@ -3101,17 +3102,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keyboard-types" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" -dependencies = [ - "bitflags 2.9.3", - "serde", - "unicode-segmentation", -] - [[package]] name = "keyboard-types" version = "0.8.1" @@ -3424,12 +3414,11 @@ dependencies = [ [[package]] name = "muda" version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +source = "git+https://github.com/tauri-apps/muda.git?rev=3f460b8fbaed59cda6d95ceea6904f000f093f15#3f460b8fbaed59cda6d95ceea6904f000f093f15" dependencies = [ "crossbeam-channel", "dpi", - "keyboard-types 0.7.0", + "keyboard-types", "objc2", "objc2-app-kit", "objc2-core-foundation", @@ -7324,7 +7313,7 @@ dependencies = [ "bitflags 2.9.3", "cursor-icon", "dpi", - "keyboard-types 0.8.1", + "keyboard-types", "raw-window-handle", "serde", "smol_str", diff --git a/Cargo.toml b/Cargo.toml index a1945d18cc..fdaa2cd74c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -135,6 +135,7 @@ web-sys = { version = "=0.3.77", features = [ "ImageBitmapRenderingContext", ] } winit = { git = "https://github.com/timon-schelling/winit.git", rev = "4be17a5ed552c7a39a671bca97880c929334751e" } +keyboard-types = "0.8" url = "2.5" tokio = { version = "1.29", features = ["fs", "macros", "io-std", "rt"] } vello = { git = "https://github.com/linebender/vello.git", rev = "87cc5bee6d3a34d15017dbbb58634ddc7f33ff9b" } # TODO switch back to stable when a release is made diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 6c53678a3c..a45b091ac4 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -66,7 +66,7 @@ windows = { version = "0.58.0", features = [ # macOS-specific dependencies [target.'cfg(target_os = "macos")'.dependencies] -muda = { version = "0.17", default-features = false } +muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false } metal = { version = "0.31.0", optional = true } objc = { version = "0.2", optional = true } core-foundation = { version = "0.10", optional = true } diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs index 0cb7b9c940..cc4ca07327 100644 --- a/desktop/src/window/mac/menu.rs +++ b/desktop/src/window/mac/menu.rs @@ -1,4 +1,5 @@ use muda::Menu as MudaMenu; +use muda::accelerator::Accelerator; use muda::{AboutMetadataBuilder, CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Submenu}; use crate::event::{AppEvent, AppEventScheduler}; @@ -52,14 +53,16 @@ fn menu_items_from_wrapper(entries: Vec) -> Vec { let mut menu_items: Vec = Vec::new(); for entry in entries { match entry { - WrapperMenuItem::Action { id, text, enabled } => { + WrapperMenuItem::Action { id, text, enabled, shortcut } => { let id = u64_to_menu_id(id); - let item = MenuItem::with_id(id, text, enabled, None); + let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key)); + let item = MenuItem::with_id(id, text, enabled, accelerator); menu_items.push(MenuItemKind::MenuItem(item)); } - WrapperMenuItem::Checkbox { id, text, enabled, checked } => { + WrapperMenuItem::Checkbox { id, text, enabled, shortcut, checked } => { let id = u64_to_menu_id(id); - let check = CheckMenuItem::with_id(id, text, enabled, checked, None); + let accelerator = shortcut.map(|s| Accelerator::new(Some(s.modifiers), s.key)); + let check = CheckMenuItem::with_id(id, text, enabled, checked, accelerator); menu_items.push(MenuItemKind::Check(check)); } WrapperMenuItem::SubMenu { text: name, items, .. } => { diff --git a/desktop/wrapper/Cargo.toml b/desktop/wrapper/Cargo.toml index abe446fbde..7c11fa7c68 100644 --- a/desktop/wrapper/Cargo.toml +++ b/desktop/wrapper/Cargo.toml @@ -32,3 +32,4 @@ vello = { workspace = true } image = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +keyboard-types = { workspace = true } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 9ebe5b1adb..57d2a7de27 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -1,12 +1,12 @@ use std::path::PathBuf; +use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{LayoutKey, LayoutKeysGroup}; +use graphite_editor::messages::input_mapper::utility_types::misc::ActionKeys; use graphite_editor::messages::layout::utility_types::widgets::menu_widgets::MenuBarEntry; use graphite_editor::messages::prelude::FrontendMessage; -use crate::messages::MenuItem; - use super::DesktopWrapperMessageDispatcher; -use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; +use super::messages::{Code, DesktopFrontendMessage, Document, FileFilter, MenuItem, Modifiers, OpenFileDialogContext, SaveFileDialogContext, Shortcut}; pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option { match message { @@ -123,29 +123,133 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences); } FrontendMessage::UpdateMenuBarLayout { layout_target, layout } => { - fn items_from_children(children: &Vec>) -> Vec { - let mut items = Vec::new(); - for (i, section) in children.iter().enumerate() { - for entry in section.iter() { - if let Some(item) = create_menu_item(entry) { - items.push(item); - } - } - if i != children.len() - 1 { - items.push(MenuItem::Separator); + fn shortcut_from_layout_keys(layout_keys: &Vec) -> Option { + let mut key: Option = None; + let mut modifiers = Modifiers::default(); + for layout_key in layout_keys { + use graphite_editor::messages::input_mapper::utility_types::input_keyboard::Key; + match layout_key.key { + Key::Shift => modifiers |= Modifiers::SHIFT, + Key::Control => modifiers |= Modifiers::CONTROL, + Key::Alt => modifiers |= Modifiers::ALT, + Key::Meta => modifiers |= Modifiers::META, + Key::Command => modifiers |= Modifiers::ALT, + Key::Accel => modifiers |= Modifiers::META, + Key::Digit0 => key = Some(Code::Digit0), + Key::Digit1 => key = Some(Code::Digit1), + Key::Digit2 => key = Some(Code::Digit2), + Key::Digit3 => key = Some(Code::Digit3), + Key::Digit4 => key = Some(Code::Digit4), + Key::Digit5 => key = Some(Code::Digit5), + Key::Digit6 => key = Some(Code::Digit6), + Key::Digit7 => key = Some(Code::Digit7), + Key::Digit8 => key = Some(Code::Digit8), + Key::Digit9 => key = Some(Code::Digit9), + Key::KeyA => key = Some(Code::KeyA), + Key::KeyB => key = Some(Code::KeyB), + Key::KeyC => key = Some(Code::KeyC), + Key::KeyD => key = Some(Code::KeyD), + Key::KeyE => key = Some(Code::KeyE), + Key::KeyF => key = Some(Code::KeyF), + Key::KeyG => key = Some(Code::KeyG), + Key::KeyH => key = Some(Code::KeyH), + Key::KeyI => key = Some(Code::KeyI), + Key::KeyJ => key = Some(Code::KeyJ), + Key::KeyK => key = Some(Code::KeyK), + Key::KeyL => key = Some(Code::KeyL), + Key::KeyM => key = Some(Code::KeyM), + Key::KeyN => key = Some(Code::KeyN), + Key::KeyO => key = Some(Code::KeyO), + Key::KeyP => key = Some(Code::KeyP), + Key::KeyQ => key = Some(Code::KeyQ), + Key::KeyR => key = Some(Code::KeyR), + Key::KeyS => key = Some(Code::KeyS), + Key::KeyT => key = Some(Code::KeyT), + Key::KeyU => key = Some(Code::KeyU), + Key::KeyV => key = Some(Code::KeyV), + Key::KeyW => key = Some(Code::KeyW), + Key::KeyX => key = Some(Code::KeyX), + Key::KeyY => key = Some(Code::KeyY), + Key::KeyZ => key = Some(Code::KeyZ), + Key::Backquote => key = Some(Code::Backquote), + Key::Backslash => key = Some(Code::Backslash), + Key::BracketLeft => key = Some(Code::BracketLeft), + Key::BracketRight => key = Some(Code::BracketRight), + Key::Comma => key = Some(Code::Comma), + Key::Equal => key = Some(Code::Equal), + Key::Minus => key = Some(Code::Minus), + Key::Period => key = Some(Code::Period), + Key::Quote => key = Some(Code::Quote), + Key::Semicolon => key = Some(Code::Semicolon), + Key::Slash => key = Some(Code::Slash), + Key::Backspace => key = Some(Code::Backspace), + Key::CapsLock => key = Some(Code::CapsLock), + Key::ContextMenu => key = Some(Code::ContextMenu), + Key::Enter => key = Some(Code::Enter), + Key::Space => key = Some(Code::Space), + Key::Tab => key = Some(Code::Tab), + Key::Delete => key = Some(Code::Delete), + Key::End => key = Some(Code::End), + Key::Help => key = Some(Code::Help), + Key::Home => key = Some(Code::Home), + Key::Insert => key = Some(Code::Insert), + Key::PageDown => key = Some(Code::PageDown), + Key::PageUp => key = Some(Code::PageUp), + Key::ArrowDown => key = Some(Code::ArrowDown), + Key::ArrowLeft => key = Some(Code::ArrowLeft), + Key::ArrowRight => key = Some(Code::ArrowRight), + Key::ArrowUp => key = Some(Code::ArrowUp), + Key::NumLock => key = Some(Code::NumLock), + Key::NumpadAdd => key = Some(Code::NumpadAdd), + Key::NumpadHash => key = Some(Code::NumpadHash), + Key::NumpadMultiply => key = Some(Code::NumpadMultiply), + Key::NumpadParenLeft => key = Some(Code::NumpadParenLeft), + Key::NumpadParenRight => key = Some(Code::NumpadParenRight), + Key::Escape => key = Some(Code::Escape), + Key::F1 => key = Some(Code::F1), + Key::F2 => key = Some(Code::F2), + Key::F3 => key = Some(Code::F3), + Key::F4 => key = Some(Code::F4), + Key::F5 => key = Some(Code::F5), + Key::F6 => key = Some(Code::F6), + Key::F7 => key = Some(Code::F7), + Key::F8 => key = Some(Code::F8), + Key::F9 => key = Some(Code::F9), + Key::F10 => key = Some(Code::F10), + Key::F11 => key = Some(Code::F11), + Key::F12 => key = Some(Code::F12), + Key::F13 => key = Some(Code::F13), + Key::F14 => key = Some(Code::F14), + Key::F15 => key = Some(Code::F15), + Key::F16 => key = Some(Code::F16), + Key::F17 => key = Some(Code::F17), + Key::F18 => key = Some(Code::F18), + Key::F19 => key = Some(Code::F19), + Key::F20 => key = Some(Code::F20), + Key::F21 => key = Some(Code::F21), + Key::F22 => key = Some(Code::F22), + Key::F23 => key = Some(Code::F23), + Key::F24 => key = Some(Code::F24), + Key::Fn => key = Some(Code::Fn), + Key::FnLock => key = Some(Code::FnLock), + Key::PrintScreen => key = Some(Code::PrintScreen), + Key::ScrollLock => key = Some(Code::ScrollLock), + Key::Pause => key = Some(Code::Pause), + Key::Unidentified => key = Some(Code::Unidentified), + _ => key = None, } } - items + if let Some(key) = key { Some(Shortcut { key, modifiers }) } else { None } } fn create_menu_item( MenuBarEntry { label, - children, + icon, + shortcut, action, + children, disabled, - icon, - .. }: &MenuBarEntry, ) -> Option { let id = action.widget_id.0; @@ -161,18 +265,56 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD return Some(MenuItem::SubMenu { id, text, enabled, items }); } + let shortcut = match shortcut { + Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => { + if let Some(shortcut) = shortcut_from_layout_keys(&keys) { + Some(shortcut) + } else { + None + } + } + _ => None, + }; + // TODO: Find a better way to determine if this is a checkbox match icon.as_deref() { Some("CheckboxChecked") => { - return Some(MenuItem::Checkbox { id, text, enabled, checked: true }); + return Some(MenuItem::Checkbox { + id, + text, + enabled, + shortcut, + checked: true, + }); } Some("CheckboxUnchecked") => { - return Some(MenuItem::Checkbox { id, text, enabled, checked: false }); + return Some(MenuItem::Checkbox { + id, + text, + enabled, + shortcut, + checked: false, + }); } _ => {} } - Some(MenuItem::Action { id, text, enabled }) + Some(MenuItem::Action { id, text, shortcut, enabled }) + } + + fn items_from_children(children: &Vec>) -> Vec { + let mut items = Vec::new(); + for (i, section) in children.iter().enumerate() { + for entry in section.iter() { + if let Some(item) = create_menu_item(entry) { + items.push(item); + } + } + if i != children.len() - 1 { + items.push(MenuItem::Separator); + } + } + items } let entries: Vec = layout.iter().filter_map(|entry| create_menu_item(entry)).collect(); diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index 257c87e55e..bf96b670f1 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -1,11 +1,10 @@ -pub use graphite_editor::messages::prelude::DocumentId; use graphite_editor::messages::prelude::FrontendMessage; use std::path::PathBuf; pub(crate) use graphite_editor::messages::prelude::Message as EditorMessage; +pub use graphite_editor::messages::prelude::DocumentId; pub use graphite_editor::messages::prelude::PreferencesMessageHandler as Preferences; - pub enum DesktopFrontendMessage { ToWeb(Vec), OpenLaunchDocuments, @@ -144,8 +143,14 @@ pub enum Platform { } pub enum MenuItem { - Action { id: u64, text: String, enabled: bool }, - Checkbox { id: u64, text: String, enabled: bool, checked: bool }, + Action { id: u64, text: String, enabled: bool, shortcut: Option }, + Checkbox { id: u64, text: String, enabled: bool, shortcut: Option, checked: bool }, SubMenu { id: u64, text: String, enabled: bool, items: Vec }, Separator, } + +pub use keyboard_types::{Code, Modifiers}; +pub struct Shortcut { + pub key: Code, + pub modifiers: Modifiers, +} diff --git a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs index 5528d4386a..9d89234768 100644 --- a/editor/src/messages/input_mapper/utility_types/input_keyboard.rs +++ b/editor/src/messages/input_mapper/utility_types/input_keyboard.rs @@ -304,16 +304,13 @@ impl fmt::Display for Key { impl From for LayoutKey { fn from(key: Key) -> Self { - Self { - key: format!("{key:?}"), - label: key.to_string(), - } + Self { key, label: key.to_string() } } } #[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, specta::Type)] -struct LayoutKey { - key: String, +pub struct LayoutKey { + pub key: Key, label: String, } @@ -359,7 +356,7 @@ impl From for String { } #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct LayoutKeysGroup(Vec); +pub struct LayoutKeysGroup(pub Vec); impl From for LayoutKeysGroup { fn from(keys_group: KeysGroup) -> Self { diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index aff6380c2e..fae16e6e67 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -337,7 +337,7 @@ export class HintInfo { // Rust enum `Key` export type KeyRaw = string; -// Serde converts a Rust `Key` enum variant into this format (via a custom serializer) with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key +// Serde converts a Rust `Key` enum variant into this format with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key export type Key = { key: KeyRaw; label: string }; export type LayoutKeysGroup = Key[]; export type ActionKeys = { keys: LayoutKeysGroup }; From 280ea9263e881688916760a1b7f2c67658b6a4ba Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Thu, 23 Oct 2025 14:12:41 +0200 Subject: [PATCH 4/5] fix fmt --- .../wrapper/src/intercept_frontend_message.rs | 209 +++++++++--------- desktop/wrapper/src/messages.rs | 26 ++- 2 files changed, 125 insertions(+), 110 deletions(-) diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 57d2a7de27..b359980aa1 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -1,12 +1,12 @@ use std::path::PathBuf; -use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{LayoutKey, LayoutKeysGroup}; +use graphite_editor::messages::input_mapper::utility_types::input_keyboard::{Key, LayoutKey, LayoutKeysGroup}; use graphite_editor::messages::input_mapper::utility_types::misc::ActionKeys; use graphite_editor::messages::layout::utility_types::widgets::menu_widgets::MenuBarEntry; use graphite_editor::messages::prelude::FrontendMessage; use super::DesktopWrapperMessageDispatcher; -use super::messages::{Code, DesktopFrontendMessage, Document, FileFilter, MenuItem, Modifiers, OpenFileDialogContext, SaveFileDialogContext, Shortcut}; +use super::messages::{DesktopFrontendMessage, Document, FileFilter, KeyCode, MenuItem, Modifiers, OpenFileDialogContext, SaveFileDialogContext, Shortcut}; pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option { match message { @@ -124,10 +124,9 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD } FrontendMessage::UpdateMenuBarLayout { layout_target, layout } => { fn shortcut_from_layout_keys(layout_keys: &Vec) -> Option { - let mut key: Option = None; + let mut key: Option = None; let mut modifiers = Modifiers::default(); for layout_key in layout_keys { - use graphite_editor::messages::input_mapper::utility_types::input_keyboard::Key; match layout_key.key { Key::Shift => modifiers |= Modifiers::SHIFT, Key::Control => modifiers |= Modifiers::CONTROL, @@ -135,107 +134,107 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD Key::Meta => modifiers |= Modifiers::META, Key::Command => modifiers |= Modifiers::ALT, Key::Accel => modifiers |= Modifiers::META, - Key::Digit0 => key = Some(Code::Digit0), - Key::Digit1 => key = Some(Code::Digit1), - Key::Digit2 => key = Some(Code::Digit2), - Key::Digit3 => key = Some(Code::Digit3), - Key::Digit4 => key = Some(Code::Digit4), - Key::Digit5 => key = Some(Code::Digit5), - Key::Digit6 => key = Some(Code::Digit6), - Key::Digit7 => key = Some(Code::Digit7), - Key::Digit8 => key = Some(Code::Digit8), - Key::Digit9 => key = Some(Code::Digit9), - Key::KeyA => key = Some(Code::KeyA), - Key::KeyB => key = Some(Code::KeyB), - Key::KeyC => key = Some(Code::KeyC), - Key::KeyD => key = Some(Code::KeyD), - Key::KeyE => key = Some(Code::KeyE), - Key::KeyF => key = Some(Code::KeyF), - Key::KeyG => key = Some(Code::KeyG), - Key::KeyH => key = Some(Code::KeyH), - Key::KeyI => key = Some(Code::KeyI), - Key::KeyJ => key = Some(Code::KeyJ), - Key::KeyK => key = Some(Code::KeyK), - Key::KeyL => key = Some(Code::KeyL), - Key::KeyM => key = Some(Code::KeyM), - Key::KeyN => key = Some(Code::KeyN), - Key::KeyO => key = Some(Code::KeyO), - Key::KeyP => key = Some(Code::KeyP), - Key::KeyQ => key = Some(Code::KeyQ), - Key::KeyR => key = Some(Code::KeyR), - Key::KeyS => key = Some(Code::KeyS), - Key::KeyT => key = Some(Code::KeyT), - Key::KeyU => key = Some(Code::KeyU), - Key::KeyV => key = Some(Code::KeyV), - Key::KeyW => key = Some(Code::KeyW), - Key::KeyX => key = Some(Code::KeyX), - Key::KeyY => key = Some(Code::KeyY), - Key::KeyZ => key = Some(Code::KeyZ), - Key::Backquote => key = Some(Code::Backquote), - Key::Backslash => key = Some(Code::Backslash), - Key::BracketLeft => key = Some(Code::BracketLeft), - Key::BracketRight => key = Some(Code::BracketRight), - Key::Comma => key = Some(Code::Comma), - Key::Equal => key = Some(Code::Equal), - Key::Minus => key = Some(Code::Minus), - Key::Period => key = Some(Code::Period), - Key::Quote => key = Some(Code::Quote), - Key::Semicolon => key = Some(Code::Semicolon), - Key::Slash => key = Some(Code::Slash), - Key::Backspace => key = Some(Code::Backspace), - Key::CapsLock => key = Some(Code::CapsLock), - Key::ContextMenu => key = Some(Code::ContextMenu), - Key::Enter => key = Some(Code::Enter), - Key::Space => key = Some(Code::Space), - Key::Tab => key = Some(Code::Tab), - Key::Delete => key = Some(Code::Delete), - Key::End => key = Some(Code::End), - Key::Help => key = Some(Code::Help), - Key::Home => key = Some(Code::Home), - Key::Insert => key = Some(Code::Insert), - Key::PageDown => key = Some(Code::PageDown), - Key::PageUp => key = Some(Code::PageUp), - Key::ArrowDown => key = Some(Code::ArrowDown), - Key::ArrowLeft => key = Some(Code::ArrowLeft), - Key::ArrowRight => key = Some(Code::ArrowRight), - Key::ArrowUp => key = Some(Code::ArrowUp), - Key::NumLock => key = Some(Code::NumLock), - Key::NumpadAdd => key = Some(Code::NumpadAdd), - Key::NumpadHash => key = Some(Code::NumpadHash), - Key::NumpadMultiply => key = Some(Code::NumpadMultiply), - Key::NumpadParenLeft => key = Some(Code::NumpadParenLeft), - Key::NumpadParenRight => key = Some(Code::NumpadParenRight), - Key::Escape => key = Some(Code::Escape), - Key::F1 => key = Some(Code::F1), - Key::F2 => key = Some(Code::F2), - Key::F3 => key = Some(Code::F3), - Key::F4 => key = Some(Code::F4), - Key::F5 => key = Some(Code::F5), - Key::F6 => key = Some(Code::F6), - Key::F7 => key = Some(Code::F7), - Key::F8 => key = Some(Code::F8), - Key::F9 => key = Some(Code::F9), - Key::F10 => key = Some(Code::F10), - Key::F11 => key = Some(Code::F11), - Key::F12 => key = Some(Code::F12), - Key::F13 => key = Some(Code::F13), - Key::F14 => key = Some(Code::F14), - Key::F15 => key = Some(Code::F15), - Key::F16 => key = Some(Code::F16), - Key::F17 => key = Some(Code::F17), - Key::F18 => key = Some(Code::F18), - Key::F19 => key = Some(Code::F19), - Key::F20 => key = Some(Code::F20), - Key::F21 => key = Some(Code::F21), - Key::F22 => key = Some(Code::F22), - Key::F23 => key = Some(Code::F23), - Key::F24 => key = Some(Code::F24), - Key::Fn => key = Some(Code::Fn), - Key::FnLock => key = Some(Code::FnLock), - Key::PrintScreen => key = Some(Code::PrintScreen), - Key::ScrollLock => key = Some(Code::ScrollLock), - Key::Pause => key = Some(Code::Pause), - Key::Unidentified => key = Some(Code::Unidentified), + Key::Digit0 => key = Some(KeyCode::Digit0), + Key::Digit1 => key = Some(KeyCode::Digit1), + Key::Digit2 => key = Some(KeyCode::Digit2), + Key::Digit3 => key = Some(KeyCode::Digit3), + Key::Digit4 => key = Some(KeyCode::Digit4), + Key::Digit5 => key = Some(KeyCode::Digit5), + Key::Digit6 => key = Some(KeyCode::Digit6), + Key::Digit7 => key = Some(KeyCode::Digit7), + Key::Digit8 => key = Some(KeyCode::Digit8), + Key::Digit9 => key = Some(KeyCode::Digit9), + Key::KeyA => key = Some(KeyCode::KeyA), + Key::KeyB => key = Some(KeyCode::KeyB), + Key::KeyC => key = Some(KeyCode::KeyC), + Key::KeyD => key = Some(KeyCode::KeyD), + Key::KeyE => key = Some(KeyCode::KeyE), + Key::KeyF => key = Some(KeyCode::KeyF), + Key::KeyG => key = Some(KeyCode::KeyG), + Key::KeyH => key = Some(KeyCode::KeyH), + Key::KeyI => key = Some(KeyCode::KeyI), + Key::KeyJ => key = Some(KeyCode::KeyJ), + Key::KeyK => key = Some(KeyCode::KeyK), + Key::KeyL => key = Some(KeyCode::KeyL), + Key::KeyM => key = Some(KeyCode::KeyM), + Key::KeyN => key = Some(KeyCode::KeyN), + Key::KeyO => key = Some(KeyCode::KeyO), + Key::KeyP => key = Some(KeyCode::KeyP), + Key::KeyQ => key = Some(KeyCode::KeyQ), + Key::KeyR => key = Some(KeyCode::KeyR), + Key::KeyS => key = Some(KeyCode::KeyS), + Key::KeyT => key = Some(KeyCode::KeyT), + Key::KeyU => key = Some(KeyCode::KeyU), + Key::KeyV => key = Some(KeyCode::KeyV), + Key::KeyW => key = Some(KeyCode::KeyW), + Key::KeyX => key = Some(KeyCode::KeyX), + Key::KeyY => key = Some(KeyCode::KeyY), + Key::KeyZ => key = Some(KeyCode::KeyZ), + Key::Backquote => key = Some(KeyCode::Backquote), + Key::Backslash => key = Some(KeyCode::Backslash), + Key::BracketLeft => key = Some(KeyCode::BracketLeft), + Key::BracketRight => key = Some(KeyCode::BracketRight), + Key::Comma => key = Some(KeyCode::Comma), + Key::Equal => key = Some(KeyCode::Equal), + Key::Minus => key = Some(KeyCode::Minus), + Key::Period => key = Some(KeyCode::Period), + Key::Quote => key = Some(KeyCode::Quote), + Key::Semicolon => key = Some(KeyCode::Semicolon), + Key::Slash => key = Some(KeyCode::Slash), + Key::Backspace => key = Some(KeyCode::Backspace), + Key::CapsLock => key = Some(KeyCode::CapsLock), + Key::ContextMenu => key = Some(KeyCode::ContextMenu), + Key::Enter => key = Some(KeyCode::Enter), + Key::Space => key = Some(KeyCode::Space), + Key::Tab => key = Some(KeyCode::Tab), + Key::Delete => key = Some(KeyCode::Delete), + Key::End => key = Some(KeyCode::End), + Key::Help => key = Some(KeyCode::Help), + Key::Home => key = Some(KeyCode::Home), + Key::Insert => key = Some(KeyCode::Insert), + Key::PageDown => key = Some(KeyCode::PageDown), + Key::PageUp => key = Some(KeyCode::PageUp), + Key::ArrowDown => key = Some(KeyCode::ArrowDown), + Key::ArrowLeft => key = Some(KeyCode::ArrowLeft), + Key::ArrowRight => key = Some(KeyCode::ArrowRight), + Key::ArrowUp => key = Some(KeyCode::ArrowUp), + Key::NumLock => key = Some(KeyCode::NumLock), + Key::NumpadAdd => key = Some(KeyCode::NumpadAdd), + Key::NumpadHash => key = Some(KeyCode::NumpadHash), + Key::NumpadMultiply => key = Some(KeyCode::NumpadMultiply), + Key::NumpadParenLeft => key = Some(KeyCode::NumpadParenLeft), + Key::NumpadParenRight => key = Some(KeyCode::NumpadParenRight), + Key::Escape => key = Some(KeyCode::Escape), + Key::F1 => key = Some(KeyCode::F1), + Key::F2 => key = Some(KeyCode::F2), + Key::F3 => key = Some(KeyCode::F3), + Key::F4 => key = Some(KeyCode::F4), + Key::F5 => key = Some(KeyCode::F5), + Key::F6 => key = Some(KeyCode::F6), + Key::F7 => key = Some(KeyCode::F7), + Key::F8 => key = Some(KeyCode::F8), + Key::F9 => key = Some(KeyCode::F9), + Key::F10 => key = Some(KeyCode::F10), + Key::F11 => key = Some(KeyCode::F11), + Key::F12 => key = Some(KeyCode::F12), + Key::F13 => key = Some(KeyCode::F13), + Key::F14 => key = Some(KeyCode::F14), + Key::F15 => key = Some(KeyCode::F15), + Key::F16 => key = Some(KeyCode::F16), + Key::F17 => key = Some(KeyCode::F17), + Key::F18 => key = Some(KeyCode::F18), + Key::F19 => key = Some(KeyCode::F19), + Key::F20 => key = Some(KeyCode::F20), + Key::F21 => key = Some(KeyCode::F21), + Key::F22 => key = Some(KeyCode::F22), + Key::F23 => key = Some(KeyCode::F23), + Key::F24 => key = Some(KeyCode::F24), + Key::Fn => key = Some(KeyCode::Fn), + Key::FnLock => key = Some(KeyCode::FnLock), + Key::PrintScreen => key = Some(KeyCode::PrintScreen), + Key::ScrollLock => key = Some(KeyCode::ScrollLock), + Key::Pause => key = Some(KeyCode::Pause), + Key::Unidentified => key = Some(KeyCode::Unidentified), _ => key = None, } } diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index bf96b670f1..a7e1a86edc 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -143,14 +143,30 @@ pub enum Platform { } pub enum MenuItem { - Action { id: u64, text: String, enabled: bool, shortcut: Option }, - Checkbox { id: u64, text: String, enabled: bool, shortcut: Option, checked: bool }, - SubMenu { id: u64, text: String, enabled: bool, items: Vec }, + Action { + id: u64, + text: String, + enabled: bool, + shortcut: Option, + }, + Checkbox { + id: u64, + text: String, + enabled: bool, + shortcut: Option, + checked: bool, + }, + SubMenu { + id: u64, + text: String, + enabled: bool, + items: Vec, + }, Separator, } -pub use keyboard_types::{Code, Modifiers}; +pub use keyboard_types::{Code as KeyCode, Modifiers}; pub struct Shortcut { - pub key: Code, + pub key: KeyCode, pub modifiers: Modifiers, } From 9e32d0ffed8097f2d804c74a262807f02835b352 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Sat, 25 Oct 2025 17:13:17 +0200 Subject: [PATCH 5/5] fix vendoring --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index a94e9c90f7..3f7d16edc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -226,3 +226,7 @@ debug = true [profile.profiling] inherits = "release" debug = true + +[patch.crates-io] +# Force cargo to use only one version of the dpi crate (vendoring breaks without this) +dpi = { git = "https://github.com/rust-windowing/winit.git" }