From d98751aeeebbde78ac5ef9c8f63396d4716f1172 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 03:33:49 +0100 Subject: [PATCH 1/9] Working :) --- Cargo.lock | 147 ++++++------------ Cargo.toml | 2 +- desktop/Cargo.toml | 3 + desktop/src/cef/input.rs | 19 ++- desktop/src/cef/input/state.rs | 2 +- desktop/src/lib.rs | 33 ++++ desktop/src/window/mac/menu.rs | 10 ++ .../wrapper/src/intercept_frontend_message.rs | 3 +- node-graph/libraries/core-types/src/text.rs | 4 +- 9 files changed, 120 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49299f85a3..9873948b58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1695,16 +1695,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "fontconfig-cache-parser" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f8afb20c8069fd676d27b214559a337cc619a605d25a87baa90b49a06f3b18" -dependencies = [ - "bytemuck", - "thiserror 1.0.69", -] - [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -1744,25 +1734,25 @@ dependencies = [ [[package]] name = "fontique" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f97079e1293b8c1e9fb03a2875d328bd2ee8f3b95ce62959c0acc04049c708" +checksum = "ff3336bc0b87fe42305047263fa60d2eabd650d29cbe62fdeb2a66c7a0a595f9" dependencies = [ "bytemuck", - "fontconfig-cache-parser", "hashbrown 0.15.5", - "icu_locid", + "icu_locale_core", + "linebender_resource_handle", "memmap2", "objc2", "objc2-core-foundation", "objc2-core-text", "objc2-foundation", - "peniko 0.4.0", - "read-fonts 0.29.3", + "read-fonts 0.35.0", "roxmltree", "smallvec", "windows", "windows-core 0.58.0", + "yeslogic-fontconfig-sys", ] [[package]] @@ -2264,6 +2254,9 @@ dependencies = [ "graphite-desktop-embedded-resources", "graphite-desktop-wrapper", "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", "open", "rand 0.9.2", "rfd", @@ -2445,6 +2438,19 @@ dependencies = [ "serde", ] +[[package]] +name = "harfrust" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" +dependencies = [ + "bitflags 2.9.3", + "bytemuck", + "core_maths", + "read-fonts 0.35.0", + "smallvec", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -2690,24 +2696,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", - "litemap 0.8.0", - "tinystr 0.8.1", - "writeable 0.6.1", + "litemap", + "tinystr", + "writeable", "zerovec", ] -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap 0.7.5", - "tinystr 0.7.6", - "writeable 0.5.5", -] - [[package]] name = "icu_normalizer" version = "2.0.0" @@ -2760,8 +2754,8 @@ dependencies = [ "displaydoc", "icu_locale_core", "stable_deref_trait", - "tinystr 0.8.1", - "writeable 0.6.1", + "tinystr", + "writeable", "yoke", "zerofrom", "zerotrie", @@ -3235,12 +3229,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" - [[package]] name = "litemap" version = "0.8.0" @@ -3810,9 +3798,9 @@ dependencies = [ [[package]] name = "objc2-core-text" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ba833d4a1cb1aac330f8c973fd92b6ff1858e4aef5cdd00a255eefb28022fb5" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.9.3", "objc2-core-foundation", @@ -4037,14 +4025,15 @@ dependencies = [ [[package]] name = "parley" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e57638545cf2ba4c3e72cc5715e53b1880b829cc3dbefda3d1700c58efe723" +checksum = "26746861bb76dbc9bcd5ed1b0b55d2fedf291100961251702a031ab2abd2ce52" dependencies = [ "fontique", + "harfrust", "hashbrown 0.15.5", - "peniko 0.4.0", - "skrifa 0.31.3", + "linebender_resource_handle", + "skrifa 0.37.0", "swash", ] @@ -4094,17 +4083,6 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" -[[package]] -name = "peniko" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f9529efd019889b2a205193c14ffb6e2839b54ed9d2720674f10f4b04d87ac9" -dependencies = [ - "color", - "kurbo 0.11.3", - "smallvec", -] - [[package]] name = "peniko" version = "0.5.0" @@ -4768,16 +4746,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "read-fonts" -version = "0.29.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04ca636dac446b5664bd16c069c00a9621806895b8bb02c2dc68542b23b8f25d" -dependencies = [ - "bytemuck", - "font-types 0.9.0", -] - [[package]] name = "read-fonts" version = "0.34.0" @@ -4795,6 +4763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" dependencies = [ "bytemuck", + "core_maths", "font-types 0.10.0", ] @@ -5433,16 +5402,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" -[[package]] -name = "skrifa" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbeb4ca4399663735553a09dd17ce7e49a0a0203f03b706b39628c4d913a8607" -dependencies = [ - "bytemuck", - "read-fonts 0.29.3", -] - [[package]] name = "skrifa" version = "0.36.0" @@ -5714,11 +5673,11 @@ dependencies = [ [[package]] name = "swash" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f745de914febc7c9ab4388dfaf94bbc87e69f57bb41133a9b0c84d4be49856f3" +checksum = "47846491253e976bdd07d0f9cc24b7daf24720d11309302ccbbc6e6b6e53550a" dependencies = [ - "skrifa 0.31.3", + "skrifa 0.37.0", "yazi", "zeno", ] @@ -5970,15 +5929,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -6579,7 +6529,7 @@ dependencies = [ "bytemuck", "futures-intrusive", "log", - "peniko 0.5.0", + "peniko", "png", "skrifa 0.37.0", "static_assertions", @@ -6596,7 +6546,7 @@ source = "git+https://github.com/linebender/vello#8f2f2564127812362d2c57ded20cad dependencies = [ "bytemuck", "guillotiere", - "peniko 0.5.0", + "peniko", "skrifa 0.37.0", "smallvec", ] @@ -7714,12 +7664,6 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "writeable" version = "0.6.1" @@ -7818,6 +7762,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 4dcd0fa693..11ede35b63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,7 +182,7 @@ image = { version = "0.25", default-features = false, features = [ "jpeg", "bmp", ] } -parley = "0.5" +parley = "0.6" skrifa = "0.36" pretty_assertions = "1.4" fern = { version = "0.7", features = ["colored"] } diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 6aad2144ec..6a22efd319 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -58,5 +58,8 @@ windows = { version = "0.58.0", features = [ # macOS-specific dependencies [target.'cfg(target_os = "macos")'.dependencies] +objc2 = { version = "0.6.1", default-features = false } +objc2-foundation = { version = "0.3.2", default-features = false } +objc2-app-kit = { version = "0.3.2", default-features = false } muda = { git = "https://github.com/tauri-apps/muda.git", rev = "3f460b8fbaed59cda6d95ceea6904f000f093f15", default-features = false } diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index 2961166764..66d6cb27dd 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -88,11 +88,28 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat key_event.native_key_code = event.physical_key.to_native_keycode(); key_event.character = event.logical_key.to_char_representation() as u16; + key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16; + // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. + // + // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. + // In some cases the `NSEvent` gets to the native Cocoa application, is considered "unhandled" and can trigger menus. + // + // Why mitigation works: + // Leaving `key_event.unmodified_character = 0` still leads to CEF forwarding a "unhandled" event to the native application + // but that event is discarded because `key_event.unmodified_character = 0` is considered non-printable and not used for shortcut matching. + // + // See https://github.com/chromiumembedded/cef/issues/3857 + // + // TODO: Remove mitigation once bug is fixed or a better solution is found. + if cfg!(target_os = "macos") && input_state.modifiers.alt_key() { + key_event.unmodified_character = 0; + } + #[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650 if key_event.character == 0 && key_event.unmodified_character == 0 && event.text_with_all_modifiers.is_some() { - key_event.unmodified_character = 1; + key_event.character = 1; } if key_event.type_ == cef_key_event_type_t::KEYEVENT_CHAR.into() { diff --git a/desktop/src/cef/input/state.rs b/desktop/src/cef/input/state.rs index 52abead503..4a7071a699 100644 --- a/desktop/src/cef/input/state.rs +++ b/desktop/src/cef/input/state.rs @@ -7,7 +7,7 @@ use crate::cef::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT}; #[derive(Default)] pub(crate) struct InputState { - modifiers: winit::keyboard::ModifiersState, + pub(super) modifiers: winit::keyboard::ModifiersState, mouse_position: MousePosition, mouse_state: MouseState, mouse_click_tracker: ClickTracker, diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index fdf504d04d..9b6cc6ece7 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -23,9 +23,42 @@ use cef::CefHandler; use cli::Cli; use event::CreateAppEventSchedulerEventLoopExt; +mod mac_app_impl { + use objc2::{ClassType, define_class, msg_send}; + use objc2_app_kit::{NSApplication, NSEvent, NSResponder}; + use objc2_foundation::NSObject; + + define_class!( + #[unsafe(super(NSApplication, NSResponder, NSObject))] + #[name = "GraphiteApplication"] + pub(super) struct GraphiteApplication; + + impl GraphiteApplication { + #[unsafe(method(sendEvent:))] + fn send_event(&self, event: &NSEvent) { + if let Some(key_window) = self.keyWindow() { + unsafe { msg_send![&key_window, sendEvent: event] } + } else { + unsafe { msg_send![super(self), sendEvent: event] } + } + } + } + ); + + impl GraphiteApplication { + pub(super) fn init() { + unsafe { + let _: &NSApplication = msg_send![GraphiteApplication::class(), sharedApplication]; + } + } + } +} + pub fn start() { tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init(); + mac_app_impl::GraphiteApplication::init(); + let cef_context_builder = cef::CefContextBuilder::::new(); if cef_context_builder.is_sub_process() { diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs index e2b437a913..48b80c7566 100644 --- a/desktop/src/window/mac/menu.rs +++ b/desktop/src/window/mac/menu.rs @@ -29,6 +29,16 @@ impl Menu { menu.init_for_nsapp(); MenuEvent::set_event_handler(Some(move |event: MenuEvent| { + let mtm = objc2::MainThreadMarker::new().expect("only ever called from main thread"); + let is_shortcut_triggered = objc2_app_kit::NSApplication::sharedApplication(mtm) + .mainMenu() + .map(|m| m.highlightedItem().is_some()) + .unwrap_or_default(); + if is_shortcut_triggered { + tracing::error!("A keyboard input triggered a menu event. This is most likely a bug. Please report!"); + return; + } + if let Some(id) = menu_id_to_u64(event.id()) { event_scheduler.schedule(AppEvent::MenuEvent { id }); } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index 1c4d1991b6..dedc61d230 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -164,8 +164,7 @@ fn convert_menu_bar_entry_to_menu_item( } let shortcut = match shortcut { - //TODO: Reenable shortcuts once a workaround for missing keyboard events is found - Some(ActionKeys::Keys(LayoutKeysGroup(keys))) if false => convert_layout_keys_to_shortcut(keys), + Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => convert_layout_keys_to_shortcut(keys), _ => None, }; diff --git a/node-graph/libraries/core-types/src/text.rs b/node-graph/libraries/core-types/src/text.rs index 9321d7f729..b58a05a6e0 100644 --- a/node-graph/libraries/core-types/src/text.rs +++ b/node-graph/libraries/core-types/src/text.rs @@ -26,9 +26,9 @@ impl From for parley::Alignment { fn from(val: TextAlign) -> Self { match val { TextAlign::Left => parley::Alignment::Left, - TextAlign::Center => parley::Alignment::Middle, + TextAlign::Center => parley::Alignment::Center, TextAlign::Right => parley::Alignment::Right, - TextAlign::JustifyLeft => parley::Alignment::Justified, + TextAlign::JustifyLeft => parley::Alignment::Justify, } } } From 0dc406f368674265934348f31cdf84a7ff5910f7 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 03:33:49 +0100 Subject: [PATCH 2/9] test --- desktop/src/cef/input.rs | 7 +++---- desktop/src/cef/input/state.rs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index 66d6cb27dd..e026e38368 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -89,8 +89,6 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat key_event.character = event.logical_key.to_char_representation() as u16; - key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16; - // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. // // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. @@ -103,8 +101,9 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat // See https://github.com/chromiumembedded/cef/issues/3857 // // TODO: Remove mitigation once bug is fixed or a better solution is found. - if cfg!(target_os = "macos") && input_state.modifiers.alt_key() { - key_event.unmodified_character = 0; + #[cfg(not(target_os = "macos"))] + { + key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16; } #[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650 diff --git a/desktop/src/cef/input/state.rs b/desktop/src/cef/input/state.rs index 4a7071a699..52abead503 100644 --- a/desktop/src/cef/input/state.rs +++ b/desktop/src/cef/input/state.rs @@ -7,7 +7,7 @@ use crate::cef::consts::{MULTICLICK_ALLOWED_TRAVEL, MULTICLICK_TIMEOUT}; #[derive(Default)] pub(crate) struct InputState { - pub(super) modifiers: winit::keyboard::ModifiersState, + modifiers: winit::keyboard::ModifiersState, mouse_position: MousePosition, mouse_state: MouseState, mouse_click_tracker: ClickTracker, From e79c0c83d4e6ce469e0b6524a75f1187230b4a61 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 03:33:49 +0100 Subject: [PATCH 3/9] remove some cocoa warning --- desktop/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 9b6cc6ece7..6224c9119f 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -25,7 +25,7 @@ use event::CreateAppEventSchedulerEventLoopExt; mod mac_app_impl { use objc2::{ClassType, define_class, msg_send}; - use objc2_app_kit::{NSApplication, NSEvent, NSResponder}; + use objc2_app_kit::{NSApplication, NSEvent, NSEventType, NSResponder}; use objc2_foundation::NSObject; define_class!( @@ -36,7 +36,7 @@ mod mac_app_impl { impl GraphiteApplication { #[unsafe(method(sendEvent:))] fn send_event(&self, event: &NSEvent) { - if let Some(key_window) = self.keyWindow() { + if event.r#type() == NSEventType::KeyDown && let Some(key_window) = self.keyWindow() { unsafe { msg_send![&key_window, sendEvent: event] } } else { unsafe { msg_send![super(self), sendEvent: event] } From 8a6dd9593f5068ca233f0793e94f969fccf2b7e1 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 03:49:19 +0100 Subject: [PATCH 4/9] rebase fixup --- node-graph/nodes/text/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node-graph/nodes/text/src/lib.rs b/node-graph/nodes/text/src/lib.rs index fb776e9088..b4ac6f4566 100644 --- a/node-graph/nodes/text/src/lib.rs +++ b/node-graph/nodes/text/src/lib.rs @@ -33,9 +33,9 @@ impl From for parley::Alignment { fn from(val: TextAlign) -> Self { match val { TextAlign::Left => parley::Alignment::Left, - TextAlign::Center => parley::Alignment::Middle, + TextAlign::Center => parley::Alignment::Center, TextAlign::Right => parley::Alignment::Right, - TextAlign::JustifyLeft => parley::Alignment::Justified, + TextAlign::JustifyLeft => parley::Alignment::Justify, } } } From d5c25e029c7aa3c40ddcadcba508934582d5ef84 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 05:04:04 +0100 Subject: [PATCH 5/9] fixup --- desktop/src/cef/input.rs | 17 ----------------- desktop/src/window/mac.rs | 3 +-- desktop/src/window/mac/menu.rs | 12 +++--------- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index e026e38368..168c8736c5 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -89,23 +89,6 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat key_event.character = event.logical_key.to_char_representation() as u16; - // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. - // - // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. - // In some cases the `NSEvent` gets to the native Cocoa application, is considered "unhandled" and can trigger menus. - // - // Why mitigation works: - // Leaving `key_event.unmodified_character = 0` still leads to CEF forwarding a "unhandled" event to the native application - // but that event is discarded because `key_event.unmodified_character = 0` is considered non-printable and not used for shortcut matching. - // - // See https://github.com/chromiumembedded/cef/issues/3857 - // - // TODO: Remove mitigation once bug is fixed or a better solution is found. - #[cfg(not(target_os = "macos"))] - { - key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16; - } - #[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650 if key_event.character == 0 && key_event.unmodified_character == 0 && event.text_with_all_modifiers.is_some() { key_event.character = 1; diff --git a/desktop/src/window/mac.rs b/desktop/src/window/mac.rs index f6cd4330d0..537dfd1d43 100644 --- a/desktop/src/window/mac.rs +++ b/desktop/src/window/mac.rs @@ -2,7 +2,6 @@ use winit::event_loop::ActiveEventLoop; use winit::platform::macos::WindowAttributesMacOS; use winit::window::{Window, WindowAttributes}; -use crate::consts::APP_NAME; use crate::event::AppEventScheduler; use crate::wrapper::messages::MenuItem; @@ -20,7 +19,7 @@ impl super::NativeWindow for NativeWindowImpl { } fn new(_window: &dyn Window, app_event_scheduler: AppEventScheduler) -> Self { - let menu = menu::Menu::new(app_event_scheduler, APP_NAME); + let menu = menu::Menu::new(app_event_scheduler); NativeWindowImpl { menu } } diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs index 48b80c7566..9fcf0826c4 100644 --- a/desktop/src/window/mac/menu.rs +++ b/desktop/src/window/mac/menu.rs @@ -1,6 +1,6 @@ use muda::Menu as MudaMenu; use muda::accelerator::Accelerator; -use muda::{AboutMetadataBuilder, CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu}; +use muda::{CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu}; use crate::event::{AppEvent, AppEventScheduler}; use crate::wrapper::messages::MenuItem as WrapperMenuItem; @@ -10,18 +10,12 @@ pub(super) struct Menu { } 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())); + pub(super) fn new(event_scheduler: AppEventScheduler) -> Self { 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 app_submenu = Submenu::with_items("", true, &[&PredefinedMenuItem::separator(), &hide, &hide_others, &show_all, &PredefinedMenuItem::separator(), &quit]).unwrap(); let menu = MudaMenu::new(); menu.prepend(&app_submenu).unwrap(); From cbee47d0d88bd87ec314a2bd3837d6bf61a82eb2 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 05:10:58 +0100 Subject: [PATCH 6/9] sadly still necessary --- desktop/src/cef/input.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/desktop/src/cef/input.rs b/desktop/src/cef/input.rs index 168c8736c5..e026e38368 100644 --- a/desktop/src/cef/input.rs +++ b/desktop/src/cef/input.rs @@ -89,6 +89,23 @@ pub(crate) fn handle_window_event(browser: &Browser, input_state: &mut InputStat key_event.character = event.logical_key.to_char_representation() as u16; + // Mitigation for CEF on Mac bug to prevent NSMenu being triggered by this key event. + // + // CEF converts the key event into an `NSEvent` internally and passes that to Chromium. + // In some cases the `NSEvent` gets to the native Cocoa application, is considered "unhandled" and can trigger menus. + // + // Why mitigation works: + // Leaving `key_event.unmodified_character = 0` still leads to CEF forwarding a "unhandled" event to the native application + // but that event is discarded because `key_event.unmodified_character = 0` is considered non-printable and not used for shortcut matching. + // + // See https://github.com/chromiumembedded/cef/issues/3857 + // + // TODO: Remove mitigation once bug is fixed or a better solution is found. + #[cfg(not(target_os = "macos"))] + { + key_event.unmodified_character = event.key_without_modifiers.to_char_representation() as u16; + } + #[cfg(target_os = "macos")] // See https://www.magpcss.org/ceforum/viewtopic.php?start=10&t=11650 if key_event.character == 0 && key_event.unmodified_character == 0 && event.text_with_all_modifiers.is_some() { key_event.character = 1; From 42073777f6b3708d1e5d85ff21c92b8096841f1c Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 16:18:33 +0100 Subject: [PATCH 7/9] disable app submenu entries for now --- desktop/src/window/mac/menu.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs index 9fcf0826c4..ba3a94df75 100644 --- a/desktop/src/window/mac/menu.rs +++ b/desktop/src/window/mac/menu.rs @@ -11,11 +11,8 @@ pub(super) struct Menu { impl Menu { pub(super) fn new(event_scheduler: AppEventScheduler) -> Self { - 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, &[&PredefinedMenuItem::separator(), &hide, &hide_others, &show_all, &PredefinedMenuItem::separator(), &quit]).unwrap(); + // TODO: Remove as much app submenu special handling as possible + let app_submenu = Submenu::with_items("", true, &[]).unwrap(); let menu = MudaMenu::new(); menu.prepend(&app_submenu).unwrap(); From cd1b6880e0789e76ee96242d2e8c7061b03d56c6 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 16:36:45 +0100 Subject: [PATCH 8/9] move NSApplication init to window code --- desktop/src/app.rs | 4 ++++ desktop/src/lib.rs | 35 ++--------------------------------- desktop/src/window.rs | 6 ++++++ desktop/src/window/mac.rs | 9 +++++++-- desktop/src/window/mac/app.rs | 27 +++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 35 deletions(-) create mode 100644 desktop/src/window/mac/app.rs diff --git a/desktop/src/app.rs b/desktop/src/app.rs index d0557cd143..f02e52ef70 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -44,6 +44,10 @@ pub(crate) struct App { } impl App { + pub(crate) fn init() { + Window::init(); + } + pub(crate) fn new( cef_context: Box, cef_view_info_sender: Sender, diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 6224c9119f..35e604c2a8 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -23,42 +23,9 @@ use cef::CefHandler; use cli::Cli; use event::CreateAppEventSchedulerEventLoopExt; -mod mac_app_impl { - use objc2::{ClassType, define_class, msg_send}; - use objc2_app_kit::{NSApplication, NSEvent, NSEventType, NSResponder}; - use objc2_foundation::NSObject; - - define_class!( - #[unsafe(super(NSApplication, NSResponder, NSObject))] - #[name = "GraphiteApplication"] - pub(super) struct GraphiteApplication; - - impl GraphiteApplication { - #[unsafe(method(sendEvent:))] - fn send_event(&self, event: &NSEvent) { - if event.r#type() == NSEventType::KeyDown && let Some(key_window) = self.keyWindow() { - unsafe { msg_send![&key_window, sendEvent: event] } - } else { - unsafe { msg_send![super(self), sendEvent: event] } - } - } - } - ); - - impl GraphiteApplication { - pub(super) fn init() { - unsafe { - let _: &NSApplication = msg_send![GraphiteApplication::class(), sharedApplication]; - } - } - } -} - pub fn start() { tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).init(); - mac_app_impl::GraphiteApplication::init(); - let cef_context_builder = cef::CefContextBuilder::::new(); if cef_context_builder.is_sub_process() { @@ -69,6 +36,8 @@ pub fn start() { return; } + App::init(); + let cli = Cli::parse(); let wgpu_context = futures::executor::block_on(gpu_context::create_wgpu_context()); diff --git a/desktop/src/window.rs b/desktop/src/window.rs index e9994d23ca..ccebeb83f6 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -4,9 +4,11 @@ use winit::window::{Window as WinitWindow, WindowAttributes}; use crate::consts::APP_NAME; use crate::event::AppEventScheduler; +use crate::window::mac::NativeWindowImpl; use crate::wrapper::messages::MenuItem; pub(crate) trait NativeWindow { + fn init(); fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes; fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self; fn update_menu(&self, _entries: Vec) {} @@ -34,6 +36,10 @@ pub(crate) struct Window { } impl Window { + pub(crate) fn init() { + NativeWindowImpl::init(); + } + pub(crate) fn new(event_loop: &dyn ActiveEventLoop, app_event_scheduler: AppEventScheduler) -> Self { let mut attributes = WindowAttributes::default() .with_title(APP_NAME) diff --git a/desktop/src/window/mac.rs b/desktop/src/window/mac.rs index 537dfd1d43..5688cbd38c 100644 --- a/desktop/src/window/mac.rs +++ b/desktop/src/window/mac.rs @@ -5,11 +5,18 @@ use winit::window::{Window, WindowAttributes}; use crate::event::AppEventScheduler; use crate::wrapper::messages::MenuItem; +mod app; +mod menu; + pub(super) struct NativeWindowImpl { menu: menu::Menu, } impl super::NativeWindow for NativeWindowImpl { + fn init() { + app::init(); + } + fn configure(attributes: WindowAttributes, _event_loop: &dyn ActiveEventLoop) -> WindowAttributes { let mac_window = WindowAttributesMacOS::default() .with_titlebar_transparent(true) @@ -28,5 +35,3 @@ impl super::NativeWindow for NativeWindowImpl { self.menu.update(entries); } } - -mod menu; diff --git a/desktop/src/window/mac/app.rs b/desktop/src/window/mac/app.rs new file mode 100644 index 0000000000..36ca65c315 --- /dev/null +++ b/desktop/src/window/mac/app.rs @@ -0,0 +1,27 @@ +use objc2::{ClassType, define_class, msg_send}; +use objc2_app_kit::{NSApplication, NSEvent, NSEventType, NSResponder}; +use objc2_foundation::NSObject; + +pub(super) fn init() { + unsafe { + let _: &NSApplication = msg_send![GraphiteApplication::class(), sharedApplication]; + } +} + +define_class!( + #[unsafe(super(NSApplication, NSResponder, NSObject))] + #[name = "GraphiteApplication"] + pub(super) struct GraphiteApplication; + + impl GraphiteApplication { + #[unsafe(method(sendEvent:))] + fn send_event(&self, event: &NSEvent) { + // Route keyDown events straight to the key window to skip native menu shortcut handling. + if event.r#type() == NSEventType::KeyDown && let Some(key_window) = self.keyWindow() { + unsafe { msg_send![&key_window, sendEvent: event] } + } else { + unsafe { msg_send![super(self), sendEvent: event] } + } + } + } +); From 7e1d293e033e14f4e906b61f7bb6540f3c42bc49 Mon Sep 17 00:00:00 2001 From: Timon Schelling Date: Wed, 19 Nov 2025 16:39:08 +0100 Subject: [PATCH 9/9] native window default impl --- desktop/src/window.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/window.rs b/desktop/src/window.rs index ccebeb83f6..6a6b2dce71 100644 --- a/desktop/src/window.rs +++ b/desktop/src/window.rs @@ -8,7 +8,7 @@ use crate::window::mac::NativeWindowImpl; use crate::wrapper::messages::MenuItem; pub(crate) trait NativeWindow { - fn init(); + fn init() {} fn configure(attributes: WindowAttributes, event_loop: &dyn ActiveEventLoop) -> WindowAttributes; fn new(window: &dyn WinitWindow, app_event_scheduler: AppEventScheduler) -> Self; fn update_menu(&self, _entries: Vec) {}