From 600fb5c28f9bdedbdcaf06b5ab1804f674c6e9d7 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 3 Dec 2025 04:41:54 -0800 Subject: [PATCH] Refactor the old menu bar plumbing to use standard TextButtons (#3444) * Refactor the old menu bar plumbing to use standard TextButtons * WIP: Fix Mac native menu bar * WIP: fix desktop menu bar mac * Refactor menu bar definitions to use the builder pattern * WIP: fixup desktop * cleanup * fix linux * Remove dead code that was failing to lint --------- Co-authored-by: Timon Schelling --- Cargo.lock | 1 + desktop/src/event.rs | 2 +- desktop/src/window/mac/menu.rs | 17 +- desktop/wrapper/Cargo.toml | 1 + .../src/handle_desktop_wrapper_message.rs | 20 +- .../wrapper/src/intercept_frontend_message.rs | 216 +-- desktop/wrapper/src/lib.rs | 2 + desktop/wrapper/src/messages.rs | 8 +- desktop/wrapper/src/utils.rs | 247 ++++ .../src/messages/frontend/frontend_message.rs | 2 +- .../messages/layout/layout_message_handler.rs | 83 +- .../layout/utility_types/layout_widget.rs | 98 +- .../src/messages/layout/utility_types/mod.rs | 1 - .../utility_types/widgets/input_widgets.rs | 34 +- .../utility_types/widgets/menu_widgets.rs | 131 -- .../layout/utility_types/widgets/mod.rs | 1 - .../node_graph/document_node_definitions.rs | 5 +- .../document/node_graph/node_properties.rs | 2 +- .../menu_bar/menu_bar_message_handler.rs | 1316 ++++++++--------- .../components/floating-menus/MenuList.svelte | 29 +- .../floating-menus/NodeCatalog.svelte | 20 +- .../src/components/layout/FloatingMenu.svelte | 86 +- .../src/components/panels/Document.svelte | 2 +- frontend/src/components/views/Graph.svelte | 86 +- .../src/components/widgets/WidgetSpan.svelte | 2 +- .../widgets/buttons/TextButton.svelte | 13 +- .../widgets/inputs/FontInput.svelte | 2 +- .../widgets/inputs/NumberInput.svelte | 1 + .../widgets/inputs/RadioInput.svelte | 2 - .../widgets/inputs/ScrollbarInput.svelte | 1 + .../widgets/labels/UserInputLabel.svelte | 2 +- .../window/title-bar/TitleBar.svelte | 55 +- frontend/src/messages.ts | 81 +- frontend/src/state-providers/node-graph.ts | 8 + 34 files changed, 1222 insertions(+), 1355 deletions(-) create mode 100644 desktop/wrapper/src/utils.rs delete mode 100644 editor/src/messages/layout/utility_types/widgets/menu_widgets.rs diff --git a/Cargo.lock b/Cargo.lock index 12464b77e..6b3e9a923 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2328,6 +2328,7 @@ dependencies = [ name = "graphite-desktop-wrapper" version = "0.1.0" dependencies = [ + "base64", "dirs", "futures", "graph-craft", diff --git a/desktop/src/event.rs b/desktop/src/event.rs index e4eaba58c..44ce71bd1 100644 --- a/desktop/src/event.rs +++ b/desktop/src/event.rs @@ -9,7 +9,7 @@ pub(crate) enum AppEvent { DesktopWrapperMessage(DesktopWrapperMessage), NodeGraphExecutionResult(NodeGraphExecutionResult), CloseWindow, - MenuEvent { id: u64 }, + MenuEvent { id: String }, } #[derive(Clone)] diff --git a/desktop/src/window/mac/menu.rs b/desktop/src/window/mac/menu.rs index d58c455bd..41358b46b 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::{CheckMenuItem, IsMenuItem, MenuEvent, MenuId, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu}; +use muda::{CheckMenuItem, IsMenuItem, MenuEvent, MenuItem, MenuItemKind, PredefinedMenuItem, Result, Submenu}; use crate::event::{AppEvent, AppEventScheduler}; use crate::wrapper::messages::MenuItem as WrapperMenuItem; @@ -30,9 +30,8 @@ impl Menu { return; } - if let Some(id) = menu_id_to_u64(event.id()) { - event_scheduler.schedule(AppEvent::MenuEvent { id }); - } + let id = event.id().0.clone(); + event_scheduler.schedule(AppEvent::MenuEvent { id }); })); Menu { inner: menu } @@ -67,13 +66,11 @@ fn menu_items_from_wrapper(entries: Vec) -> Vec { for entry in entries { match entry { WrapperMenuItem::Action { id, text, enabled, shortcut } => { - let id = u64_to_menu_id(id); 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, shortcut, checked } => { - let id = u64_to_menu_id(id); 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)); @@ -103,14 +100,6 @@ fn menu_item_kind_to_dyn(item: &MenuItemKind) -> &dyn IsMenuItem { } } -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() -} - fn replace_children<'a, T: Into>>(menu: T, new_items: Vec) { let menu: MenuContainer = menu.into(); let items = menu.items(); diff --git a/desktop/wrapper/Cargo.toml b/desktop/wrapper/Cargo.toml index b6abae2ca..2380bc7a5 100644 --- a/desktop/wrapper/Cargo.toml +++ b/desktop/wrapper/Cargo.toml @@ -31,3 +31,4 @@ image = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } keyboard-types = { workspace = true } +base64 = { workspace = true } diff --git a/desktop/wrapper/src/handle_desktop_wrapper_message.rs b/desktop/wrapper/src/handle_desktop_wrapper_message.rs index 50dab0906..4f0b5c9cf 100644 --- a/desktop/wrapper/src/handle_desktop_wrapper_message.rs +++ b/desktop/wrapper/src/handle_desktop_wrapper_message.rs @@ -1,14 +1,10 @@ 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::*; -use graphite_editor::messages::tool::tool_messages::tool_prelude::{LayoutTarget, WidgetId}; - -use crate::messages::Platform; use super::DesktopWrapperMessageDispatcher; -use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, SaveFileDialogContext}; +use super::messages::{DesktopFrontendMessage, DesktopWrapperMessage, EditorMessage, OpenFileDialogContext, Platform, SaveFileDialogContext}; pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: DesktopWrapperMessage) { match message { @@ -150,13 +146,15 @@ pub(super) fn handle_desktop_wrapper_message(dispatcher: &mut DesktopWrapperMess let message = PreferencesMessage::Load { preferences }; dispatcher.queue_editor_message(message); } + #[cfg(target_os = "macos")] 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); + if let Some(message) = crate::utils::menu::parse_item_path(id) { + dispatcher.queue_editor_message(message); + } else { + tracing::error!("Received a malformed MenuEvent id"); + } } + #[cfg(not(target_os = "macos"))] + DesktopWrapperMessage::MenuEvent { id: _ } => {} } } diff --git a/desktop/wrapper/src/intercept_frontend_message.rs b/desktop/wrapper/src/intercept_frontend_message.rs index f2a1bb1e1..5c69df58b 100644 --- a/desktop/wrapper/src/intercept_frontend_message.rs +++ b/desktop/wrapper/src/intercept_frontend_message.rs @@ -1,12 +1,9 @@ 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, KeyCode, MenuItem, Modifiers, OpenFileDialogContext, SaveFileDialogContext, Shortcut}; +use super::messages::{DesktopFrontendMessage, Document, FileFilter, OpenFileDialogContext, SaveFileDialogContext}; pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option { match message { @@ -113,11 +110,24 @@ 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 }); + #[cfg(target_os = "macos")] + FrontendMessage::UpdateMenuBarLayout { + layout_target: graphite_editor::messages::tool::tool_messages::tool_prelude::LayoutTarget::MenuBar, + diff, + } => { + use graphite_editor::messages::tool::tool_messages::tool_prelude::{DiffUpdate, WidgetDiff}; + match diff.as_slice() { + [ + WidgetDiff { + widget_path, + new_value: DiffUpdate::SubLayout(layout), + }, + ] if widget_path.is_empty() => { + let entries = crate::utils::menu::convert_menu_bar_layout_to_menu_items(layout); + dispatcher.respond(DesktopFrontendMessage::UpdateMenu { entries }); + } + _ => {} + } } FrontendMessage::WindowClose => { dispatcher.respond(DesktopFrontendMessage::WindowClose); @@ -144,191 +154,3 @@ pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageD } None } - -fn convert_menu_bar_entries_to_menu_items(layout: &[MenuBarEntry]) -> Vec { - layout.iter().filter_map(convert_menu_bar_entry_to_menu_item).collect() -} - -fn convert_menu_bar_entry_to_menu_item( - MenuBarEntry { - label, - icon, - shortcut, - action, - children, - disabled, - }: &MenuBarEntry, -) -> Option { - let id = action.widget_id.0; - let text = 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))) => convert_layout_keys_to_shortcut(keys), - _ => 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 { - 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) -> Option { - let mut key: Option = 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::FakeKeyPlus => key = Some(KeyCode::Equal), - _ => key = None, - } - } - key.map(|key| Shortcut { key, modifiers }) -} diff --git a/desktop/wrapper/src/lib.rs b/desktop/wrapper/src/lib.rs index 3050c96d2..bdb9978e6 100644 --- a/desktop/wrapper/src/lib.rs +++ b/desktop/wrapper/src/lib.rs @@ -20,6 +20,8 @@ mod handle_desktop_wrapper_message; mod intercept_editor_message; mod intercept_frontend_message; +pub(crate) mod utils; + pub struct DesktopWrapper { editor: Editor, } diff --git a/desktop/wrapper/src/messages.rs b/desktop/wrapper/src/messages.rs index af8d4b59e..8692d6df0 100644 --- a/desktop/wrapper/src/messages.rs +++ b/desktop/wrapper/src/messages.rs @@ -112,7 +112,7 @@ pub enum DesktopWrapperMessage { preferences: Option, }, MenuEvent { - id: u64, + id: String, }, } @@ -147,20 +147,20 @@ pub enum Platform { pub enum MenuItem { Action { - id: u64, + id: String, text: String, enabled: bool, shortcut: Option, }, Checkbox { - id: u64, + id: String, text: String, enabled: bool, shortcut: Option, checked: bool, }, SubMenu { - id: u64, + id: String, text: String, enabled: bool, items: Vec, diff --git a/desktop/wrapper/src/utils.rs b/desktop/wrapper/src/utils.rs new file mode 100644 index 000000000..f3a27054c --- /dev/null +++ b/desktop/wrapper/src/utils.rs @@ -0,0 +1,247 @@ +#[cfg(target_os = "macos")] +pub(crate) mod menu { + use base64::engine::Engine; + use base64::engine::general_purpose::STANDARD as BASE64; + + 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::LayoutMessage; + use graphite_editor::messages::tool::tool_messages::tool_prelude::{LayoutGroup, LayoutTarget, MenuListEntry, SubLayout, Widget, WidgetId}; + + use crate::messages::{EditorMessage, KeyCode, MenuItem, Modifiers, Shortcut}; + + pub(crate) fn convert_menu_bar_layout_to_menu_items(layout: &SubLayout) -> Vec { + let layout_group = match layout.as_slice() { + [layout_group] => layout_group, + _ => panic!("Menu bar layout is supposed to have exactly one layout group"), + }; + let LayoutGroup::Row { widgets } = layout_group else { + panic!("Menu bar layout group is supposed to be a row"); + }; + widgets + .into_iter() + .map(|widget| { + let text_button = match &widget.widget { + Widget::TextButton(text_button) => text_button, + _ => panic!("Menu bar layout top-level widgets are supposed to be text buttons"), + }; + + MenuItem::SubMenu { + id: widget.widget_id.to_string(), + text: text_button.label.clone(), + enabled: !text_button.disabled, + items: convert_menu_bar_entry_children_to_menu_items(&text_button.menu_list_children, widget.widget_id.0, Vec::new()), + } + }) + .collect::>() + } + + pub(crate) fn parse_item_path(id: String) -> Option { + let mut id_parts = id.split(':'); + let widget_id = id_parts.next()?.parse::().ok()?; + + let value = id_parts + .map(|part| { + let bytes = BASE64.decode(part).ok()?; + String::from_utf8(bytes).ok() + }) + .collect::>>()?; + let value = serde_json::to_value(value).ok()?; + + Some( + LayoutMessage::WidgetValueUpdate { + layout_target: LayoutTarget::MenuBar, + widget_id: WidgetId(widget_id), + value, + } + .into(), + ) + } + + fn item_path_to_string(widget_id: u64, path: Vec) -> String { + let path = path.into_iter().map(|element| BASE64.encode(element)).collect::>().join(":"); + format!("{widget_id}:{path}") + } + + fn convert_menu_bar_layout_to_menu_item(entry: &MenuListEntry, root_widget_id: u64, mut path: Vec) -> MenuItem { + let MenuListEntry { + value, + label, + icon, + shortcut_keys, + children, + disabled, + .. + }: &MenuListEntry = entry; + path.push(value.clone()); + let id = item_path_to_string(root_widget_id, path.clone()); + let text = label.clone(); + let enabled = !*disabled; + + if !children.is_empty() { + let items = convert_menu_bar_entry_children_to_menu_items(&children, root_widget_id, path.clone()); + return MenuItem::SubMenu { id, text, enabled, items }; + } + + let shortcut = match shortcut_keys { + Some(ActionKeys::Keys(LayoutKeysGroup(keys))) => convert_layout_keys_to_shortcut(keys), + _ => None, + }; + + match icon.as_str() { + "CheckboxChecked" => { + return MenuItem::Checkbox { + id, + text, + enabled, + shortcut, + checked: true, + }; + } + "CheckboxUnchecked" => { + return MenuItem::Checkbox { + id, + text, + enabled, + shortcut, + checked: false, + }; + } + _ => {} + } + + MenuItem::Action { id, text, shortcut, enabled } + } + + fn convert_menu_bar_entry_children_to_menu_items(children: &[Vec], root_widget_id: u64, path: Vec) -> Vec { + let mut items = Vec::new(); + for (i, section) in children.iter().enumerate() { + for entry in section.iter() { + items.push(convert_menu_bar_layout_to_menu_item(entry, root_widget_id, path.clone())); + } + if i != children.len() - 1 { + items.push(MenuItem::Separator); + } + } + items + } + + fn convert_layout_keys_to_shortcut(layout_keys: &Vec) -> Option { + let mut key: Option = 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::FakeKeyPlus => key = Some(KeyCode::Equal), + _ => key = None, + } + } + key.map(|key| Shortcut { key, modifiers }) + } +} diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index a674d70ee..9582e0472 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -279,7 +279,7 @@ pub enum FrontendMessage { UpdateMenuBarLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, - layout: Vec, + diff: Vec, }, UpdateMouseCursor { cursor: MouseCursorIcon, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index a80fa887c..304c2e41c 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -25,7 +25,6 @@ impl MessageHandler> for LayoutMessageHa LayoutMessage::ResendActiveWidget { layout_target, widget_id } => { // Find the updated diff based on the specified layout target let Some(diff) = (match &self.layouts[layout_target as usize] { - Layout::MenuLayout(_) => return, Layout::WidgetLayout(layout) => Self::get_widget_path(layout, widget_id).map(|(widget, widget_path)| { // Create a widget update diff for the relevant id let new_value = DiffUpdate::Widget(widget.clone()); @@ -112,7 +111,10 @@ impl LayoutMessageHandler { return; }; - let Some(widget_holder) = layout.iter_mut().find(|widget| widget.widget_id == widget_id) else { + let mut layout_iter = match layout { + Layout::WidgetLayout(widget_layout) => widget_layout.iter_mut(), + }; + let Some(widget_holder) = layout_iter.find(|widget| widget.widget_id == widget_id) else { warn!("handle_widget_callback was called referencing an invalid widget ID, although the layout target was valid. `widget_id: {widget_id}`, `layout_target: {layout_target:?}`",); return; }; @@ -309,14 +311,6 @@ impl LayoutMessageHandler { } Widget::ImageLabel(_) => {} Widget::IconLabel(_) => {} - Widget::InvisibleStandinInput(invisible) => { - let callback_message = match action { - WidgetValueAction::Commit => (invisible.on_commit.callback)(&()), - WidgetValueAction::Update => (invisible.on_update.callback)(&()), - }; - - responses.add(callback_message); - } Widget::NodeCatalog(node_type_input) => match action { WidgetValueAction::Commit => { let callback_message = (node_type_input.on_commit.callback)(&()); @@ -411,7 +405,34 @@ impl LayoutMessageHandler { Widget::TextButton(text_button) => { let callback_message = match action { WidgetValueAction::Commit => (text_button.on_commit.callback)(&()), - WidgetValueAction::Update => (text_button.on_update.callback)(text_button), + WidgetValueAction::Update => { + let Some(value_path) = value.as_array() else { + error!("TextButton update was not of type: array"); + return; + }; + + // Process the text button click, since no menu is involved if we're given an empty array. + if value_path.is_empty() { + (text_button.on_update.callback)(text_button) + } + // Process the text button's menu list entry click, since we have a path to the value of the contained menu entry. + else { + let mut current_submenu = &text_button.menu_list_children; + let mut final_entry: Option<&MenuListEntry> = None; + + // Loop through all menu entry value strings in the path until we reach the final entry (which we store). + // Otherwise we exit early if we can't traverse the full path. + for value in value_path.iter().filter_map(|v| v.as_str().map(|s| s.to_string())) { + let Some(next_entry) = current_submenu.iter().flatten().find(|e| e.value == value) else { return }; + + current_submenu = &next_entry.children; + final_entry = Some(next_entry); + } + + // If we've reached here without returning early, we have a final entry in the path and we should now execute its callback. + (final_entry.unwrap().on_commit.callback)(&()) + } + } }; responses.add(callback_message); @@ -447,31 +468,26 @@ impl LayoutMessageHandler { match new_layout { Layout::WidgetLayout(_) => { let mut widget_diffs = Vec::new(); - self.layouts[layout_target as usize].diff(new_layout, &mut Vec::new(), &mut widget_diffs); - // Skip sending if no diff. + let Layout::WidgetLayout(current) = &mut self.layouts[layout_target as usize]; + let Layout::WidgetLayout(new) = new_layout; + current.diff(new, &mut Vec::new(), &mut widget_diffs); + + // Skip sending if no diff if widget_diffs.is_empty() { return; } - self.send_diff(widget_diffs, layout_target, responses, action_input_mapping); - } - // We don't diff the menu bar layout yet. - Layout::MenuLayout(_) => { - // Skip update if the same - if self.layouts[layout_target as usize] == new_layout { - return; + // On Mac we need the full MenuBar layout to construct the native menu + #[cfg(target_os = "macos")] + if layout_target == LayoutTarget::MenuBar { + widget_diffs = vec![WidgetDiff { + widget_path: Vec::new(), + new_value: DiffUpdate::SubLayout(current.layout.clone()), + }]; } - // Update the backend storage - self.layouts[layout_target as usize] = new_layout; - - // Update the UI - let Some(layout) = self.layouts[layout_target as usize].clone().as_menu_layout(action_input_mapping).map(|x| x.layout) else { - error!("Called unwrap_menu_layout on a widget layout"); - return; - }; - responses.add(FrontendMessage::UpdateMenuBarLayout { layout_target, layout }); + self.send_diff(widget_diffs, layout_target, responses, action_input_mapping); } } } @@ -481,24 +497,25 @@ impl LayoutMessageHandler { diff.iter_mut().for_each(|diff| diff.new_value.apply_keyboard_shortcut(action_input_mapping)); let message = match layout_target { - LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"), + LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff }, LayoutTarget::DialogButtons => FrontendMessage::UpdateDialogButtons { layout_target, diff }, LayoutTarget::DialogColumn1 => FrontendMessage::UpdateDialogColumn1 { layout_target, diff }, LayoutTarget::DialogColumn2 => FrontendMessage::UpdateDialogColumn2 { layout_target, diff }, LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff }, LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff }, - LayoutTarget::DataPanel => FrontendMessage::UpdateDataPanelLayout { layout_target, diff }, + LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff }, LayoutTarget::LayersPanelControlLeftBar => FrontendMessage::UpdateLayersPanelControlBarLeftLayout { layout_target, diff }, LayoutTarget::LayersPanelControlRightBar => FrontendMessage::UpdateLayersPanelControlBarRightLayout { layout_target, diff }, - LayoutTarget::LayersPanelBottomBar => FrontendMessage::UpdateLayersPanelBottomBarLayout { layout_target, diff }, - LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff }, + LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout { layout_target, diff }, LayoutTarget::NodeGraphControlBar => FrontendMessage::UpdateNodeGraphControlBarLayout { layout_target, diff }, + LayoutTarget::PropertiesPanel => FrontendMessage::UpdatePropertiesPanelLayout { layout_target, diff }, LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff }, LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff }, LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, diff }, LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"), }; + responses.add(message); } } diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index acd676632..f25261d5a 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -1,7 +1,6 @@ use super::widgets::button_widgets::*; use super::widgets::input_widgets::*; use super::widgets::label_widgets::*; -use super::widgets::menu_widgets::MenuLayout; use crate::application::generate_uuid; use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; use crate::messages::input_mapper::utility_types::misc::ActionKeys; @@ -101,56 +100,11 @@ pub trait DialogLayoutHolder: LayoutHolder { } } +// TODO: Unwrap this enum /// Wraps a choice of layout type. The chosen layout contains an arrangement of widgets mounted somewhere specific in the frontend. #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub enum Layout { WidgetLayout(WidgetLayout), - MenuLayout(MenuLayout), -} - -impl Layout { - pub fn as_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option) -> Option { - if let Self::MenuLayout(mut menu) = self { - menu.layout - .iter_mut() - .for_each(|menu_column| menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping)); - Some(menu) - } else { - None - } - } - - pub fn iter(&self) -> Box + '_> { - match self { - Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter()), - Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter()), - } - } - - pub fn iter_mut(&mut self) -> Box + '_> { - match self { - Layout::MenuLayout(menu_layout) => Box::new(menu_layout.iter_mut()), - Layout::WidgetLayout(widget_layout) => Box::new(widget_layout.iter_mut()), - } - } - - /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. - pub fn diff(&mut self, new: Self, widget_path: &mut Vec, widget_diffs: &mut Vec) { - match (self, new) { - // Simply diff the internal layout - (Self::WidgetLayout(current), Self::WidgetLayout(new)) => current.diff(new, widget_path, widget_diffs), - (current, Self::WidgetLayout(widget_layout)) => { - // Update current to the new value - *current = Self::WidgetLayout(widget_layout.clone()); - - // Push an update sublayout value - let new_value = DiffUpdate::SubLayout(widget_layout.layout); - let widget_path = widget_path.to_vec(); - widget_diffs.push(WidgetDiff { widget_path, new_value }); - } - (_, Self::MenuLayout(_)) => panic!("Cannot diff menu layout"), - } - } } impl Default for Layout { @@ -159,6 +113,7 @@ impl Default for Layout { } } +// TODO: Unwrap this struct #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)] pub struct WidgetLayout { pub layout: SubLayout, @@ -327,7 +282,6 @@ pub enum LayoutGroup { #[serde(rename = "tableWidgets")] rows: Vec>, }, - // TODO: Move this from being a child of `enum LayoutGroup` to being a child of `enum Layout` #[serde(rename = "section")] Section { name: String, @@ -378,7 +332,7 @@ impl LayoutGroup { Widget::TextInput(x) => &mut x.tooltip_label, Widget::TextLabel(x) => &mut x.tooltip_label, Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_label, - Widget::InvisibleStandinInput(_) | Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue, + Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue, }; if val.is_empty() { val.clone_from(&label); @@ -414,7 +368,7 @@ impl LayoutGroup { Widget::TextInput(x) => &mut x.tooltip_description, Widget::TextLabel(x) => &mut x.tooltip_description, Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip_description, - Widget::InvisibleStandinInput(_) | Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue, + Widget::ReferencePointInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue, }; if val.is_empty() { val.clone_from(&description); @@ -520,6 +474,7 @@ impl LayoutGroup { } } +// TODO: Rename to WidgetInstance #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)] pub struct WidgetHolder { #[serde(rename = "widgetId")] @@ -609,7 +564,6 @@ pub enum Widget { IconLabel(IconLabel), ImageButton(ImageButton), ImageLabel(ImageLabel), - InvisibleStandinInput(InvisibleStandinInput), NodeCatalog(NodeCatalog), NumberInput(NumberInput), ParameterExposeButton(ParameterExposeButton), @@ -680,7 +634,6 @@ impl DiffUpdate { Widget::IconLabel(_) | Widget::ImageLabel(_) | Widget::CurveInput(_) - | Widget::InvisibleStandinInput(_) | Widget::NodeCatalog(_) | Widget::ReferencePointInput(_) | Widget::RadioInput(_) @@ -709,10 +662,45 @@ impl DiffUpdate { } }; + // Recursively fill menu list entries with their realized shortcut keys specific to the current bindings and platform + let apply_action_keys_to_menu_lists = |entry_sections: &mut MenuListEntrySections| { + struct RecursiveWrapper<'a>(&'a dyn Fn(&mut MenuListEntrySections, &RecursiveWrapper)); + let recursive_wrapper = RecursiveWrapper(&|entry_sections: &mut MenuListEntrySections, recursive_wrapper| { + for entries in entry_sections { + for entry in entries { + // Convert the shortcut actions to keys for this menu entry + if let Some(shortcut_keys) = &mut entry.shortcut_keys { + shortcut_keys.to_keys(action_input_mapping); + } + + // Recursively call this inner closure on the menu's children + (recursive_wrapper.0)(&mut entry.children, recursive_wrapper); + } + } + }); + (recursive_wrapper.0)(entry_sections, &recursive_wrapper) + }; + + // Apply shortcut conversions to all widgets that have menu lists + let convert_menu_lists = |widget_holder: &mut WidgetHolder| match &mut widget_holder.widget { + Widget::DropdownInput(dropdown_input) => apply_action_keys_to_menu_lists(&mut dropdown_input.entries), + Widget::TextButton(text_button) => apply_action_keys_to_menu_lists(&mut text_button.menu_list_children), + _ => {} + }; + match self { - Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(convert_tooltip), - Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(convert_tooltip), - Self::Widget(widget_holder) => convert_tooltip(widget_holder), + Self::SubLayout(sub_layout) => sub_layout.iter_mut().flat_map(|layout_group| layout_group.iter_mut()).for_each(|widget_holder| { + convert_tooltip(widget_holder); + convert_menu_lists(widget_holder); + }), + Self::LayoutGroup(layout_group) => layout_group.iter_mut().for_each(|widget_holder| { + convert_tooltip(widget_holder); + convert_menu_lists(widget_holder); + }), + Self::Widget(widget_holder) => { + convert_tooltip(widget_holder); + convert_menu_lists(widget_holder); + } } } } diff --git a/editor/src/messages/layout/utility_types/mod.rs b/editor/src/messages/layout/utility_types/mod.rs index 8408d0b1b..33a2ddabf 100644 --- a/editor/src/messages/layout/utility_types/mod.rs +++ b/editor/src/messages/layout/utility_types/mod.rs @@ -6,5 +6,4 @@ pub mod widget_prelude { pub use super::widgets::button_widgets::*; pub use super::widgets::input_widgets::*; pub use super::widgets::label_widgets::*; - pub use super::widgets::menu_widgets::*; } diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 242a1b1f6..304c5f5cf 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -133,15 +133,28 @@ pub struct MenuListEntry { pub label: String, + pub font: String, + pub icon: String, - pub shortcut: Vec, + pub disabled: bool, + + #[serde(rename = "tooltipLabel")] + pub tooltip_label: String, + + #[serde(rename = "tooltipDescription")] + pub tooltip_description: String, + + #[serde(rename = "tooltipShortcut")] + pub tooltip_shortcut: String, + + // TODO: Make this serde(skip) + #[serde(rename = "shortcutKeys")] + pub shortcut_keys: Option, #[serde(rename = "shortcutRequiresLock")] pub shortcut_requires_lock: bool, - pub disabled: bool, - pub children: MenuListEntrySections, // Callbacks @@ -192,21 +205,6 @@ pub struct FontInput { pub on_commit: WidgetCallback<()>, } -/// This widget allows for the flexible use of the layout system. -/// In a custom layout, one can define a widget that is just used to trigger code on the backend. -/// This is used in MenuLayout to pipe the triggering of messages from the frontend to backend. -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Default, WidgetBuilder, specta::Type)] -#[derivative(Debug, PartialEq)] -pub struct InvisibleStandinInput { - #[serde(skip)] - #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_update: WidgetCallback<()>, - - #[serde(skip)] - #[derivative(Debug = "ignore", PartialEq = "ignore")] - pub on_commit: WidgetCallback<()>, -} - #[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)] #[derivative(Debug, PartialEq, Default)] pub struct NumberInput { diff --git a/editor/src/messages/layout/utility_types/widgets/menu_widgets.rs b/editor/src/messages/layout/utility_types/widgets/menu_widgets.rs deleted file mode 100644 index e96009d06..000000000 --- a/editor/src/messages/layout/utility_types/widgets/menu_widgets.rs +++ /dev/null @@ -1,131 +0,0 @@ -use super::input_widgets::InvisibleStandinInput; -use crate::messages::input_mapper::utility_types::input_keyboard::KeysGroup; -use crate::messages::input_mapper::utility_types::misc::ActionKeys; -use crate::messages::layout::utility_types::widget_prelude::*; -use crate::messages::prelude::*; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Default, specta::Type)] -pub struct MenuBarEntryChildren(pub Vec>); - -impl MenuBarEntryChildren { - pub fn empty() -> Self { - Self(Vec::new()) - } - - pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Option) { - let entries = self.0.iter_mut().flatten(); - - for entry in entries { - if let Some(action_keys) = &mut entry.shortcut { - action_keys.to_keys(action_input_mapping); - } - - // Recursively do this for the children also - entry.children.fill_in_shortcut_actions_with_keys(action_input_mapping); - } - } -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, specta::Type)] -pub struct MenuBarEntry { - pub label: String, - pub icon: Option, - pub shortcut: Option, - pub action: WidgetHolder, - pub children: MenuBarEntryChildren, - pub disabled: bool, -} - -impl MenuBarEntry { - pub fn new_root(label: String, disabled: bool, children: MenuBarEntryChildren) -> Self { - Self { - label, - disabled, - children, - ..Default::default() - } - } - - pub fn create_action(callback: impl Fn(&()) -> Message + 'static + Send + Sync) -> WidgetHolder { - InvisibleStandinInput::new().on_update(callback).widget_holder() - } - - pub fn no_action() -> WidgetHolder { - MenuBarEntry::create_action(|_| Message::NoOp) - } -} - -impl Default for MenuBarEntry { - fn default() -> Self { - Self { - label: "".into(), - icon: None, - shortcut: None, - action: MenuBarEntry::no_action(), - children: MenuBarEntryChildren::empty(), - disabled: false, - } - } -} - -#[derive(Debug, Default, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct MenuLayout { - pub layout: Vec, -} - -impl MenuLayout { - pub fn new(layout: Vec) -> Self { - Self { layout } - } - - pub fn iter(&self) -> impl Iterator + '_ { - MenuLayoutIter { stack: self.layout.iter().collect() } - } - - pub fn iter_mut(&mut self) -> impl Iterator + '_ { - MenuLayoutIterMut { - stack: self.layout.iter_mut().collect(), - } - } -} - -#[derive(Debug, Default)] -pub struct MenuLayoutIter<'a> { - pub stack: Vec<&'a MenuBarEntry>, -} - -impl<'a> Iterator for MenuLayoutIter<'a> { - type Item = &'a WidgetHolder; - - fn next(&mut self) -> Option { - match self.stack.pop() { - Some(menu_entry) => { - let more_entries = menu_entry.children.0.iter().flat_map(|entry| entry.iter()); - self.stack.extend(more_entries); - - Some(&menu_entry.action) - } - None => None, - } - } -} - -pub struct MenuLayoutIterMut<'a> { - pub stack: Vec<&'a mut MenuBarEntry>, -} - -impl<'a> Iterator for MenuLayoutIterMut<'a> { - type Item = &'a mut WidgetHolder; - - fn next(&mut self) -> Option { - match self.stack.pop() { - Some(menu_entry) => { - let more_entries = menu_entry.children.0.iter_mut().flat_map(|entry| entry.iter_mut()); - self.stack.extend(more_entries); - - Some(&mut menu_entry.action) - } - None => None, - } - } -} diff --git a/editor/src/messages/layout/utility_types/widgets/mod.rs b/editor/src/messages/layout/utility_types/widgets/mod.rs index 922029633..0f5f1c4f3 100644 --- a/editor/src/messages/layout/utility_types/widgets/mod.rs +++ b/editor/src/messages/layout/utility_types/widgets/mod.rs @@ -1,4 +1,3 @@ pub mod button_widgets; pub mod input_widgets; pub mod label_widgets; -pub mod menu_widgets; diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index c8f805b43..ae39d47ed 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -157,7 +157,8 @@ fn static_nodes() -> Vec { }, }, description: Cow::Borrowed( - "Improves rendering performance if used in rare circumstances where automatic caching is not yet advanced enough to handle the situation. + "Improves rendering performance if used in rare circumstances where automatic caching is not yet advanced enough to handle the situation.\n\ + \n\ Stores the last evaluated data that flowed through this node, and immediately returns that data on subsequent renders if the context has not changed.", ), properties: None, @@ -1014,7 +1015,7 @@ fn static_nodes() -> Vec { ..Default::default() }, }, - description: Cow::Borrowed("Loads an image from a given URL"), + description: Cow::Borrowed("Loads an image from a given URL."), properties: None, }, #[cfg(all(feature = "gpu", target_family = "wasm"))] diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index ebf818b55..1b1c65fb7 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2112,7 +2112,7 @@ pub mod choice { } /// Not yet implemented! - pub fn into_menu_entries(self, _action: impl Fn(E) -> Message + 'static + Send + Sync) -> Vec> { + pub fn into_menu_entries(self, _action: impl Fn(E) -> Message + 'static + Send + Sync) -> MenuListEntrySections { todo!() } diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index 9ecf016cf..a8e434a42 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -28,7 +28,9 @@ pub struct MenuBarMessageHandler { impl MessageHandler for MenuBarMessageHandler { fn process_message(&mut self, message: MenuBarMessage, responses: &mut VecDeque, _: ()) { match message { - MenuBarMessage::SendLayout => self.send_layout(responses, LayoutTarget::MenuBar), + MenuBarMessage::SendLayout => { + self.send_layout(responses, LayoutTarget::MenuBar); + } } } @@ -50,737 +52,693 @@ impl LayoutHolder for MenuBarMessageHandler { let reset_node_definitions_on_open = self.reset_node_definitions_on_open; let make_path_editable_is_allowed = self.make_path_editable_is_allowed; - let about = MenuBarEntry { - #[cfg(not(target_os = "macos"))] - label: "About Graphite…".into(), - #[cfg(target_os = "macos")] - label: "About Graphite".into(), - icon: Some("GraphiteLogo".into()), - action: MenuBarEntry::create_action(|_| DialogMessage::RequestAboutGraphiteDialog.into()), - ..MenuBarEntry::default() - }; - let preferences = MenuBarEntry { - label: "Preferences…".into(), - icon: Some("Settings".into()), - shortcut: action_keys!(DialogMessageDiscriminant::RequestPreferencesDialog), - action: MenuBarEntry::create_action(|_| DialogMessage::RequestPreferencesDialog.into()), - ..MenuBarEntry::default() - }; + let about = MenuListEntry::new("About Graphite…") + .label({ + #[cfg(not(target_os = "macos"))] + { + "About Graphite…" + } + #[cfg(target_os = "macos")] + { + "About Graphite" + } + }) + .icon("GraphiteLogo") + .on_commit(|_| DialogMessage::RequestAboutGraphiteDialog.into()); + let preferences = MenuListEntry::new("Preferences…") + .label("Preferences…") + .icon("Settings") + .shortcut_keys(action_keys!(DialogMessageDiscriminant::RequestPreferencesDialog)) + .on_commit(|_| DialogMessage::RequestPreferencesDialog.into()); - let menu_bar_entries = vec![ + let menu_bar_buttons = vec![ #[cfg(not(target_os = "macos"))] - MenuBarEntry { - icon: Some("GraphiteLogo".into()), - action: MenuBarEntry::create_action(|_| FrontendMessage::TriggerVisitLink { url: "https://graphite.rs".into() }.into()), - ..Default::default() - }, + TextButton::new("Graphite") + .label("") + .flush(true) + .icon(Some("GraphiteLogo".into())) + .on_commit(|_| FrontendMessage::TriggerVisitLink { url: "https://graphite.rs".into() }.into()) + .widget_holder(), #[cfg(target_os = "macos")] - MenuBarEntry::new_root( - "".into(), - false, - MenuBarEntryChildren(vec![ + TextButton::new("Graphite") + .label("") + .flush(true) + .menu_list_children(vec![ vec![about], vec![preferences], vec![ - MenuBarEntry { - label: "Hide Graphite".into(), - shortcut: action_keys!(AppWindowMessageDiscriminant::Hide), - action: MenuBarEntry::create_action(|_| AppWindowMessage::Hide.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Hide Others".into(), - shortcut: action_keys!(AppWindowMessageDiscriminant::HideOthers), - action: MenuBarEntry::create_action(|_| AppWindowMessage::HideOthers.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Show All".into(), - shortcut: action_keys!(AppWindowMessageDiscriminant::ShowAll), - action: MenuBarEntry::create_action(|_| AppWindowMessage::ShowAll.into()), - ..MenuBarEntry::default() - }, - ], - vec![MenuBarEntry { - label: "Quit Graphite".into(), - shortcut: action_keys!(AppWindowMessageDiscriminant::Close), - action: MenuBarEntry::create_action(|_| AppWindowMessage::Close.into()), - ..MenuBarEntry::default() - }], - ]), - ), - MenuBarEntry::new_root( - "File".into(), - false, - MenuBarEntryChildren(vec![ - vec![ - MenuBarEntry { - label: "New…".into(), - icon: Some("File".into()), - action: MenuBarEntry::create_action(|_| DialogMessage::RequestNewDocumentDialog.into()), - shortcut: action_keys!(DialogMessageDiscriminant::RequestNewDocumentDialog), - children: MenuBarEntryChildren::empty(), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Open…".into(), - icon: Some("Folder".into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::OpenDocument), - action: MenuBarEntry::create_action(|_| PortfolioMessage::OpenDocument.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Open Demo Artwork…".into(), - icon: Some("Image".into()), - action: MenuBarEntry::create_action(|_| DialogMessage::RequestDemoArtworkDialog.into()), - ..MenuBarEntry::default() - }, + MenuListEntry::new("Hide Graphite") + .label("Hide Graphite") + .shortcut_keys(action_keys!(AppWindowMessageDiscriminant::Hide)) + .on_commit(|_| AppWindowMessage::Hide.into()), + MenuListEntry::new("Hide Others") + .label("Hide Others") + .shortcut_keys(action_keys!(AppWindowMessageDiscriminant::HideOthers)) + .on_commit(|_| AppWindowMessage::HideOthers.into()), + MenuListEntry::new("Show All") + .label("Show All") + .shortcut_keys(action_keys!(AppWindowMessageDiscriminant::ShowAll)) + .on_commit(|_| AppWindowMessage::ShowAll.into()), ], vec![ - MenuBarEntry { - label: "Close".into(), - icon: Some("Close".into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation), - action: MenuBarEntry::create_action(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Close All".into(), - icon: Some("CloseAll".into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::CloseAllDocumentsWithConfirmation), - action: MenuBarEntry::create_action(|_| PortfolioMessage::CloseAllDocumentsWithConfirmation.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Quit Graphite") + .label("Quit Graphite") + .shortcut_keys(action_keys!(AppWindowMessageDiscriminant::Close)) + .on_commit(|_| AppWindowMessage::Close.into()), + ], + ]) + .widget_holder(), + TextButton::new("File") + .label("File") + .flush(true) + .menu_list_children(vec![ + vec![ + MenuListEntry::new("New…") + .label("New…") + .icon("File") + .on_commit(|_| DialogMessage::RequestNewDocumentDialog.into()) + .shortcut_keys(action_keys!(DialogMessageDiscriminant::RequestNewDocumentDialog)), + MenuListEntry::new("Open…") + .label("Open…") + .icon("Folder") + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::OpenDocument)) + .on_commit(|_| PortfolioMessage::OpenDocument.into()), + MenuListEntry::new("Open Demo Artwork…") + .label("Open Demo Artwork…") + .icon("Image") + .on_commit(|_| DialogMessage::RequestDemoArtworkDialog.into()), ], vec![ - MenuBarEntry { - label: "Save".into(), - icon: Some("Save".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocument), - action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocument.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Close") + .label("Close") + .icon("Close") + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation)) + .on_commit(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()) + .disabled(no_active_document), + MenuListEntry::new("Close All") + .label("Close All") + .icon("CloseAll") + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::CloseAllDocumentsWithConfirmation)) + .on_commit(|_| PortfolioMessage::CloseAllDocumentsWithConfirmation.into()) + .disabled(no_active_document), + ], + vec![ + MenuListEntry::new("Save") + .label("Save") + .icon("Save") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SaveDocument)) + .on_commit(|_| DocumentMessage::SaveDocument.into()) + .disabled(no_active_document), #[cfg(not(target_family = "wasm"))] - MenuBarEntry { - label: "Save As…".into(), - icon: Some("Save".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SaveDocumentAs), - action: MenuBarEntry::create_action(|_| DocumentMessage::SaveDocumentAs.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Save As…") + .label("Save As…") + .icon("Save") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SaveDocumentAs)) + .on_commit(|_| DocumentMessage::SaveDocumentAs.into()) + .disabled(no_active_document), ], vec![ - MenuBarEntry { - label: "Import…".into(), - icon: Some("FileImport".into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::Import), - action: MenuBarEntry::create_action(|_| PortfolioMessage::Import.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Export…".into(), - icon: Some("FileExport".into()), - shortcut: action_keys!(DialogMessageDiscriminant::RequestExportDialog), - action: MenuBarEntry::create_action(|_| DialogMessage::RequestExportDialog.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Import…") + .label("Import…") + .icon("FileImport") + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::Import)) + .on_commit(|_| PortfolioMessage::Import.into()), + MenuListEntry::new("Export…") + .label("Export…") + .icon("FileExport") + .shortcut_keys(action_keys!(DialogMessageDiscriminant::RequestExportDialog)) + .on_commit(|_| DialogMessage::RequestExportDialog.into()) + .disabled(no_active_document), ], #[cfg(not(target_os = "macos"))] vec![preferences], - ]), - ), - MenuBarEntry::new_root( - "Edit".into(), - false, - MenuBarEntryChildren(vec![ + ]) + .widget_holder(), + TextButton::new("Edit") + .label("Edit") + .flush(true) + .menu_list_children(vec![ vec![ - MenuBarEntry { - label: "Undo".into(), - icon: Some("HistoryUndo".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::Undo), - action: MenuBarEntry::create_action(|_| DocumentMessage::Undo.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Redo".into(), - icon: Some("HistoryRedo".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::Redo), - action: MenuBarEntry::create_action(|_| DocumentMessage::Redo.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Undo") + .label("Undo") + .icon("HistoryUndo") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::Undo)) + .on_commit(|_| DocumentMessage::Undo.into()) + .disabled(no_active_document), + MenuListEntry::new("Redo") + .label("Redo") + .icon("HistoryRedo") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::Redo)) + .on_commit(|_| DocumentMessage::Redo.into()) + .disabled(no_active_document), ], vec![ - MenuBarEntry { - label: "Cut".into(), - icon: Some("Cut".into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::Cut), - action: MenuBarEntry::create_action(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Copy".into(), - icon: Some("Copy".into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::Copy), - action: MenuBarEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Paste".into(), - icon: Some("Paste".into()), - shortcut: action_keys!(FrontendMessageDiscriminant::TriggerPaste), - action: MenuBarEntry::create_action(|_| FrontendMessage::TriggerPaste.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Cut") + .label("Cut") + .icon("Cut") + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::Cut)) + .on_commit(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Copy") + .label("Copy") + .icon("Copy") + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::Copy)) + .on_commit(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Paste") + .label("Paste") + .icon("Paste") + .shortcut_keys(action_keys!(FrontendMessageDiscriminant::TriggerPaste)) + .on_commit(|_| FrontendMessage::TriggerPaste.into()) + .disabled(no_active_document), ], vec![ - MenuBarEntry { - label: "Duplicate".into(), - icon: Some("Copy".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::DuplicateSelectedLayers), - action: MenuBarEntry::create_action(|_| DocumentMessage::DuplicateSelectedLayers.into()), - disabled: no_active_document || !has_selected_nodes, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Delete".into(), - icon: Some("Trash".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers), - action: MenuBarEntry::create_action(|_| DocumentMessage::DeleteSelectedLayers.into()), - disabled: no_active_document || !has_selected_nodes, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Duplicate") + .label("Duplicate") + .icon("Copy") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::DuplicateSelectedLayers)) + .on_commit(|_| DocumentMessage::DuplicateSelectedLayers.into()) + .disabled(no_active_document || !has_selected_nodes), + MenuListEntry::new("Delete") + .label("Delete") + .icon("Trash") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::DeleteSelectedLayers)) + .on_commit(|_| DocumentMessage::DeleteSelectedLayers.into()) + .disabled(no_active_document || !has_selected_nodes), ], - vec![MenuBarEntry { - label: "Convert to Infinite Canvas".into(), - icon: Some("Artboard".into()), - action: MenuBarEntry::create_action(|_| DocumentMessage::RemoveArtboards.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }], - ]), - ), - MenuBarEntry::new_root( - "Layer".into(), - no_active_document, - MenuBarEntryChildren(vec![ - vec![MenuBarEntry { - label: "New".into(), - icon: Some("NewLayer".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder), - action: MenuBarEntry::create_action(|_| DocumentMessage::CreateEmptyFolder.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }], vec![ - MenuBarEntry { - label: "Group".into(), - icon: Some("Folder".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers), - action: MenuBarEntry::create_action(|_| { + MenuListEntry::new("Convert to Infinite Canvas") + .label("Convert to Infinite Canvas") + .icon("Artboard") + .on_commit(|_| DocumentMessage::RemoveArtboards.into()) + .disabled(no_active_document), + ], + ]) + .widget_holder(), + TextButton::new("Layer") + .label("Layer") + .flush(true) + .disabled(no_active_document) + .menu_list_children(vec![ + vec![ + MenuListEntry::new("New") + .label("New") + .icon("NewLayer") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::CreateEmptyFolder)) + .on_commit(|_| DocumentMessage::CreateEmptyFolder.into()) + .disabled(no_active_document), + ], + vec![ + MenuListEntry::new("Group") + .label("Group") + .icon("Folder") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers)) + .on_commit(|_| { DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer, } .into() - }), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Ungroup".into(), - icon: Some("FolderOpen".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::UngroupSelectedLayers), - action: MenuBarEntry::create_action(|_| DocumentMessage::UngroupSelectedLayers.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, + }) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Ungroup") + .label("Ungroup") + .icon("FolderOpen") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::UngroupSelectedLayers)) + .on_commit(|_| DocumentMessage::UngroupSelectedLayers.into()) + .disabled(no_active_document || !has_selected_layers), ], vec![ - MenuBarEntry { - label: "Hide/Show".into(), - icon: Some("EyeHide".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::ToggleSelectedVisibility), - action: MenuBarEntry::create_action(|_| DocumentMessage::ToggleSelectedVisibility.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Lock/Unlock".into(), - icon: Some("PadlockLocked".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::ToggleSelectedLocked), - action: MenuBarEntry::create_action(|_| DocumentMessage::ToggleSelectedLocked.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Hide/Show") + .label("Hide/Show") + .icon("EyeHide") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::ToggleSelectedVisibility)) + .on_commit(|_| DocumentMessage::ToggleSelectedVisibility.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Lock/Unlock") + .label("Lock/Unlock") + .icon("PadlockLocked") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::ToggleSelectedLocked)) + .on_commit(|_| DocumentMessage::ToggleSelectedLocked.into()) + .disabled(no_active_document || !has_selected_layers), ], vec![ - MenuBarEntry { - label: "Grab".into(), - icon: Some("TransformationGrab".into()), - shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginGrab), - action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginGrab.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Rotate".into(), - icon: Some("TransformationRotate".into()), - shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginRotate), - action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginRotate.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Scale".into(), - icon: Some("TransformationScale".into()), - shortcut: action_keys!(TransformLayerMessageDiscriminant::BeginScale), - action: MenuBarEntry::create_action(|_| TransformLayerMessage::BeginScale.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Grab") + .label("Grab") + .icon("TransformationGrab") + .shortcut_keys(action_keys!(TransformLayerMessageDiscriminant::BeginGrab)) + .on_commit(|_| TransformLayerMessage::BeginGrab.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Rotate") + .label("Rotate") + .icon("TransformationRotate") + .shortcut_keys(action_keys!(TransformLayerMessageDiscriminant::BeginRotate)) + .on_commit(|_| TransformLayerMessage::BeginRotate.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Scale") + .label("Scale") + .icon("TransformationScale") + .shortcut_keys(action_keys!(TransformLayerMessageDiscriminant::BeginScale)) + .on_commit(|_| TransformLayerMessage::BeginScale.into()) + .disabled(no_active_document || !has_selected_layers), ], vec![ - MenuBarEntry { - label: "Arrange".into(), - icon: Some("StackHollow".into()), - action: MenuBarEntry::no_action(), - disabled: no_active_document || !has_selected_layers, - children: MenuBarEntryChildren(vec![ + MenuListEntry::new("Arrange") + .label("Arrange") + .icon("StackHollow") + .disabled(no_active_document || !has_selected_layers) + .children(vec![ vec![ - MenuBarEntry { - label: "Raise To Front".into(), - icon: Some("Stack".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersRaiseToFront.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Raise".into(), - icon: Some("StackRaise".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersRaise), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersRaise.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Lower".into(), - icon: Some("StackLower".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLower), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersLower.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Lower to Back".into(), - icon: Some("StackBottom".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectedLayersLowerToBack), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersLowerToBack.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Raise To Front") + .label("Raise To Front") + .icon("Stack") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront)) + .on_commit(|_| DocumentMessage::SelectedLayersRaiseToFront.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Raise") + .label("Raise") + .icon("StackRaise") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectedLayersRaise)) + .on_commit(|_| DocumentMessage::SelectedLayersRaise.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Lower") + .label("Lower") + .icon("StackLower") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectedLayersLower)) + .on_commit(|_| DocumentMessage::SelectedLayersLower.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Lower to Back") + .label("Lower to Back") + .icon("StackBottom") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectedLayersLowerToBack)) + .on_commit(|_| DocumentMessage::SelectedLayersLowerToBack.into()) + .disabled(no_active_document || !has_selected_layers), + ], + vec![ + MenuListEntry::new("Reverse") + .label("Reverse") + .icon("StackReverse") + .on_commit(|_| DocumentMessage::SelectedLayersReverse.into()) + .disabled(no_active_document || !has_selected_layers), ], - vec![MenuBarEntry { - label: "Reverse".into(), - icon: Some("StackReverse".into()), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectedLayersReverse.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }], ]), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Align".into(), - icon: Some("AlignVerticalCenter".into()), - action: MenuBarEntry::no_action(), - disabled: no_active_document || !has_selected_layers, - children: MenuBarEntryChildren({ - let choices = [ - [ - (AlignAxis::X, AlignAggregate::Min, "AlignLeft", "Align Left"), - (AlignAxis::X, AlignAggregate::Center, "AlignHorizontalCenter", "Align Horizontal Center"), - (AlignAxis::X, AlignAggregate::Max, "AlignRight", "Align Right"), - ], - [ - (AlignAxis::Y, AlignAggregate::Min, "AlignTop", "Align Top"), - (AlignAxis::Y, AlignAggregate::Center, "AlignVerticalCenter", "Align Vertical Center"), - (AlignAxis::Y, AlignAggregate::Max, "AlignBottom", "Align Bottom"), - ], - ]; - - choices - .into_iter() - .map(|section| { - section - .into_iter() - .map(|(axis, aggregate, icon, name)| MenuBarEntry { - label: name.into(), - icon: Some(icon.into()), - action: MenuBarEntry::create_action(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }) - .collect() + MenuListEntry::new("Align") + .label("Align") + .icon("AlignVerticalCenter") + .disabled(no_active_document || !has_selected_layers) + .children(vec![ + vec![ + MenuListEntry::new("Align Left") + .label("Align Left") + .icon("AlignLeft") + .on_commit(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::X, + aggregate: AlignAggregate::Min, + } + .into() + }) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Align Horizontal Center") + .label("Align Horizontal Center") + .icon("AlignHorizontalCenter") + .on_commit(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::X, + aggregate: AlignAggregate::Center, + } + .into() + }) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Align Right") + .label("Align Right") + .icon("AlignRight") + .on_commit(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::X, + aggregate: AlignAggregate::Max, + } + .into() + }) + .disabled(no_active_document || !has_selected_layers), + ], + vec![ + MenuListEntry::new("Align Top") + .label("Align Top") + .icon("AlignTop") + .on_commit(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::Y, + aggregate: AlignAggregate::Min, + } + .into() + }) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Align Vertical Center") + .label("Align Vertical Center") + .icon("AlignVerticalCenter") + .on_commit(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::Y, + aggregate: AlignAggregate::Center, + } + .into() + }) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Align Bottom") + .label("Align Bottom") + .icon("AlignBottom") + .on_commit(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::Y, + aggregate: AlignAggregate::Max, + } + .into() + }) + .disabled(no_active_document || !has_selected_layers), + ], + ]), + MenuListEntry::new("Flip") + .label("Flip") + .icon("FlipVertical") + .disabled(no_active_document || !has_selected_layers) + .children(vec![vec![ + MenuListEntry::new("Flip Horizontal") + .label("Flip Horizontal") + .icon("FlipHorizontal") + .on_commit(|_| DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::X }.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Flip Vertical") + .label("Flip Vertical") + .icon("FlipVertical") + .on_commit(|_| DocumentMessage::FlipSelectedLayers { flip_axis: FlipAxis::Y }.into()) + .disabled(no_active_document || !has_selected_layers), + ]]), + MenuListEntry::new("Turn") + .label("Turn") + .icon("TurnPositive90") + .disabled(no_active_document || !has_selected_layers) + .children(vec![vec![ + MenuListEntry::new("Turn -90°") + .label("Turn -90°") + .icon("TurnNegative90") + .on_commit(|_| DocumentMessage::RotateSelectedLayers { degrees: -90. }.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Turn 90°") + .label("Turn 90°") + .icon("TurnPositive90") + .on_commit(|_| DocumentMessage::RotateSelectedLayers { degrees: 90. }.into()) + .disabled(no_active_document || !has_selected_layers), + ]]), + MenuListEntry::new("Boolean") + .label("Boolean") + .icon("BooleanSubtractFront") + .disabled(no_active_document || !has_selected_layers) + .children(vec![vec![ + MenuListEntry::new("Union") + .label("Union") + .icon("BooleanUnion") + .on_commit(|_| { + let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Union); + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() }) - .collect() - }), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Flip".into(), - icon: Some("FlipVertical".into()), - action: MenuBarEntry::no_action(), - disabled: no_active_document || !has_selected_layers, - children: MenuBarEntryChildren(vec![{ - [(FlipAxis::X, "FlipHorizontal", "Flip Horizontal"), (FlipAxis::Y, "FlipVertical", "Flip Vertical")] - .into_iter() - .map(|(flip_axis, icon, name)| MenuBarEntry { - label: name.into(), - icon: Some(icon.into()), - action: MenuBarEntry::create_action(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Subtract Front") + .label("Subtract Front") + .icon("BooleanSubtractFront") + .on_commit(|_| { + let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::SubtractFront); + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() }) - .collect() - }]), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Turn".into(), - icon: Some("TurnPositive90".into()), - action: MenuBarEntry::no_action(), - disabled: no_active_document || !has_selected_layers, - children: MenuBarEntryChildren(vec![{ - [(-90., "TurnNegative90", "Turn -90°"), (90., "TurnPositive90", "Turn 90°")] - .into_iter() - .map(|(degrees, icon, name)| MenuBarEntry { - label: name.into(), - icon: Some(icon.into()), - action: MenuBarEntry::create_action(move |_| DocumentMessage::RotateSelectedLayers { degrees }.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Subtract Back") + .label("Subtract Back") + .icon("BooleanSubtractBack") + .on_commit(|_| { + let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::SubtractBack); + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() }) - .collect() - }]), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Boolean".into(), - icon: Some("BooleanSubtractFront".into()), - action: MenuBarEntry::no_action(), - disabled: no_active_document || !has_selected_layers, - children: MenuBarEntryChildren(vec![{ - let list = ::list(); - list.iter() - .flat_map(|i| i.iter()) - .map(move |(operation, info)| MenuBarEntry { - label: info.label.to_string(), - icon: info.icon.as_ref().map(|i| i.to_string()), - action: MenuBarEntry::create_action(move |_| { - let group_folder_type = GroupFolderType::BooleanOperation(*operation); - DocumentMessage::GroupSelectedLayers { group_folder_type }.into() - }), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Intersect") + .label("Intersect") + .icon("BooleanIntersect") + .on_commit(|_| { + let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Intersect); + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() }) - .collect() - }]), - ..MenuBarEntry::default() - }, - ], - vec![MenuBarEntry { - label: "Make Path Editable".into(), - icon: Some("NodeShape".into()), - shortcut: None, - action: MenuBarEntry::create_action(|_| NodeGraphMessage::AddPathNode.into()), - disabled: !make_path_editable_is_allowed, - ..MenuBarEntry::default() - }], - ]), - ), - MenuBarEntry::new_root( - "Select".into(), - no_active_document, - MenuBarEntryChildren(vec![ - vec![ - MenuBarEntry { - label: "Select All".into(), - icon: Some("SelectAll".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectAllLayers), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectAllLayers.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Deselect All".into(), - icon: Some("DeselectAll".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::DeselectAllLayers), - action: MenuBarEntry::create_action(|_| DocumentMessage::DeselectAllLayers.into()), - disabled: no_active_document || !has_selected_nodes, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Select Parent".into(), - icon: Some("SelectParent".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectParentLayer), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectParentLayer.into()), - disabled: no_active_document || !has_selected_nodes, - ..MenuBarEntry::default() - }, + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Difference") + .label("Difference") + .icon("BooleanDifference") + .on_commit(|_| { + let group_folder_type = GroupFolderType::BooleanOperation(BooleanOperation::Difference); + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() + }) + .disabled(no_active_document || !has_selected_layers), + ]]), ], vec![ - MenuBarEntry { - label: "Previous Selection".into(), - icon: Some("HistoryUndo".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectionStepBack), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectionStepBack.into()), - disabled: !has_selection_history.0, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Next Selection".into(), - icon: Some("HistoryRedo".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::SelectionStepForward), - action: MenuBarEntry::create_action(|_| DocumentMessage::SelectionStepForward.into()), - disabled: !has_selection_history.1, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Make Path Editable") + .label("Make Path Editable") + .icon("NodeShape") + .on_commit(|_| NodeGraphMessage::AddPathNode.into()) + .disabled(!make_path_editable_is_allowed), ], - ]), - ), - MenuBarEntry::new_root( - "View".into(), - no_active_document, - MenuBarEntryChildren(vec![ + ]) + .widget_holder(), + TextButton::new("Select") + .label("Select") + .flush(true) + .disabled(no_active_document) + .menu_list_children(vec![ vec![ - MenuBarEntry { - label: "Tilt".into(), - icon: Some("Tilt".into()), - shortcut: action_keys!(NavigationMessageDiscriminant::BeginCanvasTilt), - action: MenuBarEntry::create_action(|_| NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu: true }.into()), - disabled: no_active_document || node_graph_open, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Reset Tilt".into(), - icon: Some("TiltReset".into()), - shortcut: action_keys!(NavigationMessageDiscriminant::CanvasTiltSet), - action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasTiltSet { angle_radians: 0.into() }.into()), - disabled: no_active_document || node_graph_open || !self.canvas_tilted, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Select All") + .label("Select All") + .icon("SelectAll") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectAllLayers)) + .on_commit(|_| DocumentMessage::SelectAllLayers.into()) + .disabled(no_active_document), + MenuListEntry::new("Deselect All") + .label("Deselect All") + .icon("DeselectAll") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::DeselectAllLayers)) + .on_commit(|_| DocumentMessage::DeselectAllLayers.into()) + .disabled(no_active_document || !has_selected_nodes), + MenuListEntry::new("Select Parent") + .label("Select Parent") + .icon("SelectParent") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectParentLayer)) + .on_commit(|_| DocumentMessage::SelectParentLayer.into()) + .disabled(no_active_document || !has_selected_nodes), ], vec![ - MenuBarEntry { - label: "Zoom In".into(), - icon: Some("ZoomIn".into()), - shortcut: action_keys!(NavigationMessageDiscriminant::CanvasZoomIncrease), - action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Zoom Out".into(), - icon: Some("ZoomOut".into()), - shortcut: action_keys!(NavigationMessageDiscriminant::CanvasZoomDecrease), - action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Zoom to Selection".into(), - icon: Some("FrameSelected".into()), - shortcut: action_keys!(NavigationMessageDiscriminant::FitViewportToSelection), - action: MenuBarEntry::create_action(|_| NavigationMessage::FitViewportToSelection.into()), - disabled: no_active_document || !has_selected_layers, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Zoom to Fit".into(), - icon: Some("FrameAll".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasToFitAll), - action: MenuBarEntry::create_action(|_| DocumentMessage::ZoomCanvasToFitAll.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Zoom to 100%".into(), - icon: Some("Zoom1x".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo100Percent), - action: MenuBarEntry::create_action(|_| DocumentMessage::ZoomCanvasTo100Percent.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Zoom to 200%".into(), - icon: Some("Zoom2x".into()), - shortcut: action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo200Percent), - action: MenuBarEntry::create_action(|_| DocumentMessage::ZoomCanvasTo200Percent.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }, + MenuListEntry::new("Previous Selection") + .label("Previous Selection") + .icon("HistoryUndo") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectionStepBack)) + .on_commit(|_| DocumentMessage::SelectionStepBack.into()) + .disabled(!has_selection_history.0), + MenuListEntry::new("Next Selection") + .label("Next Selection") + .icon("HistoryRedo") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::SelectionStepForward)) + .on_commit(|_| DocumentMessage::SelectionStepForward.into()) + .disabled(!has_selection_history.1), ], - vec![MenuBarEntry { - label: "Flip".into(), - icon: Some(if self.canvas_flipped { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - shortcut: action_keys!(NavigationMessageDiscriminant::CanvasFlip), - action: MenuBarEntry::create_action(|_| NavigationMessage::CanvasFlip.into()), - disabled: no_active_document || node_graph_open, - ..MenuBarEntry::default() - }], - vec![MenuBarEntry { - label: "Rulers".into(), - icon: Some(if self.rulers_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleRulers), - action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleRulers.into()), - disabled: no_active_document, - ..MenuBarEntry::default() - }], - ]), - ), - MenuBarEntry::new_root( - "Window".into(), - false, - MenuBarEntryChildren(vec![ + ]) + .widget_holder(), + TextButton::new("View") + .label("View") + .flush(true) + .disabled(no_active_document) + .menu_list_children(vec![ vec![ - MenuBarEntry { - label: "Properties".into(), - icon: Some(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen), - action: MenuBarEntry::create_action(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Layers".into(), - icon: Some(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen), - action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleLayersPanelOpen.into()), - ..MenuBarEntry::default() - }, + MenuListEntry::new("Tilt") + .label("Tilt") + .icon("Tilt") + .shortcut_keys(action_keys!(NavigationMessageDiscriminant::BeginCanvasTilt)) + .on_commit(|_| NavigationMessage::BeginCanvasTilt { was_dispatched_from_menu: true }.into()) + .disabled(no_active_document || node_graph_open), + MenuListEntry::new("Reset Tilt") + .label("Reset Tilt") + .icon("TiltReset") + .shortcut_keys(action_keys!(NavigationMessageDiscriminant::CanvasTiltSet)) + .on_commit(|_| NavigationMessage::CanvasTiltSet { angle_radians: 0.into() }.into()) + .disabled(no_active_document || node_graph_open || !self.canvas_tilted), ], - vec![MenuBarEntry { - label: "Data".into(), - icon: Some(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - shortcut: action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen), - action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleDataPanelOpen.into()), - ..MenuBarEntry::default() - }], - ]), - ), - MenuBarEntry::new_root( - "Help".into(), - false, - MenuBarEntryChildren(vec![ + vec![ + MenuListEntry::new("Zoom In") + .label("Zoom In") + .icon("ZoomIn") + .shortcut_keys(action_keys!(NavigationMessageDiscriminant::CanvasZoomIncrease)) + .on_commit(|_| NavigationMessage::CanvasZoomIncrease { center_on_mouse: false }.into()) + .disabled(no_active_document), + MenuListEntry::new("Zoom Out") + .label("Zoom Out") + .icon("ZoomOut") + .shortcut_keys(action_keys!(NavigationMessageDiscriminant::CanvasZoomDecrease)) + .on_commit(|_| NavigationMessage::CanvasZoomDecrease { center_on_mouse: false }.into()) + .disabled(no_active_document), + MenuListEntry::new("Zoom to Selection") + .label("Zoom to Selection") + .icon("FrameSelected") + .shortcut_keys(action_keys!(NavigationMessageDiscriminant::FitViewportToSelection)) + .on_commit(|_| NavigationMessage::FitViewportToSelection.into()) + .disabled(no_active_document || !has_selected_layers), + MenuListEntry::new("Zoom to Fit") + .label("Zoom to Fit") + .icon("FrameAll") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::ZoomCanvasToFitAll)) + .on_commit(|_| DocumentMessage::ZoomCanvasToFitAll.into()) + .disabled(no_active_document), + MenuListEntry::new("Zoom to 100%") + .label("Zoom to 100%") + .icon("Zoom1x") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo100Percent)) + .on_commit(|_| DocumentMessage::ZoomCanvasTo100Percent.into()) + .disabled(no_active_document), + MenuListEntry::new("Zoom to 200%") + .label("Zoom to 200%") + .icon("Zoom2x") + .shortcut_keys(action_keys!(DocumentMessageDiscriminant::ZoomCanvasTo200Percent)) + .on_commit(|_| DocumentMessage::ZoomCanvasTo200Percent.into()) + .disabled(no_active_document), + ], + vec![ + MenuListEntry::new("Flip") + .label("Flip") + .icon(if self.canvas_flipped { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .shortcut_keys(action_keys!(NavigationMessageDiscriminant::CanvasFlip)) + .on_commit(|_| NavigationMessage::CanvasFlip.into()) + .disabled(no_active_document || node_graph_open), + ], + vec![ + MenuListEntry::new("Rulers") + .label("Rulers") + .icon(if self.rulers_visible { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::ToggleRulers)) + .on_commit(|_| PortfolioMessage::ToggleRulers.into()) + .disabled(no_active_document), + ], + ]) + .widget_holder(), + TextButton::new("Window") + .label("Window") + .flush(true) + .menu_list_children(vec![ + vec![ + MenuListEntry::new("Properties") + .label("Properties") + .icon(if self.properties_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::TogglePropertiesPanelOpen)) + .on_commit(|_| PortfolioMessage::TogglePropertiesPanelOpen.into()), + MenuListEntry::new("Layers") + .label("Layers") + .icon(if self.layers_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::ToggleLayersPanelOpen)) + .on_commit(|_| PortfolioMessage::ToggleLayersPanelOpen.into()), + ], + vec![ + MenuListEntry::new("Data") + .label("Data") + .icon(if self.data_panel_open { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .shortcut_keys(action_keys!(PortfolioMessageDiscriminant::ToggleDataPanelOpen)) + .on_commit(|_| PortfolioMessage::ToggleDataPanelOpen.into()), + ], + ]) + .widget_holder(), + TextButton::new("Help") + .label("Help") + .flush(true) + .menu_list_children(vec![ #[cfg(not(target_os = "macos"))] vec![about], vec![ - MenuBarEntry { - label: "Donate to Graphite".into(), - icon: Some("Heart".into()), - action: MenuBarEntry::create_action(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://graphite.rs/donate/".into(), - } - .into() - }), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "User Manual".into(), - icon: Some("UserManual".into()), - action: MenuBarEntry::create_action(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://graphite.rs/learn/".into(), - } - .into() - }), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Report a Bug".into(), - icon: Some("Bug".into()), - action: MenuBarEntry::create_action(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(), - } - .into() - }), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Visit on GitHub".into(), - icon: Some("Website".into()), - action: MenuBarEntry::create_action(|_| { - FrontendMessage::TriggerVisitLink { - url: "https://github.com/GraphiteEditor/Graphite".into(), - } - .into() - }), - ..MenuBarEntry::default() - }, + MenuListEntry::new("Donate to Graphite").label("Donate to Graphite").icon("Heart").on_commit(|_| { + FrontendMessage::TriggerVisitLink { + url: "https://graphite.rs/donate/".into(), + } + .into() + }), + MenuListEntry::new("User Manual").label("User Manual").icon("UserManual").on_commit(|_| { + FrontendMessage::TriggerVisitLink { + url: "https://graphite.rs/learn/".into(), + } + .into() + }), + MenuListEntry::new("Report a Bug").label("Report a Bug").icon("Bug").on_commit(|_| { + FrontendMessage::TriggerVisitLink { + url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(), + } + .into() + }), + MenuListEntry::new("Visit on GitHub").label("Visit on GitHub").icon("Website").on_commit(|_| { + FrontendMessage::TriggerVisitLink { + url: "https://github.com/GraphiteEditor/Graphite".into(), + } + .into() + }), ], - vec![MenuBarEntry { - label: "Developer Debug".into(), - icon: Some("Code".into()), - action: MenuBarEntry::no_action(), - children: MenuBarEntryChildren(vec![ - vec![MenuBarEntry { - label: "Reset Nodes to Definitions on Open".into(), - icon: Some(if reset_node_definitions_on_open { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - action: MenuBarEntry::create_action(|_| PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen.into()), - ..MenuBarEntry::default() - }], - vec![ - MenuBarEntry { - label: "Print Trace Logs".into(), - icon: Some(if log::max_level() == log::LevelFilter::Trace { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), - action: MenuBarEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Print Messages: Off".into(), - icon: message_logging_verbosity_off.then_some("SmallDot".into()), - shortcut: action_keys!(DebugMessageDiscriminant::MessageOff), - action: MenuBarEntry::create_action(|_| DebugMessage::MessageOff.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Print Messages: Only Names".into(), - icon: message_logging_verbosity_names.then_some("SmallDot".into()), - shortcut: action_keys!(DebugMessageDiscriminant::MessageNames), - action: MenuBarEntry::create_action(|_| DebugMessage::MessageNames.into()), - ..MenuBarEntry::default() - }, - MenuBarEntry { - label: "Print Messages: Full Contents".into(), - icon: message_logging_verbosity_contents.then_some("SmallDot".into()), - shortcut: action_keys!(DebugMessageDiscriminant::MessageContents), - action: MenuBarEntry::create_action(|_| DebugMessage::MessageContents.into()), - ..MenuBarEntry::default() - }, - ], - vec![MenuBarEntry { - label: "Trigger a Crash".into(), - icon: Some("Warning".into()), - action: MenuBarEntry::create_action(|_| panic!()), - ..MenuBarEntry::default() - }], - ]), - ..MenuBarEntry::default() - }], - ]), - ), + vec![MenuListEntry::new("Developer Debug").label("Developer Debug").icon("Code").children(vec![ + vec![ + MenuListEntry::new("Reset Nodes to Definitions on Open") + .label("Reset Nodes to Definitions on Open") + .icon(if reset_node_definitions_on_open { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .on_commit(|_| PortfolioMessage::ToggleResetNodesToDefinitionsOnOpen.into()), + ], + vec![ + MenuListEntry::new("Print Trace Logs") + .label("Print Trace Logs") + .icon(if log::max_level() == log::LevelFilter::Trace { "CheckboxChecked" } else { "CheckboxUnchecked" }) + .on_commit(|_| DebugMessage::ToggleTraceLogs.into()), + MenuListEntry::new("Print Messages: Off") + .label("Print Messages: Off") + .icon(message_logging_verbosity_off.then_some({ + #[cfg(not(target_os = "macos"))] + { + "SmallDot".to_string() + } + #[cfg(target_os = "macos")] + { + "CheckboxChecked".to_string() + } + }).unwrap_or_default()) + .shortcut_keys(action_keys!(DebugMessageDiscriminant::MessageOff)) + .on_commit(|_| DebugMessage::MessageOff.into()), + MenuListEntry::new("Print Messages: Only Names") + .label("Print Messages: Only Names") + .icon(message_logging_verbosity_names.then_some({ + #[cfg(not(target_os = "macos"))] + { + "SmallDot".to_string() + } + #[cfg(target_os = "macos")] + { + "CheckboxChecked".to_string() + } + }).unwrap_or_default()) + .shortcut_keys(action_keys!(DebugMessageDiscriminant::MessageNames)) + .on_commit(|_| DebugMessage::MessageNames.into()), + MenuListEntry::new("Print Messages: Full Contents") + .label("Print Messages: Full Contents") + .icon(message_logging_verbosity_contents.then_some({ + #[cfg(not(target_os = "macos"))] + { + "SmallDot".to_string() + } + #[cfg(target_os = "macos")] + { + "CheckboxChecked".to_string() + } + }).unwrap_or_default()) + .shortcut_keys(action_keys!(DebugMessageDiscriminant::MessageContents)) + .on_commit(|_| DebugMessage::MessageContents.into()), + ], + vec![MenuListEntry::new("Trigger a Crash").label("Trigger a Crash").icon("Warning").on_commit(|_| panic!())], + ])], + ]) + .widget_holder(), ]; - Layout::MenuLayout(MenuLayout::new(menu_bar_entries)) + + Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets: menu_bar_buttons }])) } } diff --git a/frontend/src/components/floating-menus/MenuList.svelte b/frontend/src/components/floating-menus/MenuList.svelte index edfc81fd3..1c6bada74 100644 --- a/frontend/src/components/floating-menus/MenuList.svelte +++ b/frontend/src/components/floating-menus/MenuList.svelte @@ -19,8 +19,16 @@ let scroller: LayoutCol | undefined; let searchTextInput: TextInput | undefined; - const dispatch = createEventDispatcher<{ open: boolean; activeEntry: MenuListEntry; hoverInEntry: MenuListEntry; hoverOutEntry: undefined; naturalWidth: number }>(); + const dispatch = createEventDispatcher<{ + open: boolean; + activeEntry: MenuListEntry; + selectedEntryValuePath: string[]; + hoverInEntry: MenuListEntry; + hoverOutEntry: undefined; + naturalWidth: number; + }>(); + export let parentsValuePath: string[] = []; export let entries: MenuListEntry[][]; export let activeEntry: MenuListEntry | undefined = undefined; export let open: boolean; @@ -30,9 +38,6 @@ export let interactive = false; export let scrollableY = false; export let virtualScrollingEntryHeight = 0; - export let tooltipLabel: string | undefined = undefined; - export let tooltipDescription: string | undefined = undefined; - export let tooltipShortcut: string | undefined = undefined; // Keep the child references outside of the entries array so as to avoid infinite recursion. let childReferences: MenuList[][] = []; @@ -149,11 +154,9 @@ } function onEntryClick(menuListEntry: MenuListEntry) { - // Call the action if available - if (menuListEntry.action) menuListEntry.action(); - // Notify the parent about the clicked entry as the new active entry dispatch("activeEntry", menuListEntry); + dispatch("selectedEntryValuePath", [...parentsValuePath, menuListEntry.value]); // Close the containing menu let childReference = getChildReference(menuListEntry); @@ -425,9 +428,9 @@ class="row" classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }} styles={{ height: virtualScrollingEntryHeight || "20px" }} - {tooltipLabel} - {tooltipDescription} - {tooltipShortcut} + tooltipLabel={entry.tooltipLabel} + tooltipDescription={entry.tooltipDescription} + tooltipShortcut={entry.tooltipShortcut} on:click={() => !entry.disabled && onEntryClick(entry)} on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)} on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)} @@ -444,8 +447,8 @@ - {#if entry.shortcut?.keys.length} - + {#if entry.shortcutKeys?.keys.length} + {/if} {#if entry.children?.length} @@ -462,6 +465,8 @@ // See explanation at . dispatch("naturalWidth", detail); }} + on:selectedEntryValuePath={({ detail }) => dispatch("selectedEntryValuePath", detail)} + parentsValuePath={[...parentsValuePath, entry.value]} open={getChildReference(entry)?.open || false} direction="TopRight" entries={entry.children} diff --git a/frontend/src/components/floating-menus/NodeCatalog.svelte b/frontend/src/components/floating-menus/NodeCatalog.svelte index c1d72713b..fae48136c 100644 --- a/frontend/src/components/floating-menus/NodeCatalog.svelte +++ b/frontend/src/components/floating-menus/NodeCatalog.svelte @@ -4,6 +4,7 @@ import type { FrontendNodeType } from "@graphite/messages"; import type { NodeGraphState } from "@graphite/state-providers/node-graph"; + import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte"; import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; @@ -109,8 +110,8 @@ }); -
- (searchTerm = detail)} bind:this={nodeSearchInput} /> + + (searchTerm = detail)} bind:this={nodeSearchInput} />
{#each nodeCategories as nodeCategory}
@@ -131,15 +132,12 @@ No search results {/each}
-
+