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
This commit is contained in:
Keavon Chambers
2021-12-24 01:46:03 -08:00
parent 3500160bf7
commit d2b0411295
38 changed files with 1070 additions and 212 deletions

View File

@@ -34,7 +34,7 @@ impl Dispatcher {
if GROUP_MESSAGES.contains(&message.to_discriminant()) && self.messages.contains(&message) {
continue;
}
log_message(&message);
self.log_message(&message);
match message {
NoOp => (),
Documents(message) => self.documents_message_handler.process_action(message, &self.input_preprocessor, &mut self.messages),
@@ -74,18 +74,18 @@ impl Dispatcher {
responses: vec![],
}
}
}
fn log_message(message: &Message) {
use Message::*;
if log::max_level() == log::LevelFilter::Trace
&& !(matches!(
message,
InputPreprocessor(_) | Frontend(FrontendMessage::SetCanvasZoom { .. }) | Frontend(FrontendMessage::SetCanvasRotation { .. })
) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove"))
{
log::trace!("Message: {:?}", message);
//log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions()));
fn log_message(&self, message: &Message) {
use Message::*;
if log::max_level() == log::LevelFilter::Trace
&& !(matches!(
message,
InputPreprocessor(_) | Frontend(FrontendMessage::SetCanvasZoom { .. }) | Frontend(FrontendMessage::SetCanvasRotation { .. })
) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove"))
{
log::trace!("Message: {:?}", message);
// log::trace!("Hints: {:?}", self.input_mapper.hints(self.collect_actions()));
}
}
}

View File

@@ -1,5 +1,6 @@
use crate::document::layer_panel::{LayerPanelEntry, RawBuffer};
use crate::message_prelude::*;
use crate::misc::HintData;
use crate::tool::tool_options::ToolOptions;
use crate::Color;
use serde::{Deserialize, Serialize};
@@ -11,6 +12,7 @@ pub enum FrontendMessage {
SetActiveTool { tool_name: String, tool_options: Option<ToolOptions> },
SetActiveDocument { document_index: usize },
UpdateOpenDocumentsList { open_documents: Vec<(String, bool)> },
UpdateInputHints { hint_data: HintData },
DisplayError { title: String, description: String },
DisplayPanic { panic_info: String, title: String, description: String },
DisplayConfirmationToCloseDocument { document_index: usize },

View File

@@ -18,6 +18,7 @@ const SHIFT_NUDGE_AMOUNT: f64 = 10.;
pub enum InputMapperMessage {
PointerMove,
MouseScroll,
#[child]
KeyUp(Key),
#[child]
KeyDown(Key),
@@ -43,6 +44,7 @@ impl KeyMappingEntries {
}
None
}
fn push(&mut self, entry: MappingEntry) {
self.0.push(entry)
}
@@ -187,7 +189,8 @@ impl Default for Mapping {
entry! {action=PenMessage::Confirm, key_down=KeyEscape},
entry! {action=PenMessage::Confirm, key_down=KeyEnter},
// Fill
entry! {action=FillMessage::MouseDown, key_down=Lmb},
entry! {action=FillMessage::LeftMouseDown, key_down=Lmb},
entry! {action=FillMessage::RightMouseDown, key_down=Rmb},
// Tool Actions
entry! {action=ToolMessage::ActivateTool(ToolType::Select), key_down=KeyV},
entry! {action=ToolMessage::ActivateTool(ToolType::Eyedropper), key_down=KeyI},

View File

@@ -16,6 +16,7 @@ pub type KeyStates = BitVector<KEY_MASK_STORAGE_LENGTH>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Key {
UnknownKey,
// MouseKeys
Lmb,
Rmb,
@@ -87,6 +88,20 @@ pub enum Key {
NumKeys,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum MouseMotion {
None,
Lmb,
Rmb,
Mmb,
ScrollUp,
ScrollDown,
Drag,
LmbDrag,
RmbDrag,
MmbDrag,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BitVector<const LENGTH: usize>([StorageType; LENGTH]);

20
editor/src/misc/hints.rs Normal file
View File

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

View File

@@ -2,7 +2,9 @@
pub mod macros;
pub mod derivable_custom_traits;
mod error;
pub mod hints;
pub mod test_utils;
pub use error::EditorError;
pub use hints::*;
pub use macros::*;

View File

@@ -41,6 +41,8 @@ pub trait Fsm {
input: &InputPreprocessor,
messages: &mut VecDeque<Message>,
) -> Self;
fn update_hints(&self, responses: &mut VecDeque<Message>);
}
#[derive(Debug, Clone)]

View File

@@ -12,6 +12,7 @@ use std::collections::VecDeque;
#[impl_message(Message, Tool)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum ToolMessage {
UpdateHints,
ActivateTool(ToolType),
SelectPrimaryColor(Color),
SelectSecondaryColor(Color),
@@ -84,22 +85,28 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
ToolType::Rectangle => Some(RectangleMessage::Abort.into()),
ToolType::Ellipse => Some(EllipseMessage::Abort.into()),
ToolType::Shape => Some(ShapeMessage::Abort.into()),
ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()),
ToolType::Fill => Some(FillMessage::Abort.into()),
_ => None,
};
// Send the Abort state transition to the tool
let mut send_message_to_tool = |tool_type, message: ToolMessage| {
let mut send_message_to_tool = |tool_type, message: ToolMessage, update_hints: bool| {
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
tool.process_action(message, (document, document_data, input), responses);
if update_hints {
tool.process_action(ToolMessage::UpdateHints, (document, document_data, input), responses);
}
}
};
// Send the old and new tools a transition to their FSM Abort states
if let Some(tool_message) = reset_message(new_tool) {
send_message_to_tool(new_tool, tool_message);
send_message_to_tool(new_tool, tool_message, true);
}
if let Some(tool_message) = reset_message(old_tool) {
send_message_to_tool(old_tool, tool_message);
send_message_to_tool(old_tool, tool_message, false);
}
// Special cases for specific tools
@@ -113,7 +120,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessor)>
// Notify the frontend about the new active tool to be displayed
let tool_name = new_tool.to_string();
let tool_options = self.tool_state.document_tool_data.tool_options.get(&new_tool).map(|tool_options| *tool_options);
let tool_options = self.tool_state.document_tool_data.tool_options.get(&new_tool).copied();
responses.push_back(FrontendMessage::SetActiveTool { tool_name, tool_options }.into());
}
SelectedLayersChanged => {

View File

@@ -1,13 +1,12 @@
use crate::input::keyboard::Key;
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use crate::document::DocumentMessageHandler;
use crate::input::{keyboard::Key, keyboard::MouseMotion, InputPreprocessor};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::tool::{tools::resize::Resize, DocumentToolData, Fsm, ToolActionHandlerData};
use glam::DAffine2;
use graphene::{layers::style, Operation};
use serde::{Deserialize, Serialize};
use super::resize::*;
#[derive(Default)]
pub struct Ellipse {
fsm_state: EllipseToolFsmState,
@@ -25,13 +24,24 @@ pub enum EllipseMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
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 {
use EllipseToolFsmState::*;
match self.fsm_state {
Ready => actions!(EllipseMessageDiscriminant; DragStart),
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
Drawing => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
@@ -39,7 +49,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EllipseToolFsmState {
Ready,
Dragging,
Drawing,
}
impl Default for EllipseToolFsmState {
@@ -47,6 +57,7 @@ impl Default for EllipseToolFsmState {
EllipseToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
data: Resize,
@@ -85,7 +96,7 @@ impl Fsm for EllipseToolFsmState {
.into(),
);
Dragging
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
@@ -94,7 +105,7 @@ impl Fsm for EllipseToolFsmState {
state
}
(Dragging, DragStop) => {
(Drawing, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
@@ -104,7 +115,7 @@ impl Fsm for EllipseToolFsmState {
shape_data.cleanup();
Ready
}
(Dragging, Abort) => {
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup();
@@ -116,4 +127,45 @@ impl Fsm for EllipseToolFsmState {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<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());
}
}

View File

@@ -1,40 +1,124 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::document::DocumentMessageHandler;
use crate::input::{keyboard::MouseMotion, InputPreprocessor};
use crate::message_prelude::*;
use crate::tool::{ToolActionHandlerData, ToolMessage};
use crate::misc::{HintData, HintGroup, HintInfo};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolMessage};
use glam::DVec2;
use graphene::layers::LayerDataType;
use graphene::Quad;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Eyedropper;
pub struct Eyedropper {
fsm_state: EyedropperToolFsmState,
data: EyedropperToolData,
}
#[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum EyedropperMessage {
LeftMouseDown,
RightMouseDown,
Abort,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
let mouse_pos = data.2.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
if action == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
if let Ok(layer) = data.0.graphene_document.layer(path) {
if let LayerDataType::Shape(s) = &layer.data {
s.style.fill().and_then(|fill| {
fill.color().map(|color| match action {
ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
_ => {}
})
});
}
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
}
}
advertise_actions!(EyedropperMessageDiscriminant; LeftMouseDown, RightMouseDown);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EyedropperToolFsmState {
Ready,
}
impl Default for EyedropperToolFsmState {
fn default() -> Self {
EyedropperToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EyedropperToolData {}
impl Fsm for EyedropperToolFsmState {
type ToolData = EyedropperToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<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());
}
}

View File

@@ -1,34 +1,117 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::document::DocumentMessageHandler;
use crate::input::{keyboard::MouseMotion, InputPreprocessor};
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo};
use crate::tool::ToolActionHandlerData;
use crate::tool::{DocumentToolData, Fsm, ToolMessage};
use glam::DVec2;
use graphene::{Operation, Quad};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Fill;
pub struct Fill {
fsm_state: FillToolFsmState,
data: FillToolData,
}
#[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum FillMessage {
MouseDown,
LeftMouseDown,
RightMouseDown,
Abort,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
fn process_action(&mut self, _action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
let mouse_pos = data.2.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if let Some(path) = data.0.graphene_document.intersects_quad_root(quad).last() {
responses.push_back(
Operation::SetLayerFill {
path: path.to_vec(),
color: data.1.primary_color,
}
.into(),
);
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
}
}
advertise_actions!(FillMessageDiscriminant; MouseDown);
advertise_actions!(FillMessageDiscriminant; LeftMouseDown, RightMouseDown);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FillToolFsmState {
Ready,
}
impl Default for FillToolFsmState {
fn default() -> Self {
FillToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct FillToolData {}
impl Fsm for FillToolFsmState {
type ToolData = FillToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<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());
}
}

View File

@@ -1,6 +1,7 @@
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::input::keyboard::Key;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::tool::snapping::SnapHandler;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
use crate::{document::DocumentMessageHandler, message_prelude::*};
@@ -25,13 +26,24 @@ pub enum LineMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
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 {
use LineToolFsmState::*;
match self.fsm_state {
Ready => actions!(LineMessageDiscriminant; DragStart),
Dragging => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
Drawing => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
}
}
}
@@ -39,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LineToolFsmState {
Ready,
Dragging,
Drawing,
}
impl Default for LineToolFsmState {
@@ -96,21 +108,21 @@ impl Fsm for LineToolFsmState {
.into(),
);
Dragging
Drawing
}
(Dragging, Redraw { center, snap_angle, lock_angle }) => {
(Drawing, Redraw { center, snap_angle, lock_angle }) => {
data.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect();
responses.push_back(generate_transform(data, values[0], values[1], values[2]));
Dragging
Drawing
}
(Dragging, DragStop) => {
(Drawing, DragStop) => {
data.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
data.snap_handler.cleanup();
// TODO; introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
@@ -120,7 +132,7 @@ impl Fsm for LineToolFsmState {
Ready
}
(Dragging, Abort) => {
(Drawing, Abort) => {
data.snap_handler.cleanup();
responses.push_back(DocumentMessage::AbortTransaction.into());
data.path = None;
@@ -132,6 +144,59 @@ impl Fsm for LineToolFsmState {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<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 {

View File

@@ -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>) {
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
}
advertise_actions!();
}

View File

@@ -3,8 +3,10 @@ use crate::consts::VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE;
use crate::document::DocumentMessageHandler;
use crate::document::VectorManipulatorSegment;
use crate::document::VectorManipulatorShape;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::tool::ToolActionHandlerData;
use crate::tool::{DocumentToolData, Fsm};
use glam::{DAffine2, DVec2};
@@ -31,8 +33,19 @@ pub enum PathMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
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 {
use PathToolFsmState::*;
match self.fsm_state {
@@ -213,6 +226,74 @@ impl Fsm for PathToolFsmState {
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) {

View File

@@ -1,4 +1,6 @@
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessor;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::tool::snapping::SnapHandler;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolOptions, ToolType};
use crate::{document::DocumentMessageHandler, message_prelude::*};
@@ -26,18 +28,29 @@ pub enum PenMessage {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PenToolFsmState {
Ready,
Dragging,
Drawing,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
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 {
use PenToolFsmState::*;
match self.fsm_state {
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
Dragging => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
Drawing => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
}
}
}
@@ -92,9 +105,9 @@ impl Fsm for PenToolFsmState {
_ => 5,
};
Dragging
Drawing
}
(Dragging, DragStop) => {
(Drawing, DragStop) => {
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
@@ -106,18 +119,18 @@ impl Fsm for PenToolFsmState {
responses.extend(make_operation(data, tool_data, true));
Dragging
Drawing
}
(Dragging, PointerMove) => {
(Drawing, PointerMove) => {
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
data.next_point = pos;
responses.extend(make_operation(data, tool_data, true));
Dragging
Drawing
}
(Dragging, Confirm) | (Dragging, Abort) => {
(Drawing, Confirm) | (Drawing, Abort) => {
if data.points.len() >= 2 {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.extend(make_operation(data, tool_data, false));
@@ -138,6 +151,33 @@ impl Fsm for PenToolFsmState {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<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] {

View File

@@ -1,5 +1,6 @@
use crate::input::keyboard::Key;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessor;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::DAffine2;
@@ -25,13 +26,24 @@ pub enum RectangleMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
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 {
use RectangleToolFsmState::*;
match self.fsm_state {
Ready => actions!(RectangleMessageDiscriminant; DragStart),
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
Drawing => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
@@ -39,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RectangleToolFsmState {
Ready,
Dragging,
Drawing,
}
impl Default for RectangleToolFsmState {
@@ -85,7 +97,7 @@ impl Fsm for RectangleToolFsmState {
.into(),
);
Dragging
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
@@ -94,7 +106,7 @@ impl Fsm for RectangleToolFsmState {
state
}
(Dragging, DragStop) => {
(Drawing, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
@@ -104,7 +116,7 @@ impl Fsm for RectangleToolFsmState {
shape_data.cleanup();
Ready
}
(Dragging, Abort) => {
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup();
@@ -116,4 +128,45 @@ impl Fsm for RectangleToolFsmState {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<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());
}
}

View File

@@ -5,10 +5,13 @@ use graphene::Operation;
use graphene::Quad;
use crate::consts::COLOR_ACCENT;
use crate::input::keyboard::Key;
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::snapping::SnapHandler;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::input::{
keyboard::{Key, MouseMotion},
mouse::ViewportPosition,
InputPreprocessor,
};
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::tool::{snapping::SnapHandler, DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{
consts::SELECTION_TOLERANCE,
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
@@ -39,8 +42,19 @@ pub enum SelectMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
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 {
use SelectToolFsmState::*;
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 {
Ready,
Dragging,
@@ -122,6 +136,7 @@ impl Fsm for SelectToolFsmState {
) -> Self {
use SelectMessage::*;
use SelectToolFsmState::*;
if let ToolMessage::Select(event) = event {
match (self, event) {
(_, UpdateSelectionBoundingBox) => {
@@ -263,4 +278,121 @@ impl Fsm for SelectToolFsmState {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<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());
}
}

View File

@@ -1,5 +1,6 @@
use crate::input::keyboard::Key;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessor;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolOptions, ToolType};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::DAffine2;
@@ -25,13 +26,24 @@ pub enum ShapeMessage {
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
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 {
use ShapeToolFsmState::*;
match self.fsm_state {
Ready => actions!(ShapeMessageDiscriminant; DragStart),
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
Drawing => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
@@ -39,7 +51,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShapeToolFsmState {
Ready,
Dragging,
Drawing,
}
impl Default for ShapeToolFsmState {
@@ -93,7 +105,7 @@ impl Fsm for ShapeToolFsmState {
.into(),
);
Dragging
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
@@ -102,7 +114,7 @@ impl Fsm for ShapeToolFsmState {
state
}
(Dragging, DragStop) => {
(Drawing, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
@@ -112,7 +124,7 @@ impl Fsm for ShapeToolFsmState {
shape_data.cleanup();
Ready
}
(Dragging, Abort) => {
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup();
@@ -124,4 +136,45 @@ impl Fsm for ShapeToolFsmState {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<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());
}
}