Implement input hints based on the active tool state (#388)
* Hook up user input hints to display in the frontend status bar Closes #171 * MVP hint system based on tool FSM * Fix hints for Fill and Eyedropper tools * Add icons for keyboard shortcuts * Fix hints for Pen Tool * Cleanup
|
|
@ -34,7 +34,7 @@ impl Dispatcher {
|
||||||
if GROUP_MESSAGES.contains(&message.to_discriminant()) && self.messages.contains(&message) {
|
if GROUP_MESSAGES.contains(&message.to_discriminant()) && self.messages.contains(&message) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
log_message(&message);
|
self.log_message(&message);
|
||||||
match message {
|
match message {
|
||||||
NoOp => (),
|
NoOp => (),
|
||||||
Documents(message) => self.documents_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages),
|
Documents(message) => self.documents_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages),
|
||||||
|
|
@ -74,18 +74,18 @@ impl Dispatcher {
|
||||||
responses: vec![],
|
responses: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fn log_message(message: &Message) {
|
fn log_message(&self, message: &Message) {
|
||||||
use Message::*;
|
use Message::*;
|
||||||
if log::max_level() == log::LevelFilter::Trace
|
if log::max_level() == log::LevelFilter::Trace
|
||||||
&& !(matches!(
|
&& !(matches!(
|
||||||
message,
|
message,
|
||||||
InputPreprocessor(_) | Frontend(FrontendMessage::SetCanvasZoom { .. }) | Frontend(FrontendMessage::SetCanvasRotation { .. })
|
InputPreprocessor(_) | Frontend(FrontendMessage::SetCanvasZoom { .. }) | Frontend(FrontendMessage::SetCanvasRotation { .. })
|
||||||
) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove"))
|
) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove"))
|
||||||
{
|
{
|
||||||
log::trace!("Message: {:?}", message);
|
log::trace!("Message: {:?}", message);
|
||||||
//log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions()));
|
// log::trace!("Hints: {:?}", self.input_mapper.hints(self.collect_actions()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::document::layer_panel::{LayerPanelEntry, RawBuffer};
|
use crate::document::layer_panel::{LayerPanelEntry, RawBuffer};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
use crate::misc::HintData;
|
||||||
use crate::tool::tool_options::ToolOptions;
|
use crate::tool::tool_options::ToolOptions;
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -11,6 +12,7 @@ pub enum FrontendMessage {
|
||||||
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
|
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
|
||||||
SetActiveDocument { document_index: usize },
|
SetActiveDocument { document_index: usize },
|
||||||
UpdateOpenDocumentsList { open_documents: Vec<(String, bool)> },
|
UpdateOpenDocumentsList { open_documents: Vec<(String, bool)> },
|
||||||
|
UpdateInputHints { hint_data: HintData },
|
||||||
DisplayError { title: String, description: String },
|
DisplayError { title: String, description: String },
|
||||||
DisplayPanic { panic_info: String, title: String, description: String },
|
DisplayPanic { panic_info: String, title: String, description: String },
|
||||||
DisplayConfirmationToCloseDocument { document_index: usize },
|
DisplayConfirmationToCloseDocument { document_index: usize },
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ const SHIFT_NUDGE_AMOUNT: f64 = 10.;
|
||||||
pub enum InputMapperMessage {
|
pub enum InputMapperMessage {
|
||||||
PointerMove,
|
PointerMove,
|
||||||
MouseScroll,
|
MouseScroll,
|
||||||
|
#[child]
|
||||||
KeyUp(Key),
|
KeyUp(Key),
|
||||||
#[child]
|
#[child]
|
||||||
KeyDown(Key),
|
KeyDown(Key),
|
||||||
|
|
@ -43,6 +44,7 @@ impl KeyMappingEntries {
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, entry: MappingEntry) {
|
fn push(&mut self, entry: MappingEntry) {
|
||||||
self.0.push(entry)
|
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=KeyEscape},
|
||||||
entry! {action=PenMessage::Confirm, key_down=KeyEnter},
|
entry! {action=PenMessage::Confirm, key_down=KeyEnter},
|
||||||
// Fill
|
// Fill
|
||||||
entry! {action=FillMessage::MouseDown, key_down=Lmb},
|
entry! {action=FillMessage::LeftMouseDown, key_down=Lmb},
|
||||||
|
entry! {action=FillMessage::RightMouseDown, key_down=Rmb},
|
||||||
// Tool Actions
|
// Tool Actions
|
||||||
entry! {action=ToolMessage::ActivateTool(ToolType::Select), key_down=KeyV},
|
entry! {action=ToolMessage::ActivateTool(ToolType::Select), key_down=KeyV},
|
||||||
entry! {action=ToolMessage::ActivateTool(ToolType::Eyedropper), key_down=KeyI},
|
entry! {action=ToolMessage::ActivateTool(ToolType::Eyedropper), key_down=KeyI},
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ pub type KeyStates = BitVector<KEY_MASK_STORAGE_LENGTH>;
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
UnknownKey,
|
UnknownKey,
|
||||||
|
|
||||||
// MouseKeys
|
// MouseKeys
|
||||||
Lmb,
|
Lmb,
|
||||||
Rmb,
|
Rmb,
|
||||||
|
|
@ -87,6 +88,20 @@ pub enum Key {
|
||||||
NumKeys,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct BitVector<const LENGTH: usize>([StorageType; LENGTH]);
|
pub struct BitVector<const LENGTH: usize>([StorageType; LENGTH]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<HintGroup>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct HintGroup(pub Vec<HintInfo>);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct HintInfo {
|
||||||
|
pub key_groups: Vec<KeysGroup>,
|
||||||
|
pub mouse: Option<MouseMotion>,
|
||||||
|
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<Key>); // Only use `Key`s that exist on a physical keyboard
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
pub mod macros;
|
pub mod macros;
|
||||||
pub mod derivable_custom_traits;
|
pub mod derivable_custom_traits;
|
||||||
mod error;
|
mod error;
|
||||||
|
pub mod hints;
|
||||||
pub mod test_utils;
|
pub mod test_utils;
|
||||||
|
|
||||||
pub use error::EditorError;
|
pub use error::EditorError;
|
||||||
|
pub use hints::*;
|
||||||
pub use macros::*;
|
pub use macros::*;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ pub trait Fsm {
|
||||||
input: &InputPreprocessor,
|
input: &InputPreprocessor,
|
||||||
messages: &mut VecDeque<Message>,
|
messages: &mut VecDeque<Message>,
|
||||||
) -> Self;
|
) -> Self;
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use std::collections::VecDeque;
|
||||||
#[impl_message(Message, Tool)]
|
#[impl_message(Message, Tool)]
|
||||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum ToolMessage {
|
pub enum ToolMessage {
|
||||||
|
UpdateHints,
|
||||||
ActivateTool(ToolType),
|
ActivateTool(ToolType),
|
||||||
SelectPrimaryColor(Color),
|
SelectPrimaryColor(Color),
|
||||||
SelectSecondaryColor(Color),
|
SelectSecondaryColor(Color),
|
||||||
|
|
@ -84,22 +85,28 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
|
||||||
ToolType::Rectangle => Some(RectangleMessage::Abort.into()),
|
ToolType::Rectangle => Some(RectangleMessage::Abort.into()),
|
||||||
ToolType::Ellipse => Some(EllipseMessage::Abort.into()),
|
ToolType::Ellipse => Some(EllipseMessage::Abort.into()),
|
||||||
ToolType::Shape => Some(ShapeMessage::Abort.into()),
|
ToolType::Shape => Some(ShapeMessage::Abort.into()),
|
||||||
|
ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()),
|
||||||
|
ToolType::Fill => Some(FillMessage::Abort.into()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the Abort state transition to the tool
|
// 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) {
|
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
|
||||||
tool.process_action(message, (document, document_data, input), responses);
|
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
|
// Send the old and new tools a transition to their FSM Abort states
|
||||||
if let Some(tool_message) = reset_message(new_tool) {
|
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) {
|
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
|
// Special cases for specific tools
|
||||||
|
|
@ -113,7 +120,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
|
||||||
|
|
||||||
// Notify the frontend about the new active tool to be displayed
|
// Notify the frontend about the new active tool to be displayed
|
||||||
let tool_name = new_tool.to_string();
|
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());
|
responses.push_back(FrontendMessage::SetActiveTool { tool_name, tool_options }.into());
|
||||||
}
|
}
|
||||||
SelectedLayersChanged => {
|
SelectedLayersChanged => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
use crate::input::keyboard::Key;
|
use crate::document::DocumentMessageHandler;
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::{keyboard::Key, keyboard::MouseMotion, InputPreprocessor};
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
use crate::message_prelude::*;
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
|
use crate::tool::{tools::resize::Resize, DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
use graphene::{layers::style, Operation};
|
use graphene::{layers::style, Operation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::resize::*;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Ellipse {
|
pub struct Ellipse {
|
||||||
fsm_state: EllipseToolFsmState,
|
fsm_state: EllipseToolFsmState,
|
||||||
|
|
@ -25,13 +24,24 @@ pub enum EllipseMessage {
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
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 {
|
fn actions(&self) -> ActionList {
|
||||||
use EllipseToolFsmState::*;
|
use EllipseToolFsmState::*;
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(EllipseMessageDiscriminant; DragStart),
|
Ready => actions!(EllipseMessageDiscriminant; DragStart),
|
||||||
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
|
Drawing => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +49,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum EllipseToolFsmState {
|
enum EllipseToolFsmState {
|
||||||
Ready,
|
Ready,
|
||||||
Dragging,
|
Drawing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for EllipseToolFsmState {
|
impl Default for EllipseToolFsmState {
|
||||||
|
|
@ -47,6 +57,7 @@ impl Default for EllipseToolFsmState {
|
||||||
EllipseToolFsmState::Ready
|
EllipseToolFsmState::Ready
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct EllipseToolData {
|
struct EllipseToolData {
|
||||||
data: Resize,
|
data: Resize,
|
||||||
|
|
@ -85,7 +96,7 @@ impl Fsm for EllipseToolFsmState {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Dragging
|
Drawing
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(state, Resize { center, lock_ratio }) => {
|
||||||
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
|
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
|
||||||
|
|
@ -94,7 +105,7 @@ impl Fsm for EllipseToolFsmState {
|
||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
(Dragging, DragStop) => {
|
(Drawing, DragStop) => {
|
||||||
// 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 shape_data.drag_start == input.mouse.position {
|
match shape_data.drag_start == input.mouse.position {
|
||||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||||
|
|
@ -104,7 +115,7 @@ impl Fsm for EllipseToolFsmState {
|
||||||
shape_data.cleanup();
|
shape_data.cleanup();
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Drawing, Abort) => {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
shape_data.cleanup();
|
shape_data.cleanup();
|
||||||
|
|
||||||
|
|
@ -116,4 +127,45 @@ impl Fsm for EllipseToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,124 @@
|
||||||
use crate::consts::SELECTION_TOLERANCE;
|
use crate::consts::SELECTION_TOLERANCE;
|
||||||
|
use crate::document::DocumentMessageHandler;
|
||||||
|
use crate::input::{keyboard::MouseMotion, InputPreprocessor};
|
||||||
use crate::message_prelude::*;
|
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 glam::DVec2;
|
||||||
use graphene::layers::LayerDataType;
|
use graphene::layers::LayerDataType;
|
||||||
use graphene::Quad;
|
use graphene::Quad;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Eyedropper;
|
pub struct Eyedropper {
|
||||||
|
fsm_state: EyedropperToolFsmState,
|
||||||
|
data: EyedropperToolData,
|
||||||
|
}
|
||||||
|
|
||||||
#[impl_message(Message, ToolMessage, Eyedropper)]
|
#[impl_message(Message, ToolMessage, Eyedropper)]
|
||||||
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||||
pub enum EyedropperMessage {
|
pub enum EyedropperMessage {
|
||||||
LeftMouseDown,
|
LeftMouseDown,
|
||||||
RightMouseDown,
|
RightMouseDown,
|
||||||
|
Abort,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
let mouse_pos = data.2.mouse.position;
|
if action == ToolMessage::UpdateHints {
|
||||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
self.fsm_state.update_hints(responses);
|
||||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
|
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
|
||||||
if let Ok(layer) = data.0.graphene_document.layer(path) {
|
|
||||||
if let LayerDataType::Shape(s) = &layer.data {
|
if self.fsm_state != new_state {
|
||||||
s.style.fill().and_then(|fill| {
|
self.fsm_state = new_state;
|
||||||
fill.color().map(|color| match action {
|
self.fsm_state.update_hints(responses);
|
||||||
ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
|
|
||||||
ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
|
|
||||||
_ => {}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_actions!(EyedropperMessageDiscriminant; LeftMouseDown, RightMouseDown);
|
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<Message>,
|
||||||
|
) -> 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<Message>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,117 @@
|
||||||
use crate::consts::SELECTION_TOLERANCE;
|
use crate::consts::SELECTION_TOLERANCE;
|
||||||
|
use crate::document::DocumentMessageHandler;
|
||||||
|
use crate::input::{keyboard::MouseMotion, InputPreprocessor};
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
use crate::misc::{HintData, HintGroup, HintInfo};
|
||||||
use crate::tool::ToolActionHandlerData;
|
use crate::tool::ToolActionHandlerData;
|
||||||
|
use crate::tool::{DocumentToolData, Fsm, ToolMessage};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use graphene::{Operation, Quad};
|
use graphene::{Operation, Quad};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Fill;
|
pub struct Fill {
|
||||||
|
fsm_state: FillToolFsmState,
|
||||||
|
data: FillToolData,
|
||||||
|
}
|
||||||
|
|
||||||
#[impl_message(Message, ToolMessage, Fill)]
|
#[impl_message(Message, ToolMessage, Fill)]
|
||||||
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||||
pub enum FillMessage {
|
pub enum FillMessage {
|
||||||
MouseDown,
|
LeftMouseDown,
|
||||||
|
RightMouseDown,
|
||||||
|
Abort,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
|
||||||
fn process_action(&mut self, _action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
let mouse_pos = data.2.mouse.position;
|
if action == ToolMessage::UpdateHints {
|
||||||
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
self.fsm_state.update_hints(responses);
|
||||||
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
|
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
|
||||||
responses.push_back(
|
|
||||||
Operation::SetLayerFill {
|
if self.fsm_state != new_state {
|
||||||
path: path.to_vec(),
|
self.fsm_state = new_state;
|
||||||
color: data.1.primary_color,
|
self.fsm_state.update_hints(responses);
|
||||||
}
|
|
||||||
.into(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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<Message>,
|
||||||
|
) -> 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<Message>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
|
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::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||||
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::tool::snapping::SnapHandler;
|
use crate::tool::snapping::SnapHandler;
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||||
|
|
@ -25,13 +26,24 @@ pub enum LineMessage {
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
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 {
|
fn actions(&self) -> ActionList {
|
||||||
use LineToolFsmState::*;
|
use LineToolFsmState::*;
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(LineMessageDiscriminant; DragStart),
|
Ready => actions!(LineMessageDiscriminant; DragStart),
|
||||||
Dragging => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
|
Drawing => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum LineToolFsmState {
|
enum LineToolFsmState {
|
||||||
Ready,
|
Ready,
|
||||||
Dragging,
|
Drawing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LineToolFsmState {
|
impl Default for LineToolFsmState {
|
||||||
|
|
@ -96,21 +108,21 @@ impl Fsm for LineToolFsmState {
|
||||||
.into(),
|
.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);
|
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();
|
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]));
|
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.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
|
||||||
data.snap_handler.cleanup();
|
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 {
|
match data.drag_start == input.mouse.position {
|
||||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||||
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
|
||||||
|
|
@ -120,7 +132,7 @@ impl Fsm for LineToolFsmState {
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Drawing, Abort) => {
|
||||||
data.snap_handler.cleanup();
|
data.snap_handler.cleanup();
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
data.path = None;
|
data.path = None;
|
||||||
|
|
@ -132,6 +144,59 @@ impl Fsm for LineToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
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 {
|
fn generate_transform(data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {
|
||||||
|
|
|
||||||
|
|
@ -15,5 +15,6 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
|
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_actions!();
|
advertise_actions!();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ use crate::consts::VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE;
|
||||||
use crate::document::DocumentMessageHandler;
|
use crate::document::DocumentMessageHandler;
|
||||||
use crate::document::VectorManipulatorSegment;
|
use crate::document::VectorManipulatorSegment;
|
||||||
use crate::document::VectorManipulatorShape;
|
use crate::document::VectorManipulatorShape;
|
||||||
|
use crate::input::keyboard::{Key, MouseMotion};
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::InputPreprocessor;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::tool::ToolActionHandlerData;
|
use crate::tool::ToolActionHandlerData;
|
||||||
use crate::tool::{DocumentToolData, Fsm};
|
use crate::tool::{DocumentToolData, Fsm};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
@ -31,8 +33,19 @@ pub enum PathMessage {
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
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 {
|
fn actions(&self) -> ActionList {
|
||||||
use PathToolFsmState::*;
|
use PathToolFsmState::*;
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
|
|
@ -213,6 +226,74 @@ impl Fsm for PathToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
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<VectorManipulatorShape>) -> (usize, usize, usize) {
|
fn calculate_total_overlays_per_type(shapes_to_draw: &Vec<VectorManipulatorShape>) -> (usize, usize, usize) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
use crate::input::keyboard::{Key, MouseMotion};
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::InputPreprocessor;
|
||||||
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::tool::snapping::SnapHandler;
|
use crate::tool::snapping::SnapHandler;
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||||
|
|
@ -26,18 +28,29 @@ pub enum PenMessage {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum PenToolFsmState {
|
enum PenToolFsmState {
|
||||||
Ready,
|
Ready,
|
||||||
Dragging,
|
Drawing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
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 {
|
fn actions(&self) -> ActionList {
|
||||||
use PenToolFsmState::*;
|
use PenToolFsmState::*;
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
|
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,
|
_ => 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
Dragging
|
Drawing
|
||||||
}
|
}
|
||||||
(Dragging, DragStop) => {
|
(Drawing, DragStop) => {
|
||||||
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
|
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
|
||||||
let pos = transform.inverse() * DAffine2::from_translation(snapped_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));
|
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 snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
|
||||||
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
|
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
|
||||||
data.next_point = pos;
|
data.next_point = pos;
|
||||||
|
|
||||||
responses.extend(make_operation(data, tool_data, true));
|
responses.extend(make_operation(data, tool_data, true));
|
||||||
|
|
||||||
Dragging
|
Drawing
|
||||||
}
|
}
|
||||||
(Dragging, Confirm) | (Dragging, Abort) => {
|
(Drawing, Confirm) | (Drawing, Abort) => {
|
||||||
if data.points.len() >= 2 {
|
if data.points.len() >= 2 {
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
responses.extend(make_operation(data, tool_data, false));
|
responses.extend(make_operation(data, tool_data, false));
|
||||||
|
|
@ -138,6 +151,33 @@ impl Fsm for PenToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
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] {
|
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> [Message; 2] {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::input::keyboard::Key;
|
use crate::input::keyboard::{Key, MouseMotion};
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::InputPreprocessor;
|
||||||
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
|
|
@ -25,13 +26,24 @@ pub enum RectangleMessage {
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
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 {
|
fn actions(&self) -> ActionList {
|
||||||
use RectangleToolFsmState::*;
|
use RectangleToolFsmState::*;
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(RectangleMessageDiscriminant; DragStart),
|
Ready => actions!(RectangleMessageDiscriminant; DragStart),
|
||||||
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
|
Drawing => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum RectangleToolFsmState {
|
enum RectangleToolFsmState {
|
||||||
Ready,
|
Ready,
|
||||||
Dragging,
|
Drawing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RectangleToolFsmState {
|
impl Default for RectangleToolFsmState {
|
||||||
|
|
@ -85,7 +97,7 @@ impl Fsm for RectangleToolFsmState {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Dragging
|
Drawing
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(state, Resize { center, lock_ratio }) => {
|
||||||
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
|
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
|
||||||
|
|
@ -94,7 +106,7 @@ impl Fsm for RectangleToolFsmState {
|
||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
(Dragging, DragStop) => {
|
(Drawing, DragStop) => {
|
||||||
// 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 shape_data.drag_start == input.mouse.position {
|
match shape_data.drag_start == input.mouse.position {
|
||||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||||
|
|
@ -104,7 +116,7 @@ impl Fsm for RectangleToolFsmState {
|
||||||
shape_data.cleanup();
|
shape_data.cleanup();
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Drawing, Abort) => {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
shape_data.cleanup();
|
shape_data.cleanup();
|
||||||
|
|
||||||
|
|
@ -116,4 +128,45 @@ impl Fsm for RectangleToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@ use graphene::Operation;
|
||||||
use graphene::Quad;
|
use graphene::Quad;
|
||||||
|
|
||||||
use crate::consts::COLOR_ACCENT;
|
use crate::consts::COLOR_ACCENT;
|
||||||
use crate::input::keyboard::Key;
|
use crate::input::{
|
||||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
keyboard::{Key, MouseMotion},
|
||||||
use crate::tool::snapping::SnapHandler;
|
mouse::ViewportPosition,
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
InputPreprocessor,
|
||||||
|
};
|
||||||
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
|
use crate::tool::{snapping::SnapHandler, DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::SELECTION_TOLERANCE,
|
consts::SELECTION_TOLERANCE,
|
||||||
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
|
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
|
||||||
|
|
@ -39,8 +42,19 @@ pub enum SelectMessage {
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
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 {
|
fn actions(&self) -> ActionList {
|
||||||
use SelectToolFsmState::*;
|
use SelectToolFsmState::*;
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
|
|
@ -51,7 +65,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
enum SelectToolFsmState {
|
enum SelectToolFsmState {
|
||||||
Ready,
|
Ready,
|
||||||
Dragging,
|
Dragging,
|
||||||
|
|
@ -122,6 +136,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
) -> Self {
|
) -> Self {
|
||||||
use SelectMessage::*;
|
use SelectMessage::*;
|
||||||
use SelectToolFsmState::*;
|
use SelectToolFsmState::*;
|
||||||
|
|
||||||
if let ToolMessage::Select(event) = event {
|
if let ToolMessage::Select(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(_, UpdateSelectionBoundingBox) => {
|
(_, UpdateSelectionBoundingBox) => {
|
||||||
|
|
@ -263,4 +278,121 @@ impl Fsm for SelectToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::input::keyboard::Key;
|
use crate::input::keyboard::{Key, MouseMotion};
|
||||||
use crate::input::InputPreprocessor;
|
use crate::input::InputPreprocessor;
|
||||||
|
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolOptions, ToolType};
|
use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolOptions, ToolType};
|
||||||
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
use crate::{document::DocumentMessageHandler, message_prelude::*};
|
||||||
use glam::DAffine2;
|
use glam::DAffine2;
|
||||||
|
|
@ -25,13 +26,24 @@ pub enum ShapeMessage {
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
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 {
|
fn actions(&self) -> ActionList {
|
||||||
use ShapeToolFsmState::*;
|
use ShapeToolFsmState::*;
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(ShapeMessageDiscriminant; DragStart),
|
Ready => actions!(ShapeMessageDiscriminant; DragStart),
|
||||||
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
|
Drawing => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
enum ShapeToolFsmState {
|
enum ShapeToolFsmState {
|
||||||
Ready,
|
Ready,
|
||||||
Dragging,
|
Drawing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ShapeToolFsmState {
|
impl Default for ShapeToolFsmState {
|
||||||
|
|
@ -93,7 +105,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
|
|
||||||
Dragging
|
Drawing
|
||||||
}
|
}
|
||||||
(state, Resize { center, lock_ratio }) => {
|
(state, Resize { center, lock_ratio }) => {
|
||||||
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
|
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
|
||||||
|
|
@ -102,7 +114,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
(Dragging, DragStop) => {
|
(Drawing, DragStop) => {
|
||||||
// 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 shape_data.drag_start == input.mouse.position {
|
match shape_data.drag_start == input.mouse.position {
|
||||||
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
|
||||||
|
|
@ -112,7 +124,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
shape_data.cleanup();
|
shape_data.cleanup();
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
(Dragging, Abort) => {
|
(Drawing, Abort) => {
|
||||||
responses.push_back(DocumentMessage::AbortTransaction.into());
|
responses.push_back(DocumentMessage::AbortTransaction.into());
|
||||||
shape_data.cleanup();
|
shape_data.cleanup();
|
||||||
|
|
||||||
|
|
@ -124,4 +136,45 @@ impl Fsm for ShapeToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<polygon class="bright" points="6,11 10,6 7,6 7,1 5,1 5,6 2,6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 135 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<polygon class="bright" points="1,6 6,10 6,7 11,7 11,5 6,5 6,2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 136 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<polygon class="bright" points="11,6 6,2 6,5 1,5 1,7 6,7 6,10" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 135 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<polygon class="bright" points="6,1 2,6 5,6 5,11 7,11 7,6 10,6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 136 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<path class="bright" d="M11,3v6H4.41l-3-3l3-3H11 M12,2H4L0,6l4,4h8V2L12,2z" />
|
||||||
|
<polygon class="bright" points="9.35,4.35 8.65,3.65 7,5.29 5.35,3.65 4.65,4.35 6.29,6 4.65,7.65 5.35,8.35 7,6.71 8.65,8.35 9.35,7.65 7.71,6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 293 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<path class="bright" d="M9,7H8V5h1c1.1,0,2-0.9,2-2c0-1.1-0.9-2-2-2S7,1.9,7,3v1H5V3c0-1.1-0.9-2-2-2S1,1.9,1,3c0,1.1,0.9,2,2,2h1v2H3C1.9,7,1,7.9,1,9c0,1.1,0.9,2,2,2s2-0.9,2-2V8h2v1c0,1.1,0.9,2,2,2s2-0.9,2-2C11,7.9,10.1,7,9,7z M8,3c0-0.55,0.45-1,1-1s1,0.45,1,1S9.55,4,9,4H8V3z M3,4C2.45,4,2,3.55,2,3s0.45-1,1-1s1,0.45,1,1v1H3z M4,9c0,0.55-0.45,1-1,1S2,9.55,2,9s0.45-1,1-1h1V9z M5,7V5h2v2H5z M9,10c-0.55,0-1-0.45-1-1V8h1c0.55,0,1,0.45,1,1S9.55,10,9,10z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 522 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<path class="bright" d="M11,3v2c0,0.55-0.45,1-1,1H3V4L0,6.5L3,9V7h7c1.1,0,2-0.9,2-2V3H11z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 163 B |
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<rect class="bright" x="6" y="3" width="5" height="1" />
|
||||||
|
<polygon class="bright" points="11,9 5.72,9 2.72,4 1,4 1,3 3.28,3 6.28,8 11,8" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 209 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<path class="bright" d="M6,0L0,6h3v6h6V6h3L6,0z M8,5v6H4V5H2.41L6,1.41L9.59,5H8z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 154 B |
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<path class="bright" d="M11,8v1c0,0.55-0.45,1-1,1H2c-0.55,0-1-0.45-1-1V8H0v1c0,1.1,0.9,2,2,2h8c1.1,0,2-0.9,2-2V8H11z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 190 B |
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||||
|
<polygon class="bright" points="4,3 4,1 1,3.5 4,6 4,4 12,4 12,3" />
|
||||||
|
<polygon class="bright" points="0,1 0,6 1,6 1,3.5 1,1" />
|
||||||
|
<polygon class="bright" points="8,8 0,8 0,9 8,9 8,11 11,8.5 8,6" />
|
||||||
|
<polygon class="bright" points="11,8.5 11,11 12,11 12,6 11,6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 331 B |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
<path class="dim" d="M8,1h2c1.65,0,3,1.35,3,3v4H8V1z" />
|
<path class="bright" d="M8,1h2c1.65,0,3,1.35,3,3v4H8V1z" />
|
||||||
<path class="bright" d="M6,1h1v1H6C4.9,2,4,2.9,4,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4C3,2.35,4.35,1,6,1z" />
|
<path class="dim" d="M6,1h1v1H6C4.9,2,4,2.9,4,4v6c0,2.21,1.79,4,4,4s4-1.79,4-4V9h1v1c0,2.76-2.24,5-5,5s-5-2.24-5-5V4C3,2.35,4.35,1,6,1z" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 269 B After Width: | Height: | Size: 270 B |
|
|
@ -78,7 +78,7 @@
|
||||||
|
|
||||||
.user-input-label {
|
.user-input-label {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-left: 4px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.submenu-arrow {
|
.submenu-arrow {
|
||||||
|
|
|
||||||
|
|
@ -66,13 +66,13 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
|
||||||
ref: undefined,
|
ref: undefined,
|
||||||
children: [
|
children: [
|
||||||
[
|
[
|
||||||
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], shortcutRequiresLock: true, action: async () => editor.instance.new_document() },
|
{ label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: async () => editor.instance.new_document() },
|
||||||
{ label: "Open…", shortcut: ["Ctrl", "O"], action: async () => editor.instance.open_document() },
|
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: async () => editor.instance.open_document() },
|
||||||
{
|
{
|
||||||
label: "Open Recent",
|
label: "Open Recent",
|
||||||
shortcut: ["Ctrl", "⇧", "O"],
|
shortcut: ["KeyControl", "KeyShift", "KeyO"],
|
||||||
children: [
|
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: "Some Recent File.gdd" },
|
||||||
{ label: "Another 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", shortcut: ["KeyControl", "KeyW"], 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 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", shortcut: ["KeyControl", "KeyS"], action: async () => editor.instance.save_document() },
|
||||||
{ label: "Save As…", shortcut: ["Ctrl", "⇧", "S"], action: async () => editor.instance.save_document() },
|
{ label: "Save As…", shortcut: ["KeyControl", "KeyShift", "KeyS"], action: async () => editor.instance.save_document() },
|
||||||
{ label: "Save All", shortcut: ["Ctrl", "Alt", "S"] },
|
{ label: "Save All", shortcut: ["KeyControl", "KeyAlt", "KeyS"] },
|
||||||
{ label: "Auto-Save", checkbox: true, checked: true },
|
{ label: "Auto-Save", checkbox: true, checked: true },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ label: "Import…", shortcut: ["Ctrl", "I"] },
|
{ label: "Import…", shortcut: ["KeyControl", "KeyI"] },
|
||||||
{ label: "Export…", shortcut: ["Ctrl", "E"], action: async () => editor.instance.export_document() },
|
{ 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,
|
ref: undefined,
|
||||||
children: [
|
children: [
|
||||||
[
|
[
|
||||||
{ label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => editor.instance.undo() },
|
{ label: "Undo", shortcut: ["KeyControl", "KeyZ"], action: async () => editor.instance.undo() },
|
||||||
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => editor.instance.redo() },
|
{ label: "Redo", shortcut: ["KeyControl", "KeyShift", "KeyZ"], action: async () => editor.instance.redo() },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ label: "Cut", shortcut: ["Ctrl", "X"] },
|
{ label: "Cut", shortcut: ["KeyControl", "KeyX"] },
|
||||||
{ label: "Copy", icon: "Copy", shortcut: ["Ctrl", "C"] },
|
{ label: "Copy", icon: "Copy", shortcut: ["KeyControl", "KeyC"] },
|
||||||
{ label: "Paste", icon: "Paste", shortcut: ["Ctrl", "V"] },
|
{ label: "Paste", icon: "Paste", shortcut: ["KeyControl", "KeyV"] },
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -120,16 +120,24 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
|
||||||
ref: undefined,
|
ref: undefined,
|
||||||
children: [
|
children: [
|
||||||
[
|
[
|
||||||
{ label: "Select All", shortcut: ["Ctrl", "A"], action: async () => editor.instance.select_all_layers() },
|
{ label: "Select All", shortcut: ["KeyControl", "KeyA"], action: async () => editor.instance.select_all_layers() },
|
||||||
{ label: "Deselect All", shortcut: ["Ctrl", "Alt", "A"], action: async () => editor.instance.deselect_all_layers() },
|
{ label: "Deselect All", shortcut: ["KeyControl", "KeyAlt", "KeyA"], action: async () => editor.instance.deselect_all_layers() },
|
||||||
{
|
{
|
||||||
label: "Order",
|
label: "Order",
|
||||||
children: [
|
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: "Raise To Front",
|
||||||
{ label: "Lower", shortcut: ["Ctrl", "["], action: async () => editor.instance.reorder_selected_layers(-1) },
|
shortcut: ["KeyControl", "KeyShift", "KeyLeftBracket"],
|
||||||
{ label: "Lower to Back", shortcut: ["Ctrl", "Shift", "["], action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()) },
|
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()),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 WindowButtonWinMaximize from "@/../assets/12px-solid/window-button-win-maximize.svg";
|
||||||
import WindowButtonWinRestoreDown from "@/../assets/12px-solid/window-button-win-restore-down.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 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 MouseHintNone from "@/../assets/16px-two-tone/mouse-hint-none.svg";
|
||||||
import MouseHintLMB from "@/../assets/16px-two-tone/mouse-hint-lmb.svg";
|
import MouseHintLmb from "@/../assets/16px-two-tone/mouse-hint-lmb.svg";
|
||||||
import MouseHintRMB from "@/../assets/16px-two-tone/mouse-hint-rmb.svg";
|
import MouseHintRmb from "@/../assets/16px-two-tone/mouse-hint-rmb.svg";
|
||||||
import MouseHintMMB from "@/../assets/16px-two-tone/mouse-hint-mmb.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 MouseHintScrollUp from "@/../assets/16px-two-tone/mouse-hint-scroll-up.svg";
|
||||||
import MouseHintScrollDown from "@/../assets/16px-two-tone/mouse-hint-scroll-down.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 MouseHintDrag from "@/../assets/16px-two-tone/mouse-hint-drag.svg";
|
||||||
import MouseHintLMBDrag from "@/../assets/16px-two-tone/mouse-hint-lmb-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 MouseHintRmbDrag from "@/../assets/16px-two-tone/mouse-hint-rmb-drag.svg";
|
||||||
import MouseHintMMBDrag from "@/../assets/16px-two-tone/mouse-hint-mmb-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 NodeTypePath from "@/../assets/24px-full-color/node-type-path.svg";
|
||||||
import NodeTypeFolder from "@/../assets/24px-full-color/node-type-folder.svg";
|
import NodeTypeFolder from "@/../assets/24px-full-color/node-type-folder.svg";
|
||||||
|
|
@ -182,16 +193,27 @@ const icons = {
|
||||||
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
|
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
|
||||||
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },
|
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },
|
||||||
WindowButtonWinClose: { component: WindowButtonWinClose, 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 },
|
MouseHintNone: { component: MouseHintNone, size: 16 },
|
||||||
MouseHintLMB: { component: MouseHintLMB, size: 16 },
|
MouseHintLmb: { component: MouseHintLmb, size: 16 },
|
||||||
MouseHintRMB: { component: MouseHintRMB, size: 16 },
|
MouseHintRmb: { component: MouseHintRmb, size: 16 },
|
||||||
MouseHintMMB: { component: MouseHintMMB, size: 16 },
|
MouseHintMmb: { component: MouseHintMmb, size: 16 },
|
||||||
MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
|
MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
|
||||||
MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
|
MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
|
||||||
MouseHintDrag: { component: MouseHintDrag, size: 16 },
|
MouseHintDrag: { component: MouseHintDrag, size: 16 },
|
||||||
MouseHintLMBDrag: { component: MouseHintLMBDrag, size: 16 },
|
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
|
||||||
MouseHintRMBDrag: { component: MouseHintRMBDrag, size: 16 },
|
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
|
||||||
MouseHintMMBDrag: { component: MouseHintMMBDrag, size: 16 },
|
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
|
||||||
NodeTypePath: { component: NodeTypePath, size: 24 },
|
NodeTypePath: { component: NodeTypePath, size: 24 },
|
||||||
NodeTypeFolder: { component: NodeTypeFolder, size: 24 },
|
NodeTypeFolder: { component: NodeTypeFolder, size: 24 },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@
|
||||||
<div class="user-input-label">
|
<div class="user-input-label">
|
||||||
<template v-for="(keyGroup, keyGroupIndex) in inputKeys" :key="keyGroupIndex">
|
<template v-for="(keyGroup, keyGroupIndex) in inputKeys" :key="keyGroupIndex">
|
||||||
<span class="group-gap" v-if="keyGroupIndex > 0"></span>
|
<span class="group-gap" v-if="keyGroupIndex > 0"></span>
|
||||||
<span class="input-key" v-for="inputKey in keyGroup" :key="inputKey" :class="keyCapWidth(inputKey)">
|
<template v-for="inputKey in keyGroup" :key="((keyInfo = keyTextOrIcon(inputKey)), inputKey)">
|
||||||
{{ inputKey }}
|
<span class="input-key" :class="keyInfo.width">
|
||||||
</span>
|
<IconLabel v-if="keyInfo.icon" :icon="keyInfo.icon" />
|
||||||
|
<template v-else>{{ keyInfo.text }}</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<span class="input-mouse" v-if="inputMouse">
|
<span class="input-mouse" v-if="inputMouse">
|
||||||
<IconLabel :icon="mouseInputInteractionToIcon(inputMouse)" />
|
<IconLabel :icon="mouseMovementIcon(inputMouse)" />
|
||||||
</span>
|
</span>
|
||||||
<span class="hint-text" v-if="hasSlotContent">
|
<span class="hint-text" v-if="hasSlotContent">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
@ -46,27 +49,33 @@
|
||||||
border-color: var(--color-7-middlegray);
|
border-color: var(--color-7-middlegray);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 16px;
|
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-16 {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-key.width-24 {
|
&.width-24 {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-key.width-32 {
|
&.width-32 {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-key.width-40 {
|
&.width-40 {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-key.width-48 {
|
&.width-48 {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-label {
|
||||||
|
margin: 1px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-mouse {
|
.input-mouse {
|
||||||
|
|
@ -92,15 +101,15 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
|
||||||
export enum MouseInputInteraction {
|
export enum MouseInputInteraction {
|
||||||
"None" = "None",
|
"None" = "None",
|
||||||
"LMB" = "LMB",
|
"Lmb" = "Lmb",
|
||||||
"RMB" = "RMB",
|
"Rmb" = "Rmb",
|
||||||
"MMB" = "MMB",
|
"Mmb" = "Mmb",
|
||||||
"ScrollUp" = "ScrollUp",
|
"ScrollUp" = "ScrollUp",
|
||||||
"ScrollDown" = "ScrollDown",
|
"ScrollDown" = "ScrollDown",
|
||||||
"Drag" = "Drag",
|
"Drag" = "Drag",
|
||||||
"LMBDrag" = "LMBDrag",
|
"LmbDrag" = "LmbDrag",
|
||||||
"RMBDrag" = "RMBDrag",
|
"RmbDrag" = "RmbDrag",
|
||||||
"MMBDrag" = "MMBDrag",
|
"MmbDrag" = "MmbDrag",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
|
@ -115,29 +124,89 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
keyCapWidth(keyText: string) {
|
keyTextOrIcon(keyText: string): { text: string | null; icon: string | null; width: string } {
|
||||||
return `width-${keyText.length * 8 + 8}`;
|
// 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,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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) {
|
switch (mouseInputInteraction) {
|
||||||
case MouseInputInteraction.LMB:
|
case MouseInputInteraction.Lmb:
|
||||||
return "MouseHintLMB";
|
return "MouseHintLmb";
|
||||||
case MouseInputInteraction.RMB:
|
case MouseInputInteraction.Rmb:
|
||||||
return "MouseHintRMB";
|
return "MouseHintRmb";
|
||||||
case MouseInputInteraction.MMB:
|
case MouseInputInteraction.Mmb:
|
||||||
return "MouseHintMMB";
|
return "MouseHintMmb";
|
||||||
case MouseInputInteraction.ScrollUp:
|
case MouseInputInteraction.ScrollUp:
|
||||||
return "MouseHintScrollUp";
|
return "MouseHintScrollUp";
|
||||||
case MouseInputInteraction.ScrollDown:
|
case MouseInputInteraction.ScrollDown:
|
||||||
return "MouseHintScrollDown";
|
return "MouseHintScrollDown";
|
||||||
case MouseInputInteraction.Drag:
|
case MouseInputInteraction.Drag:
|
||||||
return "MouseHintDrag";
|
return "MouseHintDrag";
|
||||||
case MouseInputInteraction.LMBDrag:
|
case MouseInputInteraction.LmbDrag:
|
||||||
return "MouseHintLMBDrag";
|
return "MouseHintLmbDrag";
|
||||||
case MouseInputInteraction.RMBDrag:
|
case MouseInputInteraction.RmbDrag:
|
||||||
return "MouseHintRMBDrag";
|
return "MouseHintRmbDrag";
|
||||||
case MouseInputInteraction.MMBDrag:
|
case MouseInputInteraction.MmbDrag:
|
||||||
return "MouseHintMMBDrag";
|
return "MouseHintMmbDrag";
|
||||||
default:
|
default:
|
||||||
case MouseInputInteraction.None:
|
case MouseInputInteraction.None:
|
||||||
return "MouseHintNone";
|
return "MouseHintNone";
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<UserInputLabel :inputMouse="'LMBDrag'">Drag Selected</UserInputLabel>
|
<template v-for="(hintGroup, index) in hintData" :key="hintGroup">
|
||||||
<Separator :type="SeparatorType.Section" />
|
<Separator :type="SeparatorType.Section" v-if="index !== 0" />
|
||||||
<UserInputLabel :inputKeys="[['G']]">Grab Selected</UserInputLabel>
|
<template v-for="hint in hintGroup" :key="hint">
|
||||||
<UserInputLabel :inputKeys="[['R']]">Rotate Selected</UserInputLabel>
|
<span v-if="hint.plus" class="plus">+</span>
|
||||||
<UserInputLabel :inputKeys="[['S']]">Scale Selected</UserInputLabel>
|
<UserInputLabel :inputMouse="hint.mouse" :inputKeys="hint.key_groups">{{ hint.label }}</UserInputLabel>
|
||||||
<Separator :type="SeparatorType.Section" />
|
</template>
|
||||||
<UserInputLabel :inputMouse="'LMB'">Select Object</UserInputLabel>
|
</template>
|
||||||
<span class="plus">+</span>
|
|
||||||
<UserInputLabel :inputKeys="[['Ctrl']]">Innermost</UserInputLabel>
|
|
||||||
<span class="plus">+</span>
|
|
||||||
<UserInputLabel :inputKeys="[['⇧']]">Grow/Shrink Selection</UserInputLabel>
|
|
||||||
<Separator :type="SeparatorType.Section" />
|
|
||||||
<UserInputLabel :inputMouse="'LMBDrag'">Select Area</UserInputLabel>
|
|
||||||
<span class="plus">+</span>
|
|
||||||
<UserInputLabel :inputKeys="[['⇧']]">Grow/Shrink Selection</UserInputLabel>
|
|
||||||
<Separator :type="SeparatorType.Section" />
|
|
||||||
<UserInputLabel :inputKeys="[['↑'], ['→'], ['↓'], ['←']]">Nudge Selected</UserInputLabel>
|
|
||||||
<span class="plus">+</span>
|
|
||||||
<UserInputLabel :inputKeys="[['⇧']]">Big Increment Nudge</UserInputLabel>
|
|
||||||
<Separator :type="SeparatorType.Section" />
|
|
||||||
<UserInputLabel :inputKeys="[['Alt']]" :inputMouse="'LMBDrag'">Move Duplicate</UserInputLabel>
|
|
||||||
<UserInputLabel :inputKeys="[['Ctrl', 'D']]">Duplicate</UserInputLabel>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
height: 24px;
|
||||||
margin: 0 -4px;
|
margin: 0 -4px;
|
||||||
// TODO: Use CSS grid to solve issue that makes overflowed items have inconsistent left padding on second row when overflowed
|
|
||||||
|
|
||||||
> * {
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.separator.section {
|
.separator.section {
|
||||||
height: 24px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,8 +39,10 @@ import { SeparatorType } from "@/components/widgets/widgets";
|
||||||
|
|
||||||
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
||||||
import Separator from "@/components/widgets/separators/Separator.vue";
|
import Separator from "@/components/widgets/separators/Separator.vue";
|
||||||
|
import { HintData, UpdateInputHints } from "@/dispatcher/js-messages";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
inject: ["editor"],
|
||||||
components: {
|
components: {
|
||||||
UserInputLabel,
|
UserInputLabel,
|
||||||
Separator,
|
Separator,
|
||||||
|
|
@ -69,7 +50,17 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
SeparatorType,
|
SeparatorType,
|
||||||
|
hintData: [] as HintData,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.editor.dispatcher.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
|
||||||
|
this.hintData = updateInputHints.hint_data;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Switch away from, and back to, the Select Tool to make it display the correct hints in the status bar
|
||||||
|
this.editor.instance.select_tool("Path");
|
||||||
|
this.editor.instance.select_tool("Select");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,10 @@ export function createJsDispatcher() {
|
||||||
const messageConstructor = messageConstructors[messageType];
|
const messageConstructor = messageConstructors[messageType];
|
||||||
if (!messageConstructor) {
|
if (!messageConstructor) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(`Received a frontend message of type "${messageType}" but but was not able to parse the data.`);
|
console.error(
|
||||||
|
`Received a frontend message of type "${messageType}" but was not able to parse the data. ` +
|
||||||
|
"(Perhaps this message parser isn't exported in `messageConstructors` at the bottom of `js-messages.ts`.)"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,41 @@ export class JsMessage {
|
||||||
static readonly jsMessageMarker = true;
|
static readonly jsMessageMarker = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Add additional classes to replicate Rust's FrontendMessages and data structures below.
|
||||||
|
//
|
||||||
|
// Remember to add each message to the `messageConstructors` export at the bottom of the file.
|
||||||
|
//
|
||||||
|
// Read class-transformer docs at https://github.com/typestack/class-transformer#table-of-contents
|
||||||
|
// for details about how to transform the JSON from wasm-bindgen into classes.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export class UpdateOpenDocumentsList extends JsMessage {
|
export class UpdateOpenDocumentsList extends JsMessage {
|
||||||
@Transform(({ value }) => value.map((tuple: [string, boolean]) => ({ name: tuple[0], isSaved: tuple[1] })))
|
@Transform(({ value }) => value.map((tuple: [string, boolean]) => ({ name: tuple[0], isSaved: tuple[1] })))
|
||||||
readonly open_documents!: { name: string; isSaved: boolean }[];
|
readonly open_documents!: { name: string; isSaved: boolean }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UpdateInputHints extends JsMessage {
|
||||||
|
@Type(() => HintInfo)
|
||||||
|
readonly hint_data!: HintData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HintGroup extends Array<HintInfo> {}
|
||||||
|
|
||||||
|
export class HintData extends Array<HintGroup> {}
|
||||||
|
|
||||||
|
export class HintInfo {
|
||||||
|
readonly keys!: string[];
|
||||||
|
|
||||||
|
readonly mouse!: KeysGroup | null;
|
||||||
|
|
||||||
|
readonly label!: string;
|
||||||
|
|
||||||
|
readonly plus!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KeysGroup extends Array<string> {}
|
||||||
|
|
||||||
const To255Scale = Transform(({ value }) => value * 255);
|
const To255Scale = Transform(({ value }) => value * 255);
|
||||||
export class Color {
|
export class Color {
|
||||||
@To255Scale
|
@To255Scale
|
||||||
|
|
@ -277,6 +307,7 @@ export const messageConstructors: Record<string, MessageMaker> = {
|
||||||
SetActiveTool,
|
SetActiveTool,
|
||||||
SetActiveDocument,
|
SetActiveDocument,
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
|
UpdateInputHints,
|
||||||
UpdateWorkingColors,
|
UpdateWorkingColors,
|
||||||
SetCanvasZoom,
|
SetCanvasZoom,
|
||||||
SetCanvasRotation,
|
SetCanvasRotation,
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,8 @@ impl JsEditorHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Create JS -> Rust wrapper functions below
|
// Add additional JS -> Rust wrapper functions below as needed for calling the
|
||||||
|
// backend from the web frontend.
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
pub fn has_crashed(&self) -> JsValue {
|
pub fn has_crashed(&self) -> JsValue {
|
||||||
|
|
|
||||||