Desktop: Mac menu workaround (#3398)
Some checks failed
Editor: Dev & CI / build (push) Has been cancelled
Editor: Dev & CI / cargo-deny (push) Has been cancelled

This commit is contained in:
Timon 2025-11-19 17:13:35 +00:00 committed by GitHub
parent 788e82a7d0
commit 548e0df1a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 139 additions and 122 deletions

147
Cargo.lock generated
View file

@ -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]]
@ -2265,6 +2255,9 @@ dependencies = [
"graphite-desktop-embedded-resources",
"graphite-desktop-wrapper",
"muda",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"open",
"rand 0.9.2",
"rfd",
@ -2446,6 +2439,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"
@ -2691,24 +2697,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"
@ -2761,8 +2755,8 @@ dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr 0.8.1",
"writeable 0.6.1",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
@ -3236,12 +3230,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"
@ -3811,9 +3799,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",
@ -4038,14 +4026,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",
]
@ -4095,17 +4084,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"
@ -4769,16 +4747,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"
@ -4796,6 +4764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358"
dependencies = [
"bytemuck",
"core_maths",
"font-types 0.10.0",
]
@ -5434,16 +5403,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"
@ -5715,11 +5674,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",
]
@ -5971,15 +5930,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"
@ -6580,7 +6530,7 @@ dependencies = [
"bytemuck",
"futures-intrusive",
"log",
"peniko 0.5.0",
"peniko",
"png",
"skrifa 0.37.0",
"static_assertions",
@ -6597,7 +6547,7 @@ source = "git+https://github.com/linebender/vello#8f2f2564127812362d2c57ded20cad
dependencies = [
"bytemuck",
"guillotiere",
"peniko 0.5.0",
"peniko",
"skrifa 0.37.0",
"smallvec",
]
@ -7715,12 +7665,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"
@ -7819,6 +7763,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"

View file

@ -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"] }

View file

@ -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 }

View file

@ -44,6 +44,10 @@ pub(crate) struct App {
}
impl App {
pub(crate) fn init() {
Window::init();
}
pub(crate) fn new(
cef_context: Box<dyn cef::CefContext>,
cef_view_info_sender: Sender<cef::ViewInfoUpdate>,

View file

@ -88,11 +88,27 @@ 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.
#[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.unmodified_character = 1;
key_event.character = 1;
}
if key_event.type_ == cef_key_event_type_t::KEYEVENT_CHAR.into() {

View file

@ -36,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());

View file

@ -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<MenuItem>) {}
@ -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)

View file

@ -2,15 +2,21 @@ 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;
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)
@ -20,7 +26,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 }
}
@ -29,5 +35,3 @@ impl super::NativeWindow for NativeWindowImpl {
self.menu.update(entries);
}
}
mod menu;

View file

@ -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] }
}
}
}
);

View file

@ -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,9 @@ 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()));
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();
pub(super) fn new(event_scheduler: AppEventScheduler) -> Self {
// 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();
@ -29,6 +20,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 });
}

View file

@ -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,
};

View file

@ -26,9 +26,9 @@ impl From<TextAlign> 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,
}
}
}

View file

@ -33,9 +33,9 @@ impl From<TextAlign> 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,
}
}
}