From f39d6bf00c62a5c0d93530c6bd92f42c69a404e7 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Wed, 3 Aug 2022 14:12:28 -0700 Subject: [PATCH] Add full support for Mac-specific keyboard layouts (#736) * IPP for Mac, flawed initial experiments * Cleanup and progress, but not compiling yet * Fix error and rename nonmac -> standard * Extentd ipp macros to accomodate mac input * Add Mac versions of shortcuts; refactor and document the input mapper macros * Change frontend styling for user input labels in floating menus * Additional macro documentation * A little more documentation * Improve entry macro syntax * Move input mapper macros to a separate file * Adapt the keyboard shortcuts to the user's OS * Display keyboard shortcuts in the menu bar based on OS * Change Input Mapper macro syntax from {} to () * Fix esc key bug in Vue * Tweaks * Interim solution for Mac-specific hints * Feed tooltip input hotkeys from their actions * Fix hotkeys for tools because of missing actions * Make Vue respect Ctrl/Cmd differences per platform * Remove commented lines * Code review pass by me * Code review suggestions with TrueDoctor * Turn FutureKeyMapping struct into ActionKeys enum which is a bit cleaner * Add serde derive attributes for message discriminants * Re-add serde deserialize * Fix not mutating ActionKeys conversion; remove custom serializer * Add serde to dev dependencies Co-authored-by: Dennis --- Cargo.lock | 1 + .../docs/interactive-docs/wasm/Cargo.toml | 2 +- bezier-rs/lib/Cargo.toml | 2 +- editor/Cargo.toml | 2 +- editor/src/communication/dispatcher.rs | 23 +- editor/src/consts.rs | 2 + editor/src/dialog/dialog_message.rs | 6 +- editor/src/document/document_message.rs | 10 +- .../src/document/document_message_handler.rs | 141 ++-- .../src/document/menu_bar_message_handler.rs | 101 ++- .../src/document/movement_message_handler.rs | 2 + editor/src/document/portfolio_message.rs | 4 + .../src/document/portfolio_message_handler.rs | 3 + editor/src/document/utility_types.rs | 31 + editor/src/frontend/frontend_message.rs | 2 +- editor/src/frontend/utility_types.rs | 10 +- editor/src/input/input_mapper.rs | 697 +++++++++++------- editor/src/input/input_mapper_macros.rs | 159 ++++ editor/src/input/input_mapper_message.rs | 2 +- .../src/input/input_mapper_message_handler.rs | 64 +- editor/src/input/input_preprocessor.rs | 18 +- .../src/input/input_preprocessor_message.rs | 2 +- .../input_preprocessor_message_handler.rs | 46 +- editor/src/input/keyboard.rs | 43 +- editor/src/input/mod.rs | 1 + editor/src/layout/layout_message.rs | 4 +- editor/src/layout/layout_message_handler.rs | 40 +- editor/src/layout/widgets.rs | 135 +++- editor/src/lib.rs | 2 +- editor/src/misc/hints.rs | 11 +- editor/src/viewport_tools/tool.rs | 111 ++- editor/src/viewport_tools/tool_message.rs | 36 +- .../viewport_tools/tool_message_handler.rs | 63 +- .../src/viewport_tools/tools/artboard_tool.rs | 12 +- .../src/viewport_tools/tools/ellipse_tool.rs | 7 +- .../viewport_tools/tools/eyedropper_tool.rs | 4 +- editor/src/viewport_tools/tools/fill_tool.rs | 4 +- .../src/viewport_tools/tools/freehand_tool.rs | 3 +- .../src/viewport_tools/tools/gradient_tool.rs | 5 +- editor/src/viewport_tools/tools/line_tool.rs | 9 +- .../src/viewport_tools/tools/navigate_tool.rs | 11 +- editor/src/viewport_tools/tools/path_tool.rs | 12 +- editor/src/viewport_tools/tools/pen_tool.rs | 8 +- .../viewport_tools/tools/rectangle_tool.rs | 7 +- .../src/viewport_tools/tools/select_tool.rs | 22 +- editor/src/viewport_tools/tools/shape_tool.rs | 7 +- .../tools/shared/transformation_cage.rs | 28 +- .../src/viewport_tools/tools/spline_tool.rs | 3 + editor/src/viewport_tools/tools/text_tool.rs | 26 +- .../icon-12px-solid/keyboard-control.svg | 3 + frontend/src/App.vue | 4 +- .../components/floating-menus/MenuList.vue | 25 +- frontend/src/components/panels/LayerTree.vue | 40 +- .../components/widgets/inputs/FieldInput.vue | 6 +- .../widgets/inputs/MenuBarInput.vue | 2 +- .../widgets/labels/UserInputLabel.vue | 167 +++-- .../window/status-bar/StatusBar.vue | 11 +- .../window/title-bar/WindowButtonsWeb.vue | 2 +- .../src/components/window/workspace/Panel.vue | 10 +- frontend/src/io-managers/input.ts | 24 +- frontend/src/io-managers/panic.ts | 47 +- frontend/src/main.ts | 6 +- frontend/src/utility-functions/icons.ts | 2 + .../src/utility-functions/keyboard-entry.ts | 13 +- frontend/src/utility-functions/platform.ts | 54 ++ frontend/src/wasm-communication/messages.ts | 10 +- frontend/vue.config.js | 2 +- frontend/wasm/Cargo.toml | 2 +- frontend/wasm/src/editor_api.rs | 19 +- frontend/wasm/src/helpers.rs | 1 + graphene/Cargo.toml | 2 +- proc-macros/Cargo.toml | 9 +- proc-macros/src/discriminant.rs | 8 + 73 files changed, 1686 insertions(+), 727 deletions(-) create mode 100644 editor/src/input/input_mapper_macros.rs create mode 100644 frontend/assets/icon-12px-solid/keyboard-control.svg create mode 100644 frontend/src/utility-functions/platform.ts diff --git a/Cargo.lock b/Cargo.lock index f6f9767c03..66327c37b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -176,6 +176,7 @@ dependencies = [ "graphite-editor", "proc-macro2", "quote", + "serde", "syn", ] diff --git a/bezier-rs/docs/interactive-docs/wasm/Cargo.toml b/bezier-rs/docs/interactive-docs/wasm/Cargo.toml index d0e29b1d19..18e83b6884 100644 --- a/bezier-rs/docs/interactive-docs/wasm/Cargo.toml +++ b/bezier-rs/docs/interactive-docs/wasm/Cargo.toml @@ -2,7 +2,7 @@ name = "bezier-rs-wasm" publish = false version = "0.0.0" -rust-version = "1.56.0" +rust-version = "1.62.0" authors = ["Graphite Authors "] edition = "2021" readme = "../../README.md" diff --git a/bezier-rs/lib/Cargo.toml b/bezier-rs/lib/Cargo.toml index 98816f83c3..a3389d9cb6 100644 --- a/bezier-rs/lib/Cargo.toml +++ b/bezier-rs/lib/Cargo.toml @@ -2,7 +2,7 @@ name = "bezier-rs" publish = false version = "0.0.0" -rust-version = "1.56.0" +rust-version = "1.62.0" authors = ["Graphite Authors "] edition = "2021" readme = "./README.md" diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 5927ba4d80..25164dc5e3 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -2,7 +2,7 @@ name = "graphite-editor" publish = false version = "0.0.0" -rust-version = "1.56.0" +rust-version = "1.62.0" authors = ["Graphite Authors "] edition = "2021" readme = "../README.md" diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 4db130d532..c0cf995195 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -138,14 +138,25 @@ impl Dispatcher { } InputMapper(message) => { let actions = self.collect_actions(); + let keyboard_platform = self.message_handlers.portfolio_message_handler.platform.as_keyboard_platform_layout(); + self.message_handlers .input_mapper_message_handler - .process_action(message, (&self.message_handlers.input_preprocessor_message_handler, actions), &mut queue); + .process_action(message, (&self.message_handlers.input_preprocessor_message_handler, keyboard_platform, actions), &mut queue); } InputPreprocessor(message) => { - self.message_handlers.input_preprocessor_message_handler.process_action(message, (), &mut queue); + let keyboard_platform = self.message_handlers.portfolio_message_handler.platform.as_keyboard_platform_layout(); + + self.message_handlers.input_preprocessor_message_handler.process_action(message, keyboard_platform, &mut queue); + } + Layout(message) => { + let keyboard_platform = self.message_handlers.portfolio_message_handler.platform.as_keyboard_platform_layout(); + let action_input_mapping = &|action_to_find: &MessageDiscriminant| self.message_handlers.input_mapper_message_handler.action_input_mapping(action_to_find, keyboard_platform); + + self.message_handlers + .layout_message_handler + .process_action(message, (action_input_mapping, keyboard_platform), &mut queue); } - Layout(message) => self.message_handlers.layout_message_handler.process_action(message, (), &mut queue), Portfolio(message) => { self.message_handlers .portfolio_message_handler @@ -518,15 +529,15 @@ mod test { replacement_selected_layers: sorted_layers[..2].to_vec(), }); - editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 }); + editor.handle_message(DocumentMessage::SelectedLayersRaise); let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::>()); - editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 }); + editor.handle_message(DocumentMessage::SelectedLayersLower); let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap()); assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::>()); - editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MAX }); + editor.handle_message(DocumentMessage::SelectedLayersRaiseToFront); let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::>()); } diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 6c5eef4bc6..484aa5c213 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -35,6 +35,8 @@ pub const PATH_OUTLINE_WEIGHT: f64 = 2.; pub const ROTATE_SNAP_ANGLE: f64 = 15.; pub const SCALE_SNAP_INTERVAL: f64 = 0.1; pub const SLOWING_DIVISOR: f64 = 10.; +pub const NUDGE_AMOUNT: f64 = 1.; +pub const BIG_NUDGE_AMOUNT: f64 = 10.; // Select tool pub const SELECTION_TOLERANCE: f64 = 5.; diff --git a/editor/src/dialog/dialog_message.rs b/editor/src/dialog/dialog_message.rs index 103023d30f..4d8ff289b3 100644 --- a/editor/src/dialog/dialog_message.rs +++ b/editor/src/dialog/dialog_message.rs @@ -1,7 +1,7 @@ -use crate::message_prelude::*; -use serde::{Deserialize, Serialize}; - use super::{ExportDialogUpdate, NewDocumentDialogUpdate}; +use crate::message_prelude::*; + +use serde::{Deserialize, Serialize}; #[remain::sorted] #[impl_message(Message, Dialog)] diff --git a/editor/src/document/document_message.rs b/editor/src/document/document_message.rs index 05e3a01d4e..54f9795399 100644 --- a/editor/src/document/document_message.rs +++ b/editor/src/document/document_message.rs @@ -103,12 +103,16 @@ pub enum DocumentMessage { new_name: String, }, RenderDocument, - ReorderSelectedLayers { - relative_index_offset: isize, - }, RollbackTransaction, SaveDocument, SelectAllLayers, + SelectedLayersLower, + SelectedLayersLowerToBack, + SelectedLayersRaise, + SelectedLayersRaiseToFront, + SelectedLayersReorder { + relative_index_offset: isize, + }, SelectLayer { layer_path: Vec, ctrl: bool, diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index eb6f978059..42b41dbae4 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -7,6 +7,7 @@ use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler}; use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler}; use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR}; use crate::frontend::utility_types::{FileType, FrontendImageData}; +use crate::input::input_mapper::action_keys::action_shortcut; use crate::input::InputPreprocessorMessageHandler; use crate::layout::layout_message::LayoutTarget; use crate::layout::widgets::{ @@ -549,6 +550,7 @@ impl DocumentMessageHandler { icon: "Snapping".into(), tooltip: "Snapping".into(), on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()), + ..Default::default() })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { header: "Snapping".into(), @@ -564,6 +566,7 @@ impl DocumentMessageHandler { icon: "Grid".into(), tooltip: "Grid".into(), on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()), + ..Default::default() })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { header: "Grid".into(), @@ -579,6 +582,7 @@ impl DocumentMessageHandler { icon: "Overlays".into(), tooltip: "Overlays".into(), on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()), + ..Default::default() })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { header: "Overlays".into(), @@ -841,14 +845,16 @@ impl DocumentMessageHandler { })), WidgetHolder::new(Widget::IconButton(IconButton { icon: "NodeFolder".into(), - tooltip: "New Folder (Ctrl+Shift+N)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut + tooltip: "New Folder".into(), + tooltip_shortcut: action_shortcut!(DocumentMessageDiscriminant::CreateEmptyFolder), size: 24, on_update: WidgetCallback::new(|_| DocumentMessage::CreateEmptyFolder { container_path: vec![] }.into()), ..Default::default() })), WidgetHolder::new(Widget::IconButton(IconButton { icon: "Trash".into(), - tooltip: "Delete Selected (Del)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut + tooltip: "Delete Selected".into(), + tooltip_shortcut: action_shortcut!(DocumentMessageDiscriminant::DeleteSelectedLayers), size: 24, on_update: WidgetCallback::new(|_| DocumentMessage::DeleteSelectedLayers.into()), ..Default::default() @@ -864,6 +870,62 @@ impl DocumentMessageHandler { .into(), ); } + + pub fn selected_layers_reorder(&mut self, relative_index_offset: isize, responses: &mut VecDeque) { + self.backup(responses); + + let all_layer_paths = self.all_layers_sorted(); + let selected_layers = self.selected_layers_sorted(); + + let first_or_last_selected_layer = match relative_index_offset.signum() { + -1 => selected_layers.first(), + 1 => selected_layers.last(), + _ => panic!("selected_layers_reorder() must be given a non-zero value"), + }; + + if let Some(pivot_layer) = first_or_last_selected_layer { + let sibling_layer_paths: Vec<_> = all_layer_paths + .iter() + .filter(|layer| { + // Check if this is a sibling of the pivot layer + // TODO: Break this out into a reusable function `fn are_layers_siblings(layer_a, layer_b) -> bool` + let containing_folder_path = &pivot_layer[0..pivot_layer.len() - 1]; + layer.starts_with(containing_folder_path) && pivot_layer.len() == layer.len() + }) + .collect(); + + // TODO: Break this out into a reusable function: `fn layer_index_in_containing_folder(layer_path) -> usize` + let pivot_index_among_siblings = sibling_layer_paths.iter().position(|path| *path == pivot_layer); + + if let Some(pivot_index) = pivot_index_among_siblings { + let max = sibling_layer_paths.len() as i64 - 1; + let insert_index = (pivot_index as i64 + relative_index_offset as i64).clamp(0, max) as usize; + + let existing_layer_to_insert_beside = sibling_layer_paths.get(insert_index); + + // TODO: Break this block out into a call to a message called `MoveSelectedLayersNextToLayer { neighbor_path, above_or_below }` + if let Some(neighbor_path) = existing_layer_to_insert_beside { + let (neighbor_id, folder_path) = neighbor_path.split_last().expect("Can't move the root folder"); + + if let Some(folder) = self.graphene_document.layer(folder_path).ok().and_then(|layer| layer.as_folder().ok()) { + let neighbor_layer_index = folder.layer_ids.iter().position(|id| id == neighbor_id).unwrap() as isize; + + // If moving down, insert below this layer. If moving up, insert above this layer. + let insert_index = if relative_index_offset < 0 { neighbor_layer_index } else { neighbor_layer_index + 1 }; + + responses.push_back( + DocumentMessage::MoveSelectedLayersTo { + folder_path: folder_path.to_vec(), + insert_index, + reverse_index: false, + } + .into(), + ); + } + } + } + } + } } impl MessageHandler for DocumentMessageHandler { @@ -1318,61 +1380,6 @@ impl MessageHandler { - self.backup(responses); - - let all_layer_paths = self.all_layers_sorted(); - let selected_layers = self.selected_layers_sorted(); - - let first_or_last_selected_layer = match relative_index_offset.signum() { - -1 => selected_layers.first(), - 1 => selected_layers.last(), - _ => panic!("ReorderSelectedLayers must be given a non-zero value"), - }; - - if let Some(pivot_layer) = first_or_last_selected_layer { - let sibling_layer_paths: Vec<_> = all_layer_paths - .iter() - .filter(|layer| { - // Check if this is a sibling of the pivot layer - // TODO: Break this out into a reusable function `fn are_layers_siblings(layer_a, layer_b) -> bool` - let containing_folder_path = &pivot_layer[0..pivot_layer.len() - 1]; - layer.starts_with(containing_folder_path) && pivot_layer.len() == layer.len() - }) - .collect(); - - // TODO: Break this out into a reusable function: `fn layer_index_in_containing_folder(layer_path) -> usize` - let pivot_index_among_siblings = sibling_layer_paths.iter().position(|path| *path == pivot_layer); - - if let Some(pivot_index) = pivot_index_among_siblings { - let max = sibling_layer_paths.len() as i64 - 1; - let insert_index = (pivot_index as i64 + relative_index_offset as i64).clamp(0, max) as usize; - - let existing_layer_to_insert_beside = sibling_layer_paths.get(insert_index); - - // TODO: Break this block out into a call to a message called `MoveSelectedLayersNextToLayer { neighbor_path, above_or_below }` - if let Some(neighbor_path) = existing_layer_to_insert_beside { - let (neighbor_id, folder_path) = neighbor_path.split_last().expect("Can't move the root folder"); - - if let Some(folder) = self.graphene_document.layer(folder_path).ok().and_then(|layer| layer.as_folder().ok()) { - let neighbor_layer_index = folder.layer_ids.iter().position(|id| id == neighbor_id).unwrap() as isize; - - // If moving down, insert below this layer. If moving up, insert above this layer. - let insert_index = if relative_index_offset < 0 { neighbor_layer_index } else { neighbor_layer_index + 1 }; - - responses.push_back( - DocumentMessage::MoveSelectedLayersTo { - folder_path: folder_path.to_vec(), - insert_index, - reverse_index: false, - } - .into(), - ); - } - } - } - } - } RollbackTransaction => { self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e)); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); @@ -1399,6 +1406,21 @@ impl MessageHandler { + responses.push_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: -1 }.into()); + } + SelectedLayersLowerToBack => { + responses.push_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MIN }.into()); + } + SelectedLayersRaise => { + responses.push_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: 1 }.into()); + } + SelectedLayersRaiseToFront => { + responses.push_front(DocumentMessage::SelectedLayersReorder { relative_index_offset: isize::MAX }.into()); + } + SelectedLayersReorder { relative_index_offset } => { + self.selected_layers_reorder(relative_index_offset, responses); + } SelectLayer { layer_path, ctrl, shift } => { let mut paths = vec![]; let last_selection_exists = !self.layer_range_selection_reference.is_empty(); @@ -1611,7 +1633,10 @@ impl MessageHandler, copy_buffer: [Vec; INTERNAL_CLIPBOARD_COUNT as usize], font_cache: FontCache, + pub platform: Platform, } impl PortfolioMessageHandler { @@ -473,6 +475,7 @@ impl MessageHandler for Port responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into()); } SetActiveDocument { document_id } => self.active_document_id = Some(document_id), + SetPlatform { platform } => self.platform = platform, UpdateDocumentWidgets => { if let Some(document) = self.active_document() { document.update_document_widgets(responses); diff --git a/editor/src/document/utility_types.rs b/editor/src/document/utility_types.rs index 1f669158ac..8c6be16369 100644 --- a/editor/src/document/utility_types.rs +++ b/editor/src/document/utility_types.rs @@ -61,3 +61,34 @@ impl DocumentMode { } } } + +#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)] +pub enum Platform { + #[default] + Unknown, + Windows, + Mac, + Linux, +} + +impl Platform { + pub fn as_keyboard_platform_layout(&self) -> KeyboardPlatformLayout { + match self { + Platform::Mac => KeyboardPlatformLayout::Mac, + Platform::Unknown => { + log::warn!("The platform has not been set, remember to send `PortfolioMessage::SetPlatform` during editor initialization."); + KeyboardPlatformLayout::Standard + } + _ => KeyboardPlatformLayout::Standard, + } + } +} + +#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, Serialize, Deserialize)] +pub enum KeyboardPlatformLayout { + /// Standard keyboard mapping used by Windows and Linux + #[default] + Standard, + /// Keyboard mapping used by Macs where Command is sometimes used in favor of Control + Mac, +} diff --git a/editor/src/frontend/frontend_message.rs b/editor/src/frontend/frontend_message.rs index 1d3866a053..66e4abbdbc 100644 --- a/editor/src/frontend/frontend_message.rs +++ b/editor/src/frontend/frontend_message.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; #[remain::sorted] #[impl_message(Message, Frontend)] -#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum FrontendMessage { // Display prefix: make the frontend show something, like a dialog DisplayDialog { icon: String }, diff --git a/editor/src/frontend/utility_types.rs b/editor/src/frontend/utility_types.rs index 618e2dca0b..9038fe55a8 100644 --- a/editor/src/frontend/utility_types.rs +++ b/editor/src/frontend/utility_types.rs @@ -1,21 +1,21 @@ use graphene::LayerId; use serde::{Deserialize, Serialize}; -#[derive(PartialEq, Eq, Clone, Deserialize, Serialize, Debug)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct FrontendDocumentDetails { pub is_saved: bool, pub name: String, pub id: u64, } -#[derive(PartialEq, Eq, Clone, Deserialize, Serialize, Debug)] +#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)] pub struct FrontendImageData { pub path: Vec, pub mime: String, pub image_data: Vec, } -#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum MouseCursorIcon { Default, ZoomIn, @@ -35,7 +35,7 @@ impl Default for MouseCursorIcon { } } -#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum FileType { Svg, Png, @@ -58,7 +58,7 @@ impl FileType { } } -#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub enum ExportBounds { AllArtwork, Selection, diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index b1182e8338..0320502a9e 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -1,25 +1,25 @@ +use super::input_mapper_macros::*; use super::keyboard::{Key, KeyStates, NUMBER_OF_KEYS}; +use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT}; use crate::document::clipboards::Clipboard; +use crate::document::utility_types::KeyboardPlatformLayout; use crate::message_prelude::*; -use crate::viewport_tools::tool::ToolType; use glam::DVec2; - -const NUDGE_AMOUNT: f64 = 1.; -const SHIFT_NUDGE_AMOUNT: f64 = 10.; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone)] pub struct Mapping { pub key_up: [KeyMappingEntries; NUMBER_OF_KEYS], pub key_down: [KeyMappingEntries; NUMBER_OF_KEYS], - pub pointer_move: KeyMappingEntries, - pub mouse_scroll: KeyMappingEntries, pub double_click: KeyMappingEntries, + pub wheel_scroll: KeyMappingEntries, + pub pointer_move: KeyMappingEntries, } impl Default for Mapping { fn default() -> Self { - use input_mapper_macros::{entry, mapping, modifiers}; + use InputMapperMessage::*; use Key::*; // WARNING! @@ -27,210 +27,317 @@ impl Default for Mapping { // it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`). let mappings = mapping![ - // Higher priority than entries in sections below - entry! {action=MovementMessage::PointerMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, message=InputMapperMessage::PointerMove}, - // Transform layers - entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter}, - entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=Lmb}, - entry! {action=TransformLayerMessage::CancelTransformOperation, key_down=KeyEscape}, - entry! {action=TransformLayerMessage::CancelTransformOperation, key_down=Rmb}, - entry! {action=TransformLayerMessage::ConstrainX, key_down=KeyX}, - entry! {action=TransformLayerMessage::ConstrainY, key_down=KeyY}, - entry! {action=TransformLayerMessage::TypeBackspace, key_down=KeyBackspace}, - entry! {action=TransformLayerMessage::TypeNegate, key_down=KeyMinus}, - entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyComma}, - entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod}, - entry! {action=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]}, - // Select - entry! {action=SelectToolMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }, message=InputMapperMessage::PointerMove}, - entry! {action=SelectToolMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb}, - entry! {action=SelectToolMessage::DragStop, key_up=Lmb}, - entry! {action=SelectToolMessage::DragStop, key_down=KeyEnter}, - entry! {action=SelectToolMessage::EditLayer, message=InputMapperMessage::DoubleClick}, - entry! {action=SelectToolMessage::Abort, key_down=Rmb}, - entry! {action=SelectToolMessage::Abort, key_down=KeyEscape}, - // Artboard - entry! {action=ArtboardToolMessage::PointerDown, key_down=Lmb}, - entry! {action=ArtboardToolMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }, message=InputMapperMessage::PointerMove}, - entry! {action=ArtboardToolMessage::PointerUp, key_up=Lmb}, - entry! {action=ArtboardToolMessage::DeleteSelected, key_down=KeyDelete}, - entry! {action=ArtboardToolMessage::DeleteSelected, key_down=KeyBackspace}, - // Navigate - entry! {action=NavigateToolMessage::ClickZoom { zoom_in: false }, key_up=Lmb, modifiers=[KeyShift]}, - entry! {action=NavigateToolMessage::ClickZoom { zoom_in: true }, key_up=Lmb}, - entry! {action=NavigateToolMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }, message=InputMapperMessage::PointerMove}, - entry! {action=NavigateToolMessage::TranslateCanvasBegin, key_down=Mmb}, - entry! {action=NavigateToolMessage::RotateCanvasBegin, key_down=Rmb}, - entry! {action=NavigateToolMessage::ZoomCanvasBegin, key_down=Lmb}, - entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Rmb}, - entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Lmb}, - entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Mmb}, - // Eyedropper - entry! {action=EyedropperToolMessage::LeftMouseDown, key_down=Lmb}, - entry! {action=EyedropperToolMessage::RightMouseDown, key_down=Rmb}, - // Text - entry! {action=TextMessage::Interact, key_up=Lmb}, - entry! {action=TextMessage::Abort, key_down=KeyEscape}, - entry! {action=TextMessage::CommitText, key_down=KeyEnter, modifiers=[KeyControl]}, - // Gradient - entry! {action=GradientToolMessage::PointerDown, key_down=Lmb}, - entry! {action=GradientToolMessage::PointerMove { constrain_axis: KeyShift }, message=InputMapperMessage::PointerMove}, - entry! {action=GradientToolMessage::PointerUp, key_up=Lmb}, - // Rectangle - entry! {action=RectangleToolMessage::DragStart, key_down=Lmb}, - entry! {action=RectangleToolMessage::DragStop, key_up=Lmb}, - entry! {action=RectangleToolMessage::Abort, key_down=Rmb}, - entry! {action=RectangleToolMessage::Abort, key_down=KeyEscape}, - entry! {action=RectangleToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, - // Ellipse - entry! {action=EllipseToolMessage::DragStart, key_down=Lmb}, - entry! {action=EllipseToolMessage::DragStop, key_up=Lmb}, - entry! {action=EllipseToolMessage::Abort, key_down=Rmb}, - entry! {action=EllipseToolMessage::Abort, key_down=KeyEscape}, - entry! {action=EllipseToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, - // Shape - entry! {action=ShapeToolMessage::DragStart, key_down=Lmb}, - entry! {action=ShapeToolMessage::DragStop, key_up=Lmb}, - entry! {action=ShapeToolMessage::Abort, key_down=Rmb}, - entry! {action=ShapeToolMessage::Abort, key_down=KeyEscape}, - entry! {action=ShapeToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, - // Line - entry! {action=LineToolMessage::DragStart, key_down=Lmb}, - entry! {action=LineToolMessage::DragStop, key_up=Lmb}, - entry! {action=LineToolMessage::Abort, key_down=Rmb}, - entry! {action=LineToolMessage::Abort, key_down=KeyEscape}, - entry! {action=LineToolMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }, triggers=[KeyAlt, KeyShift, KeyControl]}, - // Path - entry! {action=PathToolMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb}, - entry! {action=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }, message=InputMapperMessage::PointerMove}, - entry! {action=PathToolMessage::Delete, key_down=KeyDelete}, - entry! {action=PathToolMessage::Delete, key_down=KeyBackspace}, - entry! {action=PathToolMessage::DragStop, key_up=Lmb}, - // Pen - entry! {action=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }, message=InputMapperMessage::PointerMove}, - entry! {action=PenToolMessage::DragStart, key_down=Lmb}, - entry! {action=PenToolMessage::DragStop, key_up=Lmb}, - entry! {action=PenToolMessage::Confirm, key_down=Rmb}, - entry! {action=PenToolMessage::Confirm, key_down=KeyEscape}, - entry! {action=PenToolMessage::Confirm, key_down=KeyEnter}, - // Freehand - entry! {action=FreehandToolMessage::PointerMove, message=InputMapperMessage::PointerMove}, - entry! {action=FreehandToolMessage::DragStart, key_down=Lmb}, - entry! {action=FreehandToolMessage::DragStop, key_up=Lmb}, - // Spline - entry! {action=SplineToolMessage::PointerMove, message=InputMapperMessage::PointerMove}, - entry! {action=SplineToolMessage::DragStart, key_down=Lmb}, - entry! {action=SplineToolMessage::DragStop, key_up=Lmb}, - entry! {action=SplineToolMessage::Confirm, key_down=Rmb}, - entry! {action=SplineToolMessage::Confirm, key_down=KeyEscape}, - entry! {action=SplineToolMessage::Confirm, key_down=KeyEnter}, - // Fill - entry! {action=FillToolMessage::LeftMouseDown, key_down=Lmb}, - entry! {action=FillToolMessage::RightMouseDown, key_down=Rmb}, - // Tool Actions - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Select }, key_down=KeyV}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Navigate }, key_down=KeyZ}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }, key_down=KeyI}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Text }, key_down=KeyT}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Fill }, key_down=KeyF}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Gradient }, key_down=KeyH}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Path }, key_down=KeyA}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Pen }, key_down=KeyP}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Freehand }, key_down=KeyN}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Line }, key_down=KeyL}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }, key_down=KeyM}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }, key_down=KeyE}, - entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Shape }, key_down=KeyY}, - // Colors - entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]}, - entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]}, - entry! {action=ToolMessage::SelectRandomPrimaryColor, key_down=KeyC, modifiers=[KeyAlt]}, - // Document actions - entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]}, - entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]}, - entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete}, - entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace}, - entry! {action=DialogMessage::RequestExportDialog, key_down=KeyE, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]}, - entry! {action=DocumentMessage::DebugPrintDocument, key_down=KeyP, modifiers=[KeyAlt]}, - entry! {action=DocumentMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::GroupSelectedLayers, key_down=KeyG, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::UngroupSelectedLayers, key_down=KeyG, modifiers=[KeyControl, KeyShift]}, - entry! {action=DocumentMessage::CreateEmptyFolder { container_path: vec![] }, key_down=KeyN, modifiers=[KeyControl, KeyShift]}, - // Layer transformation - entry! {action=TransformLayerMessage::BeginGrab, key_down=KeyG}, - entry! {action=TransformLayerMessage::BeginRotate, key_down=KeyR}, - entry! {action=TransformLayerMessage::BeginScale, key_down=KeyS}, - // Movement actions - entry! {action=MovementMessage::RotateCanvasBegin, key_down=Mmb, modifiers=[KeyControl]}, - entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]}, - entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb}, - entry! {action=MovementMessage::TransformCanvasEnd, key_up=Mmb}, - entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Lmb, modifiers=[KeySpace]}, - entry! {action=MovementMessage::TransformCanvasEnd, key_up=Lmb, modifiers=[KeySpace]}, - entry! {action=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }, key_down=KeyPlus, modifiers=[KeyControl]}, - entry! {action=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }, key_down=KeyEquals, modifiers=[KeyControl]}, - entry! {action=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }, key_down=KeyMinus, modifiers=[KeyControl]}, - entry! {action=MovementMessage::SetCanvasZoom { zoom_factor: 1. }, key_down=Key1, modifiers=[KeyControl]}, - entry! {action=MovementMessage::SetCanvasZoom { zoom_factor: 2. }, key_down=Key2, modifiers=[KeyControl]}, - entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]}, - entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]}, - entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }, message=InputMapperMessage::MouseScroll}, - entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(1., 0.) }, key_down=KeyPageUp, modifiers=[KeyShift]}, - entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(-1., 0.) }, key_down=KeyPageDown, modifiers=[KeyShift]}, - entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }, key_down=KeyPageUp}, - entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }, key_down=KeyPageDown}, - // Portfolio actions - entry! {action=PortfolioMessage::OpenDocument, key_down=KeyO, modifiers=[KeyControl]}, - entry! {action=PortfolioMessage::Import, key_down=KeyI, modifiers=[KeyControl]}, - entry! {action=DialogMessage::RequestNewDocumentDialog, key_down=KeyN, modifiers=[KeyControl]}, - entry! {action=PortfolioMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]}, - entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]}, - entry! {action=DialogMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]}, - entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]}, - entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::Device }, key_down=KeyC, modifiers=[KeyControl]}, - entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::Device }, key_down=KeyX, modifiers=[KeyControl]}, - // Nudging - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift, KeyArrowLeft]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift, KeyArrowRight]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyShift, KeyArrowUp]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyShift, KeyArrowDown]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowLeft, modifiers=[KeyShift]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyShift, KeyArrowUp]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyShift, KeyArrowDown]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowRight, modifiers=[KeyShift]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyArrowLeft]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyArrowRight]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyArrowLeft]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyArrowRight]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT }, key_down=KeyArrowDown}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyArrowUp]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyArrowDown]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowLeft}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyArrowUp]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyArrowDown]}, - entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowRight}, - // Reorder Layers - entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MAX }, key_down=KeyRightCurlyBracket, modifiers=[KeyControl]}, // TODO: Use KeyRightBracket with Ctrl+Shift modifiers once input system is fixed - entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 }, key_down=KeyRightBracket, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 }, key_down=KeyLeftBracket, modifiers=[KeyControl]}, - entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MIN }, key_down=KeyLeftCurlyBracket, modifiers=[KeyControl]}, // TODO: Use KeyLeftBracket with Ctrl+Shift modifiers once input system is fixed - // Debug Actions - entry! {action=DebugMessage::ToggleTraceLogs, key_down=KeyT, modifiers=[KeyAlt]}, - entry! {action=DebugMessage::MessageOff, key_down=Key0, modifiers=[KeyAlt]}, - entry! {action=DebugMessage::MessageNames, key_down=Key1, modifiers=[KeyAlt]}, - entry! {action=DebugMessage::MessageContents, key_down=Key2, modifiers=[KeyAlt]}, + // HIGHER PRIORITY: + // + // MovementMessage + entry!( + PointerMove; + refresh_keys=[KeyControl], + action_dispatch=MovementMessage::PointerMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, + ), + // NORMAL PRIORITY: + // + // TransformLayerMessage + entry!(KeyDown(KeyEnter); action_dispatch=TransformLayerMessage::ApplyTransformOperation), + entry!(KeyDown(Lmb); action_dispatch=TransformLayerMessage::ApplyTransformOperation), + entry!(KeyDown(KeyEscape); action_dispatch=TransformLayerMessage::CancelTransformOperation), + entry!(KeyDown(Rmb); action_dispatch=TransformLayerMessage::CancelTransformOperation), + entry!(KeyDown(KeyX); action_dispatch=TransformLayerMessage::ConstrainX), + entry!(KeyDown(KeyY); action_dispatch=TransformLayerMessage::ConstrainY), + entry!(KeyDown(KeyBackspace); action_dispatch=TransformLayerMessage::TypeBackspace), + entry!(KeyDown(KeyMinus); action_dispatch=TransformLayerMessage::TypeNegate), + entry!(KeyDown(KeyComma); action_dispatch=TransformLayerMessage::TypeDecimalPoint), + entry!(KeyDown(KeyPeriod); action_dispatch=TransformLayerMessage::TypeDecimalPoint), + entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }), + // SelectToolMessage + entry!(PointerMove; refresh_keys=[KeyControl, KeyShift, KeyAlt], action_dispatch=SelectToolMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }), + entry!(KeyDown(Lmb); action_dispatch=SelectToolMessage::DragStart { add_to_selection: KeyShift }), + entry!(KeyUp(Lmb); action_dispatch=SelectToolMessage::DragStop), + entry!(KeyDown(KeyEnter); action_dispatch=SelectToolMessage::DragStop), + entry!(DoubleClick; action_dispatch=SelectToolMessage::EditLayer), + entry!(KeyDown(Rmb); action_dispatch=SelectToolMessage::Abort), + entry!(KeyDown(KeyEscape); action_dispatch=SelectToolMessage::Abort), + // ArtboardToolMessage + entry!(KeyDown(Lmb); action_dispatch=ArtboardToolMessage::PointerDown), + entry!(PointerMove; refresh_keys=[KeyShift, KeyAlt], action_dispatch=ArtboardToolMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }), + entry!(KeyUp(Lmb); action_dispatch=ArtboardToolMessage::PointerUp), + entry!(KeyDown(KeyDelete); action_dispatch=ArtboardToolMessage::DeleteSelected), + entry!(KeyDown(KeyBackspace); action_dispatch=ArtboardToolMessage::DeleteSelected), + // NavigateToolMessage + entry!(KeyUp(Lmb); modifiers=[KeyShift], action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: false }), + entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: true }), + entry!(PointerMove; refresh_keys=[KeyControl], action_dispatch=NavigateToolMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }), + entry!(KeyDown(Mmb); action_dispatch=NavigateToolMessage::TranslateCanvasBegin), + entry!(KeyDown(Rmb); action_dispatch=NavigateToolMessage::RotateCanvasBegin), + entry!(KeyDown(Lmb); action_dispatch=NavigateToolMessage::ZoomCanvasBegin), + entry!(KeyUp(Rmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd), + entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd), + entry!(KeyUp(Mmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd), + // EyedropperToolMessage + entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftMouseDown), + entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightMouseDown), + // TextToolMessage + entry!(KeyUp(Lmb); action_dispatch=TextToolMessage::Interact), + entry!(KeyDown(KeyEscape); action_dispatch=TextToolMessage::Abort), + entry_multiplatform!( + standard!(KeyDown(KeyEnter); modifiers=[KeyControl], action_dispatch=TextToolMessage::CommitText), + mac_only!(KeyDown(KeyEnter); modifiers=[KeyCommand], action_dispatch=TextToolMessage::CommitText), + ), + // GradientToolMessage + entry!(KeyDown(Lmb); action_dispatch=GradientToolMessage::PointerDown), + entry!(PointerMove; refresh_keys=[KeyShift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: KeyShift }), + entry!(KeyUp(Lmb); action_dispatch=GradientToolMessage::PointerUp), + // RectangleToolMessage + entry!(KeyDown(Lmb); action_dispatch=RectangleToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=RectangleToolMessage::DragStop), + entry!(KeyDown(Rmb); action_dispatch=RectangleToolMessage::Abort), + entry!(KeyDown(KeyEscape); action_dispatch=RectangleToolMessage::Abort), + entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=RectangleToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }), + // EllipseToolMessage + entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop), + entry!(KeyDown(Rmb); action_dispatch=EllipseToolMessage::Abort), + entry!(KeyDown(KeyEscape); action_dispatch=EllipseToolMessage::Abort), + entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=EllipseToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }), + // ShapeToolMessage + entry!(KeyDown(Lmb); action_dispatch=ShapeToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=ShapeToolMessage::DragStop), + entry!(KeyDown(Rmb); action_dispatch=ShapeToolMessage::Abort), + entry!(KeyDown(KeyEscape); action_dispatch=ShapeToolMessage::Abort), + entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=ShapeToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }), + // LineToolMessage + entry!(KeyDown(Lmb); action_dispatch=LineToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop), + entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort), + entry!(KeyDown(KeyEscape); action_dispatch=LineToolMessage::Abort), + entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift, KeyControl], action_dispatch=LineToolMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }), + // PathToolMessage + entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: KeyShift }), + entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }), + entry!(KeyDown(KeyDelete); action_dispatch=PathToolMessage::Delete), + entry!(KeyDown(KeyBackspace); action_dispatch=PathToolMessage::Delete), + entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop), + // PenToolMessage + entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }), + entry!(KeyDown(Lmb); action_dispatch=PenToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=PenToolMessage::DragStop), + entry!(KeyDown(Rmb); action_dispatch=PenToolMessage::Confirm), + entry!(KeyDown(KeyEscape); action_dispatch=PenToolMessage::Confirm), + entry!(KeyDown(KeyEnter); action_dispatch=PenToolMessage::Confirm), + // FreehandToolMessage + entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove), + entry!(KeyDown(Lmb); action_dispatch=FreehandToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=FreehandToolMessage::DragStop), + // SplineToolMessage + entry!(PointerMove; action_dispatch=SplineToolMessage::PointerMove), + entry!(KeyDown(Lmb); action_dispatch=SplineToolMessage::DragStart), + entry!(KeyUp(Lmb); action_dispatch=SplineToolMessage::DragStop), + entry!(KeyDown(Rmb); action_dispatch=SplineToolMessage::Confirm), + entry!(KeyDown(KeyEscape); action_dispatch=SplineToolMessage::Confirm), + entry!(KeyDown(KeyEnter); action_dispatch=SplineToolMessage::Confirm), + // FillToolMessage + entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftMouseDown), + entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightMouseDown), + // ToolMessage + entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect), + entry!(KeyDown(KeyZ); action_dispatch=ToolMessage::ActivateToolNavigate), + entry!(KeyDown(KeyI); action_dispatch=ToolMessage::ActivateToolEyedropper), + entry!(KeyDown(KeyT); action_dispatch=ToolMessage::ActivateToolText), + entry!(KeyDown(KeyF); action_dispatch=ToolMessage::ActivateToolFill), + entry!(KeyDown(KeyH); action_dispatch=ToolMessage::ActivateToolGradient), + entry!(KeyDown(KeyA); action_dispatch=ToolMessage::ActivateToolPath), + entry!(KeyDown(KeyP); action_dispatch=ToolMessage::ActivateToolPen), + entry!(KeyDown(KeyN); action_dispatch=ToolMessage::ActivateToolFreehand), + entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolLine), + entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle), + entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse), + entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape), + entry_multiplatform!( + standard!(KeyDown(KeyX); modifiers=[KeyShift, KeyControl], action_dispatch=ToolMessage::ResetColors), + mac_only!(KeyDown(KeyX); modifiers=[KeyShift, KeyCommand], action_dispatch=ToolMessage::ResetColors), + ), + entry!(KeyDown(KeyX); modifiers=[KeyShift], action_dispatch=ToolMessage::SwapColors), + entry!(KeyDown(KeyC); modifiers=[KeyAlt], action_dispatch=ToolMessage::SelectRandomPrimaryColor), + // DocumentMessage + entry!(KeyDown(KeyDelete); action_dispatch=DocumentMessage::DeleteSelectedLayers), + entry!(KeyDown(KeyBackspace); action_dispatch=DocumentMessage::DeleteSelectedLayers), + entry!(KeyDown(KeyP); modifiers=[KeyAlt], action_dispatch=DocumentMessage::DebugPrintDocument), + entry_multiplatform!( + standard!(KeyDown(KeyZ); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::Redo), + mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::Redo), + ), + entry_multiplatform!( + standard!(KeyDown(KeyZ); modifiers=[KeyControl], action_dispatch=DocumentMessage::Undo), + mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand], action_dispatch=DocumentMessage::Undo), + ), + entry_multiplatform!( + standard!(KeyDown(KeyA); modifiers=[KeyControl, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers), + mac_only!(KeyDown(KeyA); modifiers=[KeyCommand, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers), + ), + entry_multiplatform!( + standard!(KeyDown(KeyA); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectAllLayers), + mac_only!(KeyDown(KeyA); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectAllLayers), + ), + entry_multiplatform!( + standard!(KeyDown(KeyS); modifiers=[KeyControl], action_dispatch=DocumentMessage::SaveDocument), + mac_only!(KeyDown(KeyS); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SaveDocument), + ), + entry_multiplatform!( + standard!(KeyDown(Key0); modifiers=[KeyControl], action_dispatch=DocumentMessage::ZoomCanvasToFitAll), + mac_only!(KeyDown(Key0); modifiers=[KeyCommand], action_dispatch=DocumentMessage::ZoomCanvasToFitAll), + ), + entry_multiplatform!( + standard!(KeyDown(KeyD); modifiers=[KeyControl], action_dispatch=DocumentMessage::DuplicateSelectedLayers), + mac_only!(KeyDown(KeyD); modifiers=[KeyCommand], action_dispatch=DocumentMessage::DuplicateSelectedLayers), + ), + entry_multiplatform!( + standard!(KeyDown(KeyG); modifiers=[KeyControl], action_dispatch=DocumentMessage::GroupSelectedLayers), + mac_only!(KeyDown(KeyG); modifiers=[KeyCommand], action_dispatch=DocumentMessage::GroupSelectedLayers), + ), + entry_multiplatform!( + standard!(KeyDown(KeyG); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers), + mac_only!(KeyDown(KeyG); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers), + ), + entry_multiplatform!( + standard!(KeyDown(KeyN); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }), + mac_only!(KeyDown(KeyN); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }), + ), + entry_multiplatform!( + standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack), + mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack), + ), + entry_multiplatform!( + // TODO: Delete this in favor of the KeyLeftBracket (non-shifted version of this key) mapping above once the input system can distinguish between the non-shifted and shifted keys (important for other language keyboards) + standard!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack), + mac_only!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack), + ), + entry_multiplatform!( + standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront), + mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront), + ), + entry_multiplatform!( + // TODO: Delete this in favor of the KeyRightBracket (non-shifted version of this key) mapping above once the input system can distinguish between the non-shifted and shifted keys (important for other language keyboards) + standard!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront), + mac_only!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront), + ), + entry_multiplatform!( + standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersLower), + mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersLower), + ), + entry_multiplatform!( + standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersRaise), + mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersRaise), + ), + entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift, KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift, KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift, KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift, KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift, KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift, KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0. }), + entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift, KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift, KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }), + entry!(KeyDown(KeyArrowUp); modifiers=[KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowUp); modifiers=[KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowUp); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowDown); modifiers=[KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowDown); modifiers=[KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowDown); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowLeft); modifiers=[KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowLeft); modifiers=[KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowLeft); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0. }), + entry!(KeyDown(KeyArrowRight); modifiers=[KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowRight); modifiers=[KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }), + entry!(KeyDown(KeyArrowRight); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0. }), + // TransformLayerMessage + entry!(KeyDown(KeyG); action_dispatch=TransformLayerMessage::BeginGrab), + entry!(KeyDown(KeyR); action_dispatch=TransformLayerMessage::BeginRotate), + entry!(KeyDown(KeyS); action_dispatch=TransformLayerMessage::BeginScale), + // MovementMessage + entry!(KeyDown(Mmb); modifiers=[KeyControl], action_dispatch=MovementMessage::RotateCanvasBegin), + entry!(KeyDown(Mmb); modifiers=[KeyShift], action_dispatch=MovementMessage::ZoomCanvasBegin), + entry!(KeyDown(Mmb); action_dispatch=MovementMessage::TranslateCanvasBegin), + entry!(KeyUp(Mmb); action_dispatch=MovementMessage::TransformCanvasEnd), + entry!(KeyDown(Lmb); modifiers=[KeySpace], action_dispatch=MovementMessage::TranslateCanvasBegin), + entry!(KeyUp(Lmb); modifiers=[KeySpace], action_dispatch=MovementMessage::TransformCanvasEnd), + entry_multiplatform!( + standard!(KeyDown(KeyPlus); modifiers=[KeyControl], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }), + mac_only!(KeyDown(KeyPlus); modifiers=[KeyCommand], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }), + ), + entry_multiplatform!( + standard!(KeyDown(KeyEquals); modifiers=[KeyControl], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }), + mac_only!(KeyDown(KeyEquals); modifiers=[KeyCommand], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }), + ), + entry_multiplatform!( + standard!(KeyDown(KeyMinus); modifiers=[KeyControl], action_dispatch=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }), + mac_only!(KeyDown(KeyMinus); modifiers=[KeyCommand], action_dispatch=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }), + ), + entry_multiplatform!( + standard!(KeyDown(Key1); modifiers=[KeyControl], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 1. }), + mac_only!(KeyDown(Key1); modifiers=[KeyCommand], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 1. }), + ), + entry_multiplatform!( + standard!(KeyDown(Key2); modifiers=[KeyControl], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 2. }), + mac_only!(KeyDown(Key2); modifiers=[KeyCommand], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 2. }), + ), + entry!(WheelScroll; modifiers=[KeyControl], action_dispatch=MovementMessage::WheelCanvasZoom), + entry!(WheelScroll; modifiers=[KeyShift], action_dispatch=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }), + entry!(WheelScroll; action_dispatch=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }), + entry!(KeyDown(KeyPageUp); modifiers=[KeyShift], action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(1., 0.) }), + entry!(KeyDown(KeyPageDown); modifiers=[KeyShift], action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(-1., 0.) }), + entry!(KeyDown(KeyPageUp); action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }), + entry!(KeyDown(KeyPageDown); action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }), + // PortfolioMessage + entry_multiplatform!( + standard!(KeyDown(KeyO); modifiers=[KeyControl], action_dispatch=PortfolioMessage::OpenDocument), + mac_only!(KeyDown(KeyO); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::OpenDocument), + ), + entry_multiplatform!( + standard!(KeyDown(KeyI); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Import), + mac_only!(KeyDown(KeyI); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Import), + ), + entry!(KeyDown(KeyTab); modifiers=[KeyControl], action_dispatch=PortfolioMessage::NextDocument), + entry!(KeyDown(KeyTab); modifiers=[KeyControl, KeyShift], action_dispatch=PortfolioMessage::PrevDocument), + entry_multiplatform!( + standard!(KeyDown(KeyW); modifiers=[KeyControl], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation), + mac_only!(KeyDown(KeyW); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation), + ), + entry_multiplatform!( + standard!(KeyDown(KeyX); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }), + mac_only!(KeyDown(KeyX); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }), + ), + entry_multiplatform!( + standard!(KeyDown(KeyC); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }), + mac_only!(KeyDown(KeyC); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }), + ), + entry_multiplatform!( + // This shortcut is intercepted in the frontend; it exists here only as a shortcut mapping source + standard!(KeyDown(KeyV); modifiers=[KeyControl], action_dispatch=FrontendMessage::TriggerPaste), + mac_only!(KeyDown(KeyV); modifiers=[KeyCommand], action_dispatch=FrontendMessage::TriggerPaste), + ), + // DialogMessage + entry_multiplatform!( + standard!(KeyDown(KeyN); modifiers=[KeyControl], action_dispatch=DialogMessage::RequestNewDocumentDialog), + mac_only!(KeyDown(KeyN); modifiers=[KeyCommand], action_dispatch=DialogMessage::RequestNewDocumentDialog), + ), + entry_multiplatform!( + standard!(KeyDown(KeyW); modifiers=[KeyControl, KeyAlt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation), + mac_only!(KeyDown(KeyW); modifiers=[KeyCommand, KeyAlt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation), + ), + entry_multiplatform!( + standard!(KeyDown(KeyE); modifiers=[KeyControl], action_dispatch=DialogMessage::RequestExportDialog), + mac_only!(KeyDown(KeyE); modifiers=[KeyCommand], action_dispatch=DialogMessage::RequestExportDialog), + ), + // DebugMessage + entry!(KeyDown(KeyT); modifiers=[KeyAlt], action_dispatch=DebugMessage::ToggleTraceLogs), + entry!(KeyDown(Key0); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageOff), + entry!(KeyDown(Key1); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageNames), + entry!(KeyDown(Key2); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageContents), ]; - let (mut key_up, mut key_down, mut pointer_move, mut mouse_scroll, mut double_click) = mappings; + let (mut key_up, mut key_down, mut double_click, mut wheel_scroll, mut pointer_move) = mappings; // TODO: Hardcode these 10 lines into 10 lines of declarations, or make this use a macro to do all 10 in one line const NUMBER_KEYS: [Key; 10] = [Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9]; @@ -239,8 +346,9 @@ impl Default for Mapping { 0, MappingEntry { action: TransformLayerMessage::TypeDigit { digit: i as u8 }.into(), - trigger: InputMapperMessage::KeyDown(*key), - modifiers: modifiers! {}, + input: InputMapperMessage::KeyDown(*key), + platform_layout: None, + modifiers: modifiers!(), }, ); } @@ -251,48 +359,70 @@ impl Default for Mapping { sort(sublist); } } - sort(&mut pointer_move); - sort(&mut mouse_scroll); sort(&mut double_click); + sort(&mut wheel_scroll); + sort(&mut pointer_move); Self { key_up, key_down, - pointer_move, - mouse_scroll, double_click, + wheel_scroll, + pointer_move, } } } impl Mapping { - pub fn match_message(&self, message: InputMapperMessage, keys: &KeyStates, actions: ActionList) -> Option { + pub fn match_input_message(&self, message: InputMapperMessage, keyboard_state: &KeyStates, actions: ActionList, keyboard_platform: KeyboardPlatformLayout) -> Option { let list = match message { InputMapperMessage::KeyDown(key) => &self.key_down[key as usize], InputMapperMessage::KeyUp(key) => &self.key_up[key as usize], InputMapperMessage::DoubleClick => &self.double_click, - InputMapperMessage::MouseScroll => &self.mouse_scroll, + InputMapperMessage::WheelScroll => &self.wheel_scroll, InputMapperMessage::PointerMove => &self.pointer_move, }; - list.match_mapping(keys, actions) + list.match_mapping(keyboard_state, actions, keyboard_platform) } } #[derive(PartialEq, Clone, Debug)] pub struct MappingEntry { - pub trigger: InputMapperMessage, - pub modifiers: KeyStates, + /// Serves two purposes: + /// - This is the message that gets dispatched when the hotkey is matched + /// - This message's discriminant is the action; it must be a currently active action to be considered as a shortcut pub action: Message, + /// The user input event from an input device which this input mapping matches on + pub input: InputMapperMessage, + /// Any additional keys that must be also pressed for this input mapping to match + pub modifiers: KeyStates, + /// The keyboard platform layout which this mapping is exclusive to, or `None` if it's platform-agnostic + pub platform_layout: Option, } #[derive(Debug, Clone)] pub struct KeyMappingEntries(pub Vec); impl KeyMappingEntries { - fn match_mapping(&self, keys: &KeyStates, actions: ActionList) -> Option { + fn match_mapping(&self, keyboard_state: &KeyStates, actions: ActionList, keyboard_platform: KeyboardPlatformLayout) -> Option { for entry in self.0.iter() { - let all_required_modifiers_pressed = ((*keys & entry.modifiers) ^ entry.modifiers).is_empty(); - if all_required_modifiers_pressed && actions.iter().flatten().any(|action| entry.action.to_discriminant() == *action) { + // Skip this entry if it is platform-specific, and for a layout that does not match the user's keyboard platform layout + if let Some(entry_platform_layout) = entry.platform_layout { + if entry_platform_layout != keyboard_platform { + continue; + } + } + + // Find which currently pressed keys are also the modifiers in this hotkey entry, then compare those against the required modifiers to see if there are zero missing + let pressed_modifiers = *keyboard_state & entry.modifiers; + let all_modifiers_without_pressed_modifiers = entry.modifiers ^ pressed_modifiers; + let all_required_modifiers_pressed = all_modifiers_without_pressed_modifiers.is_empty(); + // Skip this entry if any of the required modifiers are missing + if !all_required_modifiers_pressed { + continue; + } + + if actions.iter().flatten().any(|action| entry.action.to_discriminant() == *action) { return Some(entry.action.clone()); } } @@ -313,70 +443,69 @@ impl KeyMappingEntries { } } -impl Default for KeyMappingEntries { - fn default() -> Self { - Self::new() - } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ActionKeys { + Action(MessageDiscriminant), + #[serde(rename = "keys")] + Keys(Vec), } -mod input_mapper_macros { - macro_rules! modifiers { - ($($m:ident),*) => {{ - #[allow(unused_mut)] - let mut state = KeyStates::new(); - $( - state.set(Key::$m as usize); - )* - state - }}; - } +impl ActionKeys { + pub fn to_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec>) { + match self { + ActionKeys::Action(action) => { + if let Some(keys) = action_input_mapping(action).get_mut(0) { + let mut taken_keys = Vec::new(); + std::mem::swap(keys, &mut taken_keys); - macro_rules! entry { - {action=$action:expr, key_down=$key:ident $(, modifiers=[$($m:ident),* $(,)?])?} => {{ - entry!{action=$action, message=InputMapperMessage::KeyDown(Key::$key) $(, modifiers=[$($m),*])?} - }}; - {action=$action:expr, key_up=$key:ident $(, modifiers=[$($m:ident),* $(,)?])?} => {{ - entry!{action=$action, message=InputMapperMessage::KeyUp(Key::$key) $(, modifiers=[$($m),* ])?} - }}; - {action=$action:expr, message=$message:expr $(, modifiers=[$($m:ident),* $(,)?])?} => {{ - &[MappingEntry {trigger: $message, modifiers: modifiers!($($($m),*)?), action: $action.into()}] - }}; - {action=$action:expr, triggers=[$($m:ident),* $(,)?]} => {{ - &[ - MappingEntry {trigger:InputMapperMessage::PointerMove, action: $action.into(), modifiers: modifiers!()}, - $( - MappingEntry {trigger:InputMapperMessage::KeyDown(Key::$m), action: $action.into(), modifiers: modifiers!()}, - MappingEntry {trigger:InputMapperMessage::KeyUp(Key::$m), action: $action.into(), modifiers: modifiers!()}, - )* - ] - }}; - } - - macro_rules! mapping { - //[$()*] => {{ - [$($entry:expr),* $(,)?] => {{ - let mut key_up = KeyMappingEntries::key_array(); - let mut key_down = KeyMappingEntries::key_array(); - let mut pointer_move: KeyMappingEntries = Default::default(); - let mut mouse_scroll: KeyMappingEntries = Default::default(); - let mut double_click: KeyMappingEntries = Default::default(); - $( - for entry in $entry { - let arr = match entry.trigger { - InputMapperMessage::KeyDown(key) => &mut key_down[key as usize], - InputMapperMessage::KeyUp(key) => &mut key_up[key as usize], - InputMapperMessage::MouseScroll => &mut mouse_scroll, - InputMapperMessage::PointerMove => &mut pointer_move, - InputMapperMessage::DoubleClick => &mut double_click, - }; - arr.push(entry.clone()); + *self = ActionKeys::Keys(taken_keys); + } else { + *self = ActionKeys::Keys(Vec::new()); } - )* - (key_up, key_down, pointer_move, mouse_scroll, double_click) - }}; + } + ActionKeys::Keys(keys) => { + log::warn!("Calling `.to_keys()` on a `ActionKeys::Keys` is a mistake/bug. Keys are: {:?}.", keys); + } + } + } +} + +pub fn keys_text_shortcut(keys: &[Key], keyboard_platform: KeyboardPlatformLayout) -> String { + const JOINER_MARK: &str = "+"; + + let mut joined = keys + .iter() + .map(|key| { + let key_string = key.to_string(); + + if keyboard_platform == KeyboardPlatformLayout::Mac { + match key_string.as_str() { + "Command" => "⌘".to_string(), + "Control" => "⌃".to_string(), + "Alt" => "⌥".to_string(), + "Shift" => "⇧".to_string(), + _ => key_string + JOINER_MARK, + } + } else { + key_string + JOINER_MARK + } + }) + .collect::(); + + // Truncate to cut the joining character off the end if it's present + if joined.ends_with(JOINER_MARK) { + joined.truncate(joined.len() - JOINER_MARK.len()); } - pub(crate) use entry; - pub(crate) use mapping; - pub(crate) use modifiers; + joined +} + +pub mod action_keys { + macro_rules! action_shortcut { + ($action:expr) => { + Some(crate::input::input_mapper::ActionKeys::Action($action.into())) + }; + } + + pub(crate) use action_shortcut; } diff --git a/editor/src/input/input_mapper_macros.rs b/editor/src/input/input_mapper_macros.rs new file mode 100644 index 0000000000..4c5458389a --- /dev/null +++ b/editor/src/input/input_mapper_macros.rs @@ -0,0 +1,159 @@ +/// Constructs a `KeyStates` bit vector and sets the bit flags for all the given modifier `Key`s. +macro_rules! modifiers { + ($($m:ident),*) => {{ + #[allow(unused_mut)] + let mut state = KeyStates::new(); + $( + state.set(Key::$m as usize); + )* + state + }}; +} + +/// Builds a slice of `MappingEntry` struct(s) that are used to: +/// - ...dispatch the given `action_dispatch` as an output `Message` if its discriminant is a currently available action +/// - ...when the `InputMapperMessage` enum variant, as specified at the start and followed by a semicolon, is received +/// - ...while any further conditions are met, like the optional `modifiers` being pressed or `layout` matching the OS. +/// +/// Syntax: +/// ```rs +/// entry_for_layout!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message, layout: Option) +/// ``` +/// +/// To avoid having to specify the final `layout` argument, instead use the wrapper macros: [entry]!, [standard]!, and [mac]!. +/// The former sets the layout to `None` which means the key mapping is layout-agnostic and compatible with all platforms. +/// +/// The actions system controls which actions are currently available. Those are provided by the different message handlers based on the current application state and context. +/// Each handler adds or removes actions in the form of message discriminants. Here, we tie an input condition (such as a hotkey) to an action's full message. +/// When an action is currently available, and the user enters that input, the action's message is dispatched on the message bus. +macro_rules! entry_for_layout { + ($input:expr; $(modifiers=[$($modifier:ident),*],)? $(refresh_keys=[$($refresh:ident),* $(,)?],)? action_dispatch=$action_dispatch:expr,$(,)? layout=$layout:expr) => { + &[ + // Cause the `action_dispatch` message to be sent when the specified input occurs. + MappingEntry { + action: $action_dispatch.into(), + input: $input, + modifiers: modifiers!($($($modifier),*)?), + platform_layout: $layout, + }, + + // Also cause the `action_dispatch` message to be sent when any of the specified refresh keys change. + // + // For example, a snapping state bound to the Shift key may change if the user presses or releases that key. + // In that case, we want to dispatch the action's message even though the pointer didn't necessarily move so + // the input handler can update the snapping state without making the user move the mouse to see the change. + $( + $( + MappingEntry { + action: $action_dispatch.into(), + input: InputMapperMessage::KeyDown(Key::$refresh), + modifiers: modifiers!(), + platform_layout: $layout, + }, + MappingEntry { + action: $action_dispatch.into(), + input: InputMapperMessage::KeyUp(Key::$refresh), + modifiers: modifiers!(), + platform_layout: $layout, + }, + )* + )* + ] + }; +} + +/// Wraps [entry_for_layout]! and calls it with an agnostic (`None`) keyboard platform `layout` to avoid having to specify that argument. +/// +/// Syntax: +/// ```rs +/// entry!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message) +/// ``` +macro_rules! entry { + ($($arg:tt)*) => { + &[entry_for_layout!($($arg)*, layout=None)] + }; +} + +/// Wraps [entry_for_layout]! and calls it with a `Standard` keyboard platform `layout` to avoid having to specify that argument. +/// +/// Syntax: +/// ```rs +/// standard!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message) +/// ``` +macro_rules! standard { + ($($arg:tt)*) => { + entry_for_layout!($($arg)*, layout=Some(KeyboardPlatformLayout::Standard)) + }; +} + +/// Wraps [entry_for_layout]! and calls it with a `Mac` keyboard platform `layout` to avoid having to specify that argument. +/// +/// Syntax: +/// ```rs +/// mac_only!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message) +/// ``` +macro_rules! mac_only { + ($($arg:tt)*) => { + entry_for_layout!($($arg)*, layout=Some(KeyboardPlatformLayout::Mac)) + }; +} + +/// Groups multiple related entries for different platforms. +/// When a keyboard shortcut is not platform-agnostic, this should be used to contain a [mac]! and/or [standard]! entry. +/// +/// Syntax: +/// +/// ```rs +/// entry_multiplatform!( +/// standard!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message), +/// mac_only!(Key; modifiers?: Key[], refresh_keys?: Key[], action_dispatch: Message), +/// ) +/// ``` +macro_rules! entry_multiplatform { + {$($arg:expr),*,} => { + &[$($arg ),*] + }; +} + +/// Constructs a `KeyMappingEntries` list for each input type and inserts every given entry into the list corresponding to its input type. +/// Returns a tuple of `KeyMappingEntries` in the order: +/// ```rs +/// (key_up, key_down, double_click, wheel_scroll, pointer_move) +/// ``` +macro_rules! mapping { + [$($entry:expr),* $(,)?] => {{ + let mut key_up = KeyMappingEntries::key_array(); + let mut key_down = KeyMappingEntries::key_array(); + let mut double_click = KeyMappingEntries::new(); + let mut wheel_scroll = KeyMappingEntries::new(); + let mut pointer_move = KeyMappingEntries::new(); + + $( + // Each of the many entry slices, one specified per action + for entry_slice in $entry { + // Each entry in the slice (usually just one, except when `refresh_keys` adds additional key entries) + for entry in entry_slice.into_iter() { + let corresponding_list = match entry.input { + InputMapperMessage::KeyDown(key) => &mut key_down[key as usize], + InputMapperMessage::KeyUp(key) => &mut key_up[key as usize], + InputMapperMessage::DoubleClick => &mut double_click, + InputMapperMessage::WheelScroll => &mut wheel_scroll, + InputMapperMessage::PointerMove => &mut pointer_move, + }; + // Push each entry to the corresponding `KeyMappingEntries` list for its input type + corresponding_list.push(entry.clone()); + } + } + )* + + (key_up, key_down, double_click, wheel_scroll, pointer_move) + }}; +} + +pub(crate) use entry; +pub(crate) use entry_for_layout; +pub(crate) use entry_multiplatform; +pub(crate) use mac_only; +pub(crate) use mapping; +pub(crate) use modifiers; +pub(crate) use standard; diff --git a/editor/src/input/input_mapper_message.rs b/editor/src/input/input_mapper_message.rs index 9c34c49aed..ca9ed905f2 100644 --- a/editor/src/input/input_mapper_message.rs +++ b/editor/src/input/input_mapper_message.rs @@ -17,6 +17,6 @@ pub enum InputMapperMessage { // Messages DoubleClick, - MouseScroll, PointerMove, + WheelScroll, } diff --git a/editor/src/input/input_mapper_message_handler.rs b/editor/src/input/input_mapper_message_handler.rs index de48f98e74..5f91049bd2 100644 --- a/editor/src/input/input_mapper_message_handler.rs +++ b/editor/src/input/input_mapper_message_handler.rs @@ -1,6 +1,7 @@ use super::input_mapper::Mapping; use super::keyboard::Key; use super::InputPreprocessorMessageHandler; +use crate::document::utility_types::KeyboardPlatformLayout; use crate::message_prelude::*; use std::fmt::Write; @@ -31,12 +32,67 @@ impl InputMapperMessageHandler { }); output.replace("Key", "") } + + pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant, keyboard_platform: KeyboardPlatformLayout) -> Vec> { + let key_up = self.mapping.key_up.iter(); + let key_down = self.mapping.key_down.iter(); + let double_click = std::iter::once(&self.mapping.double_click); + let wheel_scroll = std::iter::once(&self.mapping.wheel_scroll); + let pointer_move = std::iter::once(&self.mapping.pointer_move); + + let all_key_mapping_entries = key_up.chain(key_down).chain(double_click).chain(wheel_scroll).chain(pointer_move); + let all_mapping_entries = all_key_mapping_entries.flat_map(|entry| entry.0.iter()); + + // Filter for the desired message + let found_actions = all_mapping_entries.filter(|entry| entry.action.to_discriminant() == *action_to_find); + // Filter for a compatible keyboard platform layout + let found_actions = found_actions.filter(|entry| if let Some(layout) = entry.platform_layout { layout == keyboard_platform } else { true }); + + // Find the key combinations for all keymaps matching the desired action + assert!(std::mem::size_of::() >= std::mem::size_of::()); + found_actions + .map(|entry| { + let mut keys = entry + .modifiers + .iter() + .map(|i| { + // TODO: Use a safe solution eventually + assert!( + i < super::keyboard::NUMBER_OF_KEYS, + "Attempting to convert a Key with enum index {}, which is larger than the number of Key enums", + i + ); + unsafe { std::mem::transmute_copy::(&i) } + }) + .collect::>(); + + if let InputMapperMessage::KeyDown(key) = entry.input { + keys.push(key); + } + + keys.sort_by(|a, b| { + // Order according to platform guidelines mentioned at https://ux.stackexchange.com/questions/58185/normative-ordering-for-modifier-key-combinations + const ORDER: [Key; 4] = [Key::KeyControl, Key::KeyAlt, Key::KeyShift, Key::KeyCommand]; + + match (ORDER.contains(a), ORDER.contains(b)) { + (true, true) => ORDER.iter().position(|key| key == a).unwrap().cmp(&ORDER.iter().position(|key| key == b).unwrap()), + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + (false, false) => std::cmp::Ordering::Equal, + } + }); + + keys + }) + .collect::>() + } } -impl MessageHandler for InputMapperMessageHandler { - fn process_action(&mut self, message: InputMapperMessage, data: (&InputPreprocessorMessageHandler, ActionList), responses: &mut VecDeque) { - let (input, actions) = data; - if let Some(message) = self.mapping.match_message(message, &input.keyboard, actions) { +impl MessageHandler for InputMapperMessageHandler { + fn process_action(&mut self, message: InputMapperMessage, data: (&InputPreprocessorMessageHandler, KeyboardPlatformLayout, ActionList), responses: &mut VecDeque) { + let (input, keyboard_platform, actions) = data; + + if let Some(message) = self.mapping.match_input_message(message, &input.keyboard, actions, keyboard_platform) { responses.push_back(message); } } diff --git a/editor/src/input/input_preprocessor.rs b/editor/src/input/input_preprocessor.rs index 1251be322d..70694bdda4 100644 --- a/editor/src/input/input_preprocessor.rs +++ b/editor/src/input/input_preprocessor.rs @@ -13,14 +13,16 @@ bitflags! { #[derive(Default, Serialize, Deserialize)] #[repr(transparent)] pub struct ModifierKeys: u8 { - const CONTROL = 0b0000_0001; - const SHIFT = 0b0000_0010; - const ALT = 0b0000_0100; + const SHIFT = 0b0000_0001; + const ALT = 0b0000_0010; + const CONTROL = 0b0000_0100; + const META_OR_COMMAND = 0b0000_1000; } } #[cfg(test)] mod test { + use crate::document::utility_types::KeyboardPlatformLayout; use crate::input::input_preprocessor::ModifierKeys; use crate::input::keyboard::Key; use crate::input::mouse::EditorMouseState; @@ -39,7 +41,7 @@ mod test { let mut responses = VecDeque::new(); - input_preprocessor.process_action(message, (), &mut responses); + input_preprocessor.process_action(message, KeyboardPlatformLayout::Standard, &mut responses); assert!(input_preprocessor.keyboard.get(Key::KeyAlt as usize)); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyAlt).into())); @@ -55,7 +57,7 @@ mod test { let mut responses = VecDeque::new(); - input_preprocessor.process_action(message, (), &mut responses); + input_preprocessor.process_action(message, KeyboardPlatformLayout::Standard, &mut responses); assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize)); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyControl).into())); @@ -71,7 +73,7 @@ mod test { let mut responses = VecDeque::new(); - input_preprocessor.process_action(message, (), &mut responses); + input_preprocessor.process_action(message, KeyboardPlatformLayout::Standard, &mut responses); assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize)); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyShift).into())); @@ -88,7 +90,7 @@ mod test { let mut responses = VecDeque::new(); - input_preprocessor.process_action(message, (), &mut responses); + input_preprocessor.process_action(message, KeyboardPlatformLayout::Standard, &mut responses); assert!(!input_preprocessor.keyboard.get(Key::KeyControl as usize)); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyUp(Key::KeyControl).into())); @@ -104,7 +106,7 @@ mod test { let mut responses = VecDeque::new(); - input_preprocessor.process_action(message, (), &mut responses); + input_preprocessor.process_action(message, KeyboardPlatformLayout::Standard, &mut responses); assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize)); assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize)); diff --git a/editor/src/input/input_preprocessor_message.rs b/editor/src/input/input_preprocessor_message.rs index a62ad5403b..942ac785f3 100644 --- a/editor/src/input/input_preprocessor_message.rs +++ b/editor/src/input/input_preprocessor_message.rs @@ -16,8 +16,8 @@ pub enum InputPreprocessorMessage { DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, KeyDown { key: Key, modifier_keys: ModifierKeys }, KeyUp { key: Key, modifier_keys: ModifierKeys }, - MouseScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, + WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, } diff --git a/editor/src/input/input_preprocessor_message_handler.rs b/editor/src/input/input_preprocessor_message_handler.rs index da03f3715c..caea17e99c 100644 --- a/editor/src/input/input_preprocessor_message_handler.rs +++ b/editor/src/input/input_preprocessor_message_handler.rs @@ -1,6 +1,7 @@ use super::input_preprocessor::ModifierKeys; use super::keyboard::{Key, KeyStates}; use super::mouse::{MouseKeys, MouseState, ViewportBounds}; +use crate::document::utility_types::KeyboardPlatformLayout; use crate::message_prelude::*; #[doc(inline)] @@ -15,9 +16,11 @@ pub struct InputPreprocessorMessageHandler { pub viewport_bounds: ViewportBounds, } -impl MessageHandler for InputPreprocessorMessageHandler { +impl MessageHandler for InputPreprocessorMessageHandler { #[remain::check] - fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque) { + fn process_action(&mut self, message: InputPreprocessorMessage, data: KeyboardPlatformLayout, responses: &mut VecDeque) { + let keyboard_platform = data; + #[remain::sorted] match message { InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } => { @@ -53,7 +56,7 @@ impl MessageHandler for InputPreprocessorMessageHa } } InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => { - self.handle_modifier_keys(modifier_keys, responses); + self.handle_modifier_keys(modifier_keys, keyboard_platform, responses); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); self.mouse.position = mouse_state.position; @@ -61,26 +64,17 @@ impl MessageHandler for InputPreprocessorMessageHa responses.push_back(InputMapperMessage::DoubleClick.into()); } InputPreprocessorMessage::KeyDown { key, modifier_keys } => { - self.handle_modifier_keys(modifier_keys, responses); + self.handle_modifier_keys(modifier_keys, keyboard_platform, responses); self.keyboard.set(key as usize); responses.push_back(InputMapperMessage::KeyDown(key).into()); } InputPreprocessorMessage::KeyUp { key, modifier_keys } => { - self.handle_modifier_keys(modifier_keys, responses); + self.handle_modifier_keys(modifier_keys, keyboard_platform, responses); self.keyboard.unset(key as usize); responses.push_back(InputMapperMessage::KeyUp(key).into()); } - InputPreprocessorMessage::MouseScroll { editor_mouse_state, modifier_keys } => { - self.handle_modifier_keys(modifier_keys, responses); - - let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); - self.mouse.position = mouse_state.position; - self.mouse.scroll_delta = mouse_state.scroll_delta; - - responses.push_back(InputMapperMessage::MouseScroll.into()); - } InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => { - self.handle_modifier_keys(modifier_keys, responses); + self.handle_modifier_keys(modifier_keys, keyboard_platform, responses); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); self.mouse.position = mouse_state.position; @@ -88,7 +82,7 @@ impl MessageHandler for InputPreprocessorMessageHa self.translate_mouse_event(mouse_state, true, responses); } InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => { - self.handle_modifier_keys(modifier_keys, responses); + self.handle_modifier_keys(modifier_keys, keyboard_platform, responses); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); self.mouse.position = mouse_state.position; @@ -99,13 +93,22 @@ impl MessageHandler for InputPreprocessorMessageHa self.translate_mouse_event(mouse_state, false, responses); } InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => { - self.handle_modifier_keys(modifier_keys, responses); + self.handle_modifier_keys(modifier_keys, keyboard_platform, responses); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); self.mouse.position = mouse_state.position; self.translate_mouse_event(mouse_state, false, responses); } + InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys } => { + self.handle_modifier_keys(modifier_keys, keyboard_platform, responses); + + let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); + self.mouse.position = mouse_state.position; + self.mouse.scroll_delta = mouse_state.scroll_delta; + + responses.push_back(InputMapperMessage::WheelScroll.into()); + } }; } @@ -137,10 +140,15 @@ impl InputPreprocessorMessageHandler { self.mouse = new_state; } - fn handle_modifier_keys(&mut self, modifier_keys: ModifierKeys, responses: &mut VecDeque) { - self.handle_modifier_key(Key::KeyControl, modifier_keys.contains(ModifierKeys::CONTROL), responses); + fn handle_modifier_keys(&mut self, modifier_keys: ModifierKeys, keyboard_platform: KeyboardPlatformLayout, responses: &mut VecDeque) { self.handle_modifier_key(Key::KeyShift, modifier_keys.contains(ModifierKeys::SHIFT), responses); self.handle_modifier_key(Key::KeyAlt, modifier_keys.contains(ModifierKeys::ALT), responses); + self.handle_modifier_key(Key::KeyControl, modifier_keys.contains(ModifierKeys::CONTROL), responses); + let meta_or_command = match keyboard_platform { + KeyboardPlatformLayout::Mac => Key::KeyCommand, + KeyboardPlatformLayout::Standard => Key::KeyMeta, + }; + self.handle_modifier_key(meta_or_command, modifier_keys.contains(ModifierKeys::META_OR_COMMAND), responses); } fn handle_modifier_key(&mut self, key: Key, key_is_down: bool, responses: &mut VecDeque) { diff --git a/editor/src/input/keyboard.rs b/editor/src/input/keyboard.rs index 3b35931c64..dd4b067ace 100644 --- a/editor/src/input/keyboard.rs +++ b/editor/src/input/keyboard.rs @@ -1,7 +1,7 @@ use crate::message_prelude::*; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; +use std::fmt::{self, Display, Formatter}; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign}; // TODO: Increase size of type @@ -21,7 +21,7 @@ pub type KeyStates = BitVector; pub enum Key { UnknownKey, - // MouseKeys + // Mouse keys Lmb, Rmb, Mmb, @@ -70,6 +70,8 @@ pub enum Key { KeyShift, KeySpace, KeyControl, + KeyCommand, + KeyMeta, KeyDelete, KeyBackspace, KeyAlt, @@ -92,6 +94,16 @@ pub enum Key { NumKeys, } +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result { + let key_name = format!("{:?}", self); + + let name = if &key_name[0..3] == "Key" { key_name.chars().skip(3).collect::() } else { key_name }; + + write!(f, "{}", name) + } +} + pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -162,6 +174,10 @@ impl BitVector { result } + + pub fn iter(&self) -> impl Iterator + '_ { + BitVectorIter:: { bitvector: self, iter_index: 0 } + } } impl Default for BitVector { @@ -170,6 +186,29 @@ impl Default for BitVector { } } +struct BitVectorIter<'a, const LENGTH: usize> { + bitvector: &'a BitVector, + iter_index: usize, +} + +impl<'a, const LENGTH: usize> Iterator for BitVectorIter<'a, LENGTH> { + type Item = usize; + + fn next(&mut self) -> Option { + while self.iter_index < (STORAGE_SIZE_BITS as usize) * LENGTH { + let bit_value = self.bitvector.get(self.iter_index); + + self.iter_index += 1; + + if bit_value { + return Some(self.iter_index - 1); + } + } + + None + } +} + impl Display for BitVector { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for storage in self.0.iter().rev() { diff --git a/editor/src/input/mod.rs b/editor/src/input/mod.rs index fd6f4d49c3..23b7bd4a2c 100644 --- a/editor/src/input/mod.rs +++ b/editor/src/input/mod.rs @@ -3,6 +3,7 @@ pub mod input_preprocessor; pub mod keyboard; pub mod mouse; +mod input_mapper_macros; mod input_mapper_message; mod input_mapper_message_handler; mod input_preprocessor_message; diff --git a/editor/src/layout/layout_message.rs b/editor/src/layout/layout_message.rs index 1555036e78..e212cbed0b 100644 --- a/editor/src/layout/layout_message.rs +++ b/editor/src/layout/layout_message.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; #[remain::sorted] #[impl_message(Message, Layout)] -#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum LayoutMessage { RefreshLayout { layout_target: LayoutTarget }, SendLayout { layout: Layout, layout_target: LayoutTarget }, @@ -13,7 +13,7 @@ pub enum LayoutMessage { } #[remain::sorted] -#[derive(PartialEq, Clone, Deserialize, Serialize, Debug, Hash, Eq, Copy)] +#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize)] #[repr(u8)] pub enum LayoutTarget { DialogDetails, diff --git a/editor/src/layout/layout_message_handler.rs b/editor/src/layout/layout_message_handler.rs index e633c48c32..673067e01e 100644 --- a/editor/src/layout/layout_message_handler.rs +++ b/editor/src/layout/layout_message_handler.rs @@ -1,5 +1,7 @@ use super::layout_message::LayoutTarget; use super::widgets::Layout; +use crate::document::utility_types::KeyboardPlatformLayout; +use crate::input::keyboard::Key; use crate::layout::widgets::Widget; use crate::message_prelude::*; @@ -15,49 +17,55 @@ pub struct LayoutMessageHandler { impl LayoutMessageHandler { #[remain::check] - fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque) { + fn send_layout( + &self, + layout_target: LayoutTarget, + responses: &mut VecDeque, + action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec>, + keyboard_platform: KeyboardPlatformLayout, + ) { let layout = &self.layouts[layout_target as usize]; #[remain::sorted] let message = match layout_target { LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout { layout_target, - layout: layout.clone().unwrap_menu_layout().layout, + layout: layout.clone().unwrap_menu_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout { layout_target, - layout: layout.clone().unwrap_widget_layout().layout, + layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout, }, #[remain::unsorted] @@ -67,19 +75,21 @@ impl LayoutMessageHandler { } } -impl MessageHandler for LayoutMessageHandler { +impl Vec>> MessageHandler for LayoutMessageHandler { #[remain::check] - fn process_action(&mut self, action: LayoutMessage, _data: (), responses: &mut std::collections::VecDeque) { + fn process_action(&mut self, action: LayoutMessage, data: (F, KeyboardPlatformLayout), responses: &mut std::collections::VecDeque) { + let (action_input_mapping, keyboard_platform) = data; + use LayoutMessage::*; #[remain::sorted] match action { RefreshLayout { layout_target } => { - self.send_layout(layout_target, responses); + self.send_layout(layout_target, responses, &action_input_mapping, keyboard_platform); } SendLayout { layout, layout_target } => { self.layouts[layout_target as usize] = layout; - self.send_layout(layout_target, responses); + self.send_layout(layout_target, responses, &action_input_mapping, keyboard_platform); } UpdateLayout { layout_target, widget_id, value } => { let layout = &mut self.layouts[layout_target as usize]; diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs index 36bed1cb61..c6700c2b81 100644 --- a/editor/src/layout/widgets.rs +++ b/editor/src/layout/widgets.rs @@ -1,4 +1,7 @@ use super::layout_message::LayoutTarget; +use crate::document::utility_types::KeyboardPlatformLayout; +use crate::input::input_mapper::keys_text_shortcut; +use crate::input::input_mapper::ActionKeys; use crate::input::keyboard::Key; use crate::message_prelude::*; use crate::Color; @@ -30,16 +33,67 @@ pub enum Layout { } impl Layout { - pub fn unwrap_widget_layout(self) -> WidgetLayout { - if let Layout::WidgetLayout(widget_layout) = self { + pub fn unwrap_widget_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec>, keyboard_platform: KeyboardPlatformLayout) -> WidgetLayout { + if let Layout::WidgetLayout(mut widget_layout) = self { + // Function used multiple times later in this code block to convert `ActionKeys::Action` to `ActionKeys::Keys` and append its shortcut to the tooltip + let apply_shortcut_to_tooltip = |tooltip_shortcut: &mut ActionKeys, tooltip: &mut String| { + tooltip_shortcut.to_keys(action_input_mapping); + + if let ActionKeys::Keys(keys) = tooltip_shortcut { + let shortcut_text = keys_text_shortcut(keys, keyboard_platform); + + if !shortcut_text.is_empty() { + if !tooltip.is_empty() { + tooltip.push(' '); + } + tooltip.push('('); + tooltip.push_str(&shortcut_text); + tooltip.push(')'); + } + } + }; + + // Go through each widget to convert `ActionKeys::Action` to `ActionKeys::Keys` and append the key combination to the widget tooltip + for widget_holder in &mut widget_layout.iter_mut() { + // Handle all the widgets that have tooltips + let mut tooltip_shortcut = match &mut widget_holder.widget { + Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)), + _ => None, + }; + if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut { + apply_shortcut_to_tooltip(tooltip_shortcut, tooltip); + } + + // Handle RadioInput separately because its tooltips are children of the widget + if let Widget::RadioInput(radio_input) = &mut widget_holder.widget { + for radio_entry_data in &mut radio_input.entries { + if let RadioEntryData { + tooltip, + tooltip_shortcut: Some(tooltip_shortcut), + .. + } = radio_entry_data + { + apply_shortcut_to_tooltip(tooltip_shortcut, tooltip); + } + } + } + } + widget_layout } else { panic!("Tried to unwrap layout as WidgetLayout. Got {:?}", self) } } - pub fn unwrap_menu_layout(self) -> MenuLayout { - if let Layout::MenuLayout(menu_layout) = self { + pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec>, _keyboard_platform: KeyboardPlatformLayout) -> MenuLayout { + if let Layout::MenuLayout(mut menu_layout) = self { + for menu_column in &mut menu_layout.layout { + menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping); + } + menu_layout } else { panic!("Tried to unwrap layout as MenuLayout. Got {:?}", self) @@ -67,13 +121,35 @@ impl Default for Layout { } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct MenuEntryGroups(pub Vec>); + +impl MenuEntryGroups { + pub fn empty() -> Self { + Self(Vec::new()) + } + + pub fn fill_in_shortcut_actions_with_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec>) { + 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, Serialize, Deserialize, PartialEq)] pub struct MenuEntry { pub label: String, pub icon: Option, - pub children: Option>>, + pub children: MenuEntryGroups, pub action: WidgetHolder, - pub shortcut: Option>, + pub shortcut: Option, } impl MenuEntry { @@ -94,7 +170,7 @@ impl Default for MenuEntry { action: MenuEntry::create_action(|_| DialogMessage::RequestComingSoonDialog { issue: None }.into()), label: "".into(), icon: None, - children: None, + children: MenuEntryGroups::empty(), shortcut: None, } } @@ -103,10 +179,10 @@ impl Default for MenuEntry { #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] pub struct MenuColumn { pub label: String, - pub children: Vec>, + pub children: MenuEntryGroups, } -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct MenuLayout { pub layout: Vec, } @@ -118,13 +194,13 @@ impl MenuLayout { pub fn iter(&self) -> impl Iterator + '_ { MenuLayoutIter { - stack: self.layout.iter().flat_map(|column| column.children.iter()).flat_map(|group| group.iter()).collect(), + stack: self.layout.iter().flat_map(|column| column.children.0.iter()).flat_map(|group| group.iter()).collect(), } } pub fn iter_mut(&mut self) -> impl Iterator + '_ { MenuLayoutIterMut { - stack: self.layout.iter_mut().flat_map(|column| column.children.iter_mut()).flat_map(|group| group.iter_mut()).collect(), + stack: self.layout.iter_mut().flat_map(|column| column.children.0.iter_mut()).flat_map(|group| group.iter_mut()).collect(), } } } @@ -140,7 +216,9 @@ impl<'a> Iterator for MenuLayoutIter<'a> { fn next(&mut self) -> Option { match self.stack.pop() { Some(menu_entry) => { - self.stack.extend(menu_entry.children.iter().flat_map(|group| group.iter()).flat_map(|entry| entry.iter())); + let more_entries = menu_entry.children.0.iter().flat_map(|entry| entry.iter()); + self.stack.extend(more_entries); + Some(&menu_entry.action) } None => None, @@ -158,7 +236,9 @@ impl<'a> Iterator for MenuLayoutIterMut<'a> { fn next(&mut self) -> Option { match self.stack.pop() { Some(menu_entry) => { - self.stack.extend(menu_entry.children.iter_mut().flat_map(|group| group.iter_mut()).flat_map(|entry| entry.iter_mut())); + 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, @@ -280,7 +360,7 @@ impl<'a> Iterator for WidgetIterMut<'a> { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct WidgetHolder { #[serde(rename = "widgetId")] pub widget_id: u64, @@ -332,7 +412,7 @@ pub enum Widget { TextLabel(TextLabel), } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq)] pub struct CheckboxInput { pub checked: bool, @@ -341,13 +421,16 @@ pub struct CheckboxInput { pub tooltip: String, + #[serde(skip)] + pub tooltip_shortcut: Option, + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derive(Clone, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq, Default)] pub struct ColorInput { pub value: Option, @@ -362,6 +445,9 @@ pub struct ColorInput { pub tooltip: String, + #[serde(skip)] + pub tooltip_shortcut: Option, + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] @@ -435,7 +521,7 @@ pub struct FontInput { pub on_update: WidgetCallback, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq)] pub struct IconButton { pub icon: String, @@ -446,6 +532,9 @@ pub struct IconButton { pub tooltip: String, + #[serde(skip)] + pub tooltip_shortcut: Option, + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] @@ -533,7 +622,7 @@ pub enum NumberInputIncrementBehavior { Callback, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq)] pub struct OptionalInput { pub checked: bool, @@ -542,6 +631,9 @@ pub struct OptionalInput { pub tooltip: String, + #[serde(skip)] + pub tooltip_shortcut: Option, + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] @@ -559,7 +651,7 @@ pub struct PopoverButton { pub text: String, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq)] pub struct RadioInput { pub entries: Vec, @@ -569,7 +661,7 @@ pub struct RadioInput { pub selected_index: u32, } -#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derive(Clone, Default, Derivative, Serialize, Deserialize)] #[derivative(Debug, PartialEq)] pub struct RadioEntryData { pub value: String, @@ -580,6 +672,9 @@ pub struct RadioEntryData { pub tooltip: String, + #[serde(skip)] + pub tooltip_shortcut: Option, + // Callbacks #[serde(skip)] #[derivative(Debug = "ignore", PartialEq = "ignore")] diff --git a/editor/src/lib.rs b/editor/src/lib.rs index e3f3ca01d6..8759ac3675 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -91,7 +91,7 @@ pub mod message_prelude { pub use crate::viewport_tools::tools::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant}; pub use crate::viewport_tools::tools::shape_tool::{ShapeToolMessage, ShapeToolMessageDiscriminant}; pub use crate::viewport_tools::tools::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant}; - pub use crate::viewport_tools::tools::text_tool::{TextMessage, TextMessageDiscriminant}; + pub use crate::viewport_tools::tools::text_tool::{TextToolMessage, TextToolMessageDiscriminant}; pub use crate::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant}; pub use graphite_proc_macros::*; diff --git a/editor/src/misc/hints.rs b/editor/src/misc/hints.rs index 86c382bcc3..57d55e4d06 100644 --- a/editor/src/misc/hints.rs +++ b/editor/src/misc/hints.rs @@ -10,10 +10,19 @@ pub struct HintGroup(pub Vec); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct HintInfo { + /// A `KeysGroup` specifies all the keys pressed simultaneously to perform an action (like "Ctrl C" to copy). + /// Usually at most one is given, but less commonly, multiple can be used to describe additional hotkeys not used simultaneously (like the four different arrow keys to nudge a layer). + #[serde(rename = "keyGroups")] pub key_groups: Vec, + /// `None` means that the regular `key_groups` should be used for all platforms, `Some` is an override for a Mac-only input hint. + #[serde(rename = "keyGroupsMac")] + pub key_groups_mac: Option>, + /// An optional `MouseMotion` that can indicate the mouse action, like which mouse button is used and whether a drag occurs. + /// No such icon is shown if `None` is given, and it can be combined with `key_groups` if desired. pub mouse: Option, + /// The text describing what occurs with this input combination. pub label: String, - /// Prepend the "+" symbol indicating that this is a refinement upon a previous entry in the group. + /// Draws a prepended "+" symbol which indicates that this is a refinement upon a previous hint in the group. pub plus: bool, } diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index 50939215a8..7e95ba6ac6 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -1,6 +1,8 @@ use super::tools::*; use crate::communication::message_handler::MessageHandler; use crate::document::DocumentMessageHandler; +use crate::input::input_mapper::action_keys::action_shortcut; +use crate::input::input_mapper::ActionKeys; use crate::input::InputPreprocessorMessageHandler; use crate::layout::widgets::{IconButton, Layout, LayoutGroup, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; @@ -115,6 +117,7 @@ impl ToolData { #[derive(Debug)] pub struct ToolBarMetadataGroup { pub tooltip: String, + pub tooltip_shortcut: Option, pub icon_name: String, pub tool_type: ToolType, } @@ -123,18 +126,24 @@ impl PropertyHolder for ToolData { fn properties(&self) -> Layout { let tool_groups_layout = list_tools_in_groups() .iter() - .map(|tool_group| tool_group.iter().map(|tool| ToolBarMetadataGroup {tooltip: tool.tooltip(), icon_name: tool.icon_name(), tool_type: tool.tool_type()}).collect::>()) + .map(|tool_group| tool_group.iter().map(|tool| ToolBarMetadataGroup { + tooltip: tool.tooltip(), + tooltip_shortcut: action_shortcut!(tool_type_to_activate_tool_message(tool.tool_type())), + icon_name: tool.icon_name(), + tool_type: tool.tool_type(), + }).collect::>()) .chain(coming_soon_tools()) .flat_map(|group| { let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator { direction: SeparatorDirection::Vertical, separator_type: SeparatorType::Section, }))); - let buttons = group.into_iter().map(|ToolBarMetadataGroup {tooltip, tool_type, icon_name}| { + let buttons = group.into_iter().map(|ToolBarMetadataGroup { tooltip, tooltip_shortcut, tool_type, icon_name }| { WidgetHolder::new(Widget::IconButton(IconButton { icon: icon_name, size: 32, tooltip: tooltip.clone(), + tooltip_shortcut, active: self.active_tool_type == tool_type, on_update: WidgetCallback::new(move |_| { if !tooltip.contains("Coming Soon") { @@ -143,7 +152,6 @@ impl PropertyHolder for ToolData { DialogMessage::RequestComingSoonDialog { issue: None }.into() } }), - ..Default::default() })) }); separator.chain(buttons) @@ -253,68 +261,107 @@ pub fn coming_soon_tools() -> Vec> { tool_type: ToolType::Brush, icon_name: "RasterBrushTool".into(), tooltip: "Coming Soon: Brush Tool (B)".into(), + tooltip_shortcut: None, }, ToolBarMetadataGroup { tool_type: ToolType::Heal, icon_name: "RasterHealTool".into(), tooltip: "Coming Soon: Heal Tool (J)".into(), + tooltip_shortcut: None, }, ToolBarMetadataGroup { tool_type: ToolType::Clone, icon_name: "RasterCloneTool".into(), - tooltip: "Coming Soon: Clone Tool (C))".into(), + tooltip: "Coming Soon: Clone Tool (C)".into(), + tooltip_shortcut: None, }, ToolBarMetadataGroup { tool_type: ToolType::Patch, icon_name: "RasterPatchTool".into(), tooltip: "Coming Soon: Patch Tool".into(), + tooltip_shortcut: None, }, ToolBarMetadataGroup { tool_type: ToolType::Detail, icon_name: "RasterDetailTool".into(), tooltip: "Coming Soon: Detail Tool (D)".into(), + tooltip_shortcut: None, }, ToolBarMetadataGroup { tool_type: ToolType::Relight, icon_name: "RasterRelightTool".into(), - tooltip: "Coming Soon: Relight Tool (O".into(), + tooltip: "Coming Soon: Relight Tool (O)".into(), + tooltip_shortcut: None, }, ]] } -pub fn message_to_tool_type(message: &ToolMessage) -> ToolType { - use ToolMessage::*; - - match message { +pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType { + match tool_message { // General tool group - Select(_) => ToolType::Select, - Artboard(_) => ToolType::Artboard, - Navigate(_) => ToolType::Navigate, - Eyedropper(_) => ToolType::Eyedropper, - Fill(_) => ToolType::Fill, - Gradient(_) => ToolType::Gradient, + ToolMessage::Select(_) => ToolType::Select, + ToolMessage::Artboard(_) => ToolType::Artboard, + ToolMessage::Navigate(_) => ToolType::Navigate, + ToolMessage::Eyedropper(_) => ToolType::Eyedropper, + ToolMessage::Fill(_) => ToolType::Fill, + ToolMessage::Gradient(_) => ToolType::Gradient, // Vector tool group - Path(_) => ToolType::Path, - Pen(_) => ToolType::Pen, - Freehand(_) => ToolType::Freehand, - Spline(_) => ToolType::Spline, - Line(_) => ToolType::Line, - Rectangle(_) => ToolType::Rectangle, - Ellipse(_) => ToolType::Ellipse, - Shape(_) => ToolType::Shape, - Text(_) => ToolType::Text, + ToolMessage::Path(_) => ToolType::Path, + ToolMessage::Pen(_) => ToolType::Pen, + ToolMessage::Freehand(_) => ToolType::Freehand, + ToolMessage::Spline(_) => ToolType::Spline, + ToolMessage::Line(_) => ToolType::Line, + ToolMessage::Rectangle(_) => ToolType::Rectangle, + ToolMessage::Ellipse(_) => ToolType::Ellipse, + ToolMessage::Shape(_) => ToolType::Shape, + ToolMessage::Text(_) => ToolType::Text, // Raster tool group - // Brush(_) => ToolType::Brush, - // Heal(_) => ToolType::Heal, - // Clone(_) => ToolType::Clone, - // Patch(_) => ToolType::Patch, - // Detail(_) => ToolType::Detail, - // Relight(_) => ToolType::Relight, + // ToolMessage::Brush(_) => ToolType::Brush, + // ToolMessage::Heal(_) => ToolType::Heal, + // ToolMessage::Clone(_) => ToolType::Clone, + // ToolMessage::Patch(_) => ToolType::Patch, + // ToolMessage::Detail(_) => ToolType::Detail, + // ToolMessage::Relight(_) => ToolType::Relight, _ => panic!( - "Conversion from message to tool type impossible because the given ToolMessage does not belong to a tool. Got: {:?}", - message + "Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}", + tool_message + ), + } +} + +pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDiscriminant { + match tool_type { + // General tool group + ToolType::Select => ToolMessageDiscriminant::ActivateToolSelect, + ToolType::Artboard => ToolMessageDiscriminant::ActivateToolArtboard, + ToolType::Navigate => ToolMessageDiscriminant::ActivateToolNavigate, + ToolType::Eyedropper => ToolMessageDiscriminant::ActivateToolEyedropper, + ToolType::Fill => ToolMessageDiscriminant::ActivateToolFill, + ToolType::Gradient => ToolMessageDiscriminant::ActivateToolGradient, + + // Vector tool group + ToolType::Path => ToolMessageDiscriminant::ActivateToolPath, + ToolType::Pen => ToolMessageDiscriminant::ActivateToolPen, + ToolType::Freehand => ToolMessageDiscriminant::ActivateToolFreehand, + ToolType::Spline => ToolMessageDiscriminant::ActivateToolSpline, + ToolType::Line => ToolMessageDiscriminant::ActivateToolLine, + ToolType::Rectangle => ToolMessageDiscriminant::ActivateToolRectangle, + ToolType::Ellipse => ToolMessageDiscriminant::ActivateToolEllipse, + ToolType::Shape => ToolMessageDiscriminant::ActivateToolShape, + ToolType::Text => ToolMessageDiscriminant::ActivateToolText, + + // Raster tool group + // ToolType::Brush => ToolMessageDiscriminant::ActivateToolBrush, + // ToolType::Heal => ToolMessageDiscriminant::ActivateToolHeal, + // ToolType::Clone => ToolMessageDiscriminant::ActivateToolClone, + // ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch, + // ToolType::Detail => ToolMessageDiscriminant::ActivateToolDetail, + // ToolType::Relight => ToolMessageDiscriminant::ActivateToolRelight, + _ => panic!( + "Conversion from ToolType to ToolMessage impossible because the given ToolType does not have a matching ToolMessage. Got: {:?}", + tool_type ), } } diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index 93a51300dd..1417ec26f4 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -28,6 +28,7 @@ pub enum ToolMessage { #[remain::unsorted] #[child] Gradient(GradientToolMessage), + #[remain::unsorted] #[child] Path(PathToolMessage), @@ -54,7 +55,8 @@ pub enum ToolMessage { Shape(ShapeToolMessage), #[remain::unsorted] #[child] - Text(TextMessage), + Text(TextToolMessage), + // #[remain::unsorted] // #[child] // Brush(BrushToolMessage), @@ -75,6 +77,38 @@ pub enum ToolMessage { // Detail(DetailToolMessage), // Messages + #[remain::unsorted] + ActivateToolSelect, + #[remain::unsorted] + ActivateToolArtboard, + #[remain::unsorted] + ActivateToolNavigate, + #[remain::unsorted] + ActivateToolEyedropper, + #[remain::unsorted] + ActivateToolText, + #[remain::unsorted] + ActivateToolFill, + #[remain::unsorted] + ActivateToolGradient, + + #[remain::unsorted] + ActivateToolPath, + #[remain::unsorted] + ActivateToolPen, + #[remain::unsorted] + ActivateToolFreehand, + #[remain::unsorted] + ActivateToolSpline, + #[remain::unsorted] + ActivateToolLine, + #[remain::unsorted] + ActivateToolRectangle, + #[remain::unsorted] + ActivateToolEllipse, + #[remain::unsorted] + ActivateToolShape, + ActivateTool { tool_type: ToolType, }, diff --git a/editor/src/viewport_tools/tool_message_handler.rs b/editor/src/viewport_tools/tool_message_handler.rs index f536919e16..169d26e29e 100644 --- a/editor/src/viewport_tools/tool_message_handler.rs +++ b/editor/src/viewport_tools/tool_message_handler.rs @@ -1,11 +1,12 @@ -use super::tool::{message_to_tool_type, ToolFsmState}; +use super::tool::{tool_message_to_tool_type, ToolFsmState}; use crate::document::DocumentMessageHandler; +use crate::input::input_mapper::action_keys::action_shortcut; use crate::input::InputPreprocessorMessageHandler; use crate::layout::layout_message::LayoutTarget; use crate::layout::widgets::PropertyHolder; use crate::layout::widgets::{IconButton, Layout, LayoutGroup, SwatchPairInput, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; -use crate::viewport_tools::tool::DocumentToolData; +use crate::viewport_tools::tool::{DocumentToolData, ToolType}; use graphene::color::Color; use graphene::layers::text_layer::FontCache; @@ -26,6 +27,38 @@ impl MessageHandler responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Select }.into()), + #[remain::unsorted] + ActivateToolArtboard => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Artboard }.into()), + #[remain::unsorted] + ActivateToolNavigate => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Navigate }.into()), + #[remain::unsorted] + ActivateToolEyedropper => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }.into()), + #[remain::unsorted] + ActivateToolText => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()), + #[remain::unsorted] + ActivateToolFill => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Fill }.into()), + #[remain::unsorted] + ActivateToolGradient => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Gradient }.into()), + + #[remain::unsorted] + ActivateToolPath => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()), + #[remain::unsorted] + ActivateToolPen => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Pen }.into()), + #[remain::unsorted] + ActivateToolFreehand => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Freehand }.into()), + #[remain::unsorted] + ActivateToolSpline => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Spline }.into()), + #[remain::unsorted] + ActivateToolLine => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Line }.into()), + #[remain::unsorted] + ActivateToolRectangle => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }.into()), + #[remain::unsorted] + ActivateToolEllipse => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }.into()), + #[remain::unsorted] + ActivateToolShape => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }.into()), + ActivateTool { tool_type } => { let tool_data = &mut self.tool_state.tool_data; let document_data = &self.tool_state.document_tool_data; @@ -151,7 +184,7 @@ impl MessageHandler { let tool_type = match &tool_message { UpdateCursor | UpdateHints => self.tool_state.tool_data.active_tool_type, - tool_message => message_to_tool_type(tool_message), + tool_message => tool_message_to_tool_type(tool_message), }; let document_data = &self.tool_state.document_tool_data; let tool_data = &mut self.tool_state.tool_data; @@ -167,7 +200,21 @@ impl MessageHandler ActionList { let mut list = actions!(ToolMessageDiscriminant; - ActivateTool, + ActivateToolSelect, + ActivateToolArtboard, + ActivateToolNavigate, + ActivateToolEyedropper, + ActivateToolText, + ActivateToolFill, + ActivateToolGradient, + ActivateToolPath, + ActivateToolPen, + ActivateToolFreehand, + ActivateToolSpline, + ActivateToolLine, + ActivateToolRectangle, + ActivateToolEllipse, + ActivateToolShape, SelectRandomPrimaryColor, ResetColors, SwapColors, @@ -191,14 +238,16 @@ fn update_working_colors(document_data: &DocumentToolData, responses: &mut VecDe WidgetHolder::new(Widget::IconButton(IconButton { size: 16, icon: "Swap".into(), - tooltip: "Swap (Shift+X)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut + tooltip: "Swap".into(), + tooltip_shortcut: action_shortcut!(ToolMessageDiscriminant::SwapColors), on_update: WidgetCallback::new(|_| ToolMessage::SwapColors.into()), ..Default::default() })), WidgetHolder::new(Widget::IconButton(IconButton { size: 16, - icon: "ResetColors".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut - tooltip: "Reset (Ctrl+Shift+X)".into(), + icon: "ResetColors".into(), + tooltip: "Reset".into(), + tooltip_shortcut: action_shortcut!(ToolMessageDiscriminant::ResetColors), on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()), ..Default::default() })), diff --git a/editor/src/viewport_tools/tools/artboard_tool.rs b/editor/src/viewport_tools/tools/artboard_tool.rs index 6e039c1d3b..74c4414045 100644 --- a/editor/src/viewport_tools/tools/artboard_tool.rs +++ b/editor/src/viewport_tools/tools/artboard_tool.rs @@ -243,8 +243,10 @@ impl Fsm for ArtboardToolFsmState { let mouse_position = input.mouse.position; let snapped_mouse_position = tool_data.snap_handler.snap_position(responses, document, mouse_position); - let [position, size] = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square); - let position = movement.center_position(position, size, from_center); + let (mut position, size) = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square); + if from_center { + position = movement.center_position(position, size); + } responses.push_back( ArtboardMessage::ResizeArtboard { @@ -408,18 +410,21 @@ impl Fsm for ArtboardToolFsmState { ArtboardToolFsmState::Ready => HintData(vec![ HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Draw Artboard"), plus: false, }]), HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Move Artboard"), plus: false, }]), HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyBackspace])], + key_groups_mac: None, mouse: None, label: String::from("Delete Artboard"), plus: false, @@ -427,6 +432,7 @@ impl Fsm for ArtboardToolFsmState { ]), ArtboardToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain to Axis"), plus: false, @@ -434,12 +440,14 @@ impl Fsm for ArtboardToolFsmState { ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain Square"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: false, diff --git a/editor/src/viewport_tools/tools/ellipse_tool.rs b/editor/src/viewport_tools/tools/ellipse_tool.rs index 1118f6b402..9f86d7c157 100644 --- a/editor/src/viewport_tools/tools/ellipse_tool.rs +++ b/editor/src/viewport_tools/tools/ellipse_tool.rs @@ -41,7 +41,7 @@ impl ToolMetadata for EllipseTool { "VectorEllipseTool".into() } fn tooltip(&self) -> String { - "Ellipse Tool (E)".into() + "Ellipse Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Ellipse @@ -185,18 +185,21 @@ impl Fsm for EllipseToolFsmState { EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Draw Ellipse"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain Circular"), plus: true, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: true, @@ -205,12 +208,14 @@ impl Fsm for EllipseToolFsmState { EllipseToolFsmState::Drawing => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain Circular"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: false, diff --git a/editor/src/viewport_tools/tools/eyedropper_tool.rs b/editor/src/viewport_tools/tools/eyedropper_tool.rs index 1c62f5dcb4..10aa443a1a 100644 --- a/editor/src/viewport_tools/tools/eyedropper_tool.rs +++ b/editor/src/viewport_tools/tools/eyedropper_tool.rs @@ -36,7 +36,7 @@ impl ToolMetadata for EyedropperTool { "GeneralEyedropperTool".into() } fn tooltip(&self) -> String { - "Eyedropper Tool (I)".into() + "Eyedropper Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Eyedropper @@ -147,12 +147,14 @@ impl Fsm for EyedropperToolFsmState { EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Sample to Primary"), plus: false, }, HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Rmb), label: String::from("Sample to Secondary"), plus: false, diff --git a/editor/src/viewport_tools/tools/fill_tool.rs b/editor/src/viewport_tools/tools/fill_tool.rs index 9394b935fa..11f2be4085 100644 --- a/editor/src/viewport_tools/tools/fill_tool.rs +++ b/editor/src/viewport_tools/tools/fill_tool.rs @@ -37,7 +37,7 @@ impl ToolMetadata for FillTool { "GeneralFillTool".into() } fn tooltip(&self) -> String { - "Fill Tool (F)".into() + "Fill Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Fill @@ -146,12 +146,14 @@ impl Fsm for FillToolFsmState { FillToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Fill with Primary"), plus: false, }, HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Rmb), label: String::from("Fill with Secondary"), plus: false, diff --git a/editor/src/viewport_tools/tools/freehand_tool.rs b/editor/src/viewport_tools/tools/freehand_tool.rs index 5cc4605752..3af5fa1f32 100644 --- a/editor/src/viewport_tools/tools/freehand_tool.rs +++ b/editor/src/viewport_tools/tools/freehand_tool.rs @@ -60,7 +60,7 @@ impl ToolMetadata for FreehandTool { "VectorFreehandTool".into() } fn tooltip(&self) -> String { - "Freehand Tool (N)".into() + "Freehand Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Freehand @@ -222,6 +222,7 @@ impl Fsm for FreehandToolFsmState { let hint_data = match self { FreehandToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Draw Polyline"), plus: false, diff --git a/editor/src/viewport_tools/tools/gradient_tool.rs b/editor/src/viewport_tools/tools/gradient_tool.rs index a0a380bb59..3e712fd7c2 100644 --- a/editor/src/viewport_tools/tools/gradient_tool.rs +++ b/editor/src/viewport_tools/tools/gradient_tool.rs @@ -65,7 +65,7 @@ impl ToolMetadata for GradientTool { "GeneralGradientTool".into() } fn tooltip(&self) -> String { - "Gradient Tool (H))".into() + "Gradient Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Gradient @@ -461,12 +461,14 @@ impl Fsm for GradientToolFsmState { GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Draw Gradient"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: true, @@ -474,6 +476,7 @@ impl Fsm for GradientToolFsmState { ])]), GradientToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: false, diff --git a/editor/src/viewport_tools/tools/line_tool.rs b/editor/src/viewport_tools/tools/line_tool.rs index a12c103ce0..ecf3331ab0 100644 --- a/editor/src/viewport_tools/tools/line_tool.rs +++ b/editor/src/viewport_tools/tools/line_tool.rs @@ -61,7 +61,7 @@ impl ToolMetadata for LineTool { "VectorLineTool".into() } fn tooltip(&self) -> String { - "Line Tool (L)".into() + "Line Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Line @@ -239,24 +239,28 @@ impl Fsm for LineToolFsmState { LineToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Draw Line"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: true, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: true, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Lock Angle"), plus: true, @@ -265,18 +269,21 @@ impl Fsm for LineToolFsmState { LineToolFsmState::Drawing => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Lock Angle"), plus: false, diff --git a/editor/src/viewport_tools/tools/navigate_tool.rs b/editor/src/viewport_tools/tools/navigate_tool.rs index f4ec3ba8d2..4924d18569 100644 --- a/editor/src/viewport_tools/tools/navigate_tool.rs +++ b/editor/src/viewport_tools/tools/navigate_tool.rs @@ -41,7 +41,7 @@ impl ToolMetadata for NavigateTool { "GeneralNavigateTool".into() } fn tooltip(&self) -> String { - "Navigate Tool (Z)".into() + "Navigate Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Navigate @@ -195,12 +195,14 @@ impl Fsm for NavigateToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Zoom In"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Zoom Out"), plus: true, @@ -209,12 +211,14 @@ impl Fsm for NavigateToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Zoom"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Snap Increments"), plus: true, @@ -222,6 +226,7 @@ impl Fsm for NavigateToolFsmState { ]), HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::MmbDrag), label: String::from("Pan"), plus: false, @@ -229,12 +234,14 @@ impl Fsm for NavigateToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::RmbDrag), label: String::from("Tilt"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: true, @@ -243,12 +250,14 @@ impl Fsm for NavigateToolFsmState { ]), NavigateToolFsmState::Tilting => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: false, }])]), NavigateToolFsmState::Zooming => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Snap Increments"), plus: false, diff --git a/editor/src/viewport_tools/tools/path_tool.rs b/editor/src/viewport_tools/tools/path_tool.rs index 6c91285145..35d7562348 100644 --- a/editor/src/viewport_tools/tools/path_tool.rs +++ b/editor/src/viewport_tools/tools/path_tool.rs @@ -50,7 +50,7 @@ impl ToolMetadata for PathTool { "VectorPathTool".into() } fn tooltip(&self) -> String { - "Path Tool (A)".into() + "Path Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Path @@ -306,12 +306,14 @@ impl Fsm for PathToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Select Point"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Grow/Shrink Selection"), plus: true, @@ -319,6 +321,7 @@ impl Fsm for PathToolFsmState { ]), HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Drag Selected"), plus: false, @@ -331,12 +334,14 @@ impl Fsm for PathToolFsmState { KeysGroup(vec![Key::KeyArrowDown]), KeysGroup(vec![Key::KeyArrowLeft]), ], + key_groups_mac: None, mouse: None, label: String::from("Nudge Selected (coming soon)"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Big Increment Nudge"), plus: true, @@ -345,18 +350,21 @@ impl Fsm for PathToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyG])], + key_groups_mac: None, mouse: None, label: String::from("Grab Selected (coming soon)"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyR])], + key_groups_mac: None, mouse: None, label: String::from("Rotate Selected (coming soon)"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyS])], + key_groups_mac: None, mouse: None, label: String::from("Scale Selected (coming soon)"), plus: false, @@ -366,12 +374,14 @@ impl Fsm for PathToolFsmState { PathToolFsmState::Dragging => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("Split/Align Handles (Toggle)"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Share Lengths of Aligned Handles"), plus: false, diff --git a/editor/src/viewport_tools/tools/pen_tool.rs b/editor/src/viewport_tools/tools/pen_tool.rs index 4539de2735..48b0c14c2e 100644 --- a/editor/src/viewport_tools/tools/pen_tool.rs +++ b/editor/src/viewport_tools/tools/pen_tool.rs @@ -78,7 +78,7 @@ impl ToolMetadata for PenTool { "VectorPenTool".into() } fn tooltip(&self) -> String { - "Pen Tool (P)".into() + "Pen Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Pen @@ -374,6 +374,7 @@ impl Fsm for PenToolFsmState { let hint_data = match self { PenToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Draw Path"), plus: false, @@ -381,30 +382,35 @@ impl Fsm for PenToolFsmState { PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => HintData(vec![ HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Add Handle"), plus: false, }]), HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Add Anchor"), plus: false, }]), HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: false, }]), HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Break Handle"), plus: false, }]), HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyEnter])], + key_groups_mac: None, mouse: None, label: String::from("End Path"), plus: false, diff --git a/editor/src/viewport_tools/tools/rectangle_tool.rs b/editor/src/viewport_tools/tools/rectangle_tool.rs index 55c76a8d58..c7b5e94141 100644 --- a/editor/src/viewport_tools/tools/rectangle_tool.rs +++ b/editor/src/viewport_tools/tools/rectangle_tool.rs @@ -80,7 +80,7 @@ impl ToolMetadata for RectangleTool { "VectorRectangleTool".into() } fn tooltip(&self) -> String { - "Rectangle Tool (M)".into() + "Rectangle Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Rectangle @@ -186,18 +186,21 @@ impl Fsm for RectangleToolFsmState { RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Draw Rectangle"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain Square"), plus: true, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: true, @@ -206,12 +209,14 @@ impl Fsm for RectangleToolFsmState { RectangleToolFsmState::Drawing => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain Square"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: false, diff --git a/editor/src/viewport_tools/tools/select_tool.rs b/editor/src/viewport_tools/tools/select_tool.rs index c3ac6f42f5..c11ebf53b8 100644 --- a/editor/src/viewport_tools/tools/select_tool.rs +++ b/editor/src/viewport_tools/tools/select_tool.rs @@ -63,7 +63,7 @@ impl ToolMetadata for SelectTool { "GeneralSelectTool".into() } fn tooltip(&self) -> String { - "Select Tool (V)".into() + "Select Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Select @@ -390,7 +390,7 @@ impl Fsm for SelectToolFsmState { match intersect.data { LayerDataType::Text(_) => { responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()); - responses.push_back(TextMessage::Interact.into()); + responses.push_back(TextToolMessage::Interact.into()); } LayerDataType::Shape(_) => { responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()); @@ -529,7 +529,7 @@ impl Fsm for SelectToolFsmState { let snapped_mouse_position = tool_data.snap_handler.snap_position(responses, document, mouse_position); - let [_position, size] = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align); + let (_, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align); let delta = movement.bounds_to_scale_transform(center, size); let selected = tool_data.layers_dragging.iter().collect::>(); @@ -702,6 +702,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Ready => HintData(vec![ HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Drag Selected"), plus: false, @@ -709,18 +710,21 @@ impl Fsm for SelectToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyG])], + key_groups_mac: None, mouse: None, label: String::from("Grab Selected"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyR])], + key_groups_mac: None, mouse: None, label: String::from("Rotate Selected"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyS])], + key_groups_mac: None, mouse: None, label: String::from("Scale Selected"), plus: false, @@ -729,18 +733,21 @@ impl Fsm for SelectToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Select Object"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand])]), mouse: None, label: String::from("Innermost"), plus: true, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Grow/Shrink Selection"), plus: true, @@ -749,12 +756,14 @@ impl Fsm for SelectToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Select Area"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Grow/Shrink Selection"), plus: true, @@ -768,12 +777,14 @@ impl Fsm for SelectToolFsmState { KeysGroup(vec![Key::KeyArrowDown]), KeysGroup(vec![Key::KeyArrowLeft]), ], + key_groups_mac: None, mouse: None, label: String::from("Nudge Selected"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Big Increment Nudge"), plus: true, @@ -782,12 +793,14 @@ impl Fsm for SelectToolFsmState { HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Move Duplicate"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyD])], + key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyD])]), mouse: None, label: String::from("Duplicate"), plus: false, @@ -797,12 +810,14 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Dragging => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain to Axis"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Snap to Points (coming soon)"), plus: false, @@ -812,6 +827,7 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::ResizingBounds => HintData(vec![]), SelectToolFsmState::RotatingBounds => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], + key_groups_mac: None, mouse: None, label: String::from("Snap 15°"), plus: false, diff --git a/editor/src/viewport_tools/tools/shape_tool.rs b/editor/src/viewport_tools/tools/shape_tool.rs index 2c56e51975..f695297cac 100644 --- a/editor/src/viewport_tools/tools/shape_tool.rs +++ b/editor/src/viewport_tools/tools/shape_tool.rs @@ -59,7 +59,7 @@ impl ToolMetadata for ShapeTool { "VectorShapeTool".into() } fn tooltip(&self) -> String { - "Shape Tool (Y)".into() + "Shape Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Shape @@ -228,18 +228,21 @@ impl Fsm for ShapeToolFsmState { ShapeToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::LmbDrag), label: String::from("Draw Shape"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain 1:1 Aspect"), plus: true, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: true, @@ -248,12 +251,14 @@ impl Fsm for ShapeToolFsmState { ShapeToolFsmState::Drawing => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyShift])], + key_groups_mac: None, mouse: None, label: String::from("Constrain 1:1 Aspect"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + key_groups_mac: None, mouse: None, label: String::from("From Center"), plus: false, diff --git a/editor/src/viewport_tools/tools/shared/transformation_cage.rs b/editor/src/viewport_tools/tools/shared/transformation_cage.rs index 74ab45ee02..5ec5fd74b3 100644 --- a/editor/src/viewport_tools/tools/shared/transformation_cage.rs +++ b/editor/src/viewport_tools/tools/shared/transformation_cage.rs @@ -61,7 +61,7 @@ impl SelectedEdges { } /// Computes the new bounds with the given mouse move and modifier keys - pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, constrain: bool) -> [DVec2; 2] { + pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, constrain: bool) -> (DVec2, DVec2) { let mouse = transform.inverse().transform_point2(mouse); let mut min = self.bounds[0]; @@ -72,7 +72,9 @@ impl SelectedEdges { max.y = mouse.y; } if self.left { - min.x = mouse.x + let delta = min.x - mouse.x; + min.x = mouse.x; + max.x += delta; } else if self.right { max.x = mouse.x; } @@ -96,34 +98,38 @@ impl SelectedEdges { } } - [min, size] + (min, size) } /// Offsets the transformation pivot in order to scale from the center fn offset_pivot(&self, center: bool, size: DVec2) -> DVec2 { let mut offset = DVec2::ZERO; - if center && self.right { + if !center { + return offset; + } + + if self.right { offset.x -= size.x / 2.; } - if center && self.left { + if self.left { offset.x += size.x / 2.; } - if center && self.bottom { + if self.bottom { offset.y -= size.y / 2.; } - if center && self.top { + if self.top { offset.y += size.y / 2.; } offset } - /// Moves the position to account for centring (only necessary with absolute transforms - e.g. with artboards) - pub fn center_position(&self, mut position: DVec2, size: DVec2, center: bool) -> DVec2 { - if center && self.right { + /// Moves the position to account for centering (only necessary with absolute transforms - e.g. with artboards) + pub fn center_position(&self, mut position: DVec2, size: DVec2) -> DVec2 { + if self.right { position.x -= size.x / 2.; } - if center && self.bottom { + if self.bottom { position.y -= size.y / 2.; } diff --git a/editor/src/viewport_tools/tools/spline_tool.rs b/editor/src/viewport_tools/tools/spline_tool.rs index b75a2f3bc4..4c7a5f5fc8 100644 --- a/editor/src/viewport_tools/tools/spline_tool.rs +++ b/editor/src/viewport_tools/tools/spline_tool.rs @@ -251,6 +251,7 @@ impl Fsm for SplineToolFsmState { let hint_data = match self { SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Draw Spline"), plus: false, @@ -258,12 +259,14 @@ impl Fsm for SplineToolFsmState { SplineToolFsmState::Drawing => HintData(vec![ HintGroup(vec![HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Extend Spline"), plus: false, }]), HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyEnter])], + key_groups_mac: None, mouse: None, label: String::from("End Spline"), plus: false, diff --git a/editor/src/viewport_tools/tools/text_tool.rs b/editor/src/viewport_tools/tools/text_tool.rs index 14787334a9..d9ec8482a7 100644 --- a/editor/src/viewport_tools/tools/text_tool.rs +++ b/editor/src/viewport_tools/tools/text_tool.rs @@ -42,7 +42,7 @@ impl Default for TextOptions { #[remain::sorted] #[impl_message(Message, ToolMessage, Text)] #[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)] -pub enum TextMessage { +pub enum TextToolMessage { // Standard messages #[remain::unsorted] Abort, @@ -74,7 +74,7 @@ impl ToolMetadata for TextTool { "VectorTextTool".into() } fn tooltip(&self) -> String { - "Text Tool (T)".into() + "Text Tool".into() } fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { ToolType::Text @@ -90,7 +90,7 @@ impl PropertyHolder for TextTool { font_family: self.options.font_name.clone(), font_style: self.options.font_style.clone(), on_update: WidgetCallback::new(|font_input: &FontInput| { - TextMessage::UpdateOptions(TextOptionsUpdate::Font { + TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { family: font_input.font_family.clone(), style: font_input.font_style.clone(), }) @@ -107,7 +107,7 @@ impl PropertyHolder for TextTool { font_family: self.options.font_name.clone(), font_style: self.options.font_style.clone(), on_update: WidgetCallback::new(|font_input: &FontInput| { - TextMessage::UpdateOptions(TextOptionsUpdate::Font { + TextToolMessage::UpdateOptions(TextOptionsUpdate::Font { family: font_input.font_family.clone(), style: font_input.font_style.clone(), }) @@ -125,7 +125,7 @@ impl PropertyHolder for TextTool { value: Some(self.options.font_size as f64), is_integer: true, min: Some(1.), - on_update: WidgetCallback::new(|number_input: &NumberInput| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()), + on_update: WidgetCallback::new(|number_input: &NumberInput| TextToolMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value.unwrap() as u32)).into()), ..NumberInput::default() })), ], @@ -145,7 +145,7 @@ impl<'a> MessageHandler> for TextTool { return; } - if let ToolMessage::Text(TextMessage::UpdateOptions(action)) = action { + if let ToolMessage::Text(TextToolMessage::UpdateOptions(action)) = action { match action { TextOptionsUpdate::Font { family, style } => { self.options.font_name = family; @@ -171,10 +171,10 @@ impl<'a> MessageHandler> for TextTool { use TextToolFsmState::*; match self.fsm_state { - Ready => actions!(TextMessageDiscriminant; + Ready => actions!(TextToolMessageDiscriminant; Interact, ), - Editing => actions!(TextMessageDiscriminant; + Editing => actions!(TextToolMessageDiscriminant; Interact, Abort, CommitText, @@ -186,8 +186,8 @@ impl<'a> MessageHandler> for TextTool { impl ToolTransition for TextTool { fn signal_to_message_map(&self) -> SignalToMessageMap { SignalToMessageMap { - document_dirty: Some(TextMessage::DocumentIsDirty.into()), - tool_abort: Some(TextMessage::Abort.into()), + document_dirty: Some(TextToolMessage::DocumentIsDirty.into()), + tool_abort: Some(TextToolMessage::Abort.into()), selection_changed: None, } } @@ -275,8 +275,8 @@ impl Fsm for TextToolFsmState { tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { - use TextMessage::*; use TextToolFsmState::*; + use TextToolMessage::*; if let ToolMessage::Text(event) = event { match (self, event) { @@ -456,12 +456,14 @@ impl Fsm for TextToolFsmState { TextToolFsmState::Ready => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Add Text"), plus: false, }, HintInfo { key_groups: vec![], + key_groups_mac: None, mouse: Some(MouseMotion::Lmb), label: String::from("Edit Text"), plus: false, @@ -470,12 +472,14 @@ impl Fsm for TextToolFsmState { TextToolFsmState::Editing => HintData(vec![HintGroup(vec![ HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyEnter])], + key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyEnter])]), mouse: None, label: String::from("Commit Edit"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyEscape])], + key_groups_mac: None, mouse: None, label: String::from("Discard Edit"), plus: false, diff --git a/frontend/assets/icon-12px-solid/keyboard-control.svg b/frontend/assets/icon-12px-solid/keyboard-control.svg new file mode 100644 index 0000000000..591d2c0be6 --- /dev/null +++ b/frontend/assets/icon-12px-solid/keyboard-control.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a35903dfbf..def44c424d 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -230,6 +230,7 @@ import { createFullscreenState, FullscreenState } from "@/state-providers/fullsc import { createPanelsState, PanelsState } from "@/state-providers/panels"; import { createPortfolioState, PortfolioState } from "@/state-providers/portfolio"; import { createWorkspaceState, WorkspaceState } from "@/state-providers/workspace"; +import { operatingSystem } from "@/utility-functions/platform"; import { createEditor, Editor } from "@/wasm-communication/editor"; import MainWindow from "@/components/window/MainWindow.vue"; @@ -293,7 +294,8 @@ export default defineComponent({ }); // Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready - this.editor.instance.init_after_frontend_ready(); + const platform = operatingSystem(); + this.editor.instance.init_after_frontend_ready(platform); }, beforeUnmount() { // Call the destructor for each manager diff --git a/frontend/src/components/floating-menus/MenuList.vue b/frontend/src/components/floating-menus/MenuList.vue index cc168d3946..26479260ab 100644 --- a/frontend/src/components/floating-menus/MenuList.vue +++ b/frontend/src/components/floating-menus/MenuList.vue @@ -33,8 +33,7 @@ - - +
@@ -64,6 +63,10 @@ .floating-menu-container .floating-menu-content { padding: 4px 0; + .separator div { + background: var(--color-4-dimgray); + } + .scroll-spacer { flex: 0 0 auto; } @@ -128,29 +131,24 @@ &.open, &.active { background: var(--color-6-lowergray); + color: var(--color-f-white); &.active { background: var(--color-accent); } - svg { + .entry-icon svg { fill: var(--color-f-white); } - - span { - color: var(--color-f-white); - } } &.disabled { + color: var(--color-8-uppergray); + &:hover { background: none; } - span { - color: var(--color-8-uppergray); - } - svg { fill: var(--color-8-uppergray); } @@ -172,11 +170,7 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue"; import Separator from "@/components/widgets/labels/Separator.vue"; import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue"; -const KEYBOARD_LOCK_USE_FULLSCREEN = "This hotkey is reserved by the browser, but becomes available in fullscreen mode"; -const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, but becomes available in Chrome, Edge, and Opera which support the Keyboard.lock() API"; - const MenuList = defineComponent({ - inject: ["fullscreen"], emits: ["update:open", "update:activeEntry", "naturalWidth"], props: { entries: { type: Array as PropType, required: true }, @@ -193,7 +187,6 @@ const MenuList = defineComponent({ data() { return { isOpen: this.open, - keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER, highlighted: this.activeEntry as MenuListEntry | undefined, virtualScrollingEntriesStart: 0, }; diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index 21fa648e47..353653612a 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -31,14 +31,18 @@ @@ -53,10 +57,10 @@ :value="listing.entry.name" :placeholder="listing.entry.layer_type" :disabled="!listing.editingName" - @change="(e) => onEditLayerNameChange(listing, e.target)" @blur="() => onEditLayerNameDeselect(listing)" + @keydown.esc="onEditLayerNameDeselect(listing)" @keydown.enter="(e) => onEditLayerNameChange(listing, e.target)" - @keydown.escape="onEditLayerNameDeselect(listing)" + @change="(e) => onEditLayerNameChange(listing, e.target)" />
@@ -263,6 +267,7 @@ diff --git a/frontend/src/components/window/status-bar/StatusBar.vue b/frontend/src/components/window/status-bar/StatusBar.vue index a0c78a09ce..fa05419923 100644 --- a/frontend/src/components/window/status-bar/StatusBar.vue +++ b/frontend/src/components/window/status-bar/StatusBar.vue @@ -5,7 +5,7 @@
@@ -44,7 +44,8 @@