diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index f68d8240..8043ece8 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -34,7 +34,7 @@ impl Dispatcher { if GROUP_MESSAGES.contains(&message.to_discriminant()) && self.messages.contains(&message) { continue; } - log_message(&message); + self.log_message(&message); match message { NoOp => (), Documents(message) => self.documents_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages), @@ -74,18 +74,18 @@ impl Dispatcher { responses: vec![], } } -} -fn log_message(message: &Message) { - use Message::*; - if log::max_level() == log::LevelFilter::Trace - && !(matches!( - message, - InputPreprocessor(_) | Frontend(FrontendMessage::SetCanvasZoom { .. }) | Frontend(FrontendMessage::SetCanvasRotation { .. }) - ) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove")) - { - log::trace!("Message: {:?}", message); - //log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions())); + fn log_message(&self, message: &Message) { + use Message::*; + if log::max_level() == log::LevelFilter::Trace + && !(matches!( + message, + InputPreprocessor(_) | Frontend(FrontendMessage::SetCanvasZoom { .. }) | Frontend(FrontendMessage::SetCanvasRotation { .. }) + ) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove")) + { + log::trace!("Message: {:?}", message); + // log::trace!("Hints: {:?}", self.input_mapper.hints(self.collect_actions())); + } } } diff --git a/editor/src/frontend/frontend_message_handler.rs b/editor/src/frontend/frontend_message_handler.rs index 4bb0fd1e..24f55629 100644 --- a/editor/src/frontend/frontend_message_handler.rs +++ b/editor/src/frontend/frontend_message_handler.rs @@ -1,5 +1,6 @@ use crate::document::layer_panel::{LayerPanelEntry, RawBuffer}; use crate::message_prelude::*; +use crate::misc::HintData; use crate::tool::tool_options::ToolOptions; use crate::Color; use serde::{Deserialize, Serialize}; @@ -11,6 +12,7 @@ pub enum FrontendMessage { SetActiveTool { tool_name: String, tool_options: Option }, SetActiveDocument { document_index: usize }, UpdateOpenDocumentsList { open_documents: Vec<(String, bool)> }, + UpdateInputHints { hint_data: HintData }, DisplayError { title: String, description: String }, DisplayPanic { panic_info: String, title: String, description: String }, DisplayConfirmationToCloseDocument { document_index: usize }, diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 167e7531..ee3fcc55 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -18,6 +18,7 @@ const SHIFT_NUDGE_AMOUNT: f64 = 10.; pub enum InputMapperMessage { PointerMove, MouseScroll, + #[child] KeyUp(Key), #[child] KeyDown(Key), @@ -43,6 +44,7 @@ impl KeyMappingEntries { } None } + fn push(&mut self, entry: MappingEntry) { self.0.push(entry) } @@ -187,7 +189,8 @@ impl Default for Mapping { entry! {action=PenMessage::Confirm, key_down=KeyEscape}, entry! {action=PenMessage::Confirm, key_down=KeyEnter}, // Fill - entry! {action=FillMessage::MouseDown, key_down=Lmb}, + entry! {action=FillMessage::LeftMouseDown, key_down=Lmb}, + entry! {action=FillMessage::RightMouseDown, key_down=Rmb}, // Tool Actions entry! {action=ToolMessage::ActivateTool(ToolType::Select), key_down=KeyV}, entry! {action=ToolMessage::ActivateTool(ToolType::Eyedropper), key_down=KeyI}, diff --git a/editor/src/input/keyboard.rs b/editor/src/input/keyboard.rs index 6fb2b4ec..f0ec9b99 100644 --- a/editor/src/input/keyboard.rs +++ b/editor/src/input/keyboard.rs @@ -16,6 +16,7 @@ pub type KeyStates = BitVector; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Key { UnknownKey, + // MouseKeys Lmb, Rmb, @@ -87,6 +88,20 @@ pub enum Key { NumKeys, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum MouseMotion { + None, + Lmb, + Rmb, + Mmb, + ScrollUp, + ScrollDown, + Drag, + LmbDrag, + RmbDrag, + MmbDrag, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct BitVector([StorageType; LENGTH]); diff --git a/editor/src/misc/hints.rs b/editor/src/misc/hints.rs new file mode 100644 index 00000000..349dbf2f --- /dev/null +++ b/editor/src/misc/hints.rs @@ -0,0 +1,20 @@ +use serde::{Deserialize, Serialize}; + +use crate::input::keyboard::{Key, MouseMotion}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HintData(pub Vec); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HintGroup(pub Vec); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HintInfo { + pub key_groups: Vec, + pub mouse: Option, + pub label: String, + pub plus: bool, // Prepend the "+" symbol indicating that this is a refinement upon a previous entry in the group +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct KeysGroup(pub Vec); // Only use `Key`s that exist on a physical keyboard diff --git a/editor/src/misc/mod.rs b/editor/src/misc/mod.rs index 963324ed..875e813b 100644 --- a/editor/src/misc/mod.rs +++ b/editor/src/misc/mod.rs @@ -2,7 +2,9 @@ pub mod macros; pub mod derivable_custom_traits; mod error; +pub mod hints; pub mod test_utils; pub use error::EditorError; +pub use hints::*; pub use macros::*; diff --git a/editor/src/tool/mod.rs b/editor/src/tool/mod.rs index 570249b9..22bb4b3c 100644 --- a/editor/src/tool/mod.rs +++ b/editor/src/tool/mod.rs @@ -41,6 +41,8 @@ pub trait Fsm { input: &InputPreprocessor, messages: &mut VecDeque, ) -> Self; + + fn update_hints(&self, responses: &mut VecDeque); } #[derive(Debug, Clone)] diff --git a/editor/src/tool/tool_message_handler.rs b/editor/src/tool/tool_message_handler.rs index a281b8f4..7a247bf7 100644 --- a/editor/src/tool/tool_message_handler.rs +++ b/editor/src/tool/tool_message_handler.rs @@ -12,6 +12,7 @@ use std::collections::VecDeque; #[impl_message(Message, Tool)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum ToolMessage { + UpdateHints, ActivateTool(ToolType), SelectPrimaryColor(Color), SelectSecondaryColor(Color), @@ -84,22 +85,28 @@ impl MessageHandler ToolType::Rectangle => Some(RectangleMessage::Abort.into()), ToolType::Ellipse => Some(EllipseMessage::Abort.into()), ToolType::Shape => Some(ShapeMessage::Abort.into()), + ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()), + ToolType::Fill => Some(FillMessage::Abort.into()), _ => None, }; // Send the Abort state transition to the tool - let mut send_message_to_tool = |tool_type, message: ToolMessage| { + let mut send_message_to_tool = |tool_type, message: ToolMessage, update_hints: bool| { if let Some(tool) = tool_data.tools.get_mut(&tool_type) { tool.process_action(message, (document, document_data, input), responses); + + if update_hints { + tool.process_action(ToolMessage::UpdateHints, (document, document_data, input), responses); + } } }; // Send the old and new tools a transition to their FSM Abort states if let Some(tool_message) = reset_message(new_tool) { - send_message_to_tool(new_tool, tool_message); + send_message_to_tool(new_tool, tool_message, true); } if let Some(tool_message) = reset_message(old_tool) { - send_message_to_tool(old_tool, tool_message); + send_message_to_tool(old_tool, tool_message, false); } // Special cases for specific tools @@ -113,7 +120,7 @@ impl MessageHandler // Notify the frontend about the new active tool to be displayed let tool_name = new_tool.to_string(); - let tool_options = self.tool_state.document_tool_data.tool_options.get(&new_tool).map(|tool_options| *tool_options); + let tool_options = self.tool_state.document_tool_data.tool_options.get(&new_tool).copied(); responses.push_back(FrontendMessage::SetActiveTool { tool_name, tool_options }.into()); } SelectedLayersChanged => { diff --git a/editor/src/tool/tools/ellipse.rs b/editor/src/tool/tools/ellipse.rs index 226908e5..1a902980 100644 --- a/editor/src/tool/tools/ellipse.rs +++ b/editor/src/tool/tools/ellipse.rs @@ -1,13 +1,12 @@ -use crate::input::keyboard::Key; -use crate::input::InputPreprocessor; -use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; -use crate::{document::DocumentMessageHandler, message_prelude::*}; +use crate::document::DocumentMessageHandler; +use crate::input::{keyboard::Key, keyboard::MouseMotion, InputPreprocessor}; +use crate::message_prelude::*; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; +use crate::tool::{tools::resize::Resize, DocumentToolData, Fsm, ToolActionHandlerData}; use glam::DAffine2; use graphene::{layers::style, Operation}; use serde::{Deserialize, Serialize}; -use super::resize::*; - #[derive(Default)] pub struct Ellipse { fsm_state: EllipseToolFsmState, @@ -25,13 +24,24 @@ pub enum EllipseMessage { impl<'a> MessageHandler> for Ellipse { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } } + fn actions(&self) -> ActionList { use EllipseToolFsmState::*; match self.fsm_state { Ready => actions!(EllipseMessageDiscriminant; DragStart), - Dragging => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize), + Drawing => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize), } } } @@ -39,7 +49,7 @@ impl<'a> MessageHandler> for Ellipse { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum EllipseToolFsmState { Ready, - Dragging, + Drawing, } impl Default for EllipseToolFsmState { @@ -47,6 +57,7 @@ impl Default for EllipseToolFsmState { EllipseToolFsmState::Ready } } + #[derive(Clone, Debug, Default)] struct EllipseToolData { data: Resize, @@ -85,7 +96,7 @@ impl Fsm for EllipseToolFsmState { .into(), ); - Dragging + Drawing } (state, Resize { center, lock_ratio }) => { if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) { @@ -94,7 +105,7 @@ impl Fsm for EllipseToolFsmState { state } - (Dragging, DragStop) => { + (Drawing, DragStop) => { // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) match shape_data.drag_start == input.mouse.position { true => responses.push_back(DocumentMessage::AbortTransaction.into()), @@ -104,7 +115,7 @@ impl Fsm for EllipseToolFsmState { shape_data.cleanup(); Ready } - (Dragging, Abort) => { + (Drawing, Abort) => { responses.push_back(DocumentMessage::AbortTransaction.into()); shape_data.cleanup(); @@ -116,4 +127,45 @@ impl Fsm for EllipseToolFsmState { self } } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Draw Ellipse"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Constrain Circular"), + plus: true, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: true, + }, + ])]), + EllipseToolFsmState::Drawing => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Constrain Circular"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } diff --git a/editor/src/tool/tools/eyedropper.rs b/editor/src/tool/tools/eyedropper.rs index a68cdb30..359a8d10 100644 --- a/editor/src/tool/tools/eyedropper.rs +++ b/editor/src/tool/tools/eyedropper.rs @@ -1,40 +1,124 @@ use crate::consts::SELECTION_TOLERANCE; +use crate::document::DocumentMessageHandler; +use crate::input::{keyboard::MouseMotion, InputPreprocessor}; use crate::message_prelude::*; -use crate::tool::{ToolActionHandlerData, ToolMessage}; +use crate::misc::{HintData, HintGroup, HintInfo}; +use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolMessage}; use glam::DVec2; use graphene::layers::LayerDataType; use graphene::Quad; use serde::{Deserialize, Serialize}; #[derive(Default)] -pub struct Eyedropper; +pub struct Eyedropper { + fsm_state: EyedropperToolFsmState, + data: EyedropperToolData, +} #[impl_message(Message, ToolMessage, Eyedropper)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum EyedropperMessage { LeftMouseDown, RightMouseDown, + Abort, } impl<'a> MessageHandler> for Eyedropper { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - let mouse_pos = data.2.mouse.position; - let tolerance = DVec2::splat(SELECTION_TOLERANCE); - let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } - if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() { - if let Ok(layer) = data.0.graphene_document.layer(path) { - if let LayerDataType::Shape(s) = &layer.data { - s.style.fill().and_then(|fill| { - fill.color().map(|color| match action { - ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()), - ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()), - _ => {} - }) - }); - } - } + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); } } + advertise_actions!(EyedropperMessageDiscriminant; LeftMouseDown, RightMouseDown); } + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum EyedropperToolFsmState { + Ready, +} + +impl Default for EyedropperToolFsmState { + fn default() -> Self { + EyedropperToolFsmState::Ready + } +} + +#[derive(Clone, Debug, Default)] +struct EyedropperToolData {} + +impl Fsm for EyedropperToolFsmState { + type ToolData = EyedropperToolData; + + fn transition( + self, + event: ToolMessage, + document: &DocumentMessageHandler, + tool_data: &DocumentToolData, + data: &mut Self::ToolData, + input: &InputPreprocessor, + responses: &mut VecDeque, + ) -> Self { + use EyedropperMessage::*; + use EyedropperToolFsmState::*; + if let ToolMessage::Eyedropper(event) = event { + match (self, event) { + (Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => { + let mouse_pos = input.mouse.position; + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); + + if let Some(path) = document.graphene_document.intersects_quad_root(quad).last() { + if let Ok(layer) = document.graphene_document.layer(path) { + if let LayerDataType::Shape(shape) = &layer.data { + if let Some(fill) = shape.style.fill() { + if let Some(color) = fill.color() { + match lmb_or_rmb { + EyedropperMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()), + EyedropperMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()), + _ => {} + } + } + } + } + } + } + + Ready + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Sample to Primary"), + plus: false, + }, + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Rmb), + label: String::from("Sample to Secondary"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } +} diff --git a/editor/src/tool/tools/fill.rs b/editor/src/tool/tools/fill.rs index c33ade5d..ce0765f1 100644 --- a/editor/src/tool/tools/fill.rs +++ b/editor/src/tool/tools/fill.rs @@ -1,34 +1,117 @@ use crate::consts::SELECTION_TOLERANCE; +use crate::document::DocumentMessageHandler; +use crate::input::{keyboard::MouseMotion, InputPreprocessor}; use crate::message_prelude::*; +use crate::misc::{HintData, HintGroup, HintInfo}; use crate::tool::ToolActionHandlerData; +use crate::tool::{DocumentToolData, Fsm, ToolMessage}; use glam::DVec2; use graphene::{Operation, Quad}; use serde::{Deserialize, Serialize}; #[derive(Default)] -pub struct Fill; +pub struct Fill { + fsm_state: FillToolFsmState, + data: FillToolData, +} #[impl_message(Message, ToolMessage, Fill)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum FillMessage { - MouseDown, + LeftMouseDown, + RightMouseDown, + Abort, } impl<'a> MessageHandler> for Fill { - fn process_action(&mut self, _action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - let mouse_pos = data.2.mouse.position; - let tolerance = DVec2::splat(SELECTION_TOLERANCE); - let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } - if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() { - responses.push_back( - Operation::SetLayerFill { - path: path.to_vec(), - color: data.1.primary_color, - } - .into(), - ); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); } } - advertise_actions!(FillMessageDiscriminant; MouseDown); + + advertise_actions!(FillMessageDiscriminant; LeftMouseDown, RightMouseDown); +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum FillToolFsmState { + Ready, +} + +impl Default for FillToolFsmState { + fn default() -> Self { + FillToolFsmState::Ready + } +} + +#[derive(Clone, Debug, Default)] +struct FillToolData {} + +impl Fsm for FillToolFsmState { + type ToolData = FillToolData; + + fn transition( + self, + event: ToolMessage, + document: &DocumentMessageHandler, + tool_data: &DocumentToolData, + data: &mut Self::ToolData, + input: &InputPreprocessor, + responses: &mut VecDeque, + ) -> Self { + use FillMessage::*; + use FillToolFsmState::*; + if let ToolMessage::Fill(event) = event { + match (self, event) { + (Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => { + let mouse_pos = input.mouse.position; + let tolerance = DVec2::splat(SELECTION_TOLERANCE); + let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); + + if let Some(path) = document.graphene_document.intersects_quad_root(quad).last() { + let color = match lmb_or_rmb { + LeftMouseDown => tool_data.primary_color, + RightMouseDown => tool_data.secondary_color, + Abort => unreachable!(), + }; + responses.push_back(Operation::SetLayerFill { path: path.to_vec(), color }.into()); + } + + Ready + } + _ => self, + } + } else { + self + } + } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + FillToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Fill with Primary"), + plus: false, + }, + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Rmb), + label: String::from("Fill with Secondary"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } diff --git a/editor/src/tool/tools/line.rs b/editor/src/tool/tools/line.rs index eeb9160a..b804d102 100644 --- a/editor/src/tool/tools/line.rs +++ b/editor/src/tool/tools/line.rs @@ -1,6 +1,7 @@ use crate::consts::LINE_ROTATE_SNAP_ANGLE; -use crate::input::keyboard::Key; +use crate::input::keyboard::{Key, MouseMotion}; use crate::input::{mouse::ViewportPosition, InputPreprocessor}; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::tool::snapping::SnapHandler; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType}; use crate::{document::DocumentMessageHandler, message_prelude::*}; @@ -25,13 +26,24 @@ pub enum LineMessage { impl<'a> MessageHandler> for Line { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } } + fn actions(&self) -> ActionList { use LineToolFsmState::*; match self.fsm_state { Ready => actions!(LineMessageDiscriminant; DragStart), - Dragging => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort), + Drawing => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort), } } } @@ -39,7 +51,7 @@ impl<'a> MessageHandler> for Line { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum LineToolFsmState { Ready, - Dragging, + Drawing, } impl Default for LineToolFsmState { @@ -96,21 +108,21 @@ impl Fsm for LineToolFsmState { .into(), ); - Dragging + Drawing } - (Dragging, Redraw { center, snap_angle, lock_angle }) => { + (Drawing, Redraw { center, snap_angle, lock_angle }) => { data.drag_current = data.snap_handler.snap_position(document, input.mouse.position); let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect(); responses.push_back(generate_transform(data, values[0], values[1], values[2])); - Dragging + Drawing } - (Dragging, DragStop) => { + (Drawing, DragStop) => { data.drag_current = data.snap_handler.snap_position(document, input.mouse.position); data.snap_handler.cleanup(); - // TODO; introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) + // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) match data.drag_start == input.mouse.position { true => responses.push_back(DocumentMessage::AbortTransaction.into()), false => responses.push_back(DocumentMessage::CommitTransaction.into()), @@ -120,7 +132,7 @@ impl Fsm for LineToolFsmState { Ready } - (Dragging, Abort) => { + (Drawing, Abort) => { data.snap_handler.cleanup(); responses.push_back(DocumentMessage::AbortTransaction.into()); data.path = None; @@ -132,6 +144,59 @@ impl Fsm for LineToolFsmState { self } } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + LineToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Draw Line"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Snap 15°"), + plus: true, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: true, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl])], + mouse: None, + label: String::from("Lock Angle"), + plus: true, + }, + ])]), + LineToolFsmState::Drawing => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Snap 15°"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl])], + mouse: None, + label: String::from("Lock Angle"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } fn generate_transform(data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message { diff --git a/editor/src/tool/tools/navigate.rs b/editor/src/tool/tools/navigate.rs index e888b97a..136b149c 100644 --- a/editor/src/tool/tools/navigate.rs +++ b/editor/src/tool/tools/navigate.rs @@ -15,5 +15,6 @@ impl<'a> MessageHandler> for Navigate { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses); } + advertise_actions!(); } diff --git a/editor/src/tool/tools/path.rs b/editor/src/tool/tools/path.rs index 70ed9956..3f74d894 100644 --- a/editor/src/tool/tools/path.rs +++ b/editor/src/tool/tools/path.rs @@ -3,8 +3,10 @@ use crate::consts::VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE; use crate::document::DocumentMessageHandler; use crate::document::VectorManipulatorSegment; use crate::document::VectorManipulatorShape; +use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessor; use crate::message_prelude::*; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::tool::ToolActionHandlerData; use crate::tool::{DocumentToolData, Fsm}; use glam::{DAffine2, DVec2}; @@ -31,8 +33,19 @@ pub enum PathMessage { impl<'a> MessageHandler> for Path { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } } + fn actions(&self) -> ActionList { use PathToolFsmState::*; match self.fsm_state { @@ -213,6 +226,74 @@ impl Fsm for PathToolFsmState { self } } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + PathToolFsmState::Ready => HintData(vec![ + HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Select Point (coming soon)"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Add/Remove Point"), + plus: true, + }, + ]), + HintGroup(vec![HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Drag Selected (coming soon)"), + plus: false, + }]), + HintGroup(vec![ + HintInfo { + key_groups: vec![ + KeysGroup(vec![Key::KeyArrowUp]), + KeysGroup(vec![Key::KeyArrowRight]), + KeysGroup(vec![Key::KeyArrowDown]), + KeysGroup(vec![Key::KeyArrowLeft]), + ], + mouse: None, + label: String::from("Nudge Selected (coming soon)"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Big Increment Nudge"), + plus: true, + }, + ]), + HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyG])], + mouse: None, + label: String::from("Grab Selected (coming soon)"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyR])], + mouse: None, + label: String::from("Rotate Selected (coming soon)"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyS])], + mouse: None, + label: String::from("Scale Selected (coming soon)"), + plus: false, + }, + ]), + ]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } fn calculate_total_overlays_per_type(shapes_to_draw: &Vec) -> (usize, usize, usize) { diff --git a/editor/src/tool/tools/pen.rs b/editor/src/tool/tools/pen.rs index 56bae86c..e0bb441d 100644 --- a/editor/src/tool/tools/pen.rs +++ b/editor/src/tool/tools/pen.rs @@ -1,4 +1,6 @@ +use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessor; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::tool::snapping::SnapHandler; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType}; use crate::{document::DocumentMessageHandler, message_prelude::*}; @@ -26,18 +28,29 @@ pub enum PenMessage { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum PenToolFsmState { Ready, - Dragging, + Drawing, } impl<'a> MessageHandler> for Pen { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } } + fn actions(&self) -> ActionList { use PenToolFsmState::*; match self.fsm_state { Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort), - Dragging => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort), + Drawing => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort), } } } @@ -92,9 +105,9 @@ impl Fsm for PenToolFsmState { _ => 5, }; - Dragging + Drawing } - (Dragging, DragStop) => { + (Drawing, DragStop) => { let snapped_position = data.snap_handler.snap_position(document, input.mouse.position); let pos = transform.inverse() * DAffine2::from_translation(snapped_position); @@ -106,18 +119,18 @@ impl Fsm for PenToolFsmState { responses.extend(make_operation(data, tool_data, true)); - Dragging + Drawing } - (Dragging, PointerMove) => { + (Drawing, PointerMove) => { let snapped_position = data.snap_handler.snap_position(document, input.mouse.position); let pos = transform.inverse() * DAffine2::from_translation(snapped_position); data.next_point = pos; responses.extend(make_operation(data, tool_data, true)); - Dragging + Drawing } - (Dragging, Confirm) | (Dragging, Abort) => { + (Drawing, Confirm) | (Drawing, Abort) => { if data.points.len() >= 2 { responses.push_back(DocumentMessage::DeselectAllLayers.into()); responses.extend(make_operation(data, tool_data, false)); @@ -138,6 +151,33 @@ impl Fsm for PenToolFsmState { self } } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + PenToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Draw Path"), + plus: false, + }])]), + PenToolFsmState::Drawing => HintData(vec![ + HintGroup(vec![HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Extend Path"), + plus: false, + }]), + HintGroup(vec![HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyEnter])], + mouse: None, + label: String::from("End Path"), + plus: false, + }]), + ]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> [Message; 2] { diff --git a/editor/src/tool/tools/rectangle.rs b/editor/src/tool/tools/rectangle.rs index 1d91440b..f5500054 100644 --- a/editor/src/tool/tools/rectangle.rs +++ b/editor/src/tool/tools/rectangle.rs @@ -1,5 +1,6 @@ -use crate::input::keyboard::Key; +use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessor; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{document::DocumentMessageHandler, message_prelude::*}; use glam::DAffine2; @@ -25,13 +26,24 @@ pub enum RectangleMessage { impl<'a> MessageHandler> for Rectangle { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } } + fn actions(&self) -> ActionList { use RectangleToolFsmState::*; match self.fsm_state { Ready => actions!(RectangleMessageDiscriminant; DragStart), - Dragging => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize), + Drawing => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize), } } } @@ -39,7 +51,7 @@ impl<'a> MessageHandler> for Rectangle { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum RectangleToolFsmState { Ready, - Dragging, + Drawing, } impl Default for RectangleToolFsmState { @@ -85,7 +97,7 @@ impl Fsm for RectangleToolFsmState { .into(), ); - Dragging + Drawing } (state, Resize { center, lock_ratio }) => { if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) { @@ -94,7 +106,7 @@ impl Fsm for RectangleToolFsmState { state } - (Dragging, DragStop) => { + (Drawing, DragStop) => { // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) match shape_data.drag_start == input.mouse.position { true => responses.push_back(DocumentMessage::AbortTransaction.into()), @@ -104,7 +116,7 @@ impl Fsm for RectangleToolFsmState { shape_data.cleanup(); Ready } - (Dragging, Abort) => { + (Drawing, Abort) => { responses.push_back(DocumentMessage::AbortTransaction.into()); shape_data.cleanup(); @@ -116,4 +128,45 @@ impl Fsm for RectangleToolFsmState { self } } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Draw Rectangle"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Constrain Square"), + plus: true, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: true, + }, + ])]), + RectangleToolFsmState::Drawing => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Constrain Square"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } diff --git a/editor/src/tool/tools/select.rs b/editor/src/tool/tools/select.rs index 78870d73..aa47e1b5 100644 --- a/editor/src/tool/tools/select.rs +++ b/editor/src/tool/tools/select.rs @@ -5,10 +5,13 @@ use graphene::Operation; use graphene::Quad; use crate::consts::COLOR_ACCENT; -use crate::input::keyboard::Key; -use crate::input::{mouse::ViewportPosition, InputPreprocessor}; -use crate::tool::snapping::SnapHandler; -use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; +use crate::input::{ + keyboard::{Key, MouseMotion}, + mouse::ViewportPosition, + InputPreprocessor, +}; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; +use crate::tool::{snapping::SnapHandler, DocumentToolData, Fsm, ToolActionHandlerData}; use crate::{ consts::SELECTION_TOLERANCE, document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis}, @@ -39,8 +42,19 @@ pub enum SelectMessage { impl<'a> MessageHandler> for Select { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } } + fn actions(&self) -> ActionList { use SelectToolFsmState::*; match self.fsm_state { @@ -51,7 +65,7 @@ impl<'a> MessageHandler> for Select { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum SelectToolFsmState { Ready, Dragging, @@ -122,6 +136,7 @@ impl Fsm for SelectToolFsmState { ) -> Self { use SelectMessage::*; use SelectToolFsmState::*; + if let ToolMessage::Select(event) = event { match (self, event) { (_, UpdateSelectionBoundingBox) => { @@ -263,4 +278,121 @@ impl Fsm for SelectToolFsmState { self } } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + SelectToolFsmState::Ready => HintData(vec![ + HintGroup(vec![HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Drag Selected"), + plus: false, + }]), + HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyG])], + mouse: None, + label: String::from("Grab Selected"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyR])], + mouse: None, + label: String::from("Rotate Selected"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyS])], + mouse: None, + label: String::from("Scale Selected"), + plus: false, + }, + ]), + HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Select Object"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl])], + mouse: None, + label: String::from("Innermost"), + plus: true, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Grow/Shrink Selection"), + plus: true, + }, + ]), + HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Select Area"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Grow/Shrink Selection"), + plus: true, + }, + ]), + HintGroup(vec![ + HintInfo { + key_groups: vec![ + KeysGroup(vec![Key::KeyArrowUp]), + KeysGroup(vec![Key::KeyArrowRight]), + KeysGroup(vec![Key::KeyArrowDown]), + KeysGroup(vec![Key::KeyArrowLeft]), + ], + mouse: None, + label: String::from("Nudge Selected"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Big Increment Nudge"), + plus: true, + }, + ]), + HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Move Duplicate"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyD])], + mouse: None, + label: String::from("Duplicate"), + plus: false, + }, + ]), + ]), + SelectToolFsmState::Dragging => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Constrain to Axis (coming soon)"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl])], + mouse: None, + label: String::from("Snap to Points (coming soon)"), + plus: false, + }, + ])]), + SelectToolFsmState::DrawingBox => HintData(vec![]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } diff --git a/editor/src/tool/tools/shape.rs b/editor/src/tool/tools/shape.rs index d5281df6..dc668d7e 100644 --- a/editor/src/tool/tools/shape.rs +++ b/editor/src/tool/tools/shape.rs @@ -1,5 +1,6 @@ -use crate::input::keyboard::Key; +use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessor; +use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolOptions, ToolType}; use crate::{document::DocumentMessageHandler, message_prelude::*}; use glam::DAffine2; @@ -25,13 +26,24 @@ pub enum ShapeMessage { impl<'a> MessageHandler> for Shape { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - self.fsm_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if action == ToolMessage::UpdateHints { + self.fsm_state.update_hints(responses); + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + + if self.fsm_state != new_state { + self.fsm_state = new_state; + self.fsm_state.update_hints(responses); + } } + fn actions(&self) -> ActionList { use ShapeToolFsmState::*; match self.fsm_state { Ready => actions!(ShapeMessageDiscriminant; DragStart), - Dragging => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize), + Drawing => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize), } } } @@ -39,7 +51,7 @@ impl<'a> MessageHandler> for Shape { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ShapeToolFsmState { Ready, - Dragging, + Drawing, } impl Default for ShapeToolFsmState { @@ -93,7 +105,7 @@ impl Fsm for ShapeToolFsmState { .into(), ); - Dragging + Drawing } (state, Resize { center, lock_ratio }) => { if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) { @@ -102,7 +114,7 @@ impl Fsm for ShapeToolFsmState { state } - (Dragging, DragStop) => { + (Drawing, DragStop) => { // TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100) match shape_data.drag_start == input.mouse.position { true => responses.push_back(DocumentMessage::AbortTransaction.into()), @@ -112,7 +124,7 @@ impl Fsm for ShapeToolFsmState { shape_data.cleanup(); Ready } - (Dragging, Abort) => { + (Drawing, Abort) => { responses.push_back(DocumentMessage::AbortTransaction.into()); shape_data.cleanup(); @@ -124,4 +136,45 @@ impl Fsm for ShapeToolFsmState { self } } + + fn update_hints(&self, responses: &mut VecDeque) { + let hint_data = match self { + ShapeToolFsmState::Ready => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Draw Shape"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Constrain 1:1 Aspect"), + plus: true, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: true, + }, + ])]), + ShapeToolFsmState::Drawing => HintData(vec![HintGroup(vec![ + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Constrain 1:1 Aspect"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyAlt])], + mouse: None, + label: String::from("From Center"), + plus: false, + }, + ])]), + }; + + responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into()); + } } diff --git a/frontend/assets/12px-solid/keyboard-arrow-down.svg b/frontend/assets/12px-solid/keyboard-arrow-down.svg new file mode 100644 index 00000000..8f269dba --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-arrow-left.svg b/frontend/assets/12px-solid/keyboard-arrow-left.svg new file mode 100644 index 00000000..3d090c9f --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-arrow-right.svg b/frontend/assets/12px-solid/keyboard-arrow-right.svg new file mode 100644 index 00000000..8614c82f --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-arrow-up.svg b/frontend/assets/12px-solid/keyboard-arrow-up.svg new file mode 100644 index 00000000..c9611e55 --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-backspace.svg b/frontend/assets/12px-solid/keyboard-backspace.svg new file mode 100644 index 00000000..e4ae8848 --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-backspace.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/12px-solid/keyboard-command.svg b/frontend/assets/12px-solid/keyboard-command.svg new file mode 100644 index 00000000..e2ea25c3 --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-command.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-enter.svg b/frontend/assets/12px-solid/keyboard-enter.svg new file mode 100644 index 00000000..c9e25d54 --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-enter.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-option.svg b/frontend/assets/12px-solid/keyboard-option.svg new file mode 100644 index 00000000..9d6c246b --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-option.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/12px-solid/keyboard-shift.svg b/frontend/assets/12px-solid/keyboard-shift.svg new file mode 100644 index 00000000..4e8b732a --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-shift.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-space.svg b/frontend/assets/12px-solid/keyboard-space.svg new file mode 100644 index 00000000..1bcb73ea --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-space.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/assets/12px-solid/keyboard-tab.svg b/frontend/assets/12px-solid/keyboard-tab.svg new file mode 100644 index 00000000..77888255 --- /dev/null +++ b/frontend/assets/12px-solid/keyboard-tab.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/assets/16px-two-tone/mouse-hint-rmb.svg b/frontend/assets/16px-two-tone/mouse-hint-rmb.svg index bd4fb8a8..9cf95a94 100644 --- a/frontend/assets/16px-two-tone/mouse-hint-rmb.svg +++ b/frontend/assets/16px-two-tone/mouse-hint-rmb.svg @@ -1,4 +1,4 @@ - - - \ No newline at end of file + + + diff --git a/frontend/src/components/widgets/floating-menus/MenuList.vue b/frontend/src/components/widgets/floating-menus/MenuList.vue index b0ccf90a..d90778c2 100644 --- a/frontend/src/components/widgets/floating-menus/MenuList.vue +++ b/frontend/src/components/widgets/floating-menus/MenuList.vue @@ -78,7 +78,7 @@ .user-input-label { margin: 0; - margin-left: 4px; + margin-left: 16px; } .submenu-arrow { diff --git a/frontend/src/components/widgets/inputs/MenuBarInput.vue b/frontend/src/components/widgets/inputs/MenuBarInput.vue index 24785823..50c19ad1 100644 --- a/frontend/src/components/widgets/inputs/MenuBarInput.vue +++ b/frontend/src/components/widgets/inputs/MenuBarInput.vue @@ -66,13 +66,13 @@ function makeMenuEntries(editor: EditorState): MenuListEntries { ref: undefined, children: [ [ - { label: "New", icon: "File", shortcut: ["Ctrl", "N"], shortcutRequiresLock: true, action: async () => editor.instance.new_document() }, - { label: "Open…", shortcut: ["Ctrl", "O"], action: async () => editor.instance.open_document() }, + { label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: async () => editor.instance.new_document() }, + { label: "Open…", shortcut: ["KeyControl", "KeyO"], action: async () => editor.instance.open_document() }, { label: "Open Recent", - shortcut: ["Ctrl", "⇧", "O"], + shortcut: ["KeyControl", "KeyShift", "KeyO"], children: [ - [{ label: "Reopen Last Closed", shortcut: ["Ctrl", "⇧", "T"], shortcutRequiresLock: true }, { label: "Clear Recently Opened" }], + [{ label: "Reopen Last Closed", shortcut: ["KeyControl", "KeyShift", "KeyT"], shortcutRequiresLock: true }, { label: "Clear Recently Opened" }], [ { label: "Some Recent File.gdd" }, { label: "Another Recent File.gdd" }, @@ -84,20 +84,20 @@ function makeMenuEntries(editor: EditorState): MenuListEntries { }, ], [ - { label: "Close", shortcut: ["Ctrl", "W"], shortcutRequiresLock: true, action: async () => editor.instance.close_active_document_with_confirmation() }, - { label: "Close All", shortcut: ["Ctrl", "Alt", "W"], action: async () => editor.instance.close_all_documents_with_confirmation() }, + { label: "Close", shortcut: ["KeyControl", "KeyW"], shortcutRequiresLock: true, action: async () => editor.instance.close_active_document_with_confirmation() }, + { label: "Close All", shortcut: ["KeyControl", "KeyAlt", "KeyW"], action: async () => editor.instance.close_all_documents_with_confirmation() }, ], [ - { label: "Save", shortcut: ["Ctrl", "S"], action: async () => editor.instance.save_document() }, - { label: "Save As…", shortcut: ["Ctrl", "⇧", "S"], action: async () => editor.instance.save_document() }, - { label: "Save All", shortcut: ["Ctrl", "Alt", "S"] }, + { label: "Save", shortcut: ["KeyControl", "KeyS"], action: async () => editor.instance.save_document() }, + { label: "Save As…", shortcut: ["KeyControl", "KeyShift", "KeyS"], action: async () => editor.instance.save_document() }, + { label: "Save All", shortcut: ["KeyControl", "KeyAlt", "KeyS"] }, { label: "Auto-Save", checkbox: true, checked: true }, ], [ - { label: "Import…", shortcut: ["Ctrl", "I"] }, - { label: "Export…", shortcut: ["Ctrl", "E"], action: async () => editor.instance.export_document() }, + { label: "Import…", shortcut: ["KeyControl", "KeyI"] }, + { label: "Export…", shortcut: ["KeyControl", "KeyE"], action: async () => editor.instance.export_document() }, ], - [{ label: "Quit", shortcut: ["Ctrl", "Q"] }], + [{ label: "Quit", shortcut: ["KeyControl", "KeyQ"] }], ], }, { @@ -105,13 +105,13 @@ function makeMenuEntries(editor: EditorState): MenuListEntries { ref: undefined, children: [ [ - { label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => editor.instance.undo() }, - { label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => editor.instance.redo() }, + { label: "Undo", shortcut: ["KeyControl", "KeyZ"], action: async () => editor.instance.undo() }, + { label: "Redo", shortcut: ["KeyControl", "KeyShift", "KeyZ"], action: async () => editor.instance.redo() }, ], [ - { label: "Cut", shortcut: ["Ctrl", "X"] }, - { label: "Copy", icon: "Copy", shortcut: ["Ctrl", "C"] }, - { label: "Paste", icon: "Paste", shortcut: ["Ctrl", "V"] }, + { label: "Cut", shortcut: ["KeyControl", "KeyX"] }, + { label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"] }, + { label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"] }, ], ], }, @@ -120,16 +120,24 @@ function makeMenuEntries(editor: EditorState): MenuListEntries { ref: undefined, children: [ [ - { label: "Select All", shortcut: ["Ctrl", "A"], action: async () => editor.instance.select_all_layers() }, - { label: "Deselect All", shortcut: ["Ctrl", "Alt", "A"], action: async () => editor.instance.deselect_all_layers() }, + { label: "Select All", shortcut: ["KeyControl", "KeyA"], action: async () => editor.instance.select_all_layers() }, + { label: "Deselect All", shortcut: ["KeyControl", "KeyAlt", "KeyA"], action: async () => editor.instance.deselect_all_layers() }, { label: "Order", children: [ [ - { label: "Raise To Front", shortcut: ["Ctrl", "Shift", "]"], action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_max()) }, - { label: "Raise", shortcut: ["Ctrl", "]"], action: async () => editor.instance.reorder_selected_layers(1) }, - { label: "Lower", shortcut: ["Ctrl", "["], action: async () => editor.instance.reorder_selected_layers(-1) }, - { label: "Lower to Back", shortcut: ["Ctrl", "Shift", "["], action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()) }, + { + label: "Raise To Front", + shortcut: ["KeyControl", "KeyShift", "KeyLeftBracket"], + action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_max()), + }, + { label: "Raise", shortcut: ["KeyControl", "KeyRightBracket"], action: async () => editor.instance.reorder_selected_layers(1) }, + { label: "Lower", shortcut: ["KeyControl", "KeyLeftBracket"], action: async () => editor.instance.reorder_selected_layers(-1) }, + { + label: "Lower to Back", + shortcut: ["KeyControl", "KeyShift", "KeyRightBracket"], + action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()), + }, ], ], }, diff --git a/frontend/src/components/widgets/labels/IconLabel.vue b/frontend/src/components/widgets/labels/IconLabel.vue index 208aaf81..101490f3 100644 --- a/frontend/src/components/widgets/labels/IconLabel.vue +++ b/frontend/src/components/widgets/labels/IconLabel.vue @@ -99,17 +99,28 @@ import WindowButtonWinMinimize from "@/../assets/12px-solid/window-button-win-mi import WindowButtonWinMaximize from "@/../assets/12px-solid/window-button-win-maximize.svg"; import WindowButtonWinRestoreDown from "@/../assets/12px-solid/window-button-win-restore-down.svg"; import WindowButtonWinClose from "@/../assets/12px-solid/window-button-win-close.svg"; +import KeyboardArrowUp from "@/../assets/12px-solid/keyboard-arrow-up.svg"; +import KeyboardArrowRight from "@/../assets/12px-solid/keyboard-arrow-right.svg"; +import KeyboardArrowDown from "@/../assets/12px-solid/keyboard-arrow-down.svg"; +import KeyboardArrowLeft from "@/../assets/12px-solid/keyboard-arrow-left.svg"; +import KeyboardBackspace from "@/../assets/12px-solid/keyboard-backspace.svg"; +import KeyboardCommand from "@/../assets/12px-solid/keyboard-command.svg"; +import KeyboardEnter from "@/../assets/12px-solid/keyboard-enter.svg"; +import KeyboardOption from "@/../assets/12px-solid/keyboard-option.svg"; +import KeyboardShift from "@/../assets/12px-solid/keyboard-shift.svg"; +import KeyboardSpace from "@/../assets/12px-solid/keyboard-space.svg"; +import KeyboardTab from "@/../assets/12px-solid/keyboard-tab.svg"; import MouseHintNone from "@/../assets/16px-two-tone/mouse-hint-none.svg"; -import MouseHintLMB from "@/../assets/16px-two-tone/mouse-hint-lmb.svg"; -import MouseHintRMB from "@/../assets/16px-two-tone/mouse-hint-rmb.svg"; -import MouseHintMMB from "@/../assets/16px-two-tone/mouse-hint-mmb.svg"; +import MouseHintLmb from "@/../assets/16px-two-tone/mouse-hint-lmb.svg"; +import MouseHintRmb from "@/../assets/16px-two-tone/mouse-hint-rmb.svg"; +import MouseHintMmb from "@/../assets/16px-two-tone/mouse-hint-mmb.svg"; import MouseHintScrollUp from "@/../assets/16px-two-tone/mouse-hint-scroll-up.svg"; import MouseHintScrollDown from "@/../assets/16px-two-tone/mouse-hint-scroll-down.svg"; import MouseHintDrag from "@/../assets/16px-two-tone/mouse-hint-drag.svg"; -import MouseHintLMBDrag from "@/../assets/16px-two-tone/mouse-hint-lmb-drag.svg"; -import MouseHintRMBDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg"; -import MouseHintMMBDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-drag.svg"; +import MouseHintLmbDrag from "@/../assets/16px-two-tone/mouse-hint-lmb-drag.svg"; +import MouseHintRmbDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg"; +import MouseHintMmbDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-drag.svg"; import NodeTypePath from "@/../assets/24px-full-color/node-type-path.svg"; import NodeTypeFolder from "@/../assets/24px-full-color/node-type-folder.svg"; @@ -182,16 +193,27 @@ const icons = { WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 }, WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 }, WindowButtonWinClose: { component: WindowButtonWinClose, size: 12 }, + KeyboardArrowUp: { component: KeyboardArrowUp, size: 12 }, + KeyboardArrowRight: { component: KeyboardArrowRight, size: 12 }, + KeyboardArrowDown: { component: KeyboardArrowDown, size: 12 }, + KeyboardArrowLeft: { component: KeyboardArrowLeft, size: 12 }, + KeyboardBackspace: { component: KeyboardBackspace, size: 12 }, + KeyboardCommand: { component: KeyboardCommand, size: 12 }, + KeyboardEnter: { component: KeyboardEnter, size: 12 }, + KeyboardOption: { component: KeyboardOption, size: 12 }, + KeyboardShift: { component: KeyboardShift, size: 12 }, + KeyboardSpace: { component: KeyboardSpace, size: 12 }, + KeyboardTab: { component: KeyboardTab, size: 12 }, MouseHintNone: { component: MouseHintNone, size: 16 }, - MouseHintLMB: { component: MouseHintLMB, size: 16 }, - MouseHintRMB: { component: MouseHintRMB, size: 16 }, - MouseHintMMB: { component: MouseHintMMB, size: 16 }, + MouseHintLmb: { component: MouseHintLmb, size: 16 }, + MouseHintRmb: { component: MouseHintRmb, size: 16 }, + MouseHintMmb: { component: MouseHintMmb, size: 16 }, MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 }, MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 }, MouseHintDrag: { component: MouseHintDrag, size: 16 }, - MouseHintLMBDrag: { component: MouseHintLMBDrag, size: 16 }, - MouseHintRMBDrag: { component: MouseHintRMBDrag, size: 16 }, - MouseHintMMBDrag: { component: MouseHintMMBDrag, size: 16 }, + MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 }, + MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 }, + MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 }, NodeTypePath: { component: NodeTypePath, size: 24 }, NodeTypeFolder: { component: NodeTypeFolder, size: 24 }, }; diff --git a/frontend/src/components/widgets/labels/UserInputLabel.vue b/frontend/src/components/widgets/labels/UserInputLabel.vue index b3baedd8..0a900639 100644 --- a/frontend/src/components/widgets/labels/UserInputLabel.vue +++ b/frontend/src/components/widgets/labels/UserInputLabel.vue @@ -2,12 +2,15 @@
- + @@ -46,27 +49,33 @@ border-color: var(--color-7-middlegray); border-radius: 4px; height: 16px; - line-height: 16px; - } + // Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height, so moving it up 1 pixel with 15px makes them agree + line-height: 15px; - .input-key.width-16 { - width: 16px; - } + &.width-16 { + width: 16px; + } - .input-key.width-24 { - width: 24px; - } + &.width-24 { + width: 24px; + } - .input-key.width-32 { - width: 32px; - } + &.width-32 { + width: 32px; + } - .input-key.width-40 { - width: 40px; - } + &.width-40 { + width: 40px; + } - .input-key.width-48 { - width: 48px; + &.width-48 { + width: 48px; + } + + .icon-label { + margin: 1px; + display: inline-block; + } } .input-mouse { @@ -92,15 +101,15 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue"; export enum MouseInputInteraction { "None" = "None", - "LMB" = "LMB", - "RMB" = "RMB", - "MMB" = "MMB", + "Lmb" = "Lmb", + "Rmb" = "Rmb", + "Mmb" = "Mmb", "ScrollUp" = "ScrollUp", "ScrollDown" = "ScrollDown", "Drag" = "Drag", - "LMBDrag" = "LMBDrag", - "RMBDrag" = "RMBDrag", - "MMBDrag" = "MMBDrag", + "LmbDrag" = "LmbDrag", + "RmbDrag" = "RmbDrag", + "MmbDrag" = "MmbDrag", } export default defineComponent({ @@ -115,29 +124,89 @@ export default defineComponent({ }, }, methods: { - keyCapWidth(keyText: string) { - return `width-${keyText.length * 8 + 8}`; + keyTextOrIcon(keyText: string): { text: string | null; icon: string | null; width: string } { + // Definitions + const textMap: Record = { + Control: "Ctrl", + Alt: "Alt", + Delete: "Del", + PageUp: "PgUp", + PageDown: "PgDn", + Equals: "=", + Minus: "-", + Plus: "+", + Escape: "Esc", + Comma: ",", + Period: ".", + LeftBracket: "[", + RightBracket: "]", + LeftCurlyBracket: "{", + RightCurlyBracket: "}", + }; + const iconsAndWidths: Record = { + ArrowUp: 1, + ArrowRight: 1, + ArrowDown: 1, + ArrowLeft: 1, + Backspace: 2, + Command: 2, + Enter: 2, + Option: 2, + Shift: 2, + Tab: 2, + Space: 3, + }; + + // Strip off the "Key" prefix + const text = keyText.replace(/^(?:Key)?(.*)$/, "$1"); + + // If it's an icon, return the icon identifier + if (text in iconsAndWidths) { + return { + text: null, + icon: `Keyboard${text}`, + width: `width-${iconsAndWidths[text] * 8 + 8}`, + }; + } + + // Otherwise, return the text string + let result; + + // Letters and numbers + if (/^[A-Z0-9]$/.test(text)) { + result = text; + } + // Abbreviated names + else if (text in textMap) { + result = textMap[text]; + } + // Other + else { + result = text; + } + + return { text: result, icon: null, width: `width-${(result || " ").length * 8 + 8}` }; }, - mouseInputInteractionToIcon(mouseInputInteraction: MouseInputInteraction) { + mouseMovementIcon(mouseInputInteraction: MouseInputInteraction) { switch (mouseInputInteraction) { - case MouseInputInteraction.LMB: - return "MouseHintLMB"; - case MouseInputInteraction.RMB: - return "MouseHintRMB"; - case MouseInputInteraction.MMB: - return "MouseHintMMB"; + case MouseInputInteraction.Lmb: + return "MouseHintLmb"; + case MouseInputInteraction.Rmb: + return "MouseHintRmb"; + case MouseInputInteraction.Mmb: + return "MouseHintMmb"; case MouseInputInteraction.ScrollUp: return "MouseHintScrollUp"; case MouseInputInteraction.ScrollDown: return "MouseHintScrollDown"; case MouseInputInteraction.Drag: return "MouseHintDrag"; - case MouseInputInteraction.LMBDrag: - return "MouseHintLMBDrag"; - case MouseInputInteraction.RMBDrag: - return "MouseHintRMBDrag"; - case MouseInputInteraction.MMBDrag: - return "MouseHintMMBDrag"; + case MouseInputInteraction.LmbDrag: + return "MouseHintLmbDrag"; + case MouseInputInteraction.RmbDrag: + return "MouseHintRmbDrag"; + case MouseInputInteraction.MmbDrag: + return "MouseHintMmbDrag"; default: case MouseInputInteraction.None: return "MouseHintNone"; diff --git a/frontend/src/components/window/status-bar/StatusBar.vue b/frontend/src/components/window/status-bar/StatusBar.vue index 2313f1e1..62b0b42f 100644 --- a/frontend/src/components/window/status-bar/StatusBar.vue +++ b/frontend/src/components/window/status-bar/StatusBar.vue @@ -1,43 +1,22 @@