Desktop: Mac native menu bar (#3301)

* macos native menu bar

* fix nix build

* Add shortcut symbols to menu

* fix fmt

* fix vendoring

* cleanup intercept frontend message

* accept into editor message in queue function
This commit is contained in:
Timon
2025-10-27 14:11:24 +00:00
committed by GitHub
parent 6ca25e4ea9
commit 5be9b1fabc
22 changed files with 458 additions and 45 deletions

View File

@@ -31,3 +31,5 @@ ron = { workspace = true}
vello = { workspace = true }
image = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
keyboard-types = { workspace = true }

View File

@@ -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;
@@ -55,7 +57,7 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
document_path: Some(path),
document_serialized_content: content,
};
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::ImportFile { path, content } => {
let extension = path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_lowercase();
@@ -80,7 +82,7 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
mouse: None,
parent_and_insert_index: None,
};
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::ImportImage { path, content } => {
let name = path.file_stem().and_then(|s| s.to_str()).map(|s| s.to_string());
@@ -106,7 +108,7 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
mouse: None,
parent_and_insert_index: None,
};
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::PollNodeGraphEvaluation => dispatcher.poll_node_graph_evaluation(),
DesktopWrapperMessage::UpdatePlatform(platform) => {
@@ -116,11 +118,11 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
Platform::Linux => AppWindowPlatform::Linux,
};
let message = AppWindowMessage::AppWindowUpdatePlatform { platform };
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::UpdateMaximized { maximized } => {
let message = FrontendMessage::UpdateMaximized { maximized };
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::LoadDocument {
id,
@@ -138,15 +140,23 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess
to_front,
select_after_open,
};
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::SelectDocument { id } => {
let message = PortfolioMessage::SelectDocument { document_id: id };
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
DesktopWrapperMessage::LoadPreferences { preferences } => {
let message = PreferencesMessage::Load { preferences };
dispatcher.queue_editor_message(message.into());
dispatcher.queue_editor_message(message);
}
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);
}
}
}

View File

@@ -1,9 +1,12 @@
use std::path::PathBuf;
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::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext};
use super::messages::{DesktopFrontendMessage, Document, FileFilter, KeyCode, MenuItem, Modifiers, OpenFileDialogContext, SaveFileDialogContext, Shortcut};
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
match message {
@@ -119,7 +122,210 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD
FrontendMessage::TriggerLoadPreferences => {
dispatcher.respond(DesktopFrontendMessage::PersistenceLoadPreferences);
}
FrontendMessage::UpdateMenuBarLayout { layout_target, layout } => {
let entries = convert_menu_bar_entries_to_menu_items(&layout);
dispatcher.respond(DesktopFrontendMessage::UpdateMenu { entries });
return Some(FrontendMessage::UpdateMenuBarLayout { layout, layout_target });
}
m => return Some(m),
}
None
}
fn convert_menu_bar_entries_to_menu_items(layout: &Vec<MenuBarEntry>) -> Vec<MenuItem> {
layout.iter().filter_map(|entry| convert_menu_bar_entry_to_menu_item(entry)).collect()
}
fn convert_menu_bar_entry_to_menu_item(
MenuBarEntry {
label,
icon,
shortcut,
action,
children,
disabled,
}: &MenuBarEntry,
) -> Option<MenuItem> {
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 = convert_menu_bar_entry_children_to_menu_items(&children.0);
return Some(MenuItem::SubMenu { id, text, enabled, items });
}
let shortcut = match shortcut {
Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => {
if let Some(shortcut) = convert_layout_keys_to_shortcut(&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,
shortcut,
checked: true,
});
}
Some("CheckboxUnchecked") => {
return Some(MenuItem::Checkbox {
id,
text,
enabled,
shortcut,
checked: false,
});
}
_ => {}
}
Some(MenuItem::Action { id, text, shortcut, enabled })
}
fn convert_menu_bar_entry_children_to_menu_items(children: &Vec<Vec<MenuBarEntry>>) -> Vec<MenuItem> {
let mut items = Vec::new();
for (i, section) in children.iter().enumerate() {
for entry in section.iter() {
if let Some(item) = convert_menu_bar_entry_to_menu_item(entry) {
items.push(item);
}
}
if i != children.len() - 1 {
items.push(MenuItem::Separator);
}
}
items
}
fn convert_layout_keys_to_shortcut(layout_keys: &Vec<LayoutKey>) -> Option<Shortcut> {
let mut key: Option<KeyCode> = None;
let mut modifiers = Modifiers::default();
for layout_key in layout_keys {
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(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,
}
}
if let Some(key) = key { Some(Shortcut { key, modifiers }) } else { None }
}

View File

@@ -32,8 +32,8 @@ impl<'a> DesktopWrapperMessageDispatcher<'a> {
self.desktop_wrapper_message_queue.push_back(message);
}
pub(super) fn queue_editor_message(&mut self, message: EditorMessage) {
if let Some(message) = intercept_editor_message(self, message) {
pub(super) fn queue_editor_message<T: Into<EditorMessage>>(&mut self, message: T) {
if let Some(message) = intercept_editor_message(self, message.into()) {
self.editor_message_queue.push(message);
}
}

View File

@@ -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<FrontendMessage>),
OpenLaunchDocuments,
@@ -56,6 +55,9 @@ pub enum DesktopFrontendMessage {
preferences: Preferences,
},
PersistenceLoadPreferences,
UpdateMenu {
entries: Vec<MenuItem>,
},
}
pub enum DesktopWrapperMessage {
@@ -106,6 +108,9 @@ pub enum DesktopWrapperMessage {
LoadPreferences {
preferences: Option<Preferences>,
},
MenuEvent {
id: u64,
},
}
#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
@@ -136,3 +141,32 @@ pub enum Platform {
Mac,
Linux,
}
pub enum MenuItem {
Action {
id: u64,
text: String,
enabled: bool,
shortcut: Option<Shortcut>,
},
Checkbox {
id: u64,
text: String,
enabled: bool,
shortcut: Option<Shortcut>,
checked: bool,
},
SubMenu {
id: u64,
text: String,
enabled: bool,
items: Vec<MenuItem>,
},
Separator,
}
pub use keyboard_types::{Code as KeyCode, Modifiers};
pub struct Shortcut {
pub key: KeyCode,
pub modifiers: Modifiers,
}