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",
"proc-macro2",
"quote",
"serde",
"syn",
]

View file

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

View file

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

View file

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

View file

@ -138,14 +138,25 @@ impl Dispatcher {
}
InputMapper(message) => {
let actions = self.collect_actions();
let keyboard_platform = self.message_handlers.portfolio_message_handler.platform.as_keyboard_platform_layout();
self.message_handlers
.input_mapper_message_handler
.process_action(message, (&self.message_handlers.input_preprocessor_message_handler, actions), &mut queue);
.process_action(message, (&self.message_handlers.input_preprocessor_message_handler, keyboard_platform, actions), &mut queue);
}
InputPreprocessor(message) => {
self.message_handlers.input_preprocessor_message_handler.process_action(message, (), &mut queue);
let keyboard_platform = self.message_handlers.portfolio_message_handler.platform.as_keyboard_platform_layout();
self.message_handlers.input_preprocessor_message_handler.process_action(message, keyboard_platform, &mut queue);
}
Layout(message) => {
let keyboard_platform = self.message_handlers.portfolio_message_handler.platform.as_keyboard_platform_layout();
let action_input_mapping = &|action_to_find: &MessageDiscriminant| self.message_handlers.input_mapper_message_handler.action_input_mapping(action_to_find, keyboard_platform);
self.message_handlers
.layout_message_handler
.process_action(message, (action_input_mapping, keyboard_platform), &mut queue);
}
Layout(message) => self.message_handlers.layout_message_handler.process_action(message, (), &mut queue),
Portfolio(message) => {
self.message_handlers
.portfolio_message_handler
@ -518,15 +529,15 @@ mod test {
replacement_selected_layers: sorted_layers[..2].to_vec(),
});
editor.handle_message(DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 });
editor.handle_message(DocumentMessage::SelectedLayersRaise);
let (all, non_selected, selected) = verify_order(editor.dispatcher.message_handlers.portfolio_message_handler.active_document_mut().unwrap());
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<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());
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());
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 SCALE_SNAP_INTERVAL: f64 = 0.1;
pub const SLOWING_DIVISOR: f64 = 10.;
pub const NUDGE_AMOUNT: f64 = 1.;
pub const BIG_NUDGE_AMOUNT: f64 = 10.;
// Select tool
pub const SELECTION_TOLERANCE: f64 = 5.;

View file

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

View file

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

View file

@ -7,6 +7,7 @@ use super::{vectorize_layer_metadata, PropertiesPanelMessageHandler};
use super::{ArtboardMessageHandler, MovementMessageHandler, OverlaysMessageHandler, TransformLayerMessageHandler};
use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR};
use crate::frontend::utility_types::{FileType, FrontendImageData};
use crate::input::input_mapper::action_keys::action_shortcut;
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::layout_message::LayoutTarget;
use crate::layout::widgets::{
@ -549,6 +550,7 @@ impl DocumentMessageHandler {
icon: "Snapping".into(),
tooltip: "Snapping".into(),
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()),
..Default::default()
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Snapping".into(),
@ -564,6 +566,7 @@ impl DocumentMessageHandler {
icon: "Grid".into(),
tooltip: "Grid".into(),
on_update: WidgetCallback::new(|_| DialogMessage::RequestComingSoonDialog { issue: Some(318) }.into()),
..Default::default()
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Grid".into(),
@ -579,6 +582,7 @@ impl DocumentMessageHandler {
icon: "Overlays".into(),
tooltip: "Overlays".into(),
on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetOverlaysVisibility { visible: optional_input.checked }.into()),
..Default::default()
})),
WidgetHolder::new(Widget::PopoverButton(PopoverButton {
header: "Overlays".into(),
@ -841,14 +845,16 @@ impl DocumentMessageHandler {
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "NodeFolder".into(),
tooltip: "New Folder (Ctrl+Shift+N)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut
tooltip: "New Folder".into(),
tooltip_shortcut: action_shortcut!(DocumentMessageDiscriminant::CreateEmptyFolder),
size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::CreateEmptyFolder { container_path: vec![] }.into()),
..Default::default()
})),
WidgetHolder::new(Widget::IconButton(IconButton {
icon: "Trash".into(),
tooltip: "Delete Selected (Del)".into(), // TODO: Customize this tooltip for the Mac version of the keyboard shortcut
tooltip: "Delete Selected".into(),
tooltip_shortcut: action_shortcut!(DocumentMessageDiscriminant::DeleteSelectedLayers),
size: 24,
on_update: WidgetCallback::new(|_| DocumentMessage::DeleteSelectedLayers.into()),
..Default::default()
@ -864,6 +870,62 @@ impl DocumentMessageHandler {
.into(),
);
}
pub fn selected_layers_reorder(&mut self, relative_index_offset: isize, responses: &mut VecDeque<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 {
@ -1318,61 +1380,6 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
.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 => {
self.rollback(responses).unwrap_or_else(|e| log::warn!("{}", e));
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();
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 } => {
let mut paths = vec![];
let last_selection_exists = !self.layer_range_selection_reference.is_empty();
@ -1611,7 +1633,10 @@ impl MessageHandler<DocumentMessage, (&InputPreprocessorMessageHandler, &FontCac
DeleteSelectedLayers,
DuplicateSelectedLayers,
NudgeSelectedLayers,
ReorderSelectedLayers,
SelectedLayersLower,
SelectedLayersLowerToBack,
SelectedLayersRaise,
SelectedLayersRaiseToFront,
GroupSelectedLayers,
UngroupSelectedLayers,
);

View file

@ -1,5 +1,5 @@
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::widgets::*;
use crate::message_prelude::*;
@ -30,31 +30,31 @@ impl PropertyHolder for MenuBarMessageHandler {
Layout::MenuLayout(MenuLayout::new(vec![
MenuColumn {
label: "File".into(),
children: vec![
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "New…".into(),
icon: Some("File".into()),
action: MenuEntry::create_action(|_| DialogMessage::RequestNewDocumentDialog.into()),
shortcut: Some(vec![Key::KeyControl, Key::KeyN]),
children: None,
shortcut: action_shortcut!(DialogMessageDiscriminant::RequestNewDocumentDialog),
children: MenuEntryGroups::empty(),
},
MenuEntry {
label: "Open…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyO]),
shortcut: action_shortcut!(PortfolioMessageDiscriminant::OpenDocument),
action: MenuEntry::create_action(|_| PortfolioMessage::OpenDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Open Recent".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyO]),
shortcut: None,
action: MenuEntry::no_action(),
icon: None,
children: Some(vec![
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "Reopen Last Closed".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyT]),
// shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyT],
..MenuEntry::default()
},
MenuEntry {
@ -90,13 +90,13 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![
MenuEntry {
label: "Close".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyW]),
shortcut: action_shortcut!(PortfolioMessageDiscriminant::CloseActiveDocumentWithConfirmation),
action: MenuEntry::create_action(|_| PortfolioMessage::CloseActiveDocumentWithConfirmation.into()),
..MenuEntry::default()
},
MenuEntry {
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()),
..MenuEntry::default()
},
@ -104,19 +104,18 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![
MenuEntry {
label: "Save".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyS]),
shortcut: action_shortcut!(DocumentMessageDiscriminant::SaveDocument),
action: MenuEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Save As…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyS]),
action: MenuEntry::create_action(|_| DocumentMessage::SaveDocument.into()),
// shortcut: [Key::KeyControl, Key::KeyShift, Key::KeyS],
..MenuEntry::default()
},
MenuEntry {
label: "Save All".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyAlt, Key::KeyS]),
// shortcut: [Key::KeyControl, Key::KeyAlt, Key::KeyS],
..MenuEntry::default()
},
MenuEntry {
@ -128,37 +127,37 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![
MenuEntry {
label: "Import…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyI]),
shortcut: action_shortcut!(PortfolioMessageDiscriminant::Import),
action: MenuEntry::create_action(|_| PortfolioMessage::Import.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Export…".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyE]),
shortcut: action_shortcut!(DialogMessageDiscriminant::RequestExportDialog),
action: MenuEntry::create_action(|_| DialogMessage::RequestExportDialog.into()),
..MenuEntry::default()
},
],
vec![MenuEntry {
label: "Quit".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyQ]),
// shortcut: [Key::KeyControl, Key::KeyQ],
..MenuEntry::default()
}],
],
]),
},
MenuColumn {
label: "Edit".into(),
children: vec![
children: MenuEntryGroups(vec![
vec![
MenuEntry {
label: "Undo".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyZ]),
shortcut: action_shortcut!(DocumentMessageDiscriminant::Undo),
action: MenuEntry::create_action(|_| DocumentMessage::Undo.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Redo".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyZ]),
shortcut: action_shortcut!(DocumentMessageDiscriminant::Redo),
action: MenuEntry::create_action(|_| DocumentMessage::Redo.into()),
..MenuEntry::default()
},
@ -166,93 +165,93 @@ impl PropertyHolder for MenuBarMessageHandler {
vec![
MenuEntry {
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()),
..MenuEntry::default()
},
MenuEntry {
label: "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()),
..MenuEntry::default()
},
MenuEntry {
label: "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()),
..MenuEntry::default()
},
],
],
]),
},
MenuColumn {
label: "Layer".into(),
children: vec![vec![
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Select All".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyA]),
shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectAllLayers),
action: MenuEntry::create_action(|_| DocumentMessage::SelectAllLayers.into()),
..MenuEntry::default()
},
MenuEntry {
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()),
..MenuEntry::default()
},
MenuEntry {
label: "Order".into(),
action: MenuEntry::no_action(),
children: Some(vec![vec![
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Raise To Front".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyLeftBracket]),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MAX }.into()),
shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersRaiseToFront),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaiseToFront.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Raise".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyRightBracket]),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 }.into()),
shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersRaise),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersRaise.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Lower".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyLeftBracket]),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 }.into()),
shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersLower),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLower.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Lower to Back".into(),
shortcut: Some(vec![Key::KeyControl, Key::KeyShift, Key::KeyRightBracket]),
action: MenuEntry::create_action(|_| DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MIN }.into()),
shortcut: action_shortcut!(DocumentMessageDiscriminant::SelectedLayersLowerToBack),
action: MenuEntry::create_action(|_| DocumentMessage::SelectedLayersLowerToBack.into()),
..MenuEntry::default()
},
]]),
..MenuEntry::default()
},
]],
]]),
},
MenuColumn {
label: "Document".into(),
children: vec![vec![MenuEntry {
children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Menu entries coming soon".into(),
..MenuEntry::default()
}]],
}]]),
},
MenuColumn {
label: "View".into(),
children: vec![vec![MenuEntry {
children: MenuEntryGroups(vec![vec![MenuEntry {
label: "Show/Hide Node Graph (In Development)".into(),
action: MenuEntry::create_action(|_| WorkspaceMessage::NodeGraphToggleVisibility.into()),
..MenuEntry::default()
}]],
}]]),
},
MenuColumn {
label: "Help".into(),
children: vec![
children: MenuEntryGroups(vec![
vec![MenuEntry {
label: "About Graphite".into(),
action: MenuEntry::create_action(|_| DialogMessage::RequestAboutGraphiteDialog.into()),
@ -284,23 +283,23 @@ impl PropertyHolder for MenuBarMessageHandler {
MenuEntry {
label: "Debug: Print Messages".into(),
action: MenuEntry::no_action(),
children: Some(vec![vec![
children: MenuEntryGroups(vec![vec![
MenuEntry {
label: "Off".into(),
// 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()),
..MenuEntry::default()
},
MenuEntry {
label: "Only Names".into(),
shortcut: Some(vec![Key::KeyAlt, Key::Key1]),
shortcut: action_shortcut!(DebugMessageDiscriminant::MessageNames),
action: MenuEntry::create_action(|_| DebugMessage::MessageNames.into()),
..MenuEntry::default()
},
MenuEntry {
label: "Full Contents".into(),
shortcut: Some(vec![Key::KeyAlt, Key::Key2]),
shortcut: action_shortcut!(DebugMessageDiscriminant::MessageContents),
action: MenuEntry::create_action(|_| DebugMessage::MessageContents.into()),
..MenuEntry::default()
},
@ -310,14 +309,14 @@ impl PropertyHolder for MenuBarMessageHandler {
MenuEntry {
label: "Debug: Print Trace Logs".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()),
..MenuEntry::default()
},
MenuEntry {
label: "Debug: Print Document".into(),
shortcut: Some(vec![Key::KeyAlt, Key::KeyP]),
action: MenuEntry::create_action(|_| DebugMessage::ToggleTraceLogs.into()),
shortcut: action_shortcut!(DocumentMessageDiscriminant::DebugPrintDocument),
action: MenuEntry::create_action(|_| DocumentMessage::DebugPrintDocument.into()),
..MenuEntry::default()
},
MenuEntry {
@ -326,7 +325,7 @@ impl PropertyHolder for MenuBarMessageHandler {
..MenuEntry::default()
},
],
],
]),
},
]))
}

View file

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

View file

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

View file

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

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]
#[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum FrontendMessage {
// Display prefix: make the frontend show something, like a dialog
DisplayDialog { icon: String },

View file

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

View file

@ -1,25 +1,25 @@
use super::input_mapper_macros::*;
use super::keyboard::{Key, KeyStates, NUMBER_OF_KEYS};
use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT};
use crate::document::clipboards::Clipboard;
use crate::document::utility_types::KeyboardPlatformLayout;
use crate::message_prelude::*;
use crate::viewport_tools::tool::ToolType;
use glam::DVec2;
const NUDGE_AMOUNT: f64 = 1.;
const SHIFT_NUDGE_AMOUNT: f64 = 10.;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct Mapping {
pub key_up: [KeyMappingEntries; NUMBER_OF_KEYS],
pub key_down: [KeyMappingEntries; NUMBER_OF_KEYS],
pub pointer_move: KeyMappingEntries,
pub mouse_scroll: KeyMappingEntries,
pub double_click: KeyMappingEntries,
pub wheel_scroll: KeyMappingEntries,
pub pointer_move: KeyMappingEntries,
}
impl Default for Mapping {
fn default() -> Self {
use input_mapper_macros::{entry, mapping, modifiers};
use InputMapperMessage::*;
use Key::*;
// WARNING!
@ -27,210 +27,317 @@ impl Default for Mapping {
// it as an available action in the respective message handler file (such as the bottom of `document_message_handler.rs`).
let mappings = mapping![
// Higher priority than entries in sections below
entry! {action=MovementMessage::PointerMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, message=InputMapperMessage::PointerMove},
// Transform layers
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter},
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=Lmb},
entry! {action=TransformLayerMessage::CancelTransformOperation, key_down=KeyEscape},
entry! {action=TransformLayerMessage::CancelTransformOperation, key_down=Rmb},
entry! {action=TransformLayerMessage::ConstrainX, key_down=KeyX},
entry! {action=TransformLayerMessage::ConstrainY, key_down=KeyY},
entry! {action=TransformLayerMessage::TypeBackspace, key_down=KeyBackspace},
entry! {action=TransformLayerMessage::TypeNegate, key_down=KeyMinus},
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyComma},
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod},
entry! {action=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]},
// Select
entry! {action=SelectToolMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }, message=InputMapperMessage::PointerMove},
entry! {action=SelectToolMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb},
entry! {action=SelectToolMessage::DragStop, key_up=Lmb},
entry! {action=SelectToolMessage::DragStop, key_down=KeyEnter},
entry! {action=SelectToolMessage::EditLayer, message=InputMapperMessage::DoubleClick},
entry! {action=SelectToolMessage::Abort, key_down=Rmb},
entry! {action=SelectToolMessage::Abort, key_down=KeyEscape},
// Artboard
entry! {action=ArtboardToolMessage::PointerDown, key_down=Lmb},
entry! {action=ArtboardToolMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }, message=InputMapperMessage::PointerMove},
entry! {action=ArtboardToolMessage::PointerUp, key_up=Lmb},
entry! {action=ArtboardToolMessage::DeleteSelected, key_down=KeyDelete},
entry! {action=ArtboardToolMessage::DeleteSelected, key_down=KeyBackspace},
// Navigate
entry! {action=NavigateToolMessage::ClickZoom { zoom_in: false }, key_up=Lmb, modifiers=[KeyShift]},
entry! {action=NavigateToolMessage::ClickZoom { zoom_in: true }, key_up=Lmb},
entry! {action=NavigateToolMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }, message=InputMapperMessage::PointerMove},
entry! {action=NavigateToolMessage::TranslateCanvasBegin, key_down=Mmb},
entry! {action=NavigateToolMessage::RotateCanvasBegin, key_down=Rmb},
entry! {action=NavigateToolMessage::ZoomCanvasBegin, key_down=Lmb},
entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Rmb},
entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Lmb},
entry! {action=NavigateToolMessage::TransformCanvasEnd, key_up=Mmb},
// Eyedropper
entry! {action=EyedropperToolMessage::LeftMouseDown, key_down=Lmb},
entry! {action=EyedropperToolMessage::RightMouseDown, key_down=Rmb},
// Text
entry! {action=TextMessage::Interact, key_up=Lmb},
entry! {action=TextMessage::Abort, key_down=KeyEscape},
entry! {action=TextMessage::CommitText, key_down=KeyEnter, modifiers=[KeyControl]},
// Gradient
entry! {action=GradientToolMessage::PointerDown, key_down=Lmb},
entry! {action=GradientToolMessage::PointerMove { constrain_axis: KeyShift }, message=InputMapperMessage::PointerMove},
entry! {action=GradientToolMessage::PointerUp, key_up=Lmb},
// Rectangle
entry! {action=RectangleToolMessage::DragStart, key_down=Lmb},
entry! {action=RectangleToolMessage::DragStop, key_up=Lmb},
entry! {action=RectangleToolMessage::Abort, key_down=Rmb},
entry! {action=RectangleToolMessage::Abort, key_down=KeyEscape},
entry! {action=RectangleToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]},
// Ellipse
entry! {action=EllipseToolMessage::DragStart, key_down=Lmb},
entry! {action=EllipseToolMessage::DragStop, key_up=Lmb},
entry! {action=EllipseToolMessage::Abort, key_down=Rmb},
entry! {action=EllipseToolMessage::Abort, key_down=KeyEscape},
entry! {action=EllipseToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]},
// Shape
entry! {action=ShapeToolMessage::DragStart, key_down=Lmb},
entry! {action=ShapeToolMessage::DragStop, key_up=Lmb},
entry! {action=ShapeToolMessage::Abort, key_down=Rmb},
entry! {action=ShapeToolMessage::Abort, key_down=KeyEscape},
entry! {action=ShapeToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]},
// Line
entry! {action=LineToolMessage::DragStart, key_down=Lmb},
entry! {action=LineToolMessage::DragStop, key_up=Lmb},
entry! {action=LineToolMessage::Abort, key_down=Rmb},
entry! {action=LineToolMessage::Abort, key_down=KeyEscape},
entry! {action=LineToolMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }, triggers=[KeyAlt, KeyShift, KeyControl]},
// Path
entry! {action=PathToolMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb},
entry! {action=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }, message=InputMapperMessage::PointerMove},
entry! {action=PathToolMessage::Delete, key_down=KeyDelete},
entry! {action=PathToolMessage::Delete, key_down=KeyBackspace},
entry! {action=PathToolMessage::DragStop, key_up=Lmb},
// Pen
entry! {action=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }, message=InputMapperMessage::PointerMove},
entry! {action=PenToolMessage::DragStart, key_down=Lmb},
entry! {action=PenToolMessage::DragStop, key_up=Lmb},
entry! {action=PenToolMessage::Confirm, key_down=Rmb},
entry! {action=PenToolMessage::Confirm, key_down=KeyEscape},
entry! {action=PenToolMessage::Confirm, key_down=KeyEnter},
// Freehand
entry! {action=FreehandToolMessage::PointerMove, message=InputMapperMessage::PointerMove},
entry! {action=FreehandToolMessage::DragStart, key_down=Lmb},
entry! {action=FreehandToolMessage::DragStop, key_up=Lmb},
// Spline
entry! {action=SplineToolMessage::PointerMove, message=InputMapperMessage::PointerMove},
entry! {action=SplineToolMessage::DragStart, key_down=Lmb},
entry! {action=SplineToolMessage::DragStop, key_up=Lmb},
entry! {action=SplineToolMessage::Confirm, key_down=Rmb},
entry! {action=SplineToolMessage::Confirm, key_down=KeyEscape},
entry! {action=SplineToolMessage::Confirm, key_down=KeyEnter},
// Fill
entry! {action=FillToolMessage::LeftMouseDown, key_down=Lmb},
entry! {action=FillToolMessage::RightMouseDown, key_down=Rmb},
// Tool Actions
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Select }, key_down=KeyV},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Navigate }, key_down=KeyZ},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Eyedropper }, key_down=KeyI},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Text }, key_down=KeyT},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Fill }, key_down=KeyF},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Gradient }, key_down=KeyH},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Path }, key_down=KeyA},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Pen }, key_down=KeyP},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Freehand }, key_down=KeyN},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Line }, key_down=KeyL},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Rectangle }, key_down=KeyM},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Ellipse }, key_down=KeyE},
entry! {action=ToolMessage::ActivateTool { tool_type: ToolType::Shape }, key_down=KeyY},
// Colors
entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]},
entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]},
entry! {action=ToolMessage::SelectRandomPrimaryColor, key_down=KeyC, modifiers=[KeyAlt]},
// Document actions
entry! {action=DocumentMessage::Redo, key_down=KeyZ, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
entry! {action=DialogMessage::RequestExportDialog, key_down=KeyE, modifiers=[KeyControl]},
entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl]},
entry! {action=DocumentMessage::SaveDocument, key_down=KeyS, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::DebugPrintDocument, key_down=KeyP, modifiers=[KeyAlt]},
entry! {action=DocumentMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]},
entry! {action=DocumentMessage::GroupSelectedLayers, key_down=KeyG, modifiers=[KeyControl]},
entry! {action=DocumentMessage::UngroupSelectedLayers, key_down=KeyG, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::CreateEmptyFolder { container_path: vec![] }, key_down=KeyN, modifiers=[KeyControl, KeyShift]},
// Layer transformation
entry! {action=TransformLayerMessage::BeginGrab, key_down=KeyG},
entry! {action=TransformLayerMessage::BeginRotate, key_down=KeyR},
entry! {action=TransformLayerMessage::BeginScale, key_down=KeyS},
// Movement actions
entry! {action=MovementMessage::RotateCanvasBegin, key_down=Mmb, modifiers=[KeyControl]},
entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]},
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb},
entry! {action=MovementMessage::TransformCanvasEnd, key_up=Mmb},
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Lmb, modifiers=[KeySpace]},
entry! {action=MovementMessage::TransformCanvasEnd, key_up=Lmb, modifiers=[KeySpace]},
entry! {action=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }, key_down=KeyPlus, modifiers=[KeyControl]},
entry! {action=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }, key_down=KeyEquals, modifiers=[KeyControl]},
entry! {action=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }, key_down=KeyMinus, modifiers=[KeyControl]},
entry! {action=MovementMessage::SetCanvasZoom { zoom_factor: 1. }, key_down=Key1, modifiers=[KeyControl]},
entry! {action=MovementMessage::SetCanvasZoom { zoom_factor: 2. }, key_down=Key2, modifiers=[KeyControl]},
entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]},
entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]},
entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }, message=InputMapperMessage::MouseScroll},
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(1., 0.) }, key_down=KeyPageUp, modifiers=[KeyShift]},
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(-1., 0.) }, key_down=KeyPageDown, modifiers=[KeyShift]},
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }, key_down=KeyPageUp},
entry! {action=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }, key_down=KeyPageDown},
// Portfolio actions
entry! {action=PortfolioMessage::OpenDocument, key_down=KeyO, modifiers=[KeyControl]},
entry! {action=PortfolioMessage::Import, key_down=KeyI, modifiers=[KeyControl]},
entry! {action=DialogMessage::RequestNewDocumentDialog, key_down=KeyN, modifiers=[KeyControl]},
entry! {action=PortfolioMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
entry! {action=PortfolioMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
entry! {action=DialogMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
entry! {action=PortfolioMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
entry! {action=PortfolioMessage::Copy { clipboard: Clipboard::Device }, key_down=KeyC, modifiers=[KeyControl]},
entry! {action=PortfolioMessage::Cut { clipboard: Clipboard::Device }, key_down=KeyX, modifiers=[KeyControl]},
// Nudging
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift, KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift, KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyShift, KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyShift, KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -SHIFT_NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowLeft, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: -SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyShift, KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: SHIFT_NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyShift, KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: SHIFT_NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowRight, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp, modifiers=[KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowUp},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowDown, modifiers=[KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT }, key_down=KeyArrowDown},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowLeft, modifiers=[KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowLeft},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }, key_down=KeyArrowRight, modifiers=[KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0. }, key_down=KeyArrowRight},
// Reorder Layers
entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MAX }, key_down=KeyRightCurlyBracket, modifiers=[KeyControl]}, // TODO: Use KeyRightBracket with Ctrl+Shift modifiers once input system is fixed
entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: 1 }, key_down=KeyRightBracket, modifiers=[KeyControl]},
entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: -1 }, key_down=KeyLeftBracket, modifiers=[KeyControl]},
entry! {action=DocumentMessage::ReorderSelectedLayers { relative_index_offset: isize::MIN }, key_down=KeyLeftCurlyBracket, modifiers=[KeyControl]}, // TODO: Use KeyLeftBracket with Ctrl+Shift modifiers once input system is fixed
// Debug Actions
entry! {action=DebugMessage::ToggleTraceLogs, key_down=KeyT, modifiers=[KeyAlt]},
entry! {action=DebugMessage::MessageOff, key_down=Key0, modifiers=[KeyAlt]},
entry! {action=DebugMessage::MessageNames, key_down=Key1, modifiers=[KeyAlt]},
entry! {action=DebugMessage::MessageContents, key_down=Key2, modifiers=[KeyAlt]},
// HIGHER PRIORITY:
//
// MovementMessage
entry!(
PointerMove;
refresh_keys=[KeyControl],
action_dispatch=MovementMessage::PointerMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None },
),
// NORMAL PRIORITY:
//
// TransformLayerMessage
entry!(KeyDown(KeyEnter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry!(KeyDown(Lmb); action_dispatch=TransformLayerMessage::ApplyTransformOperation),
entry!(KeyDown(KeyEscape); action_dispatch=TransformLayerMessage::CancelTransformOperation),
entry!(KeyDown(Rmb); action_dispatch=TransformLayerMessage::CancelTransformOperation),
entry!(KeyDown(KeyX); action_dispatch=TransformLayerMessage::ConstrainX),
entry!(KeyDown(KeyY); action_dispatch=TransformLayerMessage::ConstrainY),
entry!(KeyDown(KeyBackspace); action_dispatch=TransformLayerMessage::TypeBackspace),
entry!(KeyDown(KeyMinus); action_dispatch=TransformLayerMessage::TypeNegate),
entry!(KeyDown(KeyComma); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
entry!(KeyDown(KeyPeriod); action_dispatch=TransformLayerMessage::TypeDecimalPoint),
entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }),
// SelectToolMessage
entry!(PointerMove; refresh_keys=[KeyControl, KeyShift, KeyAlt], action_dispatch=SelectToolMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }),
entry!(KeyDown(Lmb); action_dispatch=SelectToolMessage::DragStart { add_to_selection: KeyShift }),
entry!(KeyUp(Lmb); action_dispatch=SelectToolMessage::DragStop),
entry!(KeyDown(KeyEnter); action_dispatch=SelectToolMessage::DragStop),
entry!(DoubleClick; action_dispatch=SelectToolMessage::EditLayer),
entry!(KeyDown(Rmb); action_dispatch=SelectToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=SelectToolMessage::Abort),
// ArtboardToolMessage
entry!(KeyDown(Lmb); action_dispatch=ArtboardToolMessage::PointerDown),
entry!(PointerMove; refresh_keys=[KeyShift, KeyAlt], action_dispatch=ArtboardToolMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }),
entry!(KeyUp(Lmb); action_dispatch=ArtboardToolMessage::PointerUp),
entry!(KeyDown(KeyDelete); action_dispatch=ArtboardToolMessage::DeleteSelected),
entry!(KeyDown(KeyBackspace); action_dispatch=ArtboardToolMessage::DeleteSelected),
// NavigateToolMessage
entry!(KeyUp(Lmb); modifiers=[KeyShift], action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: false }),
entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::ClickZoom { zoom_in: true }),
entry!(PointerMove; refresh_keys=[KeyControl], action_dispatch=NavigateToolMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }),
entry!(KeyDown(Mmb); action_dispatch=NavigateToolMessage::TranslateCanvasBegin),
entry!(KeyDown(Rmb); action_dispatch=NavigateToolMessage::RotateCanvasBegin),
entry!(KeyDown(Lmb); action_dispatch=NavigateToolMessage::ZoomCanvasBegin),
entry!(KeyUp(Rmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
entry!(KeyUp(Lmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
entry!(KeyUp(Mmb); action_dispatch=NavigateToolMessage::TransformCanvasEnd),
// EyedropperToolMessage
entry!(KeyDown(Lmb); action_dispatch=EyedropperToolMessage::LeftMouseDown),
entry!(KeyDown(Rmb); action_dispatch=EyedropperToolMessage::RightMouseDown),
// TextToolMessage
entry!(KeyUp(Lmb); action_dispatch=TextToolMessage::Interact),
entry!(KeyDown(KeyEscape); action_dispatch=TextToolMessage::Abort),
entry_multiplatform!(
standard!(KeyDown(KeyEnter); modifiers=[KeyControl], action_dispatch=TextToolMessage::CommitText),
mac_only!(KeyDown(KeyEnter); modifiers=[KeyCommand], action_dispatch=TextToolMessage::CommitText),
),
// GradientToolMessage
entry!(KeyDown(Lmb); action_dispatch=GradientToolMessage::PointerDown),
entry!(PointerMove; refresh_keys=[KeyShift], action_dispatch=GradientToolMessage::PointerMove { constrain_axis: KeyShift }),
entry!(KeyUp(Lmb); action_dispatch=GradientToolMessage::PointerUp),
// RectangleToolMessage
entry!(KeyDown(Lmb); action_dispatch=RectangleToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=RectangleToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=RectangleToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=RectangleToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=RectangleToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
// EllipseToolMessage
entry!(KeyDown(Lmb); action_dispatch=EllipseToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=EllipseToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=EllipseToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=EllipseToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=EllipseToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
// ShapeToolMessage
entry!(KeyDown(Lmb); action_dispatch=ShapeToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=ShapeToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=ShapeToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=ShapeToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=ShapeToolMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }),
// LineToolMessage
entry!(KeyDown(Lmb); action_dispatch=LineToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=LineToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=LineToolMessage::Abort),
entry!(KeyDown(KeyEscape); action_dispatch=LineToolMessage::Abort),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift, KeyControl], action_dispatch=LineToolMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }),
// PathToolMessage
entry!(KeyDown(Lmb); action_dispatch=PathToolMessage::DragStart { add_to_selection: KeyShift }),
entry!(PointerMove; refresh_keys=[KeyAlt, KeyShift], action_dispatch=PathToolMessage::PointerMove { alt_mirror_angle: KeyAlt, shift_mirror_distance: KeyShift }),
entry!(KeyDown(KeyDelete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyBackspace); action_dispatch=PathToolMessage::Delete),
entry!(KeyUp(Lmb); action_dispatch=PathToolMessage::DragStop),
// PenToolMessage
entry!(PointerMove; refresh_keys=[KeyShift, KeyControl], action_dispatch=PenToolMessage::PointerMove { snap_angle: KeyControl, break_handle: KeyShift }),
entry!(KeyDown(Lmb); action_dispatch=PenToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=PenToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(KeyEscape); action_dispatch=PenToolMessage::Confirm),
entry!(KeyDown(KeyEnter); action_dispatch=PenToolMessage::Confirm),
// FreehandToolMessage
entry!(PointerMove; action_dispatch=FreehandToolMessage::PointerMove),
entry!(KeyDown(Lmb); action_dispatch=FreehandToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=FreehandToolMessage::DragStop),
// SplineToolMessage
entry!(PointerMove; action_dispatch=SplineToolMessage::PointerMove),
entry!(KeyDown(Lmb); action_dispatch=SplineToolMessage::DragStart),
entry!(KeyUp(Lmb); action_dispatch=SplineToolMessage::DragStop),
entry!(KeyDown(Rmb); action_dispatch=SplineToolMessage::Confirm),
entry!(KeyDown(KeyEscape); action_dispatch=SplineToolMessage::Confirm),
entry!(KeyDown(KeyEnter); action_dispatch=SplineToolMessage::Confirm),
// FillToolMessage
entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftMouseDown),
entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightMouseDown),
// ToolMessage
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
entry!(KeyDown(KeyZ); action_dispatch=ToolMessage::ActivateToolNavigate),
entry!(KeyDown(KeyI); action_dispatch=ToolMessage::ActivateToolEyedropper),
entry!(KeyDown(KeyT); action_dispatch=ToolMessage::ActivateToolText),
entry!(KeyDown(KeyF); action_dispatch=ToolMessage::ActivateToolFill),
entry!(KeyDown(KeyH); action_dispatch=ToolMessage::ActivateToolGradient),
entry!(KeyDown(KeyA); action_dispatch=ToolMessage::ActivateToolPath),
entry!(KeyDown(KeyP); action_dispatch=ToolMessage::ActivateToolPen),
entry!(KeyDown(KeyN); action_dispatch=ToolMessage::ActivateToolFreehand),
entry!(KeyDown(KeyL); action_dispatch=ToolMessage::ActivateToolLine),
entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle),
entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse),
entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape),
entry_multiplatform!(
standard!(KeyDown(KeyX); modifiers=[KeyShift, KeyControl], action_dispatch=ToolMessage::ResetColors),
mac_only!(KeyDown(KeyX); modifiers=[KeyShift, KeyCommand], action_dispatch=ToolMessage::ResetColors),
),
entry!(KeyDown(KeyX); modifiers=[KeyShift], action_dispatch=ToolMessage::SwapColors),
entry!(KeyDown(KeyC); modifiers=[KeyAlt], action_dispatch=ToolMessage::SelectRandomPrimaryColor),
// DocumentMessage
entry!(KeyDown(KeyDelete); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(KeyBackspace); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(KeyP); modifiers=[KeyAlt], action_dispatch=DocumentMessage::DebugPrintDocument),
entry_multiplatform!(
standard!(KeyDown(KeyZ); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::Redo),
mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::Redo),
),
entry_multiplatform!(
standard!(KeyDown(KeyZ); modifiers=[KeyControl], action_dispatch=DocumentMessage::Undo),
mac_only!(KeyDown(KeyZ); modifiers=[KeyCommand], action_dispatch=DocumentMessage::Undo),
),
entry_multiplatform!(
standard!(KeyDown(KeyA); modifiers=[KeyControl, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers),
mac_only!(KeyDown(KeyA); modifiers=[KeyCommand, KeyAlt], action_dispatch=DocumentMessage::DeselectAllLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyA); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectAllLayers),
mac_only!(KeyDown(KeyA); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectAllLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyS); modifiers=[KeyControl], action_dispatch=DocumentMessage::SaveDocument),
mac_only!(KeyDown(KeyS); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SaveDocument),
),
entry_multiplatform!(
standard!(KeyDown(Key0); modifiers=[KeyControl], action_dispatch=DocumentMessage::ZoomCanvasToFitAll),
mac_only!(KeyDown(Key0); modifiers=[KeyCommand], action_dispatch=DocumentMessage::ZoomCanvasToFitAll),
),
entry_multiplatform!(
standard!(KeyDown(KeyD); modifiers=[KeyControl], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
mac_only!(KeyDown(KeyD); modifiers=[KeyCommand], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyG); modifiers=[KeyControl], action_dispatch=DocumentMessage::GroupSelectedLayers),
mac_only!(KeyDown(KeyG); modifiers=[KeyCommand], action_dispatch=DocumentMessage::GroupSelectedLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyG); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
mac_only!(KeyDown(KeyG); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
),
entry_multiplatform!(
standard!(KeyDown(KeyN); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }),
mac_only!(KeyDown(KeyN); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::CreateEmptyFolder { container_path: vec![] }),
),
entry_multiplatform!(
standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
),
entry_multiplatform!(
// TODO: Delete this in favor of the KeyLeftBracket (non-shifted version of this key) mapping above once the input system can distinguish between the non-shifted and shifted keys (important for other language keyboards)
standard!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
mac_only!(KeyDown(KeyLeftCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersLowerToBack),
),
entry_multiplatform!(
standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
),
entry_multiplatform!(
// TODO: Delete this in favor of the KeyRightBracket (non-shifted version of this key) mapping above once the input system can distinguish between the non-shifted and shifted keys (important for other language keyboards)
standard!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyControl, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
mac_only!(KeyDown(KeyRightCurlyBracket); modifiers=[KeyCommand, KeyShift], action_dispatch=DocumentMessage::SelectedLayersRaiseToFront),
),
entry_multiplatform!(
standard!(KeyDown(KeyLeftBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersLower),
mac_only!(KeyDown(KeyLeftBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersLower),
),
entry_multiplatform!(
standard!(KeyDown(KeyRightBracket); modifiers=[KeyControl], action_dispatch=DocumentMessage::SelectedLayersRaise),
mac_only!(KeyDown(KeyRightBracket); modifiers=[KeyCommand], action_dispatch=DocumentMessage::SelectedLayersRaise),
),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift, KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift, KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift, KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift, KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift, KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift, KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -BIG_NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift, KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: -BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift, KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: BIG_NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyShift], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); modifiers=[KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowUp); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyArrowLeft], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); modifiers=[KeyArrowRight], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowDown); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: 0., delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); modifiers=[KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowLeft); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: -NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyArrowUp], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); modifiers=[KeyArrowDown], action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: NUDGE_AMOUNT }),
entry!(KeyDown(KeyArrowRight); action_dispatch=DocumentMessage::NudgeSelectedLayers { delta_x: NUDGE_AMOUNT, delta_y: 0. }),
// TransformLayerMessage
entry!(KeyDown(KeyG); action_dispatch=TransformLayerMessage::BeginGrab),
entry!(KeyDown(KeyR); action_dispatch=TransformLayerMessage::BeginRotate),
entry!(KeyDown(KeyS); action_dispatch=TransformLayerMessage::BeginScale),
// MovementMessage
entry!(KeyDown(Mmb); modifiers=[KeyControl], action_dispatch=MovementMessage::RotateCanvasBegin),
entry!(KeyDown(Mmb); modifiers=[KeyShift], action_dispatch=MovementMessage::ZoomCanvasBegin),
entry!(KeyDown(Mmb); action_dispatch=MovementMessage::TranslateCanvasBegin),
entry!(KeyUp(Mmb); action_dispatch=MovementMessage::TransformCanvasEnd),
entry!(KeyDown(Lmb); modifiers=[KeySpace], action_dispatch=MovementMessage::TranslateCanvasBegin),
entry!(KeyUp(Lmb); modifiers=[KeySpace], action_dispatch=MovementMessage::TransformCanvasEnd),
entry_multiplatform!(
standard!(KeyDown(KeyPlus); modifiers=[KeyControl], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
mac_only!(KeyDown(KeyPlus); modifiers=[KeyCommand], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
),
entry_multiplatform!(
standard!(KeyDown(KeyEquals); modifiers=[KeyControl], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
mac_only!(KeyDown(KeyEquals); modifiers=[KeyCommand], action_dispatch=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }),
),
entry_multiplatform!(
standard!(KeyDown(KeyMinus); modifiers=[KeyControl], action_dispatch=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }),
mac_only!(KeyDown(KeyMinus); modifiers=[KeyCommand], action_dispatch=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }),
),
entry_multiplatform!(
standard!(KeyDown(Key1); modifiers=[KeyControl], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 1. }),
mac_only!(KeyDown(Key1); modifiers=[KeyCommand], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 1. }),
),
entry_multiplatform!(
standard!(KeyDown(Key2); modifiers=[KeyControl], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 2. }),
mac_only!(KeyDown(Key2); modifiers=[KeyCommand], action_dispatch=MovementMessage::SetCanvasZoom { zoom_factor: 2. }),
),
entry!(WheelScroll; modifiers=[KeyControl], action_dispatch=MovementMessage::WheelCanvasZoom),
entry!(WheelScroll; modifiers=[KeyShift], action_dispatch=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }),
entry!(WheelScroll; action_dispatch=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }),
entry!(KeyDown(KeyPageUp); modifiers=[KeyShift], action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(1., 0.) }),
entry!(KeyDown(KeyPageDown); modifiers=[KeyShift], action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(-1., 0.) }),
entry!(KeyDown(KeyPageUp); action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., 1.) }),
entry!(KeyDown(KeyPageDown); action_dispatch=MovementMessage::TranslateCanvasByViewportFraction { delta: DVec2::new(0., -1.) }),
// PortfolioMessage
entry_multiplatform!(
standard!(KeyDown(KeyO); modifiers=[KeyControl], action_dispatch=PortfolioMessage::OpenDocument),
mac_only!(KeyDown(KeyO); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::OpenDocument),
),
entry_multiplatform!(
standard!(KeyDown(KeyI); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Import),
mac_only!(KeyDown(KeyI); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Import),
),
entry!(KeyDown(KeyTab); modifiers=[KeyControl], action_dispatch=PortfolioMessage::NextDocument),
entry!(KeyDown(KeyTab); modifiers=[KeyControl, KeyShift], action_dispatch=PortfolioMessage::PrevDocument),
entry_multiplatform!(
standard!(KeyDown(KeyW); modifiers=[KeyControl], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
mac_only!(KeyDown(KeyW); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::CloseActiveDocumentWithConfirmation),
),
entry_multiplatform!(
standard!(KeyDown(KeyX); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
mac_only!(KeyDown(KeyX); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Cut { clipboard: Clipboard::Device }),
),
entry_multiplatform!(
standard!(KeyDown(KeyC); modifiers=[KeyControl], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
mac_only!(KeyDown(KeyC); modifiers=[KeyCommand], action_dispatch=PortfolioMessage::Copy { clipboard: Clipboard::Device }),
),
entry_multiplatform!(
// This shortcut is intercepted in the frontend; it exists here only as a shortcut mapping source
standard!(KeyDown(KeyV); modifiers=[KeyControl], action_dispatch=FrontendMessage::TriggerPaste),
mac_only!(KeyDown(KeyV); modifiers=[KeyCommand], action_dispatch=FrontendMessage::TriggerPaste),
),
// DialogMessage
entry_multiplatform!(
standard!(KeyDown(KeyN); modifiers=[KeyControl], action_dispatch=DialogMessage::RequestNewDocumentDialog),
mac_only!(KeyDown(KeyN); modifiers=[KeyCommand], action_dispatch=DialogMessage::RequestNewDocumentDialog),
),
entry_multiplatform!(
standard!(KeyDown(KeyW); modifiers=[KeyControl, KeyAlt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation),
mac_only!(KeyDown(KeyW); modifiers=[KeyCommand, KeyAlt], action_dispatch=DialogMessage::CloseAllDocumentsWithConfirmation),
),
entry_multiplatform!(
standard!(KeyDown(KeyE); modifiers=[KeyControl], action_dispatch=DialogMessage::RequestExportDialog),
mac_only!(KeyDown(KeyE); modifiers=[KeyCommand], action_dispatch=DialogMessage::RequestExportDialog),
),
// DebugMessage
entry!(KeyDown(KeyT); modifiers=[KeyAlt], action_dispatch=DebugMessage::ToggleTraceLogs),
entry!(KeyDown(Key0); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageOff),
entry!(KeyDown(Key1); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageNames),
entry!(KeyDown(Key2); modifiers=[KeyAlt], action_dispatch=DebugMessage::MessageContents),
];
let (mut key_up, mut key_down, mut pointer_move, mut mouse_scroll, mut double_click) = mappings;
let (mut key_up, mut key_down, mut double_click, mut wheel_scroll, mut pointer_move) = mappings;
// TODO: Hardcode these 10 lines into 10 lines of declarations, or make this use a macro to do all 10 in one line
const NUMBER_KEYS: [Key; 10] = [Key0, Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9];
@ -239,8 +346,9 @@ impl Default for Mapping {
0,
MappingEntry {
action: TransformLayerMessage::TypeDigit { digit: i as u8 }.into(),
trigger: InputMapperMessage::KeyDown(*key),
modifiers: modifiers! {},
input: InputMapperMessage::KeyDown(*key),
platform_layout: None,
modifiers: modifiers!(),
},
);
}
@ -251,48 +359,70 @@ impl Default for Mapping {
sort(sublist);
}
}
sort(&mut pointer_move);
sort(&mut mouse_scroll);
sort(&mut double_click);
sort(&mut wheel_scroll);
sort(&mut pointer_move);
Self {
key_up,
key_down,
pointer_move,
mouse_scroll,
double_click,
wheel_scroll,
pointer_move,
}
}
}
impl Mapping {
pub fn match_message(&self, message: InputMapperMessage, keys: &KeyStates, actions: ActionList) -> Option<Message> {
pub fn match_input_message(&self, message: InputMapperMessage, keyboard_state: &KeyStates, actions: ActionList, keyboard_platform: KeyboardPlatformLayout) -> Option<Message> {
let list = match message {
InputMapperMessage::KeyDown(key) => &self.key_down[key as usize],
InputMapperMessage::KeyUp(key) => &self.key_up[key as usize],
InputMapperMessage::DoubleClick => &self.double_click,
InputMapperMessage::MouseScroll => &self.mouse_scroll,
InputMapperMessage::WheelScroll => &self.wheel_scroll,
InputMapperMessage::PointerMove => &self.pointer_move,
};
list.match_mapping(keys, actions)
list.match_mapping(keyboard_state, actions, keyboard_platform)
}
}
#[derive(PartialEq, Clone, Debug)]
pub struct MappingEntry {
pub trigger: InputMapperMessage,
pub modifiers: KeyStates,
/// Serves two purposes:
/// - This is the message that gets dispatched when the hotkey is matched
/// - This message's discriminant is the action; it must be a currently active action to be considered as a shortcut
pub action: Message,
/// The user input event from an input device which this input mapping matches on
pub input: InputMapperMessage,
/// Any additional keys that must be also pressed for this input mapping to match
pub modifiers: KeyStates,
/// The keyboard platform layout which this mapping is exclusive to, or `None` if it's platform-agnostic
pub platform_layout: Option<KeyboardPlatformLayout>,
}
#[derive(Debug, Clone)]
pub struct KeyMappingEntries(pub Vec<MappingEntry>);
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() {
let all_required_modifiers_pressed = ((*keys & entry.modifiers) ^ entry.modifiers).is_empty();
if all_required_modifiers_pressed && actions.iter().flatten().any(|action| entry.action.to_discriminant() == *action) {
// Skip this entry if it is platform-specific, and for a layout that does not match the user's keyboard platform layout
if let Some(entry_platform_layout) = entry.platform_layout {
if entry_platform_layout != keyboard_platform {
continue;
}
}
// Find which currently pressed keys are also the modifiers in this hotkey entry, then compare those against the required modifiers to see if there are zero missing
let pressed_modifiers = *keyboard_state & entry.modifiers;
let all_modifiers_without_pressed_modifiers = entry.modifiers ^ pressed_modifiers;
let all_required_modifiers_pressed = all_modifiers_without_pressed_modifiers.is_empty();
// Skip this entry if any of the required modifiers are missing
if !all_required_modifiers_pressed {
continue;
}
if actions.iter().flatten().any(|action| entry.action.to_discriminant() == *action) {
return Some(entry.action.clone());
}
}
@ -313,70 +443,69 @@ impl KeyMappingEntries {
}
}
impl Default for KeyMappingEntries {
fn default() -> Self {
Self::new()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ActionKeys {
Action(MessageDiscriminant),
#[serde(rename = "keys")]
Keys(Vec<Key>),
}
mod input_mapper_macros {
macro_rules! modifiers {
($($m:ident),*) => {{
#[allow(unused_mut)]
let mut state = KeyStates::new();
$(
state.set(Key::$m as usize);
)*
state
}};
}
impl ActionKeys {
pub fn to_keys(&mut self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>) {
match self {
ActionKeys::Action(action) => {
if let Some(keys) = action_input_mapping(action).get_mut(0) {
let mut taken_keys = Vec::new();
std::mem::swap(keys, &mut taken_keys);
macro_rules! entry {
{action=$action:expr, key_down=$key:ident $(, modifiers=[$($m:ident),* $(,)?])?} => {{
entry!{action=$action, message=InputMapperMessage::KeyDown(Key::$key) $(, modifiers=[$($m),*])?}
}};
{action=$action:expr, key_up=$key:ident $(, modifiers=[$($m:ident),* $(,)?])?} => {{
entry!{action=$action, message=InputMapperMessage::KeyUp(Key::$key) $(, modifiers=[$($m),* ])?}
}};
{action=$action:expr, message=$message:expr $(, modifiers=[$($m:ident),* $(,)?])?} => {{
&[MappingEntry {trigger: $message, modifiers: modifiers!($($($m),*)?), action: $action.into()}]
}};
{action=$action:expr, triggers=[$($m:ident),* $(,)?]} => {{
&[
MappingEntry {trigger:InputMapperMessage::PointerMove, action: $action.into(), modifiers: modifiers!()},
$(
MappingEntry {trigger:InputMapperMessage::KeyDown(Key::$m), action: $action.into(), modifiers: modifiers!()},
MappingEntry {trigger:InputMapperMessage::KeyUp(Key::$m), action: $action.into(), modifiers: modifiers!()},
)*
]
}};
}
macro_rules! mapping {
//[$(<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());
*self = ActionKeys::Keys(taken_keys);
} else {
*self = ActionKeys::Keys(Vec::new());
}
)*
(key_up, key_down, pointer_move, mouse_scroll, double_click)
}};
}
ActionKeys::Keys(keys) => {
log::warn!("Calling `.to_keys()` on a `ActionKeys::Keys` is a mistake/bug. Keys are: {:?}.", keys);
}
}
}
}
pub fn keys_text_shortcut(keys: &[Key], keyboard_platform: KeyboardPlatformLayout) -> String {
const JOINER_MARK: &str = "+";
let mut joined = keys
.iter()
.map(|key| {
let key_string = key.to_string();
if keyboard_platform == KeyboardPlatformLayout::Mac {
match key_string.as_str() {
"Command" => "".to_string(),
"Control" => "".to_string(),
"Alt" => "".to_string(),
"Shift" => "".to_string(),
_ => key_string + JOINER_MARK,
}
} else {
key_string + JOINER_MARK
}
})
.collect::<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;
pub(crate) use mapping;
pub(crate) use modifiers;
joined
}
pub mod action_keys {
macro_rules! action_shortcut {
($action:expr) => {
Some(crate::input::input_mapper::ActionKeys::Action($action.into()))
};
}
pub(crate) use action_shortcut;
}

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
DoubleClick,
MouseScroll,
PointerMove,
WheelScroll,
}

View file

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

View file

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

View file

@ -16,8 +16,8 @@ pub enum InputPreprocessorMessage {
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
KeyDown { key: Key, modifier_keys: ModifierKeys },
KeyUp { key: Key, modifier_keys: ModifierKeys },
MouseScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
WheelScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,7 @@
use super::layout_message::LayoutTarget;
use crate::document::utility_types::KeyboardPlatformLayout;
use crate::input::input_mapper::keys_text_shortcut;
use crate::input::input_mapper::ActionKeys;
use crate::input::keyboard::Key;
use crate::message_prelude::*;
use crate::Color;
@ -30,16 +33,67 @@ pub enum Layout {
}
impl Layout {
pub fn unwrap_widget_layout(self) -> WidgetLayout {
if let Layout::WidgetLayout(widget_layout) = self {
pub fn unwrap_widget_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, keyboard_platform: KeyboardPlatformLayout) -> WidgetLayout {
if let Layout::WidgetLayout(mut widget_layout) = self {
// Function used multiple times later in this code block to convert `ActionKeys::Action` to `ActionKeys::Keys` and append its shortcut to the tooltip
let apply_shortcut_to_tooltip = |tooltip_shortcut: &mut ActionKeys, tooltip: &mut String| {
tooltip_shortcut.to_keys(action_input_mapping);
if let ActionKeys::Keys(keys) = tooltip_shortcut {
let shortcut_text = keys_text_shortcut(keys, keyboard_platform);
if !shortcut_text.is_empty() {
if !tooltip.is_empty() {
tooltip.push(' ');
}
tooltip.push('(');
tooltip.push_str(&shortcut_text);
tooltip.push(')');
}
}
};
// Go through each widget to convert `ActionKeys::Action` to `ActionKeys::Keys` and append the key combination to the widget tooltip
for widget_holder in &mut widget_layout.iter_mut() {
// Handle all the widgets that have tooltips
let mut tooltip_shortcut = match &mut widget_holder.widget {
Widget::CheckboxInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::ColorInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::IconButton(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
Widget::OptionalInput(widget) => Some((&mut widget.tooltip, &mut widget.tooltip_shortcut)),
_ => None,
};
if let Some((tooltip, Some(tooltip_shortcut))) = &mut tooltip_shortcut {
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
}
// Handle RadioInput separately because its tooltips are children of the widget
if let Widget::RadioInput(radio_input) = &mut widget_holder.widget {
for radio_entry_data in &mut radio_input.entries {
if let RadioEntryData {
tooltip,
tooltip_shortcut: Some(tooltip_shortcut),
..
} = radio_entry_data
{
apply_shortcut_to_tooltip(tooltip_shortcut, tooltip);
}
}
}
}
widget_layout
} else {
panic!("Tried to unwrap layout as WidgetLayout. Got {:?}", self)
}
}
pub fn unwrap_menu_layout(self) -> MenuLayout {
if let Layout::MenuLayout(menu_layout) = self {
pub fn unwrap_menu_layout(self, action_input_mapping: &impl Fn(&MessageDiscriminant) -> Vec<Vec<Key>>, _keyboard_platform: KeyboardPlatformLayout) -> MenuLayout {
if let Layout::MenuLayout(mut menu_layout) = self {
for menu_column in &mut menu_layout.layout {
menu_column.children.fill_in_shortcut_actions_with_keys(action_input_mapping);
}
menu_layout
} else {
panic!("Tried to unwrap layout as MenuLayout. Got {:?}", self)
@ -67,13 +121,35 @@ impl Default for Layout {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct MenuEntryGroups(pub Vec<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)]
pub struct MenuEntry {
pub label: String,
pub icon: Option<String>,
pub children: Option<Vec<Vec<MenuEntry>>>,
pub children: MenuEntryGroups,
pub action: WidgetHolder,
pub shortcut: Option<Vec<Key>>,
pub shortcut: Option<ActionKeys>,
}
impl MenuEntry {
@ -94,7 +170,7 @@ impl Default for MenuEntry {
action: MenuEntry::create_action(|_| DialogMessage::RequestComingSoonDialog { issue: None }.into()),
label: "".into(),
icon: None,
children: None,
children: MenuEntryGroups::empty(),
shortcut: None,
}
}
@ -103,10 +179,10 @@ impl Default for MenuEntry {
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
pub struct MenuColumn {
pub label: String,
pub children: Vec<Vec<MenuEntry>>,
pub children: MenuEntryGroups,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct MenuLayout {
pub layout: Vec<MenuColumn>,
}
@ -118,13 +194,13 @@ impl MenuLayout {
pub fn iter(&self) -> impl Iterator<Item = &WidgetHolder> + '_ {
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> + '_ {
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> {
match self.stack.pop() {
Some(menu_entry) => {
self.stack.extend(menu_entry.children.iter().flat_map(|group| group.iter()).flat_map(|entry| entry.iter()));
let more_entries = menu_entry.children.0.iter().flat_map(|entry| entry.iter());
self.stack.extend(more_entries);
Some(&menu_entry.action)
}
None => None,
@ -158,7 +236,9 @@ impl<'a> Iterator for MenuLayoutIterMut<'a> {
fn next(&mut self) -> Option<Self::Item> {
match self.stack.pop() {
Some(menu_entry) => {
self.stack.extend(menu_entry.children.iter_mut().flat_map(|group| group.iter_mut()).flat_map(|entry| entry.iter_mut()));
let more_entries = menu_entry.children.0.iter_mut().flat_map(|entry| entry.iter_mut());
self.stack.extend(more_entries);
Some(&mut menu_entry.action)
}
None => None,
@ -280,7 +360,7 @@ impl<'a> Iterator for WidgetIterMut<'a> {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WidgetHolder {
#[serde(rename = "widgetId")]
pub widget_id: u64,
@ -332,7 +412,7 @@ pub enum Widget {
TextLabel(TextLabel),
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct CheckboxInput {
pub checked: bool,
@ -341,13 +421,16 @@ pub struct CheckboxInput {
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
pub on_update: WidgetCallback<CheckboxInput>,
}
#[derive(Clone, Serialize, Deserialize, Derivative)]
#[derive(Clone, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq, Default)]
pub struct ColorInput {
pub value: Option<String>,
@ -362,6 +445,9 @@ pub struct ColorInput {
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@ -435,7 +521,7 @@ pub struct FontInput {
pub on_update: WidgetCallback<FontInput>,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct IconButton {
pub icon: String,
@ -446,6 +532,9 @@ pub struct IconButton {
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@ -533,7 +622,7 @@ pub enum NumberInputIncrementBehavior {
Callback,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct OptionalInput {
pub checked: bool,
@ -542,6 +631,9 @@ pub struct OptionalInput {
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
@ -559,7 +651,7 @@ pub struct PopoverButton {
pub text: String,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct RadioInput {
pub entries: Vec<RadioEntryData>,
@ -569,7 +661,7 @@ pub struct RadioInput {
pub selected_index: u32,
}
#[derive(Clone, Serialize, Deserialize, Derivative, Default)]
#[derive(Clone, Default, Derivative, Serialize, Deserialize)]
#[derivative(Debug, PartialEq)]
pub struct RadioEntryData {
pub value: String,
@ -580,6 +672,9 @@ pub struct RadioEntryData {
pub tooltip: String,
#[serde(skip)]
pub tooltip_shortcut: Option<ActionKeys>,
// Callbacks
#[serde(skip)]
#[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::shape_tool::{ShapeToolMessage, ShapeToolMessageDiscriminant};
pub use crate::viewport_tools::tools::spline_tool::{SplineToolMessage, SplineToolMessageDiscriminant};
pub use crate::viewport_tools::tools::text_tool::{TextMessage, TextMessageDiscriminant};
pub use crate::viewport_tools::tools::text_tool::{TextToolMessage, TextToolMessageDiscriminant};
pub use crate::workspace::{WorkspaceMessage, WorkspaceMessageDiscriminant};
pub use graphite_proc_macros::*;

View file

@ -10,10 +10,19 @@ pub struct HintGroup(pub Vec<HintInfo>);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HintInfo {
/// A `KeysGroup` specifies all the keys pressed simultaneously to perform an action (like "Ctrl C" to copy).
/// Usually at most one is given, but less commonly, multiple can be used to describe additional hotkeys not used simultaneously (like the four different arrow keys to nudge a layer).
#[serde(rename = "keyGroups")]
pub key_groups: Vec<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>,
/// The text describing what occurs with this input combination.
pub label: String,
/// Prepend the "+" symbol indicating that this is a refinement upon a previous entry in the group.
/// Draws a prepended "+" symbol which indicates that this is a refinement upon a previous hint in the group.
pub plus: bool,
}

View file

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

View file

@ -28,6 +28,7 @@ pub enum ToolMessage {
#[remain::unsorted]
#[child]
Gradient(GradientToolMessage),
#[remain::unsorted]
#[child]
Path(PathToolMessage),
@ -54,7 +55,8 @@ pub enum ToolMessage {
Shape(ShapeToolMessage),
#[remain::unsorted]
#[child]
Text(TextMessage),
Text(TextToolMessage),
// #[remain::unsorted]
// #[child]
// Brush(BrushToolMessage),
@ -75,6 +77,38 @@ pub enum ToolMessage {
// Detail(DetailToolMessage),
// Messages
#[remain::unsorted]
ActivateToolSelect,
#[remain::unsorted]
ActivateToolArtboard,
#[remain::unsorted]
ActivateToolNavigate,
#[remain::unsorted]
ActivateToolEyedropper,
#[remain::unsorted]
ActivateToolText,
#[remain::unsorted]
ActivateToolFill,
#[remain::unsorted]
ActivateToolGradient,
#[remain::unsorted]
ActivateToolPath,
#[remain::unsorted]
ActivateToolPen,
#[remain::unsorted]
ActivateToolFreehand,
#[remain::unsorted]
ActivateToolSpline,
#[remain::unsorted]
ActivateToolLine,
#[remain::unsorted]
ActivateToolRectangle,
#[remain::unsorted]
ActivateToolEllipse,
#[remain::unsorted]
ActivateToolShape,
ActivateTool {
tool_type: ToolType,
},

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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 { createPortfolioState, PortfolioState } from "@/state-providers/portfolio";
import { createWorkspaceState, WorkspaceState } from "@/state-providers/workspace";
import { operatingSystem } from "@/utility-functions/platform";
import { createEditor, Editor } from "@/wasm-communication/editor";
import MainWindow from "@/components/window/MainWindow.vue";
@ -293,7 +294,8 @@ export default defineComponent({
});
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
this.editor.instance.init_after_frontend_ready();
const platform = operatingSystem();
this.editor.instance.init_after_frontend_ready(platform);
},
beforeUnmount() {
// Call the destructor for each manager

View file

@ -33,8 +33,7 @@
<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-else-if="entry.shortcut?.length" :inputKeys="[entry.shortcut]" />
<UserInputLabel v-if="entry.shortcut?.keys.length" :inputKeys="[entry.shortcut.keys]" :requiresLock="entry.shortcutRequiresLock" />
<div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div>
@ -64,6 +63,10 @@
.floating-menu-container .floating-menu-content {
padding: 4px 0;
.separator div {
background: var(--color-4-dimgray);
}
.scroll-spacer {
flex: 0 0 auto;
}
@ -128,29 +131,24 @@
&.open,
&.active {
background: var(--color-6-lowergray);
color: var(--color-f-white);
&.active {
background: var(--color-accent);
}
svg {
.entry-icon svg {
fill: var(--color-f-white);
}
span {
color: var(--color-f-white);
}
}
&.disabled {
color: var(--color-8-uppergray);
&:hover {
background: none;
}
span {
color: var(--color-8-uppergray);
}
svg {
fill: var(--color-8-uppergray);
}
@ -172,11 +170,7 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
const KEYBOARD_LOCK_USE_FULLSCREEN = "This hotkey is reserved by the browser, but becomes available in fullscreen mode";
const KEYBOARD_LOCK_SWITCH_BROWSER = "This hotkey is reserved by the browser, but becomes available in Chrome, Edge, and Opera which support the Keyboard.lock() API";
const MenuList = defineComponent({
inject: ["fullscreen"],
emits: ["update:open", "update:activeEntry", "naturalWidth"],
props: {
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
@ -193,7 +187,6 @@ const MenuList = defineComponent({
data() {
return {
isOpen: this.open,
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
highlighted: this.activeEntry as MenuListEntry | undefined,
virtualScrollingEntriesStart: 0,
};

View file

@ -31,14 +31,18 @@
<LayoutRow
class="layer"
: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"
:title="`${listing.entry.name}${devMode ? '\nLayer Path: ' + listing.entry.path.join(' / ') : ''}` || null"
:draggable="draggable"
@dragstart="(e) => draggable && dragStart(e, listing.entry)"
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`.trim() || null"
@dragstart="(e) => draggable && dragStart(e, listing)"
@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">
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" />
@ -53,10 +57,10 @@
:value="listing.entry.name"
:placeholder="listing.entry.layer_type"
:disabled="!listing.editingName"
@change="(e) => onEditLayerNameChange(listing, e.target)"
@blur="() => onEditLayerNameDeselect(listing)"
@keydown.esc="onEditLayerNameDeselect(listing)"
@keydown.enter="(e) => onEditLayerNameChange(listing, e.target)"
@keydown.escape="onEditLayerNameDeselect(listing)"
@change="(e) => onEditLayerNameChange(listing, e.target)"
/>
</LayoutRow>
<div class="thumbnail" v-html="listing.entry.thumbnail"></div>
@ -263,6 +267,7 @@
<script lang="ts">
import { defineComponent, nextTick } from "vue";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import { defaultWidgetLayout, UpdateDocumentLayerTreeStructure, UpdateDocumentLayerDetails, UpdateLayerTreeOptionsLayout, LayerPanelEntry } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
@ -342,8 +347,18 @@ export default defineComponent({
await nextTick();
window.getSelection()?.removeAllRanges();
},
async selectLayer(clickedLayer: LayerPanelEntry, ctrl: boolean, shift: boolean) {
this.editor.instance.select_layer(clickedLayer.path, ctrl, shift);
async selectLayer(ctrl: boolean, cmd: boolean, shift: boolean, listing: LayerListingInfo, event: Event) {
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() {
this.editor.instance.deselect_all_layers();
@ -409,8 +424,9 @@ export default defineComponent({
return { insertFolder, insertIndex, highlightFolder, markerHeight };
},
async dragStart(event: DragEvent, layer: LayerPanelEntry) {
if (!layer.layer_metadata.selected) this.selectLayer(layer, event.ctrlKey, event.shiftKey);
async dragStart(event: DragEvent, listing: LayerListingInfo) {
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
if (event.dataTransfer) {

View file

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

View file

@ -101,7 +101,7 @@ export default defineComponent({
...entry,
children: entry.children ? menuEntryToFrontendMenuEntry(entry.children) : 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>
<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">
<span class="group-gap" v-if="keyGroupIndex > 0"></span>
<template v-for="(keyInfo, index) in keyTextOrIconList(keyGroup)" :key="index">
@ -45,15 +46,14 @@
font-family: "Inconsolata", monospace;
font-weight: 400;
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;
// 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;
box-sizing: border-box;
border: 1px solid;
border-radius: 4px;
border-color: var(--color-7-middlegray);
color: var(--color-e-nearwhite);
&.width-16 {
width: 16px;
@ -93,6 +93,40 @@
.hint-text {
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>
@ -100,92 +134,123 @@
import { defineComponent, PropType } from "vue";
import { IconName } from "@/utility-functions/icons";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import { HintInfo, KeysGroup } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.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({
components: {
IconLabel,
LayoutRow,
},
inject: ["fullscreen"],
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 },
requiresLock: { type: Boolean as PropType<boolean>, default: false },
},
computed: {
hasSlotContent(): boolean {
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: {
keyTextOrIconList(keyGroup: KeysGroup): { text: string | null; icon: IconName | null; width: string }[] {
return keyGroup.map((inputKey) => this.keyTextOrIcon(inputKey));
},
keyTextOrIcon(keyText: string): { text: string | null; icon: IconName | null; width: string } {
// Definitions
const textMap: Record<string, string> = {
Control: "Ctrl",
Alt: "Alt",
Delete: "Del",
PageUp: "PgUp",
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,
};
keyTextOrIcon(input: string): { text: string | null; icon: IconName | null; width: string } {
let keyText = input;
if (operatingSystemIsMac()) {
keyText = keyText.replace("Alt", "Option");
}
const iconsAndWidths = operatingSystemIsMac() ? { ...iconsAndWidthsStandard, ...iconsAndWidthsMac } : iconsAndWidthsStandard;
// Strip off the "Key" prefix
const text = keyText.replace(/^(?:Key)?(.*)$/, "$1");
// 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 {
text: null,
icon: this.keyboardHintIcon(text),
width: `width-${iconsAndWidths[text] * 8 + 8}`,
width: `width-${width}`,
};
}
// Otherwise, return the text string
let result;
// Letters and numbers
if (/^[A-Z0-9]$/.test(text)) result = text;
if (/^[A-Z0-9]$/.test(text)) {
result = text;
}
// 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
else result = text;
else {
result = text;
}
return { text: result, icon: null, width: `width-${(result || " ").length * 8 + 8}` };
},
mouseHintIcon(input: HintInfo["mouse"]): 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;
},
},
components: {
IconLabel,
LayoutRow,
},
});
</script>

View file

@ -5,7 +5,7 @@
<Separator :type="'Section'" v-if="index !== 0" />
<template v-for="hint in hintGroup" :key="hint">
<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>
</LayoutRow>
@ -44,7 +44,8 @@
<script lang="ts">
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 Separator from "@/components/widgets/labels/Separator.vue";
@ -57,6 +58,12 @@ export default defineComponent({
hintData: [] as HintData,
};
},
methods: {
inputKeysForPlatform(hint: HintInfo): KeysGroup[] {
if (operatingSystemIsMac() && hint.keyGroupsMac) return hint.keyGroupsMac;
return hint.keyGroups;
},
},
mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
this.hintData = updateInputHints.hint_data;

View file

@ -1,5 +1,5 @@
<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>
<IconLabel :icon="fullscreen.state.windowFullscreen ? 'FullscreenExit' : 'FullscreenEnter'" />
</LayoutRow>

View file

@ -45,8 +45,8 @@
<Separator :type="'Unrelated'" />
</LayoutCol>
<LayoutCol>
<UserInputLabel :inputKeys="[['KeyControl', 'KeyN']]" />
<UserInputLabel :inputKeys="[['KeyControl', 'KeyO']]" />
<UserInputLabel :inputKeys="[[controlOrCommandKey(), 'KeyN']]" />
<UserInputLabel :inputKeys="[[controlOrCommandKey(), 'KeyO']]" />
</LayoutCol>
</LayoutRow>
</LayoutCol>
@ -216,6 +216,8 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import Document from "@/components/panels/Document.vue";
@ -257,6 +259,10 @@ export default defineComponent({
openDocument() {
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: {
LayoutCol,

View file

@ -2,6 +2,7 @@ import { DialogState } from "@/state-providers/dialog";
import { FullscreenState } from "@/state-providers/fullscreen";
import { PortfolioState } from "@/state-providers/portfolio";
import { makeKeyboardModifiersBitfield, textInputCleanup, getLatinKey } from "@/utility-functions/keyboard-entry";
import { operatingSystemIsMac } from "@/utility-functions/platform";
import { stripIndents } from "@/utility-functions/strip-indents";
import { Editor } from "@/wasm-communication/editor";
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: "dblclick", action: (e: PointerEvent): void => onDoubleClick(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.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);
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
if (key !== "escape" && !(e.ctrlKey && key === "enter") && targetIsTextField(e.target)) {
return false;
}
if (key !== "escape" && !(ctrlOrCmd && key === "enter") && targetIsTextField(e.target)) return false;
// Don't redirect paste
if (key === "v" && e.ctrlKey) return false;
if (key === "v" && ctrlOrCmd) return false;
// Don't redirect a fullscreen request
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
if (key === "f5") return false;
if (key === "f5" || (ctrlOrCmd && key === "r")) return false;
// Don't redirect debugging tools
if (key === "f12" || key === "f8") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "c") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "i") return false;
if ((e.ctrlKey || e.metaKey) && e.shiftKey && key === "j") return false;
if (ctrlOrCmd && e.shiftKey && key === "c") return false;
if (ctrlOrCmd && e.shiftKey && key === "i") return false;
if (ctrlOrCmd && e.shiftKey && key === "j") return false;
// 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;
@ -200,7 +202,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
if (e.button === 1) e.preventDefault();
}
function onMouseScroll(e: WheelEvent): void {
function onWheelScroll(e: WheelEvent): void {
const { target } = e;
const isTargetingCanvas = target instanceof Element && target.closest("[data-canvas]");
@ -215,7 +217,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
if (isTargetingCanvas) {
e.preventDefault();
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 { DialogState } from "@/state-providers/dialog";
import { IconName } from "@/utility-functions/icons";
import { browserVersion, operatingSystem } from "@/utility-functions/platform";
import { stripIndents } from "@/utility-functions/strip-indents";
import { Editor } from "@/wasm-communication/editor";
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.
**Browser and OS**
${browserVersion()}, ${operatingSystem()}
${browserVersion()}, ${operatingSystem(true).replace("Unknown", "YOUR OPERATING SYSTEM")}
**Stack Trace**
Copied from the crash dialog in the Graphite Editor:
@ -94,47 +95,3 @@ function githubUrl(panicDetails: string): string {
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 message = stripIndents`
<style>
h2, p, a {
text-align: center;
color: white;
}
h2, p, a { text-align: center; color: white; }
#app { display: none; }
</style>
<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>

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 KeyboardBackspace from "@/../assets/icon-12px-solid/keyboard-backspace.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 KeyboardOption from "@/../assets/icon-12px-solid/keyboard-option.svg";
import KeyboardShift from "@/../assets/icon-12px-solid/keyboard-shift.svg";
@ -52,6 +53,7 @@ const SOLID_12PX = {
KeyboardArrowUp: { component: KeyboardArrowUp, size: 12 },
KeyboardBackspace: { component: KeyboardBackspace, size: 12 },
KeyboardCommand: { component: KeyboardCommand, size: 12 },
KeyboardControl: { component: KeyboardControl, size: 12 },
KeyboardEnter: { component: KeyboardEnter, size: 12 },
KeyboardOption: { component: KeyboardOption, size: 12 },
KeyboardShift: { component: KeyboardShift, size: 12 },

View file

@ -1,5 +1,14 @@
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.
@ -13,7 +22,7 @@ export function getLatinKey(e: KeyboardEvent): string | null {
const key = e.key.toLowerCase();
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;
// 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 class HintInfo {
readonly key_groups!: KeysGroup[];
readonly keyGroups!: KeysGroup[];
readonly keyGroupsMac!: KeysGroup[] | null;
readonly mouse!: MouseMotion | null;
@ -404,12 +406,14 @@ export class ColorInput extends WidgetProps {
tooltip!: string;
}
export type Keys = { keys: string[] };
export interface MenuListEntryData<Value = string> {
value?: Value;
label?: string;
icon?: IconName;
font?: URL;
shortcut?: string[];
shortcut?: Keys;
shortcutRequiresLock?: boolean;
disabled?: boolean;
action?: () => void;
@ -781,7 +785,7 @@ export type MenuColumn = {
};
export type MenuEntry = {
shortcut: string[] | undefined;
shortcut: Keys | undefined;
action: Widget;
label: string;
icon: string | undefined;

View file

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

View file

@ -2,7 +2,7 @@
name = "graphite-wasm"
publish = false
version = "0.0.0"
rust-version = "1.56.0"
rust-version = "1.62.0"
authors = ["Graphite Authors <contact@graphite.rs>"]
edition = "2021"
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 editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION};
use editor::document::utility_types::Platform;
use editor::input::input_preprocessor::ModifierKeys;
use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::message_prelude::*;
@ -105,7 +106,15 @@ impl JsEditorHandle {
// 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);
}
@ -217,13 +226,13 @@ impl JsEditorHandle {
}
/// 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());
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 message = InputPreprocessorMessage::MouseScroll { editor_mouse_state, modifier_keys };
let message = InputPreprocessorMessage::WheelScroll { editor_mouse_state, modifier_keys };
self.dispatch(message);
}
@ -280,7 +289,7 @@ impl JsEditorHandle {
/// A text box was committed
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);
Ok(())
@ -302,7 +311,7 @@ impl JsEditorHandle {
/// A text box was changed
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);
Ok(())

View file

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

View file

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

View file

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