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 <dennis@kobert.dev>
This commit is contained in:
Keavon Chambers 2022-08-03 14:12:28 -07:00
parent fa461f3157
commit f39d6bf00c
73 changed files with 1686 additions and 727 deletions

1
Cargo.lock generated
View file

@ -176,6 +176,7 @@ dependencies = [
"graphite-editor", "graphite-editor",
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde",
"syn", "syn",
] ]

View file

@ -2,7 +2,7 @@
name = "bezier-rs-wasm" name = "bezier-rs-wasm"
publish = false publish = false
version = "0.0.0" version = "0.0.0"
rust-version = "1.56.0" rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"] authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021" edition = "2021"
readme = "../../README.md" readme = "../../README.md"

View file

@ -2,7 +2,7 @@
name = "bezier-rs" name = "bezier-rs"
publish = false publish = false
version = "0.0.0" version = "0.0.0"
rust-version = "1.56.0" rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"] authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021" edition = "2021"
readme = "./README.md" readme = "./README.md"

View file

@ -2,7 +2,7 @@
name = "graphite-editor" name = "graphite-editor"
publish = false publish = false
version = "0.0.0" version = "0.0.0"
rust-version = "1.56.0" rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"] authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021" edition = "2021"
readme = "../README.md" readme = "../README.md"

View file

@ -138,14 +138,25 @@ impl Dispatcher {
} }
InputMapper(message) => { InputMapper(message) => {
let actions = self.collect_actions(); let actions = self.collect_actions();
let keyboard_platform = self.message_handlers.portfolio_message_handler.platform.as_keyboard_platform_layout();
self.message_handlers self.message_handlers
.input_mapper_message_handler .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) => { 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) => { Portfolio(message) => {
self.message_handlers self.message_handlers
.portfolio_message_handler .portfolio_message_handler
@ -518,15 +529,15 @@ mod test {
replacement_selected_layers: sorted_layers[..2].to_vec(), 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()); 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::<Vec<_>>()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
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()); 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::<Vec<_>>()); assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::<Vec<_>>());
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()); 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::<Vec<_>>()); assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
} }

View file

@ -35,6 +35,8 @@ pub const PATH_OUTLINE_WEIGHT: f64 = 2.;
pub const ROTATE_SNAP_ANGLE: f64 = 15.; pub const ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SCALE_SNAP_INTERVAL: f64 = 0.1; pub const SCALE_SNAP_INTERVAL: f64 = 0.1;
pub const SLOWING_DIVISOR: f64 = 10.; pub const SLOWING_DIVISOR: f64 = 10.;
pub const NUDGE_AMOUNT: f64 = 1.;
pub const BIG_NUDGE_AMOUNT: f64 = 10.;
// Select tool // Select tool
pub const SELECTION_TOLERANCE: f64 = 5.; pub const SELECTION_TOLERANCE: f64 = 5.;

View file

@ -1,7 +1,7 @@
use crate::message_prelude::*;
use serde::{Deserialize, Serialize};
use super::{ExportDialogUpdate, NewDocumentDialogUpdate}; use super::{ExportDialogUpdate, NewDocumentDialogUpdate};
use crate::message_prelude::*;
use serde::{Deserialize, Serialize};
#[remain::sorted] #[remain::sorted]
#[impl_message(Message, Dialog)] #[impl_message(Message, Dialog)]

View file

@ -103,12 +103,16 @@ pub enum DocumentMessage {
new_name: String, new_name: String,
}, },
RenderDocument, RenderDocument,
ReorderSelectedLayers {
relative_index_offset: isize,
},
RollbackTransaction, RollbackTransaction,
SaveDocument, SaveDocument,
SelectAllLayers, SelectAllLayers,
SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
SelectedLayersRaiseToFront,
SelectedLayersReorder {
relative_index_offset: isize,
},
SelectLayer { SelectLayer {
layer_path: Vec<LayerId>, layer_path: Vec<LayerId>,
ctrl: bool, ctrl: bool,

View file

@ -7,6 +7,7 @@ use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler};
use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler}; 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::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::frontend::utility_types::{FileType, FrontendImageData};
use crate::input::input_mapper::action_keys::action_shortcut;
use crate::input::InputPreprocessorMessageHandler; use crate::input::InputPreprocessorMessageHandler;
use crate::layout::layout_message::LayoutTarget; use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::{ use crate::layout::widgets::{
@ -549,6 +550,7 @@ impl DocumentMessageHandler {
icon: "Snapping".into(), icon: "Snapping".into(),
tooltip: "Snapping".into(), tooltip: "Snapping".into(),
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()), on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()),
..Default::default()
})), })),
WidgetHolder::new(Widget::PopoverButton(PopoverButton { WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Snapping".into(), header: "Snapping".into(),
@ -564,6 +566,7 @@ impl DocumentMessageHandler {
icon: "Grid".into(), icon: "Grid".into(),
tooltip: "Grid".into(), tooltip: "Grid".into(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()), on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()),
..Default::default()
})), })),
WidgetHolder::new(Widget::PopoverButton(PopoverButton { WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Grid".into(), header: "Grid".into(),
@ -579,6 +582,7 @@ impl DocumentMessageHandler {
icon: "Overlays".into(), icon: "Overlays".into(),
tooltip: "Overlays".into(), tooltip: "Overlays".into(),
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()), on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()),
..Default::default()
})), })),
WidgetHolder::new(Widget::PopoverButton(PopoverButton { WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Overlays".into(), header: "Overlays".into(),
@ -841,14 +845,16 @@ impl DocumentMessageHandler {
})), })),
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
icon: "NodeFolder".into(), 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, size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::CreateEmptyFolder { container_path: vec![] }.into()), on_update: WidgetCallback::new(|_| DocumentMessage::CreateEmptyFolder { container_path: vec![] }.into()),
..Default::default() ..Default::default()
})), })),
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
icon: "Trash".into(), 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, size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::DeleteSelectedLayers.into()), on_update: WidgetCallback::new(|_| DocumentMessage::DeleteSelectedLayers.into()),
..Default::default() ..Default::default()
@ -864,6 +870,62 @@ impl DocumentMessageHandler {
.into(), .into(),
); );
} }
pub fn selected_layers_reorder(&mut self, relative_index_offset: isize, responses: &mut VecDeque<Message>) {
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<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCache)> for DocumentMessageHandler { impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCache)> for DocumentMessageHandler {
@ -1318,61 +1380,6 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
.into(), .into(),
); );
} }
ReorderSelectedLayers { relative_index_offset } => {
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 => { RollbackTransaction => {
self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e)); self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]);
@ -1399,6 +1406,21 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
let all = self.all_layers().map(|path| path.to_vec()).collect(); let all = self.all_layers().map(|path| path.to_vec()).collect();
responses.push_front(SetSelectedLayers { replacement_selected_layers: all }.into()); responses.push_front(SetSelectedLayers { replacement_selected_layers: all }.into());
} }
SelectedLayersLower => {
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 } => { SelectLayer { layer_path, ctrl, shift } => {
let mut paths = vec![]; let mut paths = vec![];
let last_selection_exists = !self.layer_range_selection_reference.is_empty(); let last_selection_exists = !self.layer_range_selection_reference.is_empty();
@ -1611,7 +1633,10 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
DeleteSelectedLayers, DeleteSelectedLayers,
DuplicateSelectedLayers, DuplicateSelectedLayers,
NudgeSelectedLayers, NudgeSelectedLayers,
ReorderSelectedLayers, SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
SelectedLayersRaiseToFront,
GroupSelectedLayers, GroupSelectedLayers,
UngroupSelectedLayers, UngroupSelectedLayers,
); );

View file

@ -1,5 +1,5 @@
use super::MenuBarMessage; use super::MenuBarMessage;
use crate::input::keyboard::Key; use crate::input::input_mapper::action_keys::action_shortcut;
use crate::layout::layout_message::LayoutTarget; use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::*; use crate::layout::widgets::*;
use crate::message_prelude::*; use crate::message_prelude::*;
@ -30,31 +30,31 @@ impl PropertyHolder for MenuBarMessageHandler {
Layout::MenuLayout(MenuLayout::new(vec![ Layout::MenuLayout(MenuLayout::new(vec![
MenuColumn { MenuColumn {
label: "File".into(), label: "File".into(),
children: vec![ children: MenuEntryGroups(vec![
vec![ vec![
MenuEntry { MenuEntry {
label: "New…".into(), label: "New…".into(),
icon: Some("File".into()), icon: Some("File".into()),
action: MenuEntry::create_action(|_| DialogMessage::RequestNewDocumentDialog.into()), action: MenuEntry::create_action(|_| DialogMessage::RequestNewDocumentDialog.into()),
shortcut: Some(vec![Key::KeyControl, Key::KeyN]), shortcut: action_shortcut!(DialogMessageDiscriminant::RequestNewDocumentDialog),
children: None, children: MenuEntryGroups::empty(),
}, },
MenuEntry { MenuEntry {
label: "Open…".into(), label: "Open…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyO]), shortcut: action_shortcut!(PortfolioMessageDiscriminant::OpenDocument),
action: MenuEntry::create_action(|_| PortfolioMessage::OpenDocument.into()), action: MenuEntry::create_action(|_| PortfolioMessage::OpenDocument.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Open Recent".into(), label: "Open Recent".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyO]), shortcut: None,
action: MenuEntry::no_action(), action: MenuEntry::no_action(),
icon: None, icon: None,
children: Some(vec![ children: MenuEntryGroups(vec![
vec![ vec![
MenuEntry { MenuEntry {
label: "Reopen Last Closed".into(), label: "Reopen Last Closed".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyT]), // shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyT],
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
@ -90,13 +90,13 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![ vec![
MenuEntry { MenuEntry {
label: "Close".into(), label: "Close".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyW]), shortcut: action_shortcut!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation),
action: MenuEntry::create_action(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()), action: MenuEntry::create_action(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Close All".into(), label: "Close All".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyAlt, Key::KeyW]), shortcut: action_shortcut!(DialogMessageDiscriminant::CloseAllDocumentsWithConfirmation),
action: MenuEntry::create_action(|_| DialogMessage::CloseAllDocumentsWithConfirmation.into()), action: MenuEntry::create_action(|_| DialogMessage::CloseAllDocumentsWithConfirmation.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
@ -104,19 +104,18 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![ vec![
MenuEntry { MenuEntry {
label: "Save".into(), label: "Save".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyS]), shortcut: action_shortcut!(DocumentMessageDiscriminant::SaveDocument),
action: MenuEntry::create_action(|_| DocumentMessage::SaveDocument.into()), action: MenuEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Save As…".into(), label: "Save As…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyS]), // shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyS],
action: MenuEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Save All".into(), label: "Save All".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyAlt, Key::KeyS]), // shortcut: [Key::KeyControl, Key::KeyAlt, Key::KeyS],
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
@ -128,37 +127,37 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![ vec![
MenuEntry { MenuEntry {
label: "Import…".into(), label: "Import…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyI]), shortcut: action_shortcut!(PortfolioMessageDiscriminant::Import),
action: MenuEntry::create_action(|_| PortfolioMessage::Import.into()), action: MenuEntry::create_action(|_| PortfolioMessage::Import.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Export…".into(), label: "Export…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyE]), shortcut: action_shortcut!(DialogMessageDiscriminant::RequestExportDialog),
action: MenuEntry::create_action(|_| DialogMessage::RequestExportDialog.into()), action: MenuEntry::create_action(|_| DialogMessage::RequestExportDialog.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
], ],
vec![MenuEntry { vec![MenuEntry {
label: "Quit".into(), label: "Quit".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyQ]), // shortcut: [Key::KeyControl, Key::KeyQ],
..MenuEntry::default() ..MenuEntry::default()
}], }],
], ]),
}, },
MenuColumn { MenuColumn {
label: "Edit".into(), label: "Edit".into(),
children: vec![ children: MenuEntryGroups(vec![
vec![ vec![
MenuEntry { MenuEntry {
label: "Undo".into(), label: "Undo".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyZ]), shortcut: action_shortcut!(DocumentMessageDiscriminant::Undo),
action: MenuEntry::create_action(|_| DocumentMessage::Undo.into()), action: MenuEntry::create_action(|_| DocumentMessage::Undo.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Redo".into(), label: "Redo".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyZ]), shortcut: action_shortcut!(DocumentMessageDiscriminant::Redo),
action: MenuEntry::create_action(|_| DocumentMessage::Redo.into()), action: MenuEntry::create_action(|_| DocumentMessage::Redo.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
@ -166,93 +165,93 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![ vec![
MenuEntry { MenuEntry {
label: "Cut".into(), label: "Cut".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyX]), shortcut: action_shortcut!(PortfolioMessageDiscriminant::Cut),
action: MenuEntry::create_action(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()), action: MenuEntry::create_action(|_| PortfolioMessage::Cut { clipboard: Clipboard::Device }.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Copy".into(), label: "Copy".into(),
icon: Some("Copy".into()), icon: Some("Copy".into()),
shortcut: Some(vec![Key::KeyControl, Key::KeyC]), shortcut: action_shortcut!(PortfolioMessageDiscriminant::Copy),
action: MenuEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()), action: MenuEntry::create_action(|_| PortfolioMessage::Copy { clipboard: Clipboard::Device }.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Paste".into(), label: "Paste".into(),
icon: Some("Paste".into()), icon: Some("Paste".into()),
shortcut: Some(vec![Key::KeyControl, Key::KeyV]), shortcut: action_shortcut!(FrontendMessageDiscriminant::TriggerPaste),
action: MenuEntry::create_action(|_| FrontendMessage::TriggerPaste.into()), action: MenuEntry::create_action(|_| FrontendMessage::TriggerPaste.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
], ],
], ]),
}, },
MenuColumn { MenuColumn {
label: "Layer".into(), label: "Layer".into(),
children: vec![vec![ children: MenuEntryGroups(vec![vec![
MenuEntry { MenuEntry {
label: "Select All".into(), label: "Select All".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyA]), shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectAllLayers),
action: MenuEntry::create_action(|_| DocumentMessage::SelectAllLayers.into()), action: MenuEntry::create_action(|_| DocumentMessage::SelectAllLayers.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Deselect All".into(), label: "Deselect All".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyAlt, Key::KeyA]), shortcut: action_shortcut!(DocumentMessageDiscriminant::DeselectAllLayers),
action: MenuEntry::create_action(|_| DocumentMessage::DeselectAllLayers.into()), action: MenuEntry::create_action(|_| DocumentMessage::DeselectAllLayers.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Order".into(), label: "Order".into(),
action: MenuEntry::no_action(), action: MenuEntry::no_action(),
children: Some(vec![vec![ children: MenuEntryGroups(vec![vec![
MenuEntry { MenuEntry {
label: "Raise To Front".into(), label: "Raise To Front".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyLeftBracket]), shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MAX }.into()), action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaiseToFront.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Raise".into(), label: "Raise".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyRightBracket]), shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersRaise),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 }.into()), action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaise.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Lower".into(), label: "Lower".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyLeftBracket]), shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersLower),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 }.into()), action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLower.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Lower to Back".into(), label: "Lower to Back".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyRightBracket]), shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersLowerToBack),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MIN }.into()), action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLowerToBack.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
]]), ]]),
..MenuEntry::default() ..MenuEntry::default()
}, },
]], ]]),
}, },
MenuColumn { MenuColumn {
label: "Document".into(), label: "Document".into(),
children: vec![vec![MenuEntry { children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Menu entries coming soon".into(), label: "Menu entries coming soon".into(),
..MenuEntry::default() ..MenuEntry::default()
}]], }]]),
}, },
MenuColumn { MenuColumn {
label: "View".into(), label: "View".into(),
children: vec![vec![MenuEntry { children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Show/Hide Node Graph (In Development)".into(), label: "Show/Hide Node Graph (In Development)".into(),
action: MenuEntry::create_action(|_| WorkspaceMessage::NodeGraphToggleVisibility.into()), action: MenuEntry::create_action(|_| WorkspaceMessage::NodeGraphToggleVisibility.into()),
..MenuEntry::default() ..MenuEntry::default()
}]], }]]),
}, },
MenuColumn { MenuColumn {
label: "Help".into(), label: "Help".into(),
children: vec![ children: MenuEntryGroups(vec![
vec![MenuEntry { vec![MenuEntry {
label: "About Graphite".into(), label: "About Graphite".into(),
action: MenuEntry::create_action(|_| DialogMessage::RequestAboutGraphiteDialog.into()), action: MenuEntry::create_action(|_| DialogMessage::RequestAboutGraphiteDialog.into()),
@ -284,23 +283,23 @@ impl PropertyHolder for MenuBarMessageHandler {
MenuEntry { MenuEntry {
label: "Debug: Print Messages".into(), label: "Debug: Print Messages".into(),
action: MenuEntry::no_action(), action: MenuEntry::no_action(),
children: Some(vec![vec![ children: MenuEntryGroups(vec![vec![
MenuEntry { MenuEntry {
label: "Off".into(), label: "Off".into(),
// icon: Some("Checkmark".into()), // TODO: Find a way to set this icon on the active mode // icon: Some("Checkmark".into()), // TODO: Find a way to set this icon on the active mode
shortcut: Some(vec![Key::KeyAlt, Key::Key0]), shortcut: action_shortcut!(DebugMessageDiscriminant::MessageOff),
action: MenuEntry::create_action(|_| DebugMessage::MessageOff.into()), action: MenuEntry::create_action(|_| DebugMessage::MessageOff.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Only Names".into(), label: "Only Names".into(),
shortcut: Some(vec![Key::KeyAlt, Key::Key1]), shortcut: action_shortcut!(DebugMessageDiscriminant::MessageNames),
action: MenuEntry::create_action(|_| DebugMessage::MessageNames.into()), action: MenuEntry::create_action(|_| DebugMessage::MessageNames.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Full Contents".into(), label: "Full Contents".into(),
shortcut: Some(vec![Key::KeyAlt, Key::Key2]), shortcut: action_shortcut!(DebugMessageDiscriminant::MessageContents),
action: MenuEntry::create_action(|_| DebugMessage::MessageContents.into()), action: MenuEntry::create_action(|_| DebugMessage::MessageContents.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
@ -310,14 +309,14 @@ impl PropertyHolder for MenuBarMessageHandler {
MenuEntry { MenuEntry {
label: "Debug: Print Trace Logs".into(), label: "Debug: Print Trace Logs".into(),
icon: Some(if let log::LevelFilter::Trace = log::max_level() { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()), icon: Some(if let log::LevelFilter::Trace = log::max_level() { "CheckboxChecked" } else { "CheckboxUnchecked" }.into()),
shortcut: Some(vec![Key::KeyAlt, Key::KeyT]), shortcut: action_shortcut!(DebugMessageDiscriminant::ToggleTraceLogs),
action: MenuEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()), action: MenuEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
label: "Debug: Print Document".into(), label: "Debug: Print Document".into(),
shortcut: Some(vec![Key::KeyAlt, Key::KeyP]), shortcut: action_shortcut!(DocumentMessageDiscriminant::DebugPrintDocument),
action: MenuEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()), action: MenuEntry::create_action(|_| DocumentMessage::DebugPrintDocument.into()),
..MenuEntry::default() ..MenuEntry::default()
}, },
MenuEntry { MenuEntry {
@ -326,7 +325,7 @@ impl PropertyHolder for MenuBarMessageHandler {
..MenuEntry::default() ..MenuEntry::default()
}, },
], ],
], ]),
}, },
])) ]))
} }

View file

@ -230,6 +230,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
FrontendMessage::UpdateInputHints { FrontendMessage::UpdateInputHints {
hint_data: HintData(vec![HintGroup(vec![HintInfo { hint_data: HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: false, plus: false,
@ -311,6 +312,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
FrontendMessage::UpdateInputHints { FrontendMessage::UpdateInputHints {
hint_data: HintData(vec![HintGroup(vec![HintInfo { hint_data: HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap Increments"), label: String::from("Snap Increments"),
plus: false, plus: false,

View file

@ -1,4 +1,5 @@
use super::clipboards::Clipboard; use super::clipboards::Clipboard;
use super::utility_types::Platform;
use crate::message_prelude::*; use crate::message_prelude::*;
use graphene::layers::text_layer::Font; use graphene::layers::text_layer::Font;
@ -83,6 +84,9 @@ pub enum PortfolioMessage {
SetActiveDocument { SetActiveDocument {
document_id: u64, document_id: u64,
}, },
SetPlatform {
platform: Platform,
},
UpdateDocumentWidgets, UpdateDocumentWidgets,
UpdateOpenDocumentsList, UpdateOpenDocumentsList,
} }

View file

@ -1,4 +1,5 @@
use super::clipboards::{CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT}; use super::clipboards::{CopyBufferEntry, INTERNAL_CLIPBOARD_COUNT};
use super::utility_types::Platform;
use super::{DocumentMessageHandler, MenuBarMessageHandler}; use super::{DocumentMessageHandler, MenuBarMessageHandler};
use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION}; use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION};
use crate::dialog; use crate::dialog;
@ -23,6 +24,7 @@ pub struct PortfolioMessageHandler {
active_document_id: Option<u64>, active_document_id: Option<u64>,
copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize], copy_buffer: [Vec<CopyBufferEntry>; INTERNAL_CLIPBOARD_COUNT as usize],
font_cache: FontCache, font_cache: FontCache,
pub platform: Platform,
} }
impl PortfolioMessageHandler { impl PortfolioMessageHandler {
@ -473,6 +475,7 @@ impl MessageHandler<PortfolioMessage, &InputPreprocessorMessageHandler> for Port
responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into()); responses.push_back(MovementMessage::TranslateCanvas { delta: (0., 0.).into() }.into());
} }
SetActiveDocument { document_id } => self.active_document_id = Some(document_id), SetActiveDocument { document_id } => self.active_document_id = Some(document_id),
SetPlatform { platform } => self.platform = platform,
UpdateDocumentWidgets => { UpdateDocumentWidgets => {
if let Some(document) = self.active_document() { if let Some(document) = self.active_document() {
document.update_document_widgets(responses); document.update_document_widgets(responses);

View file

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

View file

@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
#[remain::sorted] #[remain::sorted]
#[impl_message(Message, Frontend)] #[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum FrontendMessage { pub enum FrontendMessage {
// Display prefix: make the frontend show something, like a dialog // Display prefix: make the frontend show something, like a dialog
DisplayDialog { icon: String }, DisplayDialog { icon: String },

View file

@ -1,21 +1,21 @@
use graphene::LayerId; use graphene::LayerId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Clone, Deserialize, Serialize, Debug)] #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct FrontendDocumentDetails { pub struct FrontendDocumentDetails {
pub is_saved: bool, pub is_saved: bool,
pub name: String, pub name: String,
pub id: u64, pub id: u64,
} }
#[derive(PartialEq, Eq, Clone, Deserialize, Serialize, Debug)] #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
pub struct FrontendImageData { pub struct FrontendImageData {
pub path: Vec<LayerId>, pub path: Vec<LayerId>,
pub mime: String, pub mime: String,
pub image_data: Vec<u8>, pub image_data: Vec<u8>,
} }
#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum MouseCursorIcon { pub enum MouseCursorIcon {
Default, Default,
ZoomIn, 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 { pub enum FileType {
Svg, Svg,
Png, 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 { pub enum ExportBounds {
AllArtwork, AllArtwork,
Selection, Selection,

View file

@ -1,25 +1,25 @@
use super::input_mapper_macros::*;
use super::keyboard::{Key, KeyStates, NUMBER_OF_KEYS}; use super::keyboard::{Key, KeyStates, NUMBER_OF_KEYS};
use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT};
use crate::document::clipboards::Clipboard; use crate::document::clipboards::Clipboard;
use crate::document::utility_types::KeyboardPlatformLayout;
use crate::message_prelude::*; use crate::message_prelude::*;
use crate::viewport_tools::tool::ToolType;
use glam::DVec2; use glam::DVec2;
use serde::{Deserialize, Serialize};
const NUDGE_AMOUNT: f64 = 1.;
const SHIFT_NUDGE_AMOUNT: f64 = 10.;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Mapping { pub struct Mapping {
pub key_up: [KeyMappingEntries; NUMBER_OF_KEYS], pub key_up: [KeyMappingEntries; NUMBER_OF_KEYS],
pub key_down: [KeyMappingEntries; NUMBER_OF_KEYS], pub key_down: [KeyMappingEntries; NUMBER_OF_KEYS],
pub pointer_move: KeyMappingEntries,
pub mouse_scroll: KeyMappingEntries,
pub double_click: KeyMappingEntries, pub double_click: KeyMappingEntries,
pub wheel_scroll: KeyMappingEntries,
pub pointer_move: KeyMappingEntries,
} }
impl Default for Mapping { impl Default for Mapping {
fn default() -> Self { fn default() -> Self {
use input_mapper_macros::{entry, mapping, modifiers}; use InputMapperMessage::*;
use Key::*; use Key::*;
// WARNING! // 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`). // it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`).
let mappings = mapping![ let mappings = mapping![
// Higher priority than entries in sections below // HIGHER PRIORITY:
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 // MovementMessage
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter}, entry!(
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=Lmb}, PointerMove;
entry! {action=TransformLayerMessage::CancelTransformOperation, key_down=KeyEscape}, refresh_keys=[KeyControl],
entry! {action=TransformLayerMessage::CancelTransformOperation, key_down=Rmb}, action_dispatch=MovementMessage::PointerMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None },
entry! {action=TransformLayerMessage::ConstrainX, key_down=KeyX}, ),
entry! {action=TransformLayerMessage::ConstrainY, key_down=KeyY}, // NORMAL PRIORITY:
entry! {action=TransformLayerMessage::TypeBackspace, key_down=KeyBackspace}, //
entry! {action=TransformLayerMessage::TypeNegate, key_down=KeyMinus}, // TransformLayerMessage
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyComma}, entry!(KeyDown(KeyEnter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod}, entry!(KeyDown(Lmb); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry! {action=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]}, entry!(KeyDown(KeyEscape); action_dispatch=TransformLayerMessage::CancelTransformOperation),
// Select entry!(KeyDown(Rmb); action_dispatch=TransformLayerMessage::CancelTransformOperation),
entry! {action=SelectToolMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }, message=InputMapperMessage::PointerMove}, entry!(KeyDown(KeyX); action_dispatch=TransformLayerMessage::ConstrainX),
entry! {action=SelectToolMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb}, entry!(KeyDown(KeyY); action_dispatch=TransformLayerMessage::ConstrainY),
entry! {action=SelectToolMessage::DragStop, key_up=Lmb}, entry!(KeyDown(KeyBackspace); action_dispatch=TransformLayerMessage::TypeBackspace),
entry! {action=SelectToolMessage::DragStop, key_down=KeyEnter}, entry!(KeyDown(KeyMinus); action_dispatch=TransformLayerMessage::TypeNegate),
entry! {action=SelectToolMessage::EditLayer, message=InputMapperMessage::DoubleClick}, entry!(KeyDown(KeyComma); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
entry! {action=SelectToolMessage::Abort, key_down=Rmb}, entry!(KeyDown(KeyPeriod); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
entry! {action=SelectToolMessage::Abort, key_down=KeyEscape}, entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }),
// Artboard // SelectToolMessage
entry! {action=ArtboardToolMessage::PointerDown, key_down=Lmb}, entry!(PointerMove; refresh_keys=[KeyControl, KeyShift, KeyAlt], action_dispatch=SelectToolMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }),
entry! {action=ArtboardToolMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }, message=InputMapperMessage::PointerMove}, entry!(KeyDown(Lmb); action_dispatch=SelectToolMessage::DragStart { add_to_selection: KeyShift }),
entry! {action=ArtboardToolMessage::PointerUp, key_up=Lmb}, entry!(KeyUp(Lmb); action_dispatch=SelectToolMessage::DragStop),
entry! {action=ArtboardToolMessage::DeleteSelected, key_down=KeyDelete}, entry!(KeyDown(KeyEnter); action_dispatch=SelectToolMessage::DragStop),
entry! {action=ArtboardToolMessage::DeleteSelected, key_down=KeyBackspace}, entry!(DoubleClick; action_dispatch=SelectToolMessage::EditLayer),
// Navigate entry!(KeyDown(Rmb); action_dispatch=SelectToolMessage::Abort),
entry! {action=NavigateToolMessage::ClickZoom { zoom_in: false }, key_up=Lmb, modifiers=[KeyShift]}, entry!(KeyDown(KeyEscape); action_dispatch=SelectToolMessage::Abort),
entry! {action=NavigateToolMessage::ClickZoom { zoom_in: true }, key_up=Lmb}, // ArtboardToolMessage
entry! {action=NavigateToolMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }, message=InputMapperMessage::PointerMove}, entry!(KeyDown(Lmb); action_dispatch=ArtboardToolMessage::PointerDown),
entry! {action=NavigateToolMessage::TranslateCanvasBegin, key_down=Mmb}, entry!(PointerMove; refresh_keys=[KeyShift, KeyAlt], action_dispatch=ArtboardToolMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }),
entry! {action=NavigateToolMessage::RotateCanvasBegin, key_down=Rmb}, entry!(KeyUp(Lmb); action_dispatch=ArtboardToolMessage::PointerUp),
entry! {action=NavigateToolMessage::ZoomCanvasBegin, key_down=Lmb}, entry!(KeyDown(KeyDelete); action_dispatch=ArtboardToolMessage::DeleteSelected),
entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Rmb}, entry!(KeyDown(KeyBackspace); action_dispatch=ArtboardToolMessage::DeleteSelected),
entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Lmb}, // NavigateToolMessage
entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Mmb}, entry!(KeyUp(Lmb); modifiers=[KeyShift], action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: false }),
// Eyedropper entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: true }),
entry! {action=EyedropperToolMessage::LeftMouseDown, key_down=Lmb}, entry!(PointerMove; refresh_keys=[KeyControl], action_dispatch=NavigateToolMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }),
entry! {action=EyedropperToolMessage::RightMouseDown, key_down=Rmb}, entry!(KeyDown(Mmb); action_dispatch=NavigateToolMessage::TranslateCanvasBegin),
// Text entry!(KeyDown(Rmb); action_dispatch=NavigateToolMessage::RotateCanvasBegin),
entry! {action=TextMessage::Interact, key_up=Lmb}, entry!(KeyDown(Lmb); action_dispatch=NavigateToolMessage::ZoomCanvasBegin),
entry! {action=TextMessage::Abort, key_down=KeyEscape}, entry!(KeyUp(Rmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
entry! {action=TextMessage::CommitText, key_down=KeyEnter, modifiers=[KeyControl]}, entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
// Gradient entry!(KeyUp(Mmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
entry! {action=GradientToolMessage::PointerDown, key_down=Lmb}, // EyedropperToolMessage
entry! {action=GradientToolMessage::PointerMove { constrain_axis: KeyShift }, message=InputMapperMessage::PointerMove}, entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftMouseDown),
entry! {action=GradientToolMessage::PointerUp, key_up=Lmb}, entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightMouseDown),
// Rectangle // TextToolMessage
entry! {action=RectangleToolMessage::DragStart, key_down=Lmb}, entry!(KeyUp(Lmb); action_dispatch=TextToolMessage::Interact),
entry! {action=RectangleToolMessage::DragStop, key_up=Lmb}, entry!(KeyDown(KeyEscape); action_dispatch=TextToolMessage::Abort),
entry! {action=RectangleToolMessage::Abort, key_down=Rmb}, entry_multiplatform!(
entry! {action=RectangleToolMessage::Abort, key_down=KeyEscape}, standard!(KeyDown(KeyEnter); modifiers=[KeyControl], action_dispatch=TextToolMessage::CommitText),
entry! {action=RectangleToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, mac_only!(KeyDown(KeyEnter); modifiers=[KeyCommand], action_dispatch=TextToolMessage::CommitText),
// Ellipse ),
entry! {action=EllipseToolMessage::DragStart, key_down=Lmb}, // GradientToolMessage
entry! {action=EllipseToolMessage::DragStop, key_up=Lmb}, entry!(KeyDown(Lmb); action_dispatch=GradientToolMessage::PointerDown),
entry! {action=EllipseToolMessage::Abort, key_down=Rmb}, entry!(PointerMove; refresh_keys=[KeyShift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: KeyShift }),
entry! {action=EllipseToolMessage::Abort, key_down=KeyEscape}, entry!(KeyUp(Lmb); action_dispatch=GradientToolMessage::PointerUp),
entry! {action=EllipseToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, // RectangleToolMessage
// Shape entry!(KeyDown(Lmb); action_dispatch=RectangleToolMessage::DragStart),
entry! {action=ShapeToolMessage::DragStart, key_down=Lmb}, entry!(KeyUp(Lmb); action_dispatch=RectangleToolMessage::DragStop),
entry! {action=ShapeToolMessage::DragStop, key_up=Lmb}, entry!(KeyDown(Rmb); action_dispatch=RectangleToolMessage::Abort),
entry! {action=ShapeToolMessage::Abort, key_down=Rmb}, entry!(KeyDown(KeyEscape); action_dispatch=RectangleToolMessage::Abort),
entry! {action=ShapeToolMessage::Abort, key_down=KeyEscape}, entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=RectangleToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
entry! {action=ShapeToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, // EllipseToolMessage
// Line entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart),
entry! {action=LineToolMessage::DragStart, key_down=Lmb}, entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop),
entry! {action=LineToolMessage::DragStop, key_up=Lmb}, entry!(KeyDown(Rmb); action_dispatch=EllipseToolMessage::Abort),
entry! {action=LineToolMessage::Abort, key_down=Rmb}, entry!(KeyDown(KeyEscape); action_dispatch=EllipseToolMessage::Abort),
entry! {action=LineToolMessage::Abort, key_down=KeyEscape}, entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=EllipseToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
entry! {action=LineToolMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }, triggers=[KeyAlt, KeyShift, KeyControl]}, // ShapeToolMessage
// Path entry!(KeyDown(Lmb); action_dispatch=ShapeToolMessage::DragStart),
entry! {action=PathToolMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb}, entry!(KeyUp(Lmb); action_dispatch=ShapeToolMessage::DragStop),
entry! {action=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }, message=InputMapperMessage::PointerMove}, entry!(KeyDown(Rmb); action_dispatch=ShapeToolMessage::Abort),
entry! {action=PathToolMessage::Delete, key_down=KeyDelete}, entry!(KeyDown(KeyEscape); action_dispatch=ShapeToolMessage::Abort),
entry! {action=PathToolMessage::Delete, key_down=KeyBackspace}, entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=ShapeToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
entry! {action=PathToolMessage::DragStop, key_up=Lmb}, // LineToolMessage
// Pen entry!(KeyDown(Lmb); action_dispatch=LineToolMessage::DragStart),
entry! {action=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }, message=InputMapperMessage::PointerMove}, entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop),
entry! {action=PenToolMessage::DragStart, key_down=Lmb}, entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort),
entry! {action=PenToolMessage::DragStop, key_up=Lmb}, entry!(KeyDown(KeyEscape); action_dispatch=LineToolMessage::Abort),
entry! {action=PenToolMessage::Confirm, key_down=Rmb}, entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift, KeyControl], action_dispatch=LineToolMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }),
entry! {action=PenToolMessage::Confirm, key_down=KeyEscape}, // PathToolMessage
entry! {action=PenToolMessage::Confirm, key_down=KeyEnter}, entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: KeyShift }),
// Freehand entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }),
entry! {action=FreehandToolMessage::PointerMove, message=InputMapperMessage::PointerMove}, entry!(KeyDown(KeyDelete); action_dispatch=PathToolMessage::Delete),
entry! {action=FreehandToolMessage::DragStart, key_down=Lmb}, entry!(KeyDown(KeyBackspace); action_dispatch=PathToolMessage::Delete),
entry! {action=FreehandToolMessage::DragStop, key_up=Lmb}, entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop),
// Spline // PenToolMessage
entry! {action=SplineToolMessage::PointerMove, message=InputMapperMessage::PointerMove}, entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }),
entry! {action=SplineToolMessage::DragStart, key_down=Lmb}, entry!(KeyDown(Lmb); action_dispatch=PenToolMessage::DragStart),
entry! {action=SplineToolMessage::DragStop, key_up=Lmb}, entry!(KeyUp(Lmb); action_dispatch=PenToolMessage::DragStop),
entry! {action=SplineToolMessage::Confirm, key_down=Rmb}, entry!(KeyDown(Rmb); action_dispatch=PenToolMessage::Confirm),
entry! {action=SplineToolMessage::Confirm, key_down=KeyEscape}, entry!(KeyDown(KeyEscape); action_dispatch=PenToolMessage::Confirm),
entry! {action=SplineToolMessage::Confirm, key_down=KeyEnter}, entry!(KeyDown(KeyEnter); action_dispatch=PenToolMessage::Confirm),
// Fill // FreehandToolMessage
entry! {action=FillToolMessage::LeftMouseDown, key_down=Lmb}, entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove),
entry! {action=FillToolMessage::RightMouseDown, key_down=Rmb}, entry!(KeyDown(Lmb); action_dispatch=FreehandToolMessage::DragStart),
// Tool Actions entry!(KeyUp(Lmb); action_dispatch=FreehandToolMessage::DragStop),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Select }, key_down=KeyV}, // SplineToolMessage
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Navigate }, key_down=KeyZ}, entry!(PointerMove; action_dispatch=SplineToolMessage::PointerMove),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }, key_down=KeyI}, entry!(KeyDown(Lmb); action_dispatch=SplineToolMessage::DragStart),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Text }, key_down=KeyT}, entry!(KeyUp(Lmb); action_dispatch=SplineToolMessage::DragStop),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Fill }, key_down=KeyF}, entry!(KeyDown(Rmb); action_dispatch=SplineToolMessage::Confirm),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Gradient }, key_down=KeyH}, entry!(KeyDown(KeyEscape); action_dispatch=SplineToolMessage::Confirm),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Path }, key_down=KeyA}, entry!(KeyDown(KeyEnter); action_dispatch=SplineToolMessage::Confirm),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Pen }, key_down=KeyP}, // FillToolMessage
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Freehand }, key_down=KeyN}, entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftMouseDown),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Line }, key_down=KeyL}, entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightMouseDown),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }, key_down=KeyM}, // ToolMessage
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }, key_down=KeyE}, entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Shape }, key_down=KeyY}, entry!(KeyDown(KeyZ); action_dispatch=ToolMessage::ActivateToolNavigate),
// Colors entry!(KeyDown(KeyI); action_dispatch=ToolMessage::ActivateToolEyedropper),
entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]}, entry!(KeyDown(KeyT); action_dispatch=ToolMessage::ActivateToolText),
entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]}, entry!(KeyDown(KeyF); action_dispatch=ToolMessage::ActivateToolFill),
entry! {action=ToolMessage::SelectRandomPrimaryColor, key_down=KeyC, modifiers=[KeyAlt]}, entry!(KeyDown(KeyH); action_dispatch=ToolMessage::ActivateToolGradient),
// Document actions entry!(KeyDown(KeyA); action_dispatch=ToolMessage::ActivateToolPath),
entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]}, entry!(KeyDown(KeyP); action_dispatch=ToolMessage::ActivateToolPen),
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]}, entry!(KeyDown(KeyN); action_dispatch=ToolMessage::ActivateToolFreehand),
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]}, entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolLine),
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]}, entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle),
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete}, entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse),
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace}, entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape),
entry! {action=DialogMessage::RequestExportDialog, key_down=KeyE, modifiers=[KeyControl]}, entry_multiplatform!(
entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl]}, standard!(KeyDown(KeyX); modifiers=[KeyShift, KeyControl], action_dispatch=ToolMessage::ResetColors),
entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]}, mac_only!(KeyDown(KeyX); modifiers=[KeyShift, KeyCommand], action_dispatch=ToolMessage::ResetColors),
entry! {action=DocumentMessage::DebugPrintDocument, key_down=KeyP, modifiers=[KeyAlt]}, ),
entry! {action=DocumentMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]}, entry!(KeyDown(KeyX); modifiers=[KeyShift], action_dispatch=ToolMessage::SwapColors),
entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]}, entry!(KeyDown(KeyC); modifiers=[KeyAlt], action_dispatch=ToolMessage::SelectRandomPrimaryColor),
entry! {action=DocumentMessage::GroupSelectedLayers, key_down=KeyG, modifiers=[KeyControl]}, // DocumentMessage
entry! {action=DocumentMessage::UngroupSelectedLayers, key_down=KeyG, modifiers=[KeyControl, KeyShift]}, entry!(KeyDown(KeyDelete); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry! {action=DocumentMessage::CreateEmptyFolder { container_path: vec![] }, key_down=KeyN, modifiers=[KeyControl, KeyShift]}, entry!(KeyDown(KeyBackspace); action_dispatch=DocumentMessage::DeleteSelectedLayers),
// Layer transformation entry!(KeyDown(KeyP); modifiers=[KeyAlt], action_dispatch=DocumentMessage::DebugPrintDocument),
entry! {action=TransformLayerMessage::BeginGrab, key_down=KeyG}, entry_multiplatform!(
entry! {action=TransformLayerMessage::BeginRotate, key_down=KeyR}, standard!(KeyDown(KeyZ); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::Redo),
entry! {action=TransformLayerMessage::BeginScale, key_down=KeyS}, mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::Redo),
// Movement actions ),
entry! {action=MovementMessage::RotateCanvasBegin, key_down=Mmb, modifiers=[KeyControl]}, entry_multiplatform!(
entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]}, standard!(KeyDown(KeyZ); modifiers=[KeyControl], action_dispatch=DocumentMessage::Undo),
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb}, mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand], action_dispatch=DocumentMessage::Undo),
entry! {action=MovementMessage::TransformCanvasEnd, key_up=Mmb}, ),
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Lmb, modifiers=[KeySpace]}, entry_multiplatform!(
entry! {action=MovementMessage::TransformCanvasEnd, key_up=Lmb, modifiers=[KeySpace]}, standard!(KeyDown(KeyA); modifiers=[KeyControl, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers),
entry! {action=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }, key_down=KeyPlus, modifiers=[KeyControl]}, mac_only!(KeyDown(KeyA); modifiers=[KeyCommand, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers),
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_multiplatform!(
entry! {action=MovementMessage::SetCanvasZoom { zoom_factor: 1. }, key_down=Key1, modifiers=[KeyControl]}, standard!(KeyDown(KeyA); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectAllLayers),
entry! {action=MovementMessage::SetCanvasZoom { zoom_factor: 2. }, key_down=Key2, modifiers=[KeyControl]}, mac_only!(KeyDown(KeyA); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectAllLayers),
entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]}, ),
entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]}, entry_multiplatform!(
entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }, message=InputMapperMessage::MouseScroll}, standard!(KeyDown(KeyS); modifiers=[KeyControl], action_dispatch=DocumentMessage::SaveDocument),
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(1., 0.) }, key_down=KeyPageUp, modifiers=[KeyShift]}, mac_only!(KeyDown(KeyS); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SaveDocument),
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_multiplatform!(
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }, key_down=KeyPageDown}, standard!(KeyDown(Key0); modifiers=[KeyControl], action_dispatch=DocumentMessage::ZoomCanvasToFitAll),
// Portfolio actions mac_only!(KeyDown(Key0); modifiers=[KeyCommand], action_dispatch=DocumentMessage::ZoomCanvasToFitAll),
entry! {action=PortfolioMessage::OpenDocument, key_down=KeyO, modifiers=[KeyControl]}, ),
entry! {action=PortfolioMessage::Import, key_down=KeyI, modifiers=[KeyControl]}, entry_multiplatform!(
entry! {action=DialogMessage::RequestNewDocumentDialog, key_down=KeyN, modifiers=[KeyControl]}, standard!(KeyDown(KeyD); modifiers=[KeyControl], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
entry! {action=PortfolioMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]}, mac_only!(KeyDown(KeyD); modifiers=[KeyCommand], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]}, ),
entry! {action=DialogMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]}, entry_multiplatform!(
entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]}, standard!(KeyDown(KeyG); modifiers=[KeyControl], action_dispatch=DocumentMessage::GroupSelectedLayers),
entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::Device }, key_down=KeyC, modifiers=[KeyControl]}, mac_only!(KeyDown(KeyG); modifiers=[KeyCommand], action_dispatch=DocumentMessage::GroupSelectedLayers),
entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::Device }, key_down=KeyX, modifiers=[KeyControl]}, ),
// Nudging entry_multiplatform!(
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]}, standard!(KeyDown(KeyG); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]}, mac_only!(KeyDown(KeyG); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
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_multiplatform!(
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift, KeyArrowRight]}, standard!(KeyDown(KeyN); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }),
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift]}, mac_only!(KeyDown(KeyN); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }),
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_multiplatform!(
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowLeft, modifiers=[KeyShift]}, standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyShift, KeyArrowUp]}, mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
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_multiplatform!(
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyArrowLeft]}, // 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)
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyArrowRight]}, standard!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp}, mac_only!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
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_multiplatform!(
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT }, key_down=KeyArrowDown}, standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyArrowUp]}, mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
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_multiplatform!(
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyArrowUp]}, // 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)
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyArrowDown]}, standard!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowRight}, mac_only!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
// 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_multiplatform!(
entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 }, key_down=KeyRightBracket, modifiers=[KeyControl]}, standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersLower),
entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 }, key_down=KeyLeftBracket, modifiers=[KeyControl]}, mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersLower),
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_multiplatform!(
entry! {action=DebugMessage::ToggleTraceLogs, key_down=KeyT, modifiers=[KeyAlt]}, standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersRaise),
entry! {action=DebugMessage::MessageOff, key_down=Key0, modifiers=[KeyAlt]}, mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersRaise),
entry! {action=DebugMessage::MessageNames, key_down=Key1, modifiers=[KeyAlt]}, ),
entry! {action=DebugMessage::MessageContents, key_down=Key2, modifiers=[KeyAlt]}, 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 // 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]; const NUMBER_KEYS: [Key; 10] = [Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9];
@ -239,8 +346,9 @@ impl Default for Mapping {
0, 0,
MappingEntry { MappingEntry {
action: TransformLayerMessage::TypeDigit { digit: i as u8 }.into(), action: TransformLayerMessage::TypeDigit { digit: i as u8 }.into(),
trigger: InputMapperMessage::KeyDown(*key), input: InputMapperMessage::KeyDown(*key),
modifiers: modifiers! {}, platform_layout: None,
modifiers: modifiers!(),
}, },
); );
} }
@ -251,48 +359,70 @@ impl Default for Mapping {
sort(sublist); sort(sublist);
} }
} }
sort(&mut pointer_move);
sort(&mut mouse_scroll);
sort(&mut double_click); sort(&mut double_click);
sort(&mut wheel_scroll);
sort(&mut pointer_move);
Self { Self {
key_up, key_up,
key_down, key_down,
pointer_move,
mouse_scroll,
double_click, double_click,
wheel_scroll,
pointer_move,
} }
} }
} }
impl Mapping { impl Mapping {
pub fn match_message(&self, message: InputMapperMessage, keys: &KeyStates, actions: ActionList) -> Option<Message> { pub fn match_input_message(&self, message: InputMapperMessage, keyboard_state: &KeyStates, actions: ActionList, keyboard_platform: KeyboardPlatformLayout) -> Option<Message> {
let list = match message { let list = match message {
InputMapperMessage::KeyDown(key) => &self.key_down[key as usize], InputMapperMessage::KeyDown(key) => &self.key_down[key as usize],
InputMapperMessage::KeyUp(key) => &self.key_up[key as usize], InputMapperMessage::KeyUp(key) => &self.key_up[key as usize],
InputMapperMessage::DoubleClick => &self.double_click, InputMapperMessage::DoubleClick => &self.double_click,
InputMapperMessage::MouseScroll => &self.mouse_scroll, InputMapperMessage::WheelScroll => &self.wheel_scroll,
InputMapperMessage::PointerMove => &self.pointer_move, InputMapperMessage::PointerMove => &self.pointer_move,
}; };
list.match_mapping(keys, actions) list.match_mapping(keyboard_state, actions, keyboard_platform)
} }
} }
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub struct MappingEntry { pub struct MappingEntry {
pub trigger: InputMapperMessage, /// Serves two purposes:
pub modifiers: KeyStates, /// - 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, 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<KeyboardPlatformLayout>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct KeyMappingEntries(pub Vec<MappingEntry>); pub struct KeyMappingEntries(pub Vec<MappingEntry>);
impl KeyMappingEntries { impl KeyMappingEntries {
fn match_mapping(&self, keys: &KeyStates, actions: ActionList) -> Option<Message> { fn match_mapping(&self, keyboard_state: &KeyStates, actions: ActionList, keyboard_platform: KeyboardPlatformLayout) -> Option<Message> {
for entry in self.0.iter() { for entry in self.0.iter() {
let all_required_modifiers_pressed = ((*keys & entry.modifiers) ^ entry.modifiers).is_empty(); // Skip this entry if it is platform-specific, and for a layout that does not match the user's keyboard platform layout
if all_required_modifiers_pressed && actions.iter().flatten().any(|action| entry.action.to_discriminant() == *action) { 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()); return Some(entry.action.clone());
} }
} }
@ -313,70 +443,69 @@ impl KeyMappingEntries {
} }
} }
impl Default for KeyMappingEntries { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
fn default() -> Self { pub enum ActionKeys {
Self::new() Action(MessageDiscriminant),
} #[serde(rename = "keys")]
Keys(Vec<Key>),
} }
mod input_mapper_macros { impl ActionKeys {
macro_rules! modifiers { pub fn to_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>) {
($($m:ident),*) => {{ match self {
#[allow(unused_mut)] ActionKeys::Action(action) => {
let mut state = KeyStates::new(); if let Some(keys) = action_input_mapping(action).get_mut(0) {
$( let mut taken_keys = Vec::new();
state.set(Key::$m as usize); std::mem::swap(keys, &mut taken_keys);
)*
state
}};
}
macro_rules! entry { *self = ActionKeys::Keys(taken_keys);
{action=$action:expr, key_down=$key:ident $(, modifiers=[$($m:ident),* $(,)?])?} => {{ } else {
entry!{action=$action, message=InputMapperMessage::KeyDown(Key::$key) $(, modifiers=[$($m),*])?} *self = ActionKeys::Keys(Vec::new());
}};
{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 {
//[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{
[$($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());
} }
)* }
(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::<String>();
// 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; joined
pub(crate) use mapping; }
pub(crate) use modifiers;
pub mod action_keys {
macro_rules! action_shortcut {
($action:expr) => {
Some(crate::input::input_mapper::ActionKeys::Action($action.into()))
};
}
pub(crate) use action_shortcut;
} }

View file

@ -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<KeyboardPlatformLayout>)
/// ```
///
/// 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;

View file

@ -17,6 +17,6 @@ pub enum InputMapperMessage {
// Messages // Messages
DoubleClick, DoubleClick,
MouseScroll,
PointerMove, PointerMove,
WheelScroll,
} }

View file

@ -1,6 +1,7 @@
use super::input_mapper::Mapping; use super::input_mapper::Mapping;
use super::keyboard::Key; use super::keyboard::Key;
use super::InputPreprocessorMessageHandler; use super::InputPreprocessorMessageHandler;
use crate::document::utility_types::KeyboardPlatformLayout;
use crate::message_prelude::*; use crate::message_prelude::*;
use std::fmt::Write; use std::fmt::Write;
@ -31,12 +32,67 @@ impl InputMapperMessageHandler {
}); });
output.replace("Key", "") output.replace("Key", "")
} }
pub fn action_input_mapping(&self, action_to_find: &MessageDiscriminant, keyboard_platform: KeyboardPlatformLayout) -> Vec<Vec<Key>> {
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::<usize>() >= std::mem::size_of::<Key>());
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::<usize, Key>(&i) }
})
.collect::<Vec<_>>();
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::<Vec<_>>()
}
} }
impl MessageHandler<InputMapperMessage, (&InputPreprocessorMessageHandler, ActionList)> for InputMapperMessageHandler { impl MessageHandler<InputMapperMessage, (&InputPreprocessorMessageHandler, KeyboardPlatformLayout, ActionList)> for InputMapperMessageHandler {
fn process_action(&mut self, message: InputMapperMessage, data: (&InputPreprocessorMessageHandler, ActionList), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: InputMapperMessage, data: (&InputPreprocessorMessageHandler, KeyboardPlatformLayout, ActionList), responses: &mut VecDeque<Message>) {
let (input, actions) = data; let (input, keyboard_platform, actions) = data;
if let Some(message) = self.mapping.match_message(message, &input.keyboard, actions) {
if let Some(message) = self.mapping.match_input_message(message, &input.keyboard, actions, keyboard_platform) {
responses.push_back(message); responses.push_back(message);
} }
} }

View file

@ -13,14 +13,16 @@ bitflags! {
#[derive(Default, Serialize, Deserialize)] #[derive(Default, Serialize, Deserialize)]
#[repr(transparent)] #[repr(transparent)]
pub struct ModifierKeys: u8 { pub struct ModifierKeys: u8 {
const CONTROL = 0b0000_0001; const SHIFT = 0b0000_0001;
const SHIFT = 0b0000_0010; const ALT = 0b0000_0010;
const ALT = 0b0000_0100; const CONTROL = 0b0000_0100;
const META_OR_COMMAND = 0b0000_1000;
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use crate::document::utility_types::KeyboardPlatformLayout;
use crate::input::input_preprocessor::ModifierKeys; use crate::input::input_preprocessor::ModifierKeys;
use crate::input::keyboard::Key; use crate::input::keyboard::Key;
use crate::input::mouse::EditorMouseState; use crate::input::mouse::EditorMouseState;
@ -39,7 +41,7 @@ mod test {
let mut responses = VecDeque::new(); 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!(input_preprocessor.keyboard.get(Key::KeyAlt as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyAlt).into())); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyAlt).into()));
@ -55,7 +57,7 @@ mod test {
let mut responses = VecDeque::new(); 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::KeyControl as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyControl).into())); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyControl).into()));
@ -71,7 +73,7 @@ mod test {
let mut responses = VecDeque::new(); 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!(input_preprocessor.keyboard.get(Key::KeyShift as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyShift).into())); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyShift).into()));
@ -88,7 +90,7 @@ mod test {
let mut responses = VecDeque::new(); 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::KeyControl as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyUp(Key::KeyControl).into())); assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyUp(Key::KeyControl).into()));
@ -104,7 +106,7 @@ mod test {
let mut responses = VecDeque::new(); 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::KeyControl as usize));
assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize)); assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize));

View file

@ -16,8 +16,8 @@ pub enum InputPreprocessorMessage {
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
KeyDown { key: Key, modifier_keys: ModifierKeys }, KeyDown { key: Key, modifier_keys: ModifierKeys },
KeyUp { 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 }, PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
} }

View file

@ -1,6 +1,7 @@
use super::input_preprocessor::ModifierKeys; use super::input_preprocessor::ModifierKeys;
use super::keyboard::{Key, KeyStates}; use super::keyboard::{Key, KeyStates};
use super::mouse::{MouseKeys, MouseState, ViewportBounds}; use super::mouse::{MouseKeys, MouseState, ViewportBounds};
use crate::document::utility_types::KeyboardPlatformLayout;
use crate::message_prelude::*; use crate::message_prelude::*;
#[doc(inline)] #[doc(inline)]
@ -15,9 +16,11 @@ pub struct InputPreprocessorMessageHandler {
pub viewport_bounds: ViewportBounds, pub viewport_bounds: ViewportBounds,
} }
impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHandler { impl MessageHandler<InputPreprocessorMessage, KeyboardPlatformLayout> for InputPreprocessorMessageHandler {
#[remain::check] #[remain::check]
fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque<Message>) { fn process_action(&mut self, message: InputPreprocessorMessage, data: KeyboardPlatformLayout, responses: &mut VecDeque<Message>) {
let keyboard_platform = data;
#[remain::sorted] #[remain::sorted]
match message { match message {
InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } => { InputPreprocessorMessage::BoundsOfViewports { bounds_of_viewports } => {
@ -53,7 +56,7 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHa
} }
} }
InputPreprocessorMessage::DoubleClick { editor_mouse_state, modifier_keys } => { 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); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position; self.mouse.position = mouse_state.position;
@ -61,26 +64,17 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHa
responses.push_back(InputMapperMessage::DoubleClick.into()); responses.push_back(InputMapperMessage::DoubleClick.into());
} }
InputPreprocessorMessage::KeyDown { key, modifier_keys } => { 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); self.keyboard.set(key as usize);
responses.push_back(InputMapperMessage::KeyDown(key).into()); responses.push_back(InputMapperMessage::KeyDown(key).into());
} }
InputPreprocessorMessage::KeyUp { key, modifier_keys } => { 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); self.keyboard.unset(key as usize);
responses.push_back(InputMapperMessage::KeyUp(key).into()); 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 } => { 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); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position; self.mouse.position = mouse_state.position;
@ -88,7 +82,7 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHa
self.translate_mouse_event(mouse_state, true, responses); self.translate_mouse_event(mouse_state, true, responses);
} }
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => { 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); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position; self.mouse.position = mouse_state.position;
@ -99,13 +93,22 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHa
self.translate_mouse_event(mouse_state, false, responses); self.translate_mouse_event(mouse_state, false, responses);
} }
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => { 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); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
self.mouse.position = mouse_state.position; self.mouse.position = mouse_state.position;
self.translate_mouse_event(mouse_state, false, responses); 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; self.mouse = new_state;
} }
fn handle_modifier_keys(&mut self, modifier_keys: ModifierKeys, responses: &mut VecDeque<Message>) { fn handle_modifier_keys(&mut self, modifier_keys: ModifierKeys, keyboard_platform: KeyboardPlatformLayout, responses: &mut VecDeque<Message>) {
self.handle_modifier_key(Key::KeyControl, modifier_keys.contains(ModifierKeys::CONTROL), responses);
self.handle_modifier_key(Key::KeyShift, modifier_keys.contains(ModifierKeys::SHIFT), responses); 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::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<Message>) { fn handle_modifier_key(&mut self, key: Key, key_is_down: bool, responses: &mut VecDeque<Message>) {

View file

@ -1,7 +1,7 @@
use crate::message_prelude::*; use crate::message_prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter}; use std::fmt::{self, Display, Formatter};
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign}; use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign};
// TODO: Increase size of type // TODO: Increase size of type
@ -21,7 +21,7 @@ pub type KeyStates = BitVector<KEY_MASK_STORAGE_LENGTH>;
pub enum Key { pub enum Key {
UnknownKey, UnknownKey,
// MouseKeys // Mouse keys
Lmb, Lmb,
Rmb, Rmb,
Mmb, Mmb,
@ -70,6 +70,8 @@ pub enum Key {
KeyShift, KeyShift,
KeySpace, KeySpace,
KeyControl, KeyControl,
KeyCommand,
KeyMeta,
KeyDelete, KeyDelete,
KeyBackspace, KeyBackspace,
KeyAlt, KeyAlt,
@ -92,6 +94,16 @@ pub enum Key {
NumKeys, 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::<String>() } else { key_name };
write!(f, "{}", name)
}
}
pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize; pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -162,6 +174,10 @@ impl<const LENGTH: usize> BitVector<LENGTH> {
result result
} }
pub fn iter(&self) -> impl Iterator<Item = usize> + '_ {
BitVectorIter::<LENGTH> { bitvector: self, iter_index: 0 }
}
} }
impl<const LENGTH: usize> Default for BitVector<LENGTH> { impl<const LENGTH: usize> Default for BitVector<LENGTH> {
@ -170,6 +186,29 @@ impl<const LENGTH: usize> Default for BitVector<LENGTH> {
} }
} }
struct BitVectorIter<'a, const LENGTH: usize> {
bitvector: &'a BitVector<LENGTH>,
iter_index: usize,
}
impl<'a, const LENGTH: usize> Iterator for BitVectorIter<'a, LENGTH> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
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<const LENGTH: usize> Display for BitVector<LENGTH> { impl<const LENGTH: usize> Display for BitVector<LENGTH> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for storage in self.0.iter().rev() { for storage in self.0.iter().rev() {

View file

@ -3,6 +3,7 @@ pub mod input_preprocessor;
pub mod keyboard; pub mod keyboard;
pub mod mouse; pub mod mouse;
mod input_mapper_macros;
mod input_mapper_message; mod input_mapper_message;
mod input_mapper_message_handler; mod input_mapper_message_handler;
mod input_preprocessor_message; mod input_preprocessor_message;

View file

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
#[remain::sorted] #[remain::sorted]
#[impl_message(Message, Layout)] #[impl_message(Message, Layout)]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum LayoutMessage { pub enum LayoutMessage {
RefreshLayout { layout_target: LayoutTarget }, RefreshLayout { layout_target: LayoutTarget },
SendLayout { layout: Layout, layout_target: LayoutTarget }, SendLayout { layout: Layout, layout_target: LayoutTarget },
@ -13,7 +13,7 @@ pub enum LayoutMessage {
} }
#[remain::sorted] #[remain::sorted]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug, Hash, Eq, Copy)] #[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, Serialize, Deserialize)]
#[repr(u8)] #[repr(u8)]
pub enum LayoutTarget { pub enum LayoutTarget {
DialogDetails, DialogDetails,

View file

@ -1,5 +1,7 @@
use super::layout_message::LayoutTarget; use super::layout_message::LayoutTarget;
use super::widgets::Layout; use super::widgets::Layout;
use crate::document::utility_types::KeyboardPlatformLayout;
use crate::input::keyboard::Key;
use crate::layout::widgets::Widget; use crate::layout::widgets::Widget;
use crate::message_prelude::*; use crate::message_prelude::*;
@ -15,49 +17,55 @@ pub struct LayoutMessageHandler {
impl LayoutMessageHandler { impl LayoutMessageHandler {
#[remain::check] #[remain::check]
fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque<Message>) { fn send_layout(
&self,
layout_target: LayoutTarget,
responses: &mut VecDeque<Message>,
action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>,
keyboard_platform: KeyboardPlatformLayout,
) {
let layout = &self.layouts[layout_target as usize]; let layout = &self.layouts[layout_target as usize];
#[remain::sorted] #[remain::sorted]
let message = match layout_target { let message = match layout_target {
LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails { LayoutTarget::DialogDetails => FrontendMessage::UpdateDialogDetails {
layout_target, 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 { LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout {
layout_target, 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 { LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout {
layout_target, 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 { LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout {
layout_target, 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 { LayoutTarget::MenuBar => FrontendMessage::UpdateMenuBarLayout {
layout_target, 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 { LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout {
layout_target, 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 { LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout {
layout_target, 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 { LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout {
layout_target, 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 { LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout {
layout_target, 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 { LayoutTarget::WorkingColors => FrontendMessage::UpdateWorkingColorsLayout {
layout_target, layout_target,
layout: layout.clone().unwrap_widget_layout().layout, layout: layout.clone().unwrap_widget_layout(action_input_mapping, keyboard_platform).layout,
}, },
#[remain::unsorted] #[remain::unsorted]
@ -67,19 +75,21 @@ impl LayoutMessageHandler {
} }
} }
impl MessageHandler<LayoutMessage, ()> for LayoutMessageHandler { impl<F: Fn(&MessageDiscriminant) -> Vec<Vec<Key>>> MessageHandler<LayoutMessage, (F, KeyboardPlatformLayout)> for LayoutMessageHandler {
#[remain::check] #[remain::check]
fn process_action(&mut self, action: LayoutMessage, _data: (), responses: &mut std::collections::VecDeque<crate::message_prelude::Message>) { fn process_action(&mut self, action: LayoutMessage, data: (F, KeyboardPlatformLayout), responses: &mut std::collections::VecDeque<crate::message_prelude::Message>) {
let (action_input_mapping, keyboard_platform) = data;
use LayoutMessage::*; use LayoutMessage::*;
#[remain::sorted] #[remain::sorted]
match action { match action {
RefreshLayout { layout_target } => { RefreshLayout { layout_target } => {
self.send_layout(layout_target, responses); self.send_layout(layout_target, responses, &action_input_mapping, keyboard_platform);
} }
SendLayout { layout, layout_target } => { SendLayout { layout, layout_target } => {
self.layouts[layout_target as usize] = layout; 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 } => { UpdateLayout { layout_target, widget_id, value } => {
let layout = &mut self.layouts[layout_target as usize]; let layout = &mut self.layouts[layout_target as usize];

View file

@ -1,4 +1,7 @@
use super::layout_message::LayoutTarget; 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::input::keyboard::Key;
use crate::message_prelude::*; use crate::message_prelude::*;
use crate::Color; use crate::Color;
@ -30,16 +33,67 @@ pub enum Layout {
} }
impl Layout { impl Layout {
pub fn unwrap_widget_layout(self) -> WidgetLayout { pub fn unwrap_widget_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, keyboard_platform: KeyboardPlatformLayout) -> WidgetLayout {
if let Layout::WidgetLayout(widget_layout) = self { 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 widget_layout
} else { } else {
panic!("Tried to unwrap layout as WidgetLayout. Got {:?}", self) panic!("Tried to unwrap layout as WidgetLayout. Got {:?}", self)
} }
} }
pub fn unwrap_menu_layout(self) -> MenuLayout { pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, _keyboard_platform: KeyboardPlatformLayout) -> MenuLayout {
if let Layout::MenuLayout(menu_layout) = self { 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 menu_layout
} else { } else {
panic!("Tried to unwrap layout as MenuLayout. Got {:?}", self) 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<Vec<MenuEntry>>);
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<Vec<Key>>) {
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)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct MenuEntry { pub struct MenuEntry {
pub label: String, pub label: String,
pub icon: Option<String>, pub icon: Option<String>,
pub children: Option<Vec<Vec<MenuEntry>>>, pub children: MenuEntryGroups,
pub action: WidgetHolder, pub action: WidgetHolder,
pub shortcut: Option<Vec<Key>>, pub shortcut: Option<ActionKeys>,
} }
impl MenuEntry { impl MenuEntry {
@ -94,7 +170,7 @@ impl Default for MenuEntry {
action: MenuEntry::create_action(|_| DialogMessage::RequestComingSoonDialog { issue: None }.into()), action: MenuEntry::create_action(|_| DialogMessage::RequestComingSoonDialog { issue: None }.into()),
label: "".into(), label: "".into(),
icon: None, icon: None,
children: None, children: MenuEntryGroups::empty(),
shortcut: None, shortcut: None,
} }
} }
@ -103,10 +179,10 @@ impl Default for MenuEntry {
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct MenuColumn { pub struct MenuColumn {
pub label: String, pub label: String,
pub children: Vec<Vec<MenuEntry>>, pub children: MenuEntryGroups,
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct MenuLayout { pub struct MenuLayout {
pub layout: Vec<MenuColumn>, pub layout: Vec<MenuColumn>,
} }
@ -118,13 +194,13 @@ impl MenuLayout {
pub fn iter(&self) -> impl Iterator<Item = &WidgetHolder> + '_ { pub fn iter(&self) -> impl Iterator<Item = &WidgetHolder> + '_ {
MenuLayoutIter { 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<Item = &mut WidgetHolder> + '_ { pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut WidgetHolder> + '_ {
MenuLayoutIterMut { 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<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() { match self.stack.pop() {
Some(menu_entry) => { 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) Some(&menu_entry.action)
} }
None => None, None => None,
@ -158,7 +236,9 @@ impl<'a> Iterator for MenuLayoutIterMut<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() { match self.stack.pop() {
Some(menu_entry) => { 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) Some(&mut menu_entry.action)
} }
None => None, 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 { pub struct WidgetHolder {
#[serde(rename = "widgetId")] #[serde(rename = "widgetId")]
pub widget_id: u64, pub widget_id: u64,
@ -332,7 +412,7 @@ pub enum Widget {
TextLabel(TextLabel), TextLabel(TextLabel),
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct CheckboxInput { pub struct CheckboxInput {
pub checked: bool, pub checked: bool,
@ -341,13 +421,16 @@ pub struct CheckboxInput {
pub tooltip: String, pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<CheckboxInput>, pub on_update: WidgetCallback<CheckboxInput>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative)] #[derive(Clone, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq, Default)] #[derivative(Debug, PartialEq, Default)]
pub struct ColorInput { pub struct ColorInput {
pub value: Option<String>, pub value: Option<String>,
@ -362,6 +445,9 @@ pub struct ColorInput {
pub tooltip: String, pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@ -435,7 +521,7 @@ pub struct FontInput {
pub on_update: WidgetCallback<FontInput>, pub on_update: WidgetCallback<FontInput>,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct IconButton { pub struct IconButton {
pub icon: String, pub icon: String,
@ -446,6 +532,9 @@ pub struct IconButton {
pub tooltip: String, pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@ -533,7 +622,7 @@ pub enum NumberInputIncrementBehavior {
Callback, Callback,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct OptionalInput { pub struct OptionalInput {
pub checked: bool, pub checked: bool,
@ -542,6 +631,9 @@ pub struct OptionalInput {
pub tooltip: String, pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]
@ -559,7 +651,7 @@ pub struct PopoverButton {
pub text: String, pub text: String,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct RadioInput { pub struct RadioInput {
pub entries: Vec<RadioEntryData>, pub entries: Vec<RadioEntryData>,
@ -569,7 +661,7 @@ pub struct RadioInput {
pub selected_index: u32, pub selected_index: u32,
} }
#[derive(Clone, Serialize, Deserialize, Derivative, Default)] #[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub struct RadioEntryData { pub struct RadioEntryData {
pub value: String, pub value: String,
@ -580,6 +672,9 @@ pub struct RadioEntryData {
pub tooltip: String, pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks // Callbacks
#[serde(skip)] #[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")] #[derivative(Debug = "ignore", PartialEq = "ignore")]

View file

@ -91,7 +91,7 @@ pub mod message_prelude {
pub use crate::viewport_tools::tools::select_tool::{SelectToolMessage, SelectToolMessageDiscriminant}; 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::shape_tool::{ShapeToolMessage, ShapeToolMessageDiscriminant};
pub use crate::viewport_tools::tools::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant}; 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 crate::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant};
pub use graphite_proc_macros::*; pub use graphite_proc_macros::*;

View file

@ -10,10 +10,19 @@ pub struct HintGroup(pub Vec<HintInfo>);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HintInfo { 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<KeysGroup>, pub key_groups: Vec<KeysGroup>,
/// `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<Vec<KeysGroup>>,
/// 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<MouseMotion>, pub mouse: Option<MouseMotion>,
/// The text describing what occurs with this input combination.
pub label: String, 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, pub plus: bool,
} }

View file

@ -1,6 +1,8 @@
use super::tools::*; use super::tools::*;
use crate::communication::message_handler::MessageHandler; use crate::communication::message_handler::MessageHandler;
use crate::document::DocumentMessageHandler; 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::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::{IconButton, Layout, LayoutGroup, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::layout::widgets::{IconButton, Layout, LayoutGroup, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*; use crate::message_prelude::*;
@ -115,6 +117,7 @@ impl ToolData {
#[derive(Debug)] #[derive(Debug)]
pub struct ToolBarMetadataGroup { pub struct ToolBarMetadataGroup {
pub tooltip: String, pub tooltip: String,
pub tooltip_shortcut: Option<ActionKeys>,
pub icon_name: String, pub icon_name: String,
pub tool_type: ToolType, pub tool_type: ToolType,
} }
@ -123,18 +126,24 @@ impl PropertyHolder for ToolData {
fn properties(&self) -> Layout { fn properties(&self) -> Layout {
let tool_groups_layout = list_tools_in_groups() let tool_groups_layout = list_tools_in_groups()
.iter() .iter()
.map(|tool_group| tool_group.iter().map(|tool| ToolBarMetadataGroup {tooltip: tool.tooltip(), icon_name: tool.icon_name(), tool_type: tool.tool_type()}).collect::<Vec<_>>()) .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::<Vec<_>>())
.chain(coming_soon_tools()) .chain(coming_soon_tools())
.flat_map(|group| { .flat_map(|group| {
let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator { let separator = std::iter::once(WidgetHolder::new(Widget::Separator(Separator {
direction: SeparatorDirection::Vertical, direction: SeparatorDirection::Vertical,
separator_type: SeparatorType::Section, 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 { WidgetHolder::new(Widget::IconButton(IconButton {
icon: icon_name, icon: icon_name,
size: 32, size: 32,
tooltip: tooltip.clone(), tooltip: tooltip.clone(),
tooltip_shortcut,
active: self.active_tool_type == tool_type, active: self.active_tool_type == tool_type,
on_update: WidgetCallback::new(move |_| { on_update: WidgetCallback::new(move |_| {
if !tooltip.contains("Coming Soon") { if !tooltip.contains("Coming Soon") {
@ -143,7 +152,6 @@ impl PropertyHolder for ToolData {
DialogMessage::RequestComingSoonDialog { issue: None }.into() DialogMessage::RequestComingSoonDialog { issue: None }.into()
} }
}), }),
..Default::default()
})) }))
}); });
separator.chain(buttons) separator.chain(buttons)
@ -253,68 +261,107 @@ pub fn coming_soon_tools() -> Vec<Vec<ToolBarMetadataGroup>> {
tool_type: ToolType::Brush, tool_type: ToolType::Brush,
icon_name: "RasterBrushTool".into(), icon_name: "RasterBrushTool".into(),
tooltip: "Coming Soon: Brush Tool (B)".into(), tooltip: "Coming Soon: Brush Tool (B)".into(),
tooltip_shortcut: None,
}, },
ToolBarMetadataGroup { ToolBarMetadataGroup {
tool_type: ToolType::Heal, tool_type: ToolType::Heal,
icon_name: "RasterHealTool".into(), icon_name: "RasterHealTool".into(),
tooltip: "Coming Soon: Heal Tool (J)".into(), tooltip: "Coming Soon: Heal Tool (J)".into(),
tooltip_shortcut: None,
}, },
ToolBarMetadataGroup { ToolBarMetadataGroup {
tool_type: ToolType::Clone, tool_type: ToolType::Clone,
icon_name: "RasterCloneTool".into(), icon_name: "RasterCloneTool".into(),
tooltip: "Coming Soon: Clone Tool (C))".into(), tooltip: "Coming Soon: Clone Tool (C)".into(),
tooltip_shortcut: None,
}, },
ToolBarMetadataGroup { ToolBarMetadataGroup {
tool_type: ToolType::Patch, tool_type: ToolType::Patch,
icon_name: "RasterPatchTool".into(), icon_name: "RasterPatchTool".into(),
tooltip: "Coming Soon: Patch Tool".into(), tooltip: "Coming Soon: Patch Tool".into(),
tooltip_shortcut: None,
}, },
ToolBarMetadataGroup { ToolBarMetadataGroup {
tool_type: ToolType::Detail, tool_type: ToolType::Detail,
icon_name: "RasterDetailTool".into(), icon_name: "RasterDetailTool".into(),
tooltip: "Coming Soon: Detail Tool (D)".into(), tooltip: "Coming Soon: Detail Tool (D)".into(),
tooltip_shortcut: None,
}, },
ToolBarMetadataGroup { ToolBarMetadataGroup {
tool_type: ToolType::Relight, tool_type: ToolType::Relight,
icon_name: "RasterRelightTool".into(), 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 { pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
use ToolMessage::*; match tool_message {
match message {
// General tool group // General tool group
Select(_) => ToolType::Select, ToolMessage::Select(_) => ToolType::Select,
Artboard(_) => ToolType::Artboard, ToolMessage::Artboard(_) => ToolType::Artboard,
Navigate(_) => ToolType::Navigate, ToolMessage::Navigate(_) => ToolType::Navigate,
Eyedropper(_) => ToolType::Eyedropper, ToolMessage::Eyedropper(_) => ToolType::Eyedropper,
Fill(_) => ToolType::Fill, ToolMessage::Fill(_) => ToolType::Fill,
Gradient(_) => ToolType::Gradient, ToolMessage::Gradient(_) => ToolType::Gradient,
// Vector tool group // Vector tool group
Path(_) => ToolType::Path, ToolMessage::Path(_) => ToolType::Path,
Pen(_) => ToolType::Pen, ToolMessage::Pen(_) => ToolType::Pen,
Freehand(_) => ToolType::Freehand, ToolMessage::Freehand(_) => ToolType::Freehand,
Spline(_) => ToolType::Spline, ToolMessage::Spline(_) => ToolType::Spline,
Line(_) => ToolType::Line, ToolMessage::Line(_) => ToolType::Line,
Rectangle(_) => ToolType::Rectangle, ToolMessage::Rectangle(_) => ToolType::Rectangle,
Ellipse(_) => ToolType::Ellipse, ToolMessage::Ellipse(_) => ToolType::Ellipse,
Shape(_) => ToolType::Shape, ToolMessage::Shape(_) => ToolType::Shape,
Text(_) => ToolType::Text, ToolMessage::Text(_) => ToolType::Text,
// Raster tool group // Raster tool group
// Brush(_) => ToolType::Brush, // ToolMessage::Brush(_) => ToolType::Brush,
// Heal(_) => ToolType::Heal, // ToolMessage::Heal(_) => ToolType::Heal,
// Clone(_) => ToolType::Clone, // ToolMessage::Clone(_) => ToolType::Clone,
// Patch(_) => ToolType::Patch, // ToolMessage::Patch(_) => ToolType::Patch,
// Detail(_) => ToolType::Detail, // ToolMessage::Detail(_) => ToolType::Detail,
// Relight(_) => ToolType::Relight, // ToolMessage::Relight(_) => ToolType::Relight,
_ => panic!( _ => panic!(
"Conversion from message to tool type impossible because the given ToolMessage does not belong to a tool. Got: {:?}", "Conversion from ToolMessage to ToolType impossible because the given ToolMessage does not have a matching ToolType. Got: {:?}",
message 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
), ),
} }
} }

View file

@ -28,6 +28,7 @@ pub enum ToolMessage {
#[remain::unsorted] #[remain::unsorted]
#[child] #[child]
Gradient(GradientToolMessage), Gradient(GradientToolMessage),
#[remain::unsorted] #[remain::unsorted]
#[child] #[child]
Path(PathToolMessage), Path(PathToolMessage),
@ -54,7 +55,8 @@ pub enum ToolMessage {
Shape(ShapeToolMessage), Shape(ShapeToolMessage),
#[remain::unsorted] #[remain::unsorted]
#[child] #[child]
Text(TextMessage), Text(TextToolMessage),
// #[remain::unsorted] // #[remain::unsorted]
// #[child] // #[child]
// Brush(BrushToolMessage), // Brush(BrushToolMessage),
@ -75,6 +77,38 @@ pub enum ToolMessage {
// Detail(DetailToolMessage), // Detail(DetailToolMessage),
// Messages // 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 { ActivateTool {
tool_type: ToolType, tool_type: ToolType,
}, },

View file

@ -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::document::DocumentMessageHandler;
use crate::input::input_mapper::action_keys::action_shortcut;
use crate::input::InputPreprocessorMessageHandler; use crate::input::InputPreprocessorMessageHandler;
use crate::layout::layout_message::LayoutTarget; use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::PropertyHolder; use crate::layout::widgets::PropertyHolder;
use crate::layout::widgets::{IconButton, Layout, LayoutGroup, SwatchPairInput, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::layout::widgets::{IconButton, Layout, LayoutGroup, SwatchPairInput, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
use crate::message_prelude::*; use crate::message_prelude::*;
use crate::viewport_tools::tool::DocumentToolData; use crate::viewport_tools::tool::{DocumentToolData, ToolType};
use graphene::color::Color; use graphene::color::Color;
use graphene::layers::text_layer::FontCache; use graphene::layers::text_layer::FontCache;
@ -26,6 +27,38 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
#[remain::sorted] #[remain::sorted]
match message { match message {
// Messages // Messages
#[remain::unsorted]
ActivateToolSelect => 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 } => { ActivateTool { tool_type } => {
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
let document_data = &self.tool_state.document_tool_data; let document_data = &self.tool_state.document_tool_data;
@ -151,7 +184,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
tool_message => { tool_message => {
let tool_type = match &tool_message { let tool_type = match &tool_message {
UpdateCursor | UpdateHints => self.tool_state.tool_data.active_tool_type, 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 document_data = &self.tool_state.document_tool_data;
let tool_data = &mut self.tool_state.tool_data; let tool_data = &mut self.tool_state.tool_data;
@ -167,7 +200,21 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
fn actions(&self) -> ActionList { fn actions(&self) -> ActionList {
let mut list = actions!(ToolMessageDiscriminant; let mut list = actions!(ToolMessageDiscriminant;
ActivateTool, ActivateToolSelect,
ActivateToolArtboard,
ActivateToolNavigate,
ActivateToolEyedropper,
ActivateToolText,
ActivateToolFill,
ActivateToolGradient,
ActivateToolPath,
ActivateToolPen,
ActivateToolFreehand,
ActivateToolSpline,
ActivateToolLine,
ActivateToolRectangle,
ActivateToolEllipse,
ActivateToolShape,
SelectRandomPrimaryColor, SelectRandomPrimaryColor,
ResetColors, ResetColors,
SwapColors, SwapColors,
@ -191,14 +238,16 @@ fn update_working_colors(document_data: &DocumentToolData, responses: &mut VecDe
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
size: 16, size: 16,
icon: "Swap".into(), 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()), on_update: WidgetCallback::new(|_| ToolMessage::SwapColors.into()),
..Default::default() ..Default::default()
})), })),
WidgetHolder::new(Widget::IconButton(IconButton { WidgetHolder::new(Widget::IconButton(IconButton {
size: 16, size: 16,
icon: "ResetColors".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut icon: "ResetColors".into(),
tooltip: "Reset (Ctrl+Shift+X)".into(), tooltip: "Reset".into(),
tooltip_shortcut: action_shortcut!(ToolMessageDiscriminant::ResetColors),
on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()), on_update: WidgetCallback::new(|_| ToolMessage::ResetColors.into()),
..Default::default() ..Default::default()
})), })),

View file

@ -243,8 +243,10 @@ impl Fsm for ArtboardToolFsmState {
let mouse_position = input.mouse.position; let mouse_position = input.mouse.position;
let snapped_mouse_position = tool_data.snap_handler.snap_position(responses, document, 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 (mut position, size) = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square);
let position = movement.center_position(position, size, from_center); if from_center {
position = movement.center_position(position, size);
}
responses.push_back( responses.push_back(
ArtboardMessage::ResizeArtboard { ArtboardMessage::ResizeArtboard {
@ -408,18 +410,21 @@ impl Fsm for ArtboardToolFsmState {
ArtboardToolFsmState::Ready => HintData(vec![ ArtboardToolFsmState::Ready => HintData(vec![
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Artboard"), label: String::from("Draw Artboard"),
plus: false, plus: false,
}]), }]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Move Artboard"), label: String::from("Move Artboard"),
plus: false, plus: false,
}]), }]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyBackspace])], key_groups: vec![KeysGroup(vec![Key::KeyBackspace])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Delete Artboard"), label: String::from("Delete Artboard"),
plus: false, plus: false,
@ -427,6 +432,7 @@ impl Fsm for ArtboardToolFsmState {
]), ]),
ArtboardToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo { ArtboardToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain to Axis"), label: String::from("Constrain to Axis"),
plus: false, plus: false,
@ -434,12 +440,14 @@ impl Fsm for ArtboardToolFsmState {
ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds => HintData(vec![HintGroup(vec![ ArtboardToolFsmState::Drawing | ArtboardToolFsmState::ResizingBounds => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain Square"), label: String::from("Constrain Square"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: false, plus: false,

View file

@ -41,7 +41,7 @@ impl ToolMetadata for EllipseTool {
"VectorEllipseTool".into() "VectorEllipseTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Ellipse Tool (E)".into() "Ellipse Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Ellipse ToolType::Ellipse
@ -185,18 +185,21 @@ impl Fsm for EllipseToolFsmState {
EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![ EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Ellipse"), label: String::from("Draw Ellipse"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain Circular"), label: String::from("Constrain Circular"),
plus: true, plus: true,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: true, plus: true,
@ -205,12 +208,14 @@ impl Fsm for EllipseToolFsmState {
EllipseToolFsmState::Drawing => HintData(vec![HintGroup(vec![ EllipseToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain Circular"), label: String::from("Constrain Circular"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: false, plus: false,

View file

@ -36,7 +36,7 @@ impl ToolMetadata for EyedropperTool {
"GeneralEyedropperTool".into() "GeneralEyedropperTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Eyedropper Tool (I)".into() "Eyedropper Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Eyedropper ToolType::Eyedropper
@ -147,12 +147,14 @@ impl Fsm for EyedropperToolFsmState {
EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![ EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Sample to Primary"), label: String::from("Sample to Primary"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Rmb), mouse: Some(MouseMotion::Rmb),
label: String::from("Sample to Secondary"), label: String::from("Sample to Secondary"),
plus: false, plus: false,

View file

@ -37,7 +37,7 @@ impl ToolMetadata for FillTool {
"GeneralFillTool".into() "GeneralFillTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Fill Tool (F)".into() "Fill Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Fill ToolType::Fill
@ -146,12 +146,14 @@ impl Fsm for FillToolFsmState {
FillToolFsmState::Ready => HintData(vec![HintGroup(vec![ FillToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Fill with Primary"), label: String::from("Fill with Primary"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Rmb), mouse: Some(MouseMotion::Rmb),
label: String::from("Fill with Secondary"), label: String::from("Fill with Secondary"),
plus: false, plus: false,

View file

@ -60,7 +60,7 @@ impl ToolMetadata for FreehandTool {
"VectorFreehandTool".into() "VectorFreehandTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Freehand Tool (N)".into() "Freehand Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Freehand ToolType::Freehand
@ -222,6 +222,7 @@ impl Fsm for FreehandToolFsmState {
let hint_data = match self { let hint_data = match self {
FreehandToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { FreehandToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Polyline"), label: String::from("Draw Polyline"),
plus: false, plus: false,

View file

@ -65,7 +65,7 @@ impl ToolMetadata for GradientTool {
"GeneralGradientTool".into() "GeneralGradientTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Gradient Tool (H))".into() "Gradient Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Gradient ToolType::Gradient
@ -461,12 +461,14 @@ impl Fsm for GradientToolFsmState {
GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![ GradientToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Gradient"), label: String::from("Draw Gradient"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: true, plus: true,
@ -474,6 +476,7 @@ impl Fsm for GradientToolFsmState {
])]), ])]),
GradientToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo { GradientToolFsmState::Drawing => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: false, plus: false,

View file

@ -61,7 +61,7 @@ impl ToolMetadata for LineTool {
"VectorLineTool".into() "VectorLineTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Line Tool (L)".into() "Line Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Line ToolType::Line
@ -239,24 +239,28 @@ impl Fsm for LineToolFsmState {
LineToolFsmState::Ready => HintData(vec![HintGroup(vec![ LineToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Line"), label: String::from("Draw Line"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: true, plus: true,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: true, plus: true,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Lock Angle"), label: String::from("Lock Angle"),
plus: true, plus: true,
@ -265,18 +269,21 @@ impl Fsm for LineToolFsmState {
LineToolFsmState::Drawing => HintData(vec![HintGroup(vec![ LineToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Lock Angle"), label: String::from("Lock Angle"),
plus: false, plus: false,

View file

@ -41,7 +41,7 @@ impl ToolMetadata for NavigateTool {
"GeneralNavigateTool".into() "GeneralNavigateTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Navigate Tool (Z)".into() "Navigate Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Navigate ToolType::Navigate
@ -195,12 +195,14 @@ impl Fsm for NavigateToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Zoom In"), label: String::from("Zoom In"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Zoom Out"), label: String::from("Zoom Out"),
plus: true, plus: true,
@ -209,12 +211,14 @@ impl Fsm for NavigateToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Zoom"), label: String::from("Zoom"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap Increments"), label: String::from("Snap Increments"),
plus: true, plus: true,
@ -222,6 +226,7 @@ impl Fsm for NavigateToolFsmState {
]), ]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::MmbDrag), mouse: Some(MouseMotion::MmbDrag),
label: String::from("Pan"), label: String::from("Pan"),
plus: false, plus: false,
@ -229,12 +234,14 @@ impl Fsm for NavigateToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::RmbDrag), mouse: Some(MouseMotion::RmbDrag),
label: String::from("Tilt"), label: String::from("Tilt"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: true, plus: true,
@ -243,12 +250,14 @@ impl Fsm for NavigateToolFsmState {
]), ]),
NavigateToolFsmState::Tilting => HintData(vec![HintGroup(vec![HintInfo { NavigateToolFsmState::Tilting => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: false, plus: false,
}])]), }])]),
NavigateToolFsmState::Zooming => HintData(vec![HintGroup(vec![HintInfo { NavigateToolFsmState::Zooming => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap Increments"), label: String::from("Snap Increments"),
plus: false, plus: false,

View file

@ -50,7 +50,7 @@ impl ToolMetadata for PathTool {
"VectorPathTool".into() "VectorPathTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Path Tool (A)".into() "Path Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Path ToolType::Path
@ -306,12 +306,14 @@ impl Fsm for PathToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Select Point"), label: String::from("Select Point"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Grow/Shrink Selection"), label: String::from("Grow/Shrink Selection"),
plus: true, plus: true,
@ -319,6 +321,7 @@ impl Fsm for PathToolFsmState {
]), ]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Drag Selected"), label: String::from("Drag Selected"),
plus: false, plus: false,
@ -331,12 +334,14 @@ impl Fsm for PathToolFsmState {
KeysGroup(vec![Key::KeyArrowDown]), KeysGroup(vec![Key::KeyArrowDown]),
KeysGroup(vec![Key::KeyArrowLeft]), KeysGroup(vec![Key::KeyArrowLeft]),
], ],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Nudge Selected (coming soon)"), label: String::from("Nudge Selected (coming soon)"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Big Increment Nudge"), label: String::from("Big Increment Nudge"),
plus: true, plus: true,
@ -345,18 +350,21 @@ impl Fsm for PathToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyG])], key_groups: vec![KeysGroup(vec![Key::KeyG])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Grab Selected (coming soon)"), label: String::from("Grab Selected (coming soon)"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyR])], key_groups: vec![KeysGroup(vec![Key::KeyR])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Rotate Selected (coming soon)"), label: String::from("Rotate Selected (coming soon)"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyS])], key_groups: vec![KeysGroup(vec![Key::KeyS])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Scale Selected (coming soon)"), label: String::from("Scale Selected (coming soon)"),
plus: false, plus: false,
@ -366,12 +374,14 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Dragging => HintData(vec![HintGroup(vec![ PathToolFsmState::Dragging => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Split/Align Handles (Toggle)"), label: String::from("Split/Align Handles (Toggle)"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Share Lengths of Aligned Handles"), label: String::from("Share Lengths of Aligned Handles"),
plus: false, plus: false,

View file

@ -78,7 +78,7 @@ impl ToolMetadata for PenTool {
"VectorPenTool".into() "VectorPenTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Pen Tool (P)".into() "Pen Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Pen ToolType::Pen
@ -374,6 +374,7 @@ impl Fsm for PenToolFsmState {
let hint_data = match self { let hint_data = match self {
PenToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { PenToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Draw Path"), label: String::from("Draw Path"),
plus: false, plus: false,
@ -381,30 +382,35 @@ impl Fsm for PenToolFsmState {
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => HintData(vec![ PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => HintData(vec![
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Add Handle"), label: String::from("Add Handle"),
plus: false, plus: false,
}]), }]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Add Anchor"), label: String::from("Add Anchor"),
plus: false, plus: false,
}]), }]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: false, plus: false,
}]), }]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Break Handle"), label: String::from("Break Handle"),
plus: false, plus: false,
}]), }]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyEnter])], key_groups: vec![KeysGroup(vec![Key::KeyEnter])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("End Path"), label: String::from("End Path"),
plus: false, plus: false,

View file

@ -80,7 +80,7 @@ impl ToolMetadata for RectangleTool {
"VectorRectangleTool".into() "VectorRectangleTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Rectangle Tool (M)".into() "Rectangle Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Rectangle ToolType::Rectangle
@ -186,18 +186,21 @@ impl Fsm for RectangleToolFsmState {
RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![ RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Rectangle"), label: String::from("Draw Rectangle"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain Square"), label: String::from("Constrain Square"),
plus: true, plus: true,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: true, plus: true,
@ -206,12 +209,14 @@ impl Fsm for RectangleToolFsmState {
RectangleToolFsmState::Drawing => HintData(vec![HintGroup(vec![ RectangleToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain Square"), label: String::from("Constrain Square"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: false, plus: false,

View file

@ -63,7 +63,7 @@ impl ToolMetadata for SelectTool {
"GeneralSelectTool".into() "GeneralSelectTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Select Tool (V)".into() "Select Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Select ToolType::Select
@ -390,7 +390,7 @@ impl Fsm for SelectToolFsmState {
match intersect.data { match intersect.data {
LayerDataType::Text(_) => { LayerDataType::Text(_) => {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into()); responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }.into());
responses.push_back(TextMessage::Interact.into()); responses.push_back(TextToolMessage::Interact.into());
} }
LayerDataType::Shape(_) => { LayerDataType::Shape(_) => {
responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }.into()); 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 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 delta = movement.bounds_to_scale_transform(center, size);
let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>(); let selected = tool_data.layers_dragging.iter().collect::<Vec<_>>();
@ -702,6 +702,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::Ready => HintData(vec![ SelectToolFsmState::Ready => HintData(vec![
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Drag Selected"), label: String::from("Drag Selected"),
plus: false, plus: false,
@ -709,18 +710,21 @@ impl Fsm for SelectToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyG])], key_groups: vec![KeysGroup(vec![Key::KeyG])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Grab Selected"), label: String::from("Grab Selected"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyR])], key_groups: vec![KeysGroup(vec![Key::KeyR])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Rotate Selected"), label: String::from("Rotate Selected"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyS])], key_groups: vec![KeysGroup(vec![Key::KeyS])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Scale Selected"), label: String::from("Scale Selected"),
plus: false, plus: false,
@ -729,18 +733,21 @@ impl Fsm for SelectToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Select Object"), label: String::from("Select Object"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand])]),
mouse: None, mouse: None,
label: String::from("Innermost"), label: String::from("Innermost"),
plus: true, plus: true,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Grow/Shrink Selection"), label: String::from("Grow/Shrink Selection"),
plus: true, plus: true,
@ -749,12 +756,14 @@ impl Fsm for SelectToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Select Area"), label: String::from("Select Area"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Grow/Shrink Selection"), label: String::from("Grow/Shrink Selection"),
plus: true, plus: true,
@ -768,12 +777,14 @@ impl Fsm for SelectToolFsmState {
KeysGroup(vec![Key::KeyArrowDown]), KeysGroup(vec![Key::KeyArrowDown]),
KeysGroup(vec![Key::KeyArrowLeft]), KeysGroup(vec![Key::KeyArrowLeft]),
], ],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Nudge Selected"), label: String::from("Nudge Selected"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Big Increment Nudge"), label: String::from("Big Increment Nudge"),
plus: true, plus: true,
@ -782,12 +793,14 @@ impl Fsm for SelectToolFsmState {
HintGroup(vec![ HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Move Duplicate"), label: String::from("Move Duplicate"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyD])], key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyD])],
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyD])]),
mouse: None, mouse: None,
label: String::from("Duplicate"), label: String::from("Duplicate"),
plus: false, plus: false,
@ -797,12 +810,14 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::Dragging => HintData(vec![HintGroup(vec![ SelectToolFsmState::Dragging => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain to Axis"), label: String::from("Constrain to Axis"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap to Points (coming soon)"), label: String::from("Snap to Points (coming soon)"),
plus: false, plus: false,
@ -812,6 +827,7 @@ impl Fsm for SelectToolFsmState {
SelectToolFsmState::ResizingBounds => HintData(vec![]), SelectToolFsmState::ResizingBounds => HintData(vec![]),
SelectToolFsmState::RotatingBounds => HintData(vec![HintGroup(vec![HintInfo { SelectToolFsmState::RotatingBounds => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])], key_groups: vec![KeysGroup(vec![Key::KeyControl])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Snap 15°"), label: String::from("Snap 15°"),
plus: false, plus: false,

View file

@ -59,7 +59,7 @@ impl ToolMetadata for ShapeTool {
"VectorShapeTool".into() "VectorShapeTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Shape Tool (Y)".into() "Shape Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Shape ToolType::Shape
@ -228,18 +228,21 @@ impl Fsm for ShapeToolFsmState {
ShapeToolFsmState::Ready => HintData(vec![HintGroup(vec![ ShapeToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::LmbDrag), mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Shape"), label: String::from("Draw Shape"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain 1:1 Aspect"), label: String::from("Constrain 1:1 Aspect"),
plus: true, plus: true,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: true, plus: true,
@ -248,12 +251,14 @@ impl Fsm for ShapeToolFsmState {
ShapeToolFsmState::Drawing => HintData(vec![HintGroup(vec![ ShapeToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])], key_groups: vec![KeysGroup(vec![Key::KeyShift])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Constrain 1:1 Aspect"), label: String::from("Constrain 1:1 Aspect"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])], key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("From Center"), label: String::from("From Center"),
plus: false, plus: false,

View file

@ -61,7 +61,7 @@ impl SelectedEdges {
} }
/// Computes the new bounds with the given mouse move and modifier keys /// 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 mouse = transform.inverse().transform_point2(mouse);
let mut min = self.bounds[0]; let mut min = self.bounds[0];
@ -72,7 +72,9 @@ impl SelectedEdges {
max.y = mouse.y; max.y = mouse.y;
} }
if self.left { if self.left {
min.x = mouse.x let delta = min.x - mouse.x;
min.x = mouse.x;
max.x += delta;
} else if self.right { } else if self.right {
max.x = mouse.x; 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 /// Offsets the transformation pivot in order to scale from the center
fn offset_pivot(&self, center: bool, size: DVec2) -> DVec2 { fn offset_pivot(&self, center: bool, size: DVec2) -> DVec2 {
let mut offset = DVec2::ZERO; let mut offset = DVec2::ZERO;
if center && self.right { if !center {
return offset;
}
if self.right {
offset.x -= size.x / 2.; offset.x -= size.x / 2.;
} }
if center && self.left { if self.left {
offset.x += size.x / 2.; offset.x += size.x / 2.;
} }
if center && self.bottom { if self.bottom {
offset.y -= size.y / 2.; offset.y -= size.y / 2.;
} }
if center && self.top { if self.top {
offset.y += size.y / 2.; offset.y += size.y / 2.;
} }
offset offset
} }
/// Moves the position to account for centring (only necessary with absolute transforms - e.g. with artboards) /// 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, center: bool) -> DVec2 { pub fn center_position(&self, mut position: DVec2, size: DVec2) -> DVec2 {
if center && self.right { if self.right {
position.x -= size.x / 2.; position.x -= size.x / 2.;
} }
if center && self.bottom { if self.bottom {
position.y -= size.y / 2.; position.y -= size.y / 2.;
} }

View file

@ -251,6 +251,7 @@ impl Fsm for SplineToolFsmState {
let hint_data = match self { let hint_data = match self {
SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { SplineToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Draw Spline"), label: String::from("Draw Spline"),
plus: false, plus: false,
@ -258,12 +259,14 @@ impl Fsm for SplineToolFsmState {
SplineToolFsmState::Drawing => HintData(vec![ SplineToolFsmState::Drawing => HintData(vec![
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Extend Spline"), label: String::from("Extend Spline"),
plus: false, plus: false,
}]), }]),
HintGroup(vec![HintInfo { HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyEnter])], key_groups: vec![KeysGroup(vec![Key::KeyEnter])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("End Spline"), label: String::from("End Spline"),
plus: false, plus: false,

View file

@ -42,7 +42,7 @@ impl Default for TextOptions {
#[remain::sorted] #[remain::sorted]
#[impl_message(Message, ToolMessage, Text)] #[impl_message(Message, ToolMessage, Text)]
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)] #[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum TextMessage { pub enum TextToolMessage {
// Standard messages // Standard messages
#[remain::unsorted] #[remain::unsorted]
Abort, Abort,
@ -74,7 +74,7 @@ impl ToolMetadata for TextTool {
"VectorTextTool".into() "VectorTextTool".into()
} }
fn tooltip(&self) -> String { fn tooltip(&self) -> String {
"Text Tool (T)".into() "Text Tool".into()
} }
fn tool_type(&self) -> crate::viewport_tools::tool::ToolType { fn tool_type(&self) -> crate::viewport_tools::tool::ToolType {
ToolType::Text ToolType::Text
@ -90,7 +90,7 @@ impl PropertyHolder for TextTool {
font_family: self.options.font_name.clone(), font_family: self.options.font_name.clone(),
font_style: self.options.font_style.clone(), font_style: self.options.font_style.clone(),
on_update: WidgetCallback::new(|font_input: &FontInput| { on_update: WidgetCallback::new(|font_input: &FontInput| {
TextMessage::UpdateOptions(TextOptionsUpdate::Font { TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
family: font_input.font_family.clone(), family: font_input.font_family.clone(),
style: font_input.font_style.clone(), style: font_input.font_style.clone(),
}) })
@ -107,7 +107,7 @@ impl PropertyHolder for TextTool {
font_family: self.options.font_name.clone(), font_family: self.options.font_name.clone(),
font_style: self.options.font_style.clone(), font_style: self.options.font_style.clone(),
on_update: WidgetCallback::new(|font_input: &FontInput| { on_update: WidgetCallback::new(|font_input: &FontInput| {
TextMessage::UpdateOptions(TextOptionsUpdate::Font { TextToolMessage::UpdateOptions(TextOptionsUpdate::Font {
family: font_input.font_family.clone(), family: font_input.font_family.clone(),
style: font_input.font_style.clone(), style: font_input.font_style.clone(),
}) })
@ -125,7 +125,7 @@ impl PropertyHolder for TextTool {
value: Some(self.options.font_size as f64), value: Some(self.options.font_size as f64),
is_integer: true, is_integer: true,
min: Some(1.), 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() ..NumberInput::default()
})), })),
], ],
@ -145,7 +145,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for TextTool {
return; return;
} }
if let ToolMessage::Text(TextMessage::UpdateOptions(action)) = action { if let ToolMessage::Text(TextToolMessage::UpdateOptions(action)) = action {
match action { match action {
TextOptionsUpdate::Font { family, style } => { TextOptionsUpdate::Font { family, style } => {
self.options.font_name = family; self.options.font_name = family;
@ -171,10 +171,10 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for TextTool {
use TextToolFsmState::*; use TextToolFsmState::*;
match self.fsm_state { match self.fsm_state {
Ready => actions!(TextMessageDiscriminant; Ready => actions!(TextToolMessageDiscriminant;
Interact, Interact,
), ),
Editing => actions!(TextMessageDiscriminant; Editing => actions!(TextToolMessageDiscriminant;
Interact, Interact,
Abort, Abort,
CommitText, CommitText,
@ -186,8 +186,8 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for TextTool {
impl ToolTransition for TextTool { impl ToolTransition for TextTool {
fn signal_to_message_map(&self) -> SignalToMessageMap { fn signal_to_message_map(&self) -> SignalToMessageMap {
SignalToMessageMap { SignalToMessageMap {
document_dirty: Some(TextMessage::DocumentIsDirty.into()), document_dirty: Some(TextToolMessage::DocumentIsDirty.into()),
tool_abort: Some(TextMessage::Abort.into()), tool_abort: Some(TextToolMessage::Abort.into()),
selection_changed: None, selection_changed: None,
} }
} }
@ -275,8 +275,8 @@ impl Fsm for TextToolFsmState {
tool_options: &Self::ToolOptions, tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Self { ) -> Self {
use TextMessage::*;
use TextToolFsmState::*; use TextToolFsmState::*;
use TextToolMessage::*;
if let ToolMessage::Text(event) = event { if let ToolMessage::Text(event) = event {
match (self, event) { match (self, event) {
@ -456,12 +456,14 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Ready => HintData(vec![HintGroup(vec![ TextToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Add Text"), label: String::from("Add Text"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![], key_groups: vec![],
key_groups_mac: None,
mouse: Some(MouseMotion::Lmb), mouse: Some(MouseMotion::Lmb),
label: String::from("Edit Text"), label: String::from("Edit Text"),
plus: false, plus: false,
@ -470,12 +472,14 @@ impl Fsm for TextToolFsmState {
TextToolFsmState::Editing => HintData(vec![HintGroup(vec![ TextToolFsmState::Editing => HintData(vec![HintGroup(vec![
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyEnter])], key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyEnter])],
key_groups_mac: Some(vec![KeysGroup(vec![Key::KeyCommand, Key::KeyEnter])]),
mouse: None, mouse: None,
label: String::from("Commit Edit"), label: String::from("Commit Edit"),
plus: false, plus: false,
}, },
HintInfo { HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyEscape])], key_groups: vec![KeysGroup(vec![Key::KeyEscape])],
key_groups_mac: None,
mouse: None, mouse: None,
label: String::from("Discard Edit"), label: String::from("Discard Edit"),
plus: false, plus: false,

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
<polygon points="6,1 1,5 1.6,5.8 6,2.3 10.4,5.8 11,5" />
</svg>

After

Width:  |  Height:  |  Size: 126 B

View file

@ -230,6 +230,7 @@ import { createFullscreenState, FullscreenState } from "@/state-providers/fullsc
import { createPanelsState, PanelsState } from "@/state-providers/panels"; import { createPanelsState, PanelsState } from "@/state-providers/panels";
import { createPortfolioState, PortfolioState } from "@/state-providers/portfolio"; import { createPortfolioState, PortfolioState } from "@/state-providers/portfolio";
import { createWorkspaceState, WorkspaceState } from "@/state-providers/workspace"; import { createWorkspaceState, WorkspaceState } from "@/state-providers/workspace";
import { operatingSystem } from "@/utility-functions/platform";
import { createEditor, Editor } from "@/wasm-communication/editor"; import { createEditor, Editor } from "@/wasm-communication/editor";
import MainWindow from "@/components/window/MainWindow.vue"; 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 // 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() { beforeUnmount() {
// Call the destructor for each manager // Call the destructor for each manager

View file

@ -33,8 +33,7 @@
<span class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</span> <span class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</span>
<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" /> <UserInputLabel v-if="entry.shortcut?.keys.length" :inputKeys="[entry.shortcut.keys]" :requiresLock="entry.shortcutRequiresLock" />
<UserInputLabel v-else-if="entry.shortcut?.length" :inputKeys="[entry.shortcut]" />
<div class="submenu-arrow" v-if="entry.children?.length"></div> <div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div> <div class="no-submenu-arrow" v-else></div>
@ -64,6 +63,10 @@
.floating-menu-container .floating-menu-content { .floating-menu-container .floating-menu-content {
padding: 4px 0; padding: 4px 0;
.separator div {
background: var(--color-4-dimgray);
}
.scroll-spacer { .scroll-spacer {
flex: 0 0 auto; flex: 0 0 auto;
} }
@ -128,29 +131,24 @@
&.open, &.open,
&.active { &.active {
background: var(--color-6-lowergray); background: var(--color-6-lowergray);
color: var(--color-f-white);
&.active { &.active {
background: var(--color-accent); background: var(--color-accent);
} }
svg { .entry-icon svg {
fill: var(--color-f-white); fill: var(--color-f-white);
} }
span {
color: var(--color-f-white);
}
} }
&.disabled { &.disabled {
color: var(--color-8-uppergray);
&:hover { &:hover {
background: none; background: none;
} }
span {
color: var(--color-8-uppergray);
}
svg { svg {
fill: var(--color-8-uppergray); 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 Separator from "@/components/widgets/labels/Separator.vue";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.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({ const MenuList = defineComponent({
inject: ["fullscreen"],
emits: ["update:open", "update:activeEntry", "naturalWidth"], emits: ["update:open", "update:activeEntry", "naturalWidth"],
props: { props: {
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true }, entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
@ -193,7 +187,6 @@ const MenuList = defineComponent({
data() { data() {
return { return {
isOpen: this.open, isOpen: this.open,
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
highlighted: this.activeEntry as MenuListEntry | undefined, highlighted: this.activeEntry as MenuListEntry | undefined,
virtualScrollingEntriesStart: 0, virtualScrollingEntriesStart: 0,
}; };

View file

@ -31,14 +31,18 @@
<LayoutRow <LayoutRow
class="layer" class="layer"
:class="{ selected: listing.entry.layer_metadata.selected }" :class="{ selected: listing.entry.layer_metadata.selected }"
@click.shift.exact.stop="!listing.editingName && selectLayer(listing.entry, false, true)"
@click.shift.ctrl.exact.stop="!listing.editingName && selectLayer(listing.entry, true, true)"
@click.ctrl.exact.stop="!listing.editingName && selectLayer(listing.entry, true, false)"
@click.exact.stop="!listing.editingName && selectLayer(listing.entry, false, false)"
:data-index="index" :data-index="index"
:title="`${listing.entry.name}${devMode ? '\nLayer Path: ' + listing.entry.path.join(' / ') : ''}` || null"
:draggable="draggable" :draggable="draggable"
@dragstart="(e) => draggable && dragStart(e, listing.entry)" @dragstart="(e) => draggable && dragStart(e, listing)"
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`.trim() || null" @click.exact="(e) => selectLayer(false, false, false, listing, e)"
@click.shift.exact="(e) => selectLayer(false, false, true, listing, e)"
@click.ctrl.exact="(e) => selectLayer(true, false, false, listing, e)"
@click.ctrl.shift.exact="(e) => selectLayer(true, false, true, listing, e)"
@click.meta.exact="(e) => selectLayer(false, true, false, listing, e)"
@click.meta.shift.exact="(e) => selectLayer(false, true, true, listing, e)"
@click.ctrl.meta="(e) => e.stopPropagation()"
@click.alt="(e) => e.stopPropagation()"
> >
<LayoutRow class="layer-type-icon"> <LayoutRow class="layer-type-icon">
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" /> <IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" />
@ -53,10 +57,10 @@
:value="listing.entry.name" :value="listing.entry.name"
:placeholder="listing.entry.layer_type" :placeholder="listing.entry.layer_type"
:disabled="!listing.editingName" :disabled="!listing.editingName"
@change="(e) => onEditLayerNameChange(listing, e.target)"
@blur="() => onEditLayerNameDeselect(listing)" @blur="() => onEditLayerNameDeselect(listing)"
@keydown.esc="onEditLayerNameDeselect(listing)"
@keydown.enter="(e) => onEditLayerNameChange(listing, e.target)" @keydown.enter="(e) => onEditLayerNameChange(listing, e.target)"
@keydown.escape="onEditLayerNameDeselect(listing)" @change="(e) => onEditLayerNameChange(listing, e.target)"
/> />
</LayoutRow> </LayoutRow>
<div class="thumbnail" v-html="listing.entry.thumbnail"></div> <div class="thumbnail" v-html="listing.entry.thumbnail"></div>
@ -263,6 +267,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, nextTick } from "vue"; import { defineComponent, nextTick } from "vue";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import { defaultWidgetLayout, UpdateDocumentLayerTreeStructure, UpdateDocumentLayerDetails, UpdateLayerTreeOptionsLayout, LayerPanelEntry } from "@/wasm-communication/messages"; import { defaultWidgetLayout, UpdateDocumentLayerTreeStructure, UpdateDocumentLayerDetails, UpdateLayerTreeOptionsLayout, LayerPanelEntry } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue"; import LayoutCol from "@/components/layout/LayoutCol.vue";
@ -342,8 +347,18 @@ export default defineComponent({
await nextTick(); await nextTick();
window.getSelection()?.removeAllRanges(); window.getSelection()?.removeAllRanges();
}, },
async selectLayer(clickedLayer: LayerPanelEntry, ctrl: boolean, shift: boolean) { async selectLayer(ctrl: boolean, cmd: boolean, shift: boolean, listing: LayerListingInfo, event: Event) {
this.editor.instance.select_layer(clickedLayer.path, ctrl, shift); if (listing.editingName) return;
const ctrlOrCmd = operatingSystemIsMac() ? cmd : ctrl;
// Pressing the Ctrl key on a Mac, or the Cmd key on another platform, is a violation of the `.exact` qualifier so we filter it out here
const opposite = operatingSystemIsMac() ? ctrl : cmd;
if (!opposite) this.editor.instance.select_layer(listing.entry.path, ctrlOrCmd, shift);
// We always want to stop propagation so the click event doesn't pass through the layer and cause a deselection by clicking the layer panel background
// This is also why we cover the remaining cases not considered by the `.exact` qualifier, in the last two bindings on the layer element, with a `stopPropagation()` call
event.stopPropagation();
}, },
async deselectAllLayers() { async deselectAllLayers() {
this.editor.instance.deselect_all_layers(); this.editor.instance.deselect_all_layers();
@ -409,8 +424,9 @@ export default defineComponent({
return { insertFolder, insertIndex, highlightFolder, markerHeight }; return { insertFolder, insertIndex, highlightFolder, markerHeight };
}, },
async dragStart(event: DragEvent, layer: LayerPanelEntry) { async dragStart(event: DragEvent, listing: LayerListingInfo) {
if (!layer.layer_metadata.selected) this.selectLayer(layer, event.ctrlKey, event.shiftKey); const layer = listing.entry;
if (!layer.layer_metadata.selected) this.selectLayer(event.ctrlKey, event.metaKey, event.shiftKey, listing, event);
// Set style of cursor for drag // Set style of cursor for drag
if (event.dataTransfer) { if (event.dataTransfer) {

View file

@ -29,7 +29,8 @@
@focus="() => $emit('textFocused')" @focus="() => $emit('textFocused')"
@blur="() => $emit('textChanged')" @blur="() => $emit('textChanged')"
@change="() => $emit('textChanged')" @change="() => $emit('textChanged')"
@keydown.ctrl.enter="() => $emit('textChanged')" @keydown.ctrl.enter="() => !macKeyboardLayout && $emit('textChanged')"
@keydown.meta.enter="() => macKeyboardLayout && $emit('textChanged')"
@keydown.esc="() => $emit('cancelTextChange')" @keydown.esc="() => $emit('cancelTextChange')"
></textarea> ></textarea>
<label v-if="label" :for="`field-input-${id}`">{{ label }}</label> <label v-if="label" :for="`field-input-${id}`">{{ label }}</label>
@ -116,6 +117,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
export default defineComponent({ export default defineComponent({
@ -130,6 +133,7 @@ export default defineComponent({
data() { data() {
return { return {
id: `${Math.random()}`.substring(2), id: `${Math.random()}`.substring(2),
macKeyboardLayout: operatingSystemIsMac(),
}; };
}, },
computed: { computed: {

View file

@ -101,7 +101,7 @@ export default defineComponent({
...entry, ...entry,
children: entry.children ? menuEntryToFrontendMenuEntry(entry.children) : undefined, children: entry.children ? menuEntryToFrontendMenuEntry(entry.children) : undefined,
action: (): void => this.editor.instance.update_layout(updateMenuBarLayout.layout_target, entry.action.widgetId, undefined), action: (): void => this.editor.instance.update_layout(updateMenuBarLayout.layout_target, entry.action.widgetId, undefined),
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut) : undefined, shortcutRequiresLock: entry.shortcut?.keys ? shortcutRequiresLock(entry.shortcut.keys) : undefined,
})) }))
); );

View file

@ -1,5 +1,6 @@
<template> <template>
<LayoutRow class="user-input-label"> <IconLabel class="user-input-label keyboard-lock-notice" v-if="displayKeyboardLockNotice" :icon="'Info'" :title="keyboardLockInfoMessage" />
<LayoutRow class="user-input-label" v-else>
<template v-for="(keyGroup, keyGroupIndex) in inputKeys" :key="keyGroupIndex"> <template v-for="(keyGroup, keyGroupIndex) in inputKeys" :key="keyGroupIndex">
<span class="group-gap" v-if="keyGroupIndex > 0"></span> <span class="group-gap" v-if="keyGroupIndex > 0"></span>
<template v-for="(keyInfo, index) in keyTextOrIconList(keyGroup)" :key="index"> <template v-for="(keyInfo, index) in keyTextOrIconList(keyGroup)" :key="index">
@ -45,15 +46,14 @@
font-family: "Inconsolata", monospace; font-family: "Inconsolata", monospace;
font-weight: 400; font-weight: 400;
text-align: center; text-align: center;
color: var(--color-e-nearwhite);
border: 1px;
box-sizing: border-box;
border-style: solid;
border-color: var(--color-7-middlegray);
border-radius: 4px;
height: 16px; height: 16px;
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height, so moving it up 1 pixel by using 15px makes them agree // Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height, so moving it up 1 pixel by using 15px makes them agree
line-height: 15px; line-height: 15px;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-7-middlegray);
color: var(--color-e-nearwhite);
&.width-16 { &.width-16 {
width: 16px; width: 16px;
@ -93,6 +93,40 @@
.hint-text { .hint-text {
margin-left: 4px; margin-left: 4px;
} }
.floating-menu-content & {
.input-key {
border-color: var(--color-4-dimgray);
color: var(--color-8-uppergray);
}
.input-key .icon-label svg,
&.keyboard-lock-notice.keyboard-lock-notice svg,
.input-mouse .bright {
fill: var(--color-8-uppergray);
}
.input-mouse .dim {
fill: var(--color-4-dimgray);
}
}
.floating-menu-content .row:hover > & {
.input-key {
border-color: var(--color-7-middlegray);
color: var(--color-9-palegray);
}
.input-key .icon-label svg,
&.keyboard-lock-notice.keyboard-lock-notice svg,
.input-mouse .bright {
fill: var(--color-9-palegray);
}
.input-mouse .dim {
fill: var(--color-7-middlegray);
}
}
} }
</style> </style>
@ -100,92 +134,123 @@
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import { IconName } from "@/utility-functions/icons"; import { IconName } from "@/utility-functions/icons";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import { HintInfo, KeysGroup } from "@/wasm-communication/messages"; import { HintInfo, KeysGroup } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue"; import IconLabel from "@/components/widgets/labels/IconLabel.vue";
// Definitions
const textMap = {
Shift: "Shift",
Control: "Ctrl",
Alt: "Alt",
Delete: "Del",
PageUp: "PgUp",
PageDown: "PgDn",
Equals: "=",
Minus: "-",
Plus: "+",
Escape: "Esc",
Comma: ",",
Period: ".",
LeftBracket: "[",
RightBracket: "]",
LeftCurlyBracket: "{",
RightCurlyBracket: "}",
};
const iconsAndWidthsStandard = {
ArrowUp: 1,
ArrowRight: 1,
ArrowDown: 1,
ArrowLeft: 1,
Backspace: 2,
Enter: 2,
Tab: 2,
Space: 3,
};
const iconsAndWidthsMac = {
Shift: 2,
Control: 2,
Option: 2,
Command: 2,
};
export default defineComponent({ export default defineComponent({
components: { inject: ["fullscreen"],
IconLabel,
LayoutRow,
},
props: { props: {
inputKeys: { type: Array as PropType<HintInfo["key_groups"]>, default: () => [] }, inputKeys: { type: Array as PropType<HintInfo["keyGroups"]>, default: () => [] },
inputMouse: { type: String as PropType<HintInfo["mouse"]>, default: null }, inputMouse: { type: String as PropType<HintInfo["mouse"]>, default: null },
requiresLock: { type: Boolean as PropType<boolean>, default: false },
}, },
computed: { computed: {
hasSlotContent(): boolean { hasSlotContent(): boolean {
return Boolean(this.$slots.default); return Boolean(this.$slots.default);
}, },
keyboardLockInfoMessage(): string {
const USE_FULLSCREEN = "This hotkey is reserved by the browser, but becomes available in fullscreen mode";
const SWITCH_BROWSER = "This hotkey is reserved by the browser, but becomes available in Chrome, Edge, and Opera which support the Keyboard.lock() API";
return this.fullscreen.keyboardLockApiSupported ? USE_FULLSCREEN : SWITCH_BROWSER;
},
displayKeyboardLockNotice(): boolean {
return this.requiresLock && !this.fullscreen.state.keyboardLocked;
},
}, },
methods: { methods: {
keyTextOrIconList(keyGroup: KeysGroup): { text: string | null; icon: IconName | null; width: string }[] { keyTextOrIconList(keyGroup: KeysGroup): { text: string | null; icon: IconName | null; width: string }[] {
return keyGroup.map((inputKey) => this.keyTextOrIcon(inputKey)); return keyGroup.map((inputKey) => this.keyTextOrIcon(inputKey));
}, },
keyTextOrIcon(keyText: string): { text: string | null; icon: IconName | null; width: string } { keyTextOrIcon(input: string): { text: string | null; icon: IconName | null; width: string } {
// Definitions let keyText = input;
const textMap: Record<string, string> = { if (operatingSystemIsMac()) {
Control: "Ctrl", keyText = keyText.replace("Alt", "Option");
Alt: "Alt", }
Delete: "Del",
PageUp: "PgUp", const iconsAndWidths = operatingSystemIsMac() ? { ...iconsAndWidthsStandard, ...iconsAndWidthsMac } : iconsAndWidthsStandard;
PageDown: "PgDn",
Equals: "=",
Minus: "-",
Plus: "+",
Escape: "Esc",
Comma: ",",
Period: ".",
LeftBracket: "[",
RightBracket: "]",
LeftCurlyBracket: "{",
RightCurlyBracket: "}",
};
const iconsAndWidths: Record<string, number> = {
ArrowUp: 1,
ArrowRight: 1,
ArrowDown: 1,
ArrowLeft: 1,
Backspace: 2,
Command: 2,
Enter: 2,
Option: 2,
Shift: 2,
Tab: 2,
Space: 3,
};
// Strip off the "Key" prefix // Strip off the "Key" prefix
const text = keyText.replace(/^(?:Key)?(.*)$/, "$1"); const text = keyText.replace(/^(?:Key)?(.*)$/, "$1");
// If it's an icon, return the icon identifier // If it's an icon, return the icon identifier
if (text in iconsAndWidths) { if (Object.keys(iconsAndWidths).includes(text)) {
// @ts-expect-error This is safe because of the if block we are in
const width = iconsAndWidths[text] * 8 + 8;
return { return {
text: null, text: null,
icon: this.keyboardHintIcon(text), icon: this.keyboardHintIcon(text),
width: `width-${iconsAndWidths[text] * 8 + 8}`, width: `width-${width}`,
}; };
} }
// Otherwise, return the text string // Otherwise, return the text string
let result; let result;
// Letters and numbers // Letters and numbers
if (/^[A-Z0-9]$/.test(text)) result = text; if (/^[A-Z0-9]$/.test(text)) {
result = text;
}
// Abbreviated names // Abbreviated names
else if (text in textMap) result = textMap[text]; else if (Object.keys(textMap).includes(text)) {
// @ts-expect-error This is safe because of the if block we are in
result = textMap[text];
}
// Other // Other
else result = text; else {
result = text;
}
return { text: result, icon: null, width: `width-${(result || " ").length * 8 + 8}` }; return { text: result, icon: null, width: `width-${(result || " ").length * 8 + 8}` };
}, },
mouseHintIcon(input: HintInfo["mouse"]): IconName { mouseHintIcon(input: HintInfo["mouse"]): IconName {
return `MouseHint${input}` as IconName; return `MouseHint${input}` as IconName;
}, },
keyboardHintIcon(input: HintInfo["key_groups"][0][0]): IconName { keyboardHintIcon(input: HintInfo["keyGroups"][0][0]): IconName {
return `Keyboard${input}` as IconName; return `Keyboard${input}` as IconName;
}, },
}, },
components: {
IconLabel,
LayoutRow,
},
}); });
</script> </script>

View file

@ -5,7 +5,7 @@
<Separator :type="'Section'" v-if="index !== 0" /> <Separator :type="'Section'" v-if="index !== 0" />
<template v-for="hint in hintGroup" :key="hint"> <template v-for="hint in hintGroup" :key="hint">
<LayoutRow v-if="hint.plus" class="plus">+</LayoutRow> <LayoutRow v-if="hint.plus" class="plus">+</LayoutRow>
<UserInputLabel :inputMouse="hint.mouse" :inputKeys="hint.key_groups">{{ hint.label }}</UserInputLabel> <UserInputLabel :inputMouse="hint.mouse" :inputKeys="inputKeysForPlatform(hint)">{{ hint.label }}</UserInputLabel>
</template> </template>
</template> </template>
</LayoutRow> </LayoutRow>
@ -44,7 +44,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { HintData, UpdateInputHints } from "@/wasm-communication/messages"; import { operatingSystemIsMac } from "@/utility-functions/platform";
import { HintData, HintInfo, KeysGroup, UpdateInputHints } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
import Separator from "@/components/widgets/labels/Separator.vue"; import Separator from "@/components/widgets/labels/Separator.vue";
@ -57,6 +58,12 @@ export default defineComponent({
hintData: [] as HintData, hintData: [] as HintData,
}; };
}, },
methods: {
inputKeysForPlatform(hint: HintInfo): KeysGroup[] {
if (operatingSystemIsMac() && hint.keyGroupsMac) return hint.keyGroupsMac;
return hint.keyGroups;
},
},
mounted() { mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdateInputHints, (updateInputHints) => { this.editor.subscriptions.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
this.hintData = updateInputHints.hint_data; this.hintData = updateInputHints.hint_data;

View file

@ -1,5 +1,5 @@
<template> <template>
<LayoutRow class="window-buttons-web" @click="() => handleClick()" :title="fullscreen.state.windowFullscreen ? 'Exit Fullscreen (F11)' : 'Enter Fullscreen (F11)'"> <LayoutRow class="window-buttons-web" @click="() => handleClick()" :title="(fullscreen.state.windowFullscreen ? 'Exit' : 'Enter') + ' Fullscreen (F11)'">
<TextLabel v-if="requestFullscreenHotkeys" :italic="true">Go fullscreen to access all hotkeys</TextLabel> <TextLabel v-if="requestFullscreenHotkeys" :italic="true">Go fullscreen to access all hotkeys</TextLabel>
<IconLabel :icon="fullscreen.state.windowFullscreen ? 'FullscreenExit' : 'FullscreenEnter'" /> <IconLabel :icon="fullscreen.state.windowFullscreen ? 'FullscreenExit' : 'FullscreenEnter'" />
</LayoutRow> </LayoutRow>

View file

@ -45,8 +45,8 @@
<Separator :type="'Unrelated'" /> <Separator :type="'Unrelated'" />
</LayoutCol> </LayoutCol>
<LayoutCol> <LayoutCol>
<UserInputLabel :inputKeys="[['KeyControl', 'KeyN']]" /> <UserInputLabel :inputKeys="[[controlOrCommandKey(), 'KeyN']]" />
<UserInputLabel :inputKeys="[['KeyControl', 'KeyO']]" /> <UserInputLabel :inputKeys="[[controlOrCommandKey(), 'KeyO']]" />
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>
@ -216,6 +216,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import LayoutCol from "@/components/layout/LayoutCol.vue"; import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
import Document from "@/components/panels/Document.vue"; import Document from "@/components/panels/Document.vue";
@ -257,6 +259,10 @@ export default defineComponent({
openDocument() { openDocument() {
this.editor.instance.document_open(); this.editor.instance.document_open();
}, },
controlOrCommandKey() {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
return operatingSystemIsMac() ? "KeyCommand" : "KeyControl";
},
}, },
components: { components: {
LayoutCol, LayoutCol,

View file

@ -2,6 +2,7 @@ import { DialogState } from "@/state-providers/dialog";
import { FullscreenState } from "@/state-providers/fullscreen"; import { FullscreenState } from "@/state-providers/fullscreen";
import { PortfolioState } from "@/state-providers/portfolio"; import { PortfolioState } from "@/state-providers/portfolio";
import { makeKeyboardModifiersBitfield, textInputCleanup, getLatinKey } from "@/utility-functions/keyboard-entry"; import { makeKeyboardModifiersBitfield, textInputCleanup, getLatinKey } from "@/utility-functions/keyboard-entry";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import { stripIndents } from "@/utility-functions/strip-indents"; import { stripIndents } from "@/utility-functions/strip-indents";
import { Editor } from "@/wasm-communication/editor"; import { Editor } from "@/wasm-communication/editor";
import { TriggerPaste } from "@/wasm-communication/messages"; import { TriggerPaste } from "@/wasm-communication/messages";
@ -39,7 +40,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
{ target: window, eventName: "pointerup", action: (e: PointerEvent): void => onPointerUp(e) }, { target: window, eventName: "pointerup", action: (e: PointerEvent): void => onPointerUp(e) },
{ target: window, eventName: "dblclick", action: (e: PointerEvent): void => onDoubleClick(e) }, { target: window, eventName: "dblclick", action: (e: PointerEvent): void => onDoubleClick(e) },
{ target: window, eventName: "mousedown", action: (e: MouseEvent): void => onMouseDown(e) }, { target: window, eventName: "mousedown", action: (e: MouseEvent): void => onMouseDown(e) },
{ target: window, eventName: "wheel", action: (e: WheelEvent): void => onMouseScroll(e), options: { passive: false } }, { target: window, eventName: "wheel", action: (e: WheelEvent): void => onWheelScroll(e), options: { passive: false } },
{ target: window, eventName: "modifyinputfield", action: (e: CustomEvent): void => onModifyInputField(e) }, { target: window, eventName: "modifyinputfield", action: (e: CustomEvent): void => onModifyInputField(e) },
{ target: window.document.body, eventName: "paste", action: (e: ClipboardEvent): void => onPaste(e) }, { target: window.document.body, eventName: "paste", action: (e: ClipboardEvent): void => onPaste(e) },
{ {
@ -69,13 +70,14 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
const key = getLatinKey(e); const key = getLatinKey(e);
if (!key) return false; if (!key) return false;
// TODO: Switch to a system where everything is sent to the backend, then the input preprocessor makes decisions and kicks some inputs back to the frontend
const ctrlOrCmd = operatingSystemIsMac() ? e.metaKey : e.ctrlKey;
// Don't redirect user input from text entry into HTML elements // Don't redirect user input from text entry into HTML elements
if (key !== "escape" && !(e.ctrlKey && key === "enter") && targetIsTextField(e.target)) { if (key !== "escape" && !(ctrlOrCmd && key === "enter") && targetIsTextField(e.target)) return false;
return false;
}
// Don't redirect paste // Don't redirect paste
if (key === "v" && e.ctrlKey) return false; if (key === "v" && ctrlOrCmd) return false;
// Don't redirect a fullscreen request // Don't redirect a fullscreen request
if (key === "f11" && e.type === "keydown" && !e.repeat) { if (key === "f11" && e.type === "keydown" && !e.repeat) {
@ -85,13 +87,13 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
} }
// Don't redirect a reload request // Don't redirect a reload request
if (key === "f5") return false; if (key === "f5" || (ctrlOrCmd && key === "r")) return false;
// Don't redirect debugging tools // Don't redirect debugging tools
if (key === "f12" || key === "f8") return false; if (key === "f12" || key === "f8") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "c") return false; if (ctrlOrCmd && e.shiftKey && key === "c") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "i") return false; if (ctrlOrCmd && e.shiftKey && key === "i") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "j") return false; if (ctrlOrCmd && e.shiftKey && key === "j") return false;
// Don't redirect tab or enter if not in canvas (to allow navigating elements) // Don't redirect tab or enter if not in canvas (to allow navigating elements)
if (!canvasFocused && !targetIsTextField(e.target) && ["tab", "enter", " ", "arrowdown", "arrowup", "arrowleft", "arrowright"].includes(key.toLowerCase())) return false; if (!canvasFocused && !targetIsTextField(e.target) && ["tab", "enter", " ", "arrowdown", "arrowup", "arrowleft", "arrowright"].includes(key.toLowerCase())) return false;
@ -200,7 +202,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
if (e.button === 1) e.preventDefault(); if (e.button === 1) e.preventDefault();
} }
function onMouseScroll(e: WheelEvent): void { function onWheelScroll(e: WheelEvent): void {
const { target } = e; const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-canvas]"); const isTargetingCanvas = target instanceof Element && target.closest("[data-canvas]");
@ -215,7 +217,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
if (isTargetingCanvas) { if (isTargetingCanvas) {
e.preventDefault(); e.preventDefault();
const modifiers = makeKeyboardModifiersBitfield(e); const modifiers = makeKeyboardModifiersBitfield(e);
editor.instance.on_mouse_scroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers); editor.instance.on_wheel_scroll(e.clientX, e.clientY, e.buttons, e.deltaX, e.deltaY, e.deltaZ, modifiers);
} }
} }

View file

@ -1,6 +1,7 @@
import { TextButtonWidget } from "@/components/widgets/buttons/TextButton"; import { TextButtonWidget } from "@/components/widgets/buttons/TextButton";
import { DialogState } from "@/state-providers/dialog"; import { DialogState } from "@/state-providers/dialog";
import { IconName } from "@/utility-functions/icons"; import { IconName } from "@/utility-functions/icons";
import { browserVersion, operatingSystem } from "@/utility-functions/platform";
import { stripIndents } from "@/utility-functions/strip-indents"; import { stripIndents } from "@/utility-functions/strip-indents";
import { Editor } from "@/wasm-communication/editor"; import { Editor } from "@/wasm-communication/editor";
import { DisplayDialogPanic, Widget, WidgetLayout } from "@/wasm-communication/messages"; import { DisplayDialogPanic, Widget, WidgetLayout } from "@/wasm-communication/messages";
@ -68,7 +69,7 @@ function githubUrl(panicDetails: string): string {
Provide any further information or context that you think would be helpful in fixing the issue. Screenshots or video can be linked or attached to this issue. Provide any further information or context that you think would be helpful in fixing the issue. Screenshots or video can be linked or attached to this issue.
**Browser and OS** **Browser and OS**
${browserVersion()}, ${operatingSystem()} ${browserVersion()}, ${operatingSystem(true).replace("Unknown", "YOUR OPERATING SYSTEM")}
**Stack Trace** **Stack Trace**
Copied from the crash dialog in the Graphite Editor: Copied from the crash dialog in the Graphite Editor:
@ -94,47 +95,3 @@ function githubUrl(panicDetails: string): string {
return url.toString(); return url.toString();
} }
function browserVersion(): string {
const agent = window.navigator.userAgent;
let match = agent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if (/trident/i.test(match[1])) {
const browser = /\brv[ :]+(\d+)/g.exec(agent) || [];
return `IE ${browser[1] || ""}`.trim();
}
if (match[1] === "Chrome") {
let browser = agent.match(/\bEdg\/(\d+)/);
if (browser !== null) return `Edge (Chromium) ${browser[1]}`;
browser = agent.match(/\bOPR\/(\d+)/);
if (browser !== null) return `Opera ${browser[1]}`;
}
match = match[2] ? [match[1], match[2]] : [navigator.appName, navigator.appVersion, "-?"];
const browser = agent.match(/version\/(\d+)/i);
if (browser !== null) match.splice(1, 1, browser[1]);
return `${match[0]} ${match[1]}`;
}
function operatingSystem(): string {
const osTable: Record<string, string> = {
"Windows NT 10": "Windows 10 or 11",
"Windows NT 6.3": "Windows 8.1",
"Windows NT 6.2": "Windows 8",
"Windows NT 6.1": "Windows 7",
"Windows NT 6.0": "Windows Vista",
"Windows NT 5.1": "Windows XP",
"Windows NT 5.0": "Windows 2000",
Mac: "Mac",
X11: "Unix",
Linux: "Linux",
Unknown: "YOUR OPERATING SYSTEM",
};
const userAgentOS = Object.keys(osTable).find((key) => window.navigator.userAgent.includes(key));
return osTable[userAgentOS || "Unknown"];
}

View file

@ -14,10 +14,8 @@ import App from "@/App.vue";
const body = document.body; const body = document.body;
const message = stripIndents` const message = stripIndents`
<style> <style>
h2, p, a { h2, p, a { text-align: center; color: white; }
text-align: center; #app { display: none; }
color: white;
}
</style> </style>
<h2>This browser is too old</h2> <h2>This browser is too old</h2>
<p>Please upgrade to a modern web browser such as the latest Firefox, Chrome, Edge, or Safari version 15 or newer.</p> <p>Please upgrade to a modern web browser such as the latest Firefox, Chrome, Edge, or Safari version 15 or newer.</p>

View file

@ -21,6 +21,7 @@ import KeyboardArrowRight from "@/../assets/icon-12px-solid/keyboard-arrow-right
import KeyboardArrowUp from "@/../assets/icon-12px-solid/keyboard-arrow-up.svg"; import KeyboardArrowUp from "@/../assets/icon-12px-solid/keyboard-arrow-up.svg";
import KeyboardBackspace from "@/../assets/icon-12px-solid/keyboard-backspace.svg"; import KeyboardBackspace from "@/../assets/icon-12px-solid/keyboard-backspace.svg";
import KeyboardCommand from "@/../assets/icon-12px-solid/keyboard-command.svg"; import KeyboardCommand from "@/../assets/icon-12px-solid/keyboard-command.svg";
import KeyboardControl from "@/../assets/icon-12px-solid/keyboard-control.svg";
import KeyboardEnter from "@/../assets/icon-12px-solid/keyboard-enter.svg"; import KeyboardEnter from "@/../assets/icon-12px-solid/keyboard-enter.svg";
import KeyboardOption from "@/../assets/icon-12px-solid/keyboard-option.svg"; import KeyboardOption from "@/../assets/icon-12px-solid/keyboard-option.svg";
import KeyboardShift from "@/../assets/icon-12px-solid/keyboard-shift.svg"; import KeyboardShift from "@/../assets/icon-12px-solid/keyboard-shift.svg";
@ -52,6 +53,7 @@ const SOLID_12PX = {
KeyboardArrowUp: { component: KeyboardArrowUp, size: 12 }, KeyboardArrowUp: { component: KeyboardArrowUp, size: 12 },
KeyboardBackspace: { component: KeyboardBackspace, size: 12 }, KeyboardBackspace: { component: KeyboardBackspace, size: 12 },
KeyboardCommand: { component: KeyboardCommand, size: 12 }, KeyboardCommand: { component: KeyboardCommand, size: 12 },
KeyboardControl: { component: KeyboardControl, size: 12 },
KeyboardEnter: { component: KeyboardEnter, size: 12 }, KeyboardEnter: { component: KeyboardEnter, size: 12 },
KeyboardOption: { component: KeyboardOption, size: 12 }, KeyboardOption: { component: KeyboardOption, size: 12 },
KeyboardShift: { component: KeyboardShift, size: 12 }, KeyboardShift: { component: KeyboardShift, size: 12 },

View file

@ -1,5 +1,14 @@
export function makeKeyboardModifiersBitfield(e: WheelEvent | PointerEvent | KeyboardEvent): number { export function makeKeyboardModifiersBitfield(e: WheelEvent | PointerEvent | KeyboardEvent): number {
return Number(e.ctrlKey) | (Number(e.shiftKey) << 1) | (Number(e.altKey) << 2); return (
// Shift (all platforms)
(Number(e.shiftKey) << 0) |
// Alt (all platforms, also called Option on Mac)
(Number(e.altKey) << 1) |
// Control (all platforms)
(Number(e.ctrlKey) << 2) |
// Meta (Windows/Linux) or Command (Mac)
(Number(e.metaKey) << 3)
);
} }
// Necessary because innerText puts an extra newline character at the end when the text is more than one line. // Necessary because innerText puts an extra newline character at the end when the text is more than one line.
@ -13,7 +22,7 @@ export function getLatinKey(e: KeyboardEvent): string | null {
const key = e.key.toLowerCase(); const key = e.key.toLowerCase();
const isPrintable = !ALL_PRINTABLE_KEYS.has(e.key); const isPrintable = !ALL_PRINTABLE_KEYS.has(e.key);
// Control (non-printable) characters are handled normally // Control characters (those which are non-printable) are handled normally
if (!isPrintable) return key; if (!isPrintable) return key;
// These non-Latin characters should fall back to the Latin equivalent at the key location // These non-Latin characters should fall back to the Latin equivalent at the key location

View file

@ -0,0 +1,54 @@
export function browserVersion(): string {
const agent = window.navigator.userAgent;
let match = agent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
if (/trident/i.test(match[1])) {
const browser = /\brv[ :]+(\d+)/g.exec(agent) || [];
return `IE ${browser[1] || ""}`.trim();
}
if (match[1] === "Chrome") {
let browser = agent.match(/\bEdg\/(\d+)/);
if (browser !== null) return `Edge (Chromium) ${browser[1]}`;
browser = agent.match(/\bOPR\/(\d+)/);
if (browser !== null) return `Opera ${browser[1]}`;
}
match = match[2] ? [match[1], match[2]] : [navigator.appName, navigator.appVersion, "-?"];
const browser = agent.match(/version\/(\d+)/i);
if (browser !== null) match.splice(1, 1, browser[1]);
return `${match[0]} ${match[1]}`;
}
export function operatingSystem(detailed = false): string {
const osTableDetailed: Record<string, string> = {
"Windows NT 10": "Windows 10 or 11",
"Windows NT 6.3": "Windows 8.1",
"Windows NT 6.2": "Windows 8",
"Windows NT 6.1": "Windows 7",
"Windows NT 6.0": "Windows Vista",
"Windows NT 5.1": "Windows XP",
"Windows NT 5.0": "Windows 2000",
Mac: "Mac",
X11: "Unix",
Linux: "Linux",
Unknown: "Unknown",
};
const osTableSimple: Record<string, string> = {
Windows: "Windows",
Mac: "Mac",
Linux: "Linux",
Unknown: "Unknown",
};
const osTable = detailed ? osTableDetailed : osTableSimple;
const userAgentOS = Object.keys(osTable).find((key) => window.navigator.userAgent.includes(key));
return osTable[userAgentOS || "Unknown"];
}
export function operatingSystemIsMac(): boolean {
return operatingSystem() === "Mac";
}

View file

@ -79,7 +79,9 @@ export type HintData = HintGroup[];
export type HintGroup = HintInfo[]; export type HintGroup = HintInfo[];
export class HintInfo { export class HintInfo {
readonly key_groups!: KeysGroup[]; readonly keyGroups!: KeysGroup[];
readonly keyGroupsMac!: KeysGroup[] | null;
readonly mouse!: MouseMotion | null; readonly mouse!: MouseMotion | null;
@ -404,12 +406,14 @@ export class ColorInput extends WidgetProps {
tooltip!: string; tooltip!: string;
} }
export type Keys = { keys: string[] };
export interface MenuListEntryData<Value = string> { export interface MenuListEntryData<Value = string> {
value?: Value; value?: Value;
label?: string; label?: string;
icon?: IconName; icon?: IconName;
font?: URL; font?: URL;
shortcut?: string[]; shortcut?: Keys;
shortcutRequiresLock?: boolean; shortcutRequiresLock?: boolean;
disabled?: boolean; disabled?: boolean;
action?: () => void; action?: () => void;
@ -781,7 +785,7 @@ export type MenuColumn = {
}; };
export type MenuEntry = { export type MenuEntry = {
shortcut: string[] | undefined; shortcut: Keys | undefined;
action: Widget; action: Widget;
label: string; label: string;
icon: string | undefined; icon: string | undefined;

View file

@ -79,7 +79,7 @@ function formatThirdPartyLicenses(jsLicenses) {
if (process.env.NODE_ENV === "production" && process.env.SKIP_CARGO_ABOUT === undefined) { if (process.env.NODE_ENV === "production" && process.env.SKIP_CARGO_ABOUT === undefined) {
try { try {
rustLicenses = generateRustLicenses(); rustLicenses = generateRustLicenses();
} catch (e) { } catch (err) {
// Nothing to show. Error messages were printed above. // Nothing to show. Error messages were printed above.
} }

View file

@ -2,7 +2,7 @@
name = "graphite-wasm" name = "graphite-wasm"
publish = false publish = false
version = "0.0.0" version = "0.0.0"
rust-version = "1.56.0" rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"] authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021" edition = "2021"
readme = "../../README.md" readme = "../../README.md"

View file

@ -6,6 +6,7 @@ use crate::helpers::{translate_key, Error};
use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES, JS_EDITOR_HANDLES}; use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES, JS_EDITOR_HANDLES};
use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION}; use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
use editor::document::utility_types::Platform;
use editor::input::input_preprocessor::ModifierKeys; use editor::input::input_preprocessor::ModifierKeys;
use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::message_prelude::*; use editor::message_prelude::*;
@ -105,7 +106,15 @@ impl JsEditorHandle {
// the backend from the web frontend. // the backend from the web frontend.
// ======================================================================== // ========================================================================
pub fn init_after_frontend_ready(&self) { pub fn init_after_frontend_ready(&self, platform: String) {
let platform = match platform.as_str() {
"Windows" => Platform::Windows,
"Mac" => Platform::Mac,
"Linux" => Platform::Linux,
_ => Platform::Unknown,
};
self.dispatch(PortfolioMessage::SetPlatform { platform });
self.dispatch(Message::Init); self.dispatch(Message::Init);
} }
@ -217,13 +226,13 @@ impl JsEditorHandle {
} }
/// Mouse scrolling within the screenspace bounds of the viewport /// Mouse scrolling within the screenspace bounds of the viewport
pub fn on_mouse_scroll(&self, x: f64, y: f64, mouse_keys: u8, wheel_delta_x: i32, wheel_delta_y: i32, wheel_delta_z: i32, modifiers: u8) { pub fn on_wheel_scroll(&self, x: f64, y: f64, mouse_keys: u8, wheel_delta_x: i32, wheel_delta_y: i32, wheel_delta_z: i32, modifiers: u8) {
let mut editor_mouse_state = EditorMouseState::from_keys_and_editor_position(mouse_keys, (x, y).into()); let mut editor_mouse_state = EditorMouseState::from_keys_and_editor_position(mouse_keys, (x, y).into());
editor_mouse_state.scroll_delta = ScrollDelta::new(wheel_delta_x, wheel_delta_y, wheel_delta_z); editor_mouse_state.scroll_delta = ScrollDelta::new(wheel_delta_x, wheel_delta_y, wheel_delta_z);
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys"); let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
let message = InputPreprocessorMessage::MouseScroll { editor_mouse_state, modifier_keys }; let message = InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys };
self.dispatch(message); self.dispatch(message);
} }
@ -280,7 +289,7 @@ impl JsEditorHandle {
/// A text box was committed /// A text box was committed
pub fn on_change_text(&self, new_text: String) -> Result<(), JsValue> { pub fn on_change_text(&self, new_text: String) -> Result<(), JsValue> {
let message = TextMessage::TextChange { new_text }; let message = TextToolMessage::TextChange { new_text };
self.dispatch(message); self.dispatch(message);
Ok(()) Ok(())
@ -302,7 +311,7 @@ impl JsEditorHandle {
/// A text box was changed /// A text box was changed
pub fn update_bounds(&self, new_text: String) -> Result<(), JsValue> { pub fn update_bounds(&self, new_text: String) -> Result<(), JsValue> {
let message = TextMessage::UpdateBounds { new_text }; let message = TextToolMessage::UpdateBounds { new_text };
self.dispatch(message); self.dispatch(message);
Ok(()) Ok(())

View file

@ -122,6 +122,7 @@ pub fn translate_key(name: &str) -> Key {
"capslock" => KeyShift, "capslock" => KeyShift,
" " => KeySpace, " " => KeySpace,
"control" => KeyControl, "control" => KeyControl,
"command" => KeyCommand,
"delete" => KeyDelete, "delete" => KeyDelete,
"backspace" => KeyBackspace, "backspace" => KeyBackspace,
"alt" => KeyAlt, "alt" => KeyAlt,

View file

@ -2,7 +2,7 @@
name = "graphite-graphene" name = "graphite-graphene"
publish = false publish = false
version = "0.0.0" version = "0.0.0"
rust-version = "1.56.0" rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"] authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021" edition = "2021"
readme = "../README.md" readme = "../README.md"

View file

@ -2,7 +2,7 @@
name = "graphite-proc-macros" name = "graphite-proc-macros"
publish = false publish = false
version = "0.0.0" version = "0.0.0"
rust-version = "1.56.0" rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"] authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021" edition = "2021"
readme = "../README.md" readme = "../README.md"
@ -14,6 +14,10 @@ license = "Apache-2.0"
path = "src/lib.rs" path = "src/lib.rs"
proc-macro = true proc-macro = true
[features]
default = ["serde-discriminant"]
serde-discriminant = []
[dependencies] [dependencies]
proc-macro2 = "1.0.26" proc-macro2 = "1.0.26"
syn = { version = "1.0.68", features = ["full"] } syn = { version = "1.0.68", features = ["full"] }
@ -22,3 +26,6 @@ quote = "1.0.9"
[dev-dependencies.editor] [dev-dependencies.editor]
path = "../editor" path = "../editor"
package = "graphite-editor" package = "graphite-editor"
[dev-dependencies]
serde = "1"

View file

@ -112,8 +112,16 @@ pub fn derive_discriminant_impl(input_item: TokenStream) -> syn::Result<TokenStr
) )
}) })
.unzip::<_, _, Vec<_>, Vec<_>>(); .unzip::<_, _, Vec<_>, Vec<_>>();
#[cfg(feature = "serde-discriminant")]
let serde = quote::quote! {
#[derive(serde::Serialize, serde::Deserialize)]
};
#[cfg(not(feature = "serde-discriminant"))]
let serde = quote::quote! {};
let res = quote::quote! { let res = quote::quote! {
#serde
#discriminant #discriminant
impl ToDiscriminant for #input_type { impl ToDiscriminant for #input_type {