Major overhaul of input and communication systems

* Add input manager

* WIP lifetime hell

* Hell yeah, dark lifetime magic

* Replace events with actions in tools

* Fix borrow in GlobalEventHandler

* Fix typo in response-handler

* Distribute dispatch structure

* Add translation from events to actions

* Port default key actions to input_mapper

* Split actions macro

* Add handling for Ambiguous Mouse events

* Fix warnings and clippy lints

* Add actions macro

* WIP rework

* Add AsMessage macro

* Add implementation for derived enums

* Add macro implementation for top level message

* Add #[child] attribute to indicate derivation

* Replace some mentions of Actions and Responses with Message

* It compiles !!!

* Add functionality to some message handlers

* Add document rendering

* ICE

* Rework the previous code while keeping basic functionality

* Reduce parent-top-level macro args to only two

* Add workaround for ICE

* Fix cyclic reference in document.rs

* Make derive_transitive_child a bit more powerful

This addresses the todo that was left,
enabling arbitrary expressions to be passed as the last
parameter of a #[parent] attribute

* Adapt frontend message format

* Make responses use VecDeque

Our responses are a queue so we should use a queue type for them

* Move traits to sensible location

* Are we rectangle yet?

* Simplify, improve & document `derive_discriminant`

* Change `child` to `sub_discriminant`

This only applies to `ToDiscriminant`.
Code using `#[impl_message]` continues to work.

* Add docs for `derive_transitive_child`

* Finish docs and improve macros

The improvements are that impl_message now uses trait
resolution to obtain the parent's discriminant
and that derive_as_message now allows for non-unit
variants (which we don't use but it's nice to have,
just in case)

* Remove logging call

* Move files around and cleanup structure

* Fix proc macro doc tests

* Improve actions_fn!() macro

* Add ellipse tool

* Pass populated actions list to the input mapper

* Add KeyState bitvector

* Merge mouse buttons into "keyboard"

* Add macro for initialization of key mapper table

* Add syntactic sugar for the macro

* Implement mapping function

* Translate the remaining tools

* Fix shape tool

* Add keybindings for line and pen tool

* Fix modifiers

* Cleanup

* Add doc comments for the actions macro

* Fix formatting

* Rename MouseMove to PointerMove

* Add keybinds for tools

* Apply review suggestions

* Rename KeyMappings -> KeyMappingEntries

* Apply review changes

Co-authored-by: T0mstone <realt0mstone@gmail.com>
Co-authored-by: Paul Kupper <kupper.pa@gmail.com>
This commit is contained in:
TrueDoctor
2021-05-23 01:26:24 +02:00
committed by Keavon Chambers
parent c08b2d4b31
commit 4b19a459b7
72 changed files with 3028 additions and 1856 deletions

186
core/editor/src/tool/mod.rs Normal file
View File

@@ -0,0 +1,186 @@
pub mod tool_message_handler;
pub mod tool_settings;
pub mod tools;
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use crate::SvgDocument;
use crate::{
communication::{message::Message, MessageHandler},
Color,
};
use std::collections::VecDeque;
use std::{
collections::HashMap,
fmt::{self, Debug},
};
pub use tool_message_handler::ToolMessageHandler;
use tool_settings::ToolSettings;
pub use tool_settings::*;
use tools::*;
pub mod tool_messages {
pub use super::tool_message_handler::{ToolMessage, ToolMessageDiscriminant};
pub use super::tools::ellipse::{EllipseMessage, EllipseMessageDiscriminant};
pub use super::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant};
}
pub type ToolActionHandlerData<'a> = (&'a SvgDocument, &'a DocumentToolData, &'a InputPreprocessor);
pub trait Fsm {
type ToolData;
fn transition(self, message: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, messages: &mut VecDeque<Message>) -> Self;
}
#[derive(Debug, Clone)]
pub struct DocumentToolData {
pub primary_color: Color,
pub secondary_color: Color,
tool_settings: HashMap<ToolType, ToolSettings>,
}
type SubToolMessageHandler = dyn for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>>;
pub struct ToolData {
pub active_tool_type: ToolType,
pub tools: HashMap<ToolType, Box<SubToolMessageHandler>>,
}
impl fmt::Debug for ToolData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ToolData").field("active_tool_type", &self.active_tool_type).field("tool_settings", &"[…]").finish()
}
}
impl ToolData {
pub fn active_tool_mut(&mut self) -> &mut Box<SubToolMessageHandler> {
self.tools.get_mut(&self.active_tool_type).expect("The active tool is not initialized")
}
pub fn active_tool(&self) -> &SubToolMessageHandler {
self.tools.get(&self.active_tool_type).map(|x| x.as_ref()).expect("The active tool is not initialized")
}
}
#[derive(Debug)]
pub struct ToolFsmState {
pub document_tool_data: DocumentToolData,
pub tool_data: ToolData,
}
impl Default for ToolFsmState {
fn default() -> Self {
ToolFsmState {
tool_data: ToolData {
active_tool_type: ToolType::Select,
tools: gen_tools_hash_map! {
Rectangle => rectangle::Rectangle,
Select => select::Select,
Crop => crop::Crop,
Navigate => navigate::Navigate,
Eyedropper => eyedropper::Eyedropper,
Path => path::Path,
Pen => pen::Pen,
Line => line::Line,
Shape => shape::Shape,
Ellipse => ellipse::Ellipse,
},
},
document_tool_data: DocumentToolData {
primary_color: Color::BLACK,
secondary_color: Color::WHITE,
tool_settings: default_tool_settings(),
},
}
}
}
impl ToolFsmState {
pub fn new() -> Self {
Self::default()
}
pub fn swap_colors(&mut self) {
std::mem::swap(&mut self.document_tool_data.primary_color, &mut self.document_tool_data.secondary_color);
}
}
fn default_tool_settings() -> HashMap<ToolType, ToolSettings> {
let tool_init = |tool: ToolType| (tool, tool.default_settings());
std::array::IntoIter::new([
tool_init(ToolType::Select),
tool_init(ToolType::Ellipse),
tool_init(ToolType::Shape), // TODO: Add more tool defaults
])
.collect()
}
#[repr(usize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ToolType {
Select,
Crop,
Navigate,
Eyedropper,
Text,
Fill,
Gradient,
Brush,
Heal,
Clone,
Patch,
BlurSharpen,
Relight,
Path,
Pen,
Freehand,
Spline,
Line,
Rectangle,
Ellipse,
Shape,
}
impl fmt::Display for ToolType {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
use ToolType::*;
let name = match_variant_name!(match (self) {
Select,
Crop,
Navigate,
Eyedropper,
Text,
Fill,
Gradient,
Brush,
Heal,
Clone,
Patch,
BlurSharpen,
Relight,
Path,
Pen,
Freehand,
Spline,
Line,
Rectangle,
Ellipse,
Shape
});
formatter.write_str(name)
}
}
impl ToolType {
fn default_settings(&self) -> ToolSettings {
match self {
ToolType::Select => ToolSettings::Select { append_mode: SelectAppendMode::New },
ToolType::Ellipse => ToolSettings::Ellipse,
ToolType::Shape => ToolSettings::Shape {
shape: Shape::Polygon { vertices: 3 },
},
_ => todo!(),
}
}
}

View File

@@ -0,0 +1,101 @@
use crate::message_prelude::*;
use document_core::color::Color;
use crate::input::InputPreprocessor;
use crate::{
tool::{ToolFsmState, ToolType},
SvgDocument,
};
use std::collections::VecDeque;
#[impl_message(Message, Tool)]
#[derive(PartialEq, Clone, Debug)]
pub enum ToolMessage {
SelectTool(ToolType),
SelectPrimaryColor(Color),
SelectSecondaryColor(Color),
SwapColors,
ResetColors,
#[child]
Rectangle(RectangleMessage),
#[child]
Ellipse(EllipseMessage),
#[child]
Select(SelectMessage),
#[child]
Line(LineMessage),
#[child]
Crop(CropMessage),
#[child]
Eyedropper(EyedropperMessage),
#[child]
Navigate(NavigateMessage),
#[child]
Path(PathMessage),
#[child]
Pen(PenMessage),
#[child]
Shape(ShapeMessage),
}
#[derive(Debug, Default)]
pub struct ToolMessageHandler {
tool_state: ToolFsmState,
}
impl MessageHandler<ToolMessage, (&SvgDocument, &InputPreprocessor)> for ToolMessageHandler {
fn process_action(&mut self, message: ToolMessage, data: (&SvgDocument, &InputPreprocessor), responses: &mut VecDeque<Message>) {
let (document, input) = data;
use ToolMessage::*;
match message {
SelectPrimaryColor(c) => self.tool_state.document_tool_data.primary_color = c,
SelectSecondaryColor(c) => self.tool_state.document_tool_data.secondary_color = c,
SelectTool(tool) => {
let mut reset = |tool| match tool {
ToolType::Ellipse => responses.push_back(EllipseMessage::Abort.into()),
ToolType::Rectangle => responses.push_back(RectangleMessage::Abort.into()),
ToolType::Shape => responses.push_back(ShapeMessage::Abort.into()),
ToolType::Line => responses.push_back(LineMessage::Abort.into()),
ToolType::Pen => responses.push_back(PenMessage::Abort.into()),
_ => (),
};
reset(tool);
reset(self.tool_state.tool_data.active_tool_type);
self.tool_state.tool_data.active_tool_type = tool;
responses.push_back(FrontendMessage::SetActiveTool { tool_name: tool.to_string() }.into())
}
SwapColors => {
let doc_data = &mut self.tool_state.document_tool_data;
std::mem::swap(&mut doc_data.primary_color, &mut doc_data.secondary_color);
}
ResetColors => {
let doc_data = &mut self.tool_state.document_tool_data;
doc_data.primary_color = Color::WHITE;
doc_data.secondary_color = Color::BLACK;
}
message => {
let tool_type = match message {
Rectangle(_) => ToolType::Rectangle,
Ellipse(_) => ToolType::Ellipse,
Shape(_) => ToolType::Shape,
Line(_) => ToolType::Line,
Pen(_) => ToolType::Pen,
Select(_) => ToolType::Select,
Crop(_) => ToolType::Crop,
Eyedropper(_) => ToolType::Eyedropper,
Navigate(_) => ToolType::Navigate,
Path(_) => ToolType::Path,
_ => unreachable!(),
};
if let Some(tool) = self.tool_state.tool_data.tools.get_mut(&tool_type) {
tool.process_action(message, (&document, &self.tool_state.document_tool_data, input), responses);
}
}
}
}
fn actions(&self) -> ActionList {
let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, SelectTool);
list.extend(self.tool_state.tool_data.active_tool().actions());
list
}
}

View File

@@ -0,0 +1,20 @@
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolSettings {
Select { append_mode: SelectAppendMode },
Ellipse,
Shape { shape: Shape },
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SelectAppendMode {
New,
Add,
Subtract,
Intersect,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Shape {
Star { vertices: u32 },
Polygon { vertices: u32 },
}

View File

@@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Crop;
#[impl_message(Message, ToolMessage, Crop)]
#[derive(PartialEq, Clone, Debug)]
pub enum CropMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Crop {
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

@@ -0,0 +1,173 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
use document_core::{layers::style, Operation};
#[derive(Default)]
pub struct Ellipse {
fsm_state: EllipseToolFsmState,
data: EllipseToolData,
}
#[impl_message(Message, ToolMessage, Ellipse)]
#[derive(PartialEq, Clone, Debug)]
pub enum EllipseMessage {
Undo,
DragStart,
DragStop,
MouseMove,
Abort,
Center,
UnCenter,
LockAspectRatio,
UnlockAspectRatio,
}
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);
}
fn actions(&self) -> ActionList {
use EllipseToolFsmState::*;
match self.fsm_state {
Ready => actions!(EllipseMessageDiscriminant; Undo, DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EllipseToolFsmState {
Ready,
Dragging,
}
impl Default for EllipseToolFsmState {
fn default() -> Self {
EllipseToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
constrain_to_circle: bool,
center_around_cursor: bool,
}
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
use EllipseMessage::*;
use EllipseToolFsmState::*;
if let ToolMessage::Ellipse(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data));
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data));
responses.push_back(Operation::CommitTransaction.into());
}
Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
Ready
}
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, true, Ready),
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_circle, false, Ready),
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, true, tool_data, data, responses, Dragging),
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_circle, false, tool_data, data, responses, Dragging),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging),
_ => self,
}
} else {
self
}
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: EllipseToolFsmState) -> EllipseToolFsmState {
*state = value;
new_state
}
fn update_state(
state: fn(&mut EllipseToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut EllipseToolData,
responses: &mut VecDeque<Message>,
new_state: EllipseToolFsmState,
) -> EllipseToolFsmState {
*(state(data)) = value;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(&data, tool_data));
new_state
}
fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
if data.constrain_to_circle {
let (cx, cy, r) = if data.center_around_cursor {
(x0, y0, f64::hypot(x1 - x0, y1 - y0))
} else {
let diameter = f64::max((x1 - x0).abs(), (y1 - y0).abs());
let (x2, y2) = (x0 + (x1 - x0).signum() * diameter, y0 + (y1 - y0).signum() * diameter);
((x0 + x2) * 0.5, (y0 + y2) * 0.5, diameter * 0.5)
};
Operation::AddCircle {
path: vec![],
insert_index: -1,
cx,
cy,
r,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
} else {
let (cx, cy, r_scale) = if data.center_around_cursor { (x0, y0, 1.0) } else { ((x0 + x1) * 0.5, (y0 + y1) * 0.5, 0.5) };
let (rx, ry) = ((x1 - x0).abs() * r_scale, (y1 - y0).abs() * r_scale);
Operation::AddEllipse {
path: vec![],
insert_index: -1,
cx,
cy,
rx,
ry,
rot: 0.0,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
}
.into()
}

View File

@@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Eyedropper;
#[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Clone, Debug)]
pub enum EyedropperMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
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

@@ -0,0 +1,184 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
use document_core::{layers::style, Operation};
use std::f64::consts::PI;
#[derive(Default)]
pub struct Line {
fsm_state: LineToolFsmState,
data: LineToolData,
}
#[impl_message(Message, ToolMessage, Line)]
#[derive(PartialEq, Clone, Debug)]
pub enum LineMessage {
DragStart,
DragStop,
MouseMove,
Abort,
Center,
UnCenter,
LockAngle,
UnlockAngle,
SnapToAngle,
UnSnapToAngle,
}
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);
}
fn actions(&self) -> ActionList {
use LineToolFsmState::*;
match self.fsm_state {
Ready => actions!(LineMessageDiscriminant; DragStart, Center, UnCenter, LockAngle, UnlockAngle, SnapToAngle, UnSnapToAngle),
Dragging => actions!(LineMessageDiscriminant; DragStop, MouseMove, Abort, Center, UnCenter, LockAngle, UnlockAngle, SnapToAngle, UnSnapToAngle),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LineToolFsmState {
Ready,
Dragging,
}
impl Default for LineToolFsmState {
fn default() -> Self {
LineToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct LineToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
angle: f64,
snap_angle: bool,
lock_angle: bool,
center_around_cursor: bool,
}
impl Fsm for LineToolFsmState {
type ToolData = LineToolData;
fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
use LineMessage::*;
use LineToolFsmState::*;
if let ToolMessage::Line(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data));
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data));
responses.push_back(Operation::CommitTransaction.into());
}
Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
Ready
}
(Ready, LockAngle) => update_state_no_op(&mut data.lock_angle, true, Ready),
(Ready, UnlockAngle) => update_state_no_op(&mut data.lock_angle, false, Ready),
(Dragging, LockAngle) => update_state(|data| &mut data.lock_angle, true, tool_data, data, responses, Dragging),
(Dragging, UnlockAngle) => update_state(|data| &mut data.lock_angle, false, tool_data, data, responses, Dragging),
(Ready, SnapToAngle) => update_state_no_op(&mut data.snap_angle, true, Ready),
(Ready, UnSnapToAngle) => update_state_no_op(&mut data.snap_angle, false, Ready),
(Dragging, SnapToAngle) => update_state(|data| &mut data.snap_angle, true, tool_data, data, responses, Dragging),
(Dragging, UnSnapToAngle) => update_state(|data| &mut data.snap_angle, false, tool_data, data, responses, Dragging),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging),
_ => self,
}
} else {
self
}
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: LineToolFsmState) -> LineToolFsmState {
*state = value;
new_state
}
fn update_state(
state: fn(&mut LineToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut LineToolData,
responses: &mut VecDeque<Message>,
new_state: LineToolFsmState,
) -> LineToolFsmState {
*(state(data)) = value;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data));
new_state
}
fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
let (dx, dy) = (x1 - x0, y1 - y0);
let mut angle = f64::atan2(dx, dy);
if data.lock_angle {
angle = data.angle
};
if data.snap_angle {
let snap_resolution = 12.0;
angle = (angle * snap_resolution / PI).round() / snap_resolution * PI;
}
data.angle = angle;
let (dir_x, dir_y) = (f64::sin(angle), f64::cos(angle));
let projected_length = dx * dir_x + dy * dir_y;
let (x1, y1) = (x0 + dir_x * projected_length, y0 + dir_y * projected_length);
let (x0, y0) = if data.center_around_cursor { (x0 - (x1 - x0), y0 - (y1 - y0)) } else { (x0, y0) };
Operation::AddLine {
path: vec![],
insert_index: -1,
x0,
y0,
x1,
y1,
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None),
}
.into()
}

View File

@@ -0,0 +1,13 @@
// already implemented
pub mod ellipse;
pub mod line;
pub mod pen;
pub mod rectangle;
pub mod shape;
// not implemented yet
pub mod crop;
pub mod eyedropper;
pub mod navigate;
pub mod path;
pub mod select;

View File

@@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Navigate;
#[impl_message(Message, ToolMessage, Navigate)]
#[derive(PartialEq, Clone, Debug)]
pub enum NavigateMessage {
MouseMove,
}
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

@@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Path;
#[impl_message(Message, ToolMessage, Path)]
#[derive(PartialEq, Clone, Debug)]
pub enum PathMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
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

@@ -0,0 +1,130 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
use document_core::{layers::style, Operation};
#[derive(Default)]
pub struct Pen {
fsm_state: PenToolFsmState,
data: PenToolData,
}
#[impl_message(Message, ToolMessage, Pen)]
#[derive(PartialEq, Clone, Debug)]
pub enum PenMessage {
Undo,
DragStart,
DragStop,
MouseMove,
Confirm,
Abort,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PenToolFsmState {
Ready,
Dragging,
}
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);
}
fn actions(&self) -> ActionList {
use PenToolFsmState::*;
match self.fsm_state {
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
Dragging => actions!(PenMessageDiscriminant; DragStop, MouseMove, Confirm, Abort),
}
}
}
impl Default for PenToolFsmState {
fn default() -> Self {
PenToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct PenToolData {
points: Vec<ViewportPosition>,
next_point: ViewportPosition,
}
impl Fsm for PenToolFsmState {
type ToolData = PenToolData;
fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
use PenMessage::*;
use PenToolFsmState::*;
if let ToolMessage::Pen(event) = event {
match (self, event) {
(Ready, DragStart) => {
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
data.points.push(input.mouse.position);
data.next_point = input.mouse.position;
Dragging
}
(Dragging, DragStop) => {
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.points.last() != Some(&input.mouse.position) {
data.points.push(input.mouse.position);
data.next_point = input.mouse.position;
}
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, true));
Dragging
}
(Dragging, MouseMove) => {
data.next_point = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, true));
Dragging
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Confirm) => {
responses.push_back(Operation::ClearWorkingFolder.into());
if data.points.len() >= 2 {
responses.push_back(make_operation(data, tool_data, false));
responses.push_back(Operation::CommitTransaction.into());
} else {
responses.push_back(Operation::DiscardWorkingFolder.into());
}
data.points.clear();
Ready
}
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
data.points.clear();
Ready
}
_ => self,
}
} else {
self
}
}
}
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message {
let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.x as f64, p.y as f64)).collect();
if show_preview {
points.push((data.next_point.x as f64, data.next_point.y as f64))
}
Operation::AddPen {
path: vec![],
insert_index: -1,
points,
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), Some(style::Fill::none())),
}
.into()
}

View File

@@ -0,0 +1,171 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
use document_core::{layers::style, Operation};
#[derive(Default)]
pub struct Rectangle {
fsm_state: RectangleToolFsmState,
data: RectangleToolData,
}
#[impl_message(Message, ToolMessage, Rectangle)]
#[derive(PartialEq, Clone, Debug)]
pub enum RectangleMessage {
DragStart,
DragStop,
MouseMove,
Abort,
Center,
UnCenter,
LockAspectRatio,
UnlockAspectRatio,
}
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);
}
fn actions(&self) -> ActionList {
use RectangleToolFsmState::*;
match self.fsm_state {
Ready => actions!(RectangleMessageDiscriminant; DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RectangleToolFsmState {
Ready,
Dragging,
}
impl Default for RectangleToolFsmState {
fn default() -> Self {
RectangleToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct RectangleToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
constrain_to_square: bool,
center_around_cursor: bool,
}
impl Fsm for RectangleToolFsmState {
type ToolData = RectangleToolData;
fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
use RectangleMessage::*;
use RectangleToolFsmState::*;
if let ToolMessage::Rectangle(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data));
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data));
responses.push_back(Operation::CommitTransaction.into());
}
Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
Ready
}
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready),
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready),
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging),
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging),
_ => self,
}
} else {
self
}
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: RectangleToolFsmState) -> RectangleToolFsmState {
*state = value;
new_state
}
fn update_state(
state: fn(&mut RectangleToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut RectangleToolData,
responses: &mut VecDeque<Message>,
new_state: RectangleToolFsmState,
) -> RectangleToolFsmState {
*(state(data)) = value;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data));
new_state
}
fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
let (x0, y0, x1, y1) = if data.constrain_to_square {
let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum());
let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs());
if data.center_around_cursor {
(x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
} else {
(x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
}
} else {
let (x0, y0) = if data.center_around_cursor {
let delta_x = x1 - x0;
let delta_y = y1 - y0;
(x0 - delta_x, y0 - delta_y)
} else {
(x0, y0)
};
(x0, y0, x1, y1)
};
Operation::AddRect {
path: vec![],
insert_index: -1,
x0,
y0,
x1,
y1,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into()
}

View File

@@ -0,0 +1,60 @@
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
#[derive(Default)]
pub struct Select {
fsm_state: SelectToolFsmState,
data: SelectToolData,
}
#[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Clone, Debug)]
pub enum SelectMessage {
MouseMove,
}
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);
}
advertise_actions!();
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SelectToolFsmState {
Ready,
}
impl Default for SelectToolFsmState {
fn default() -> Self {
SelectToolFsmState::Ready
}
}
#[derive(Default)]
struct SelectToolData;
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
fn transition(
self,
event: ToolMessage,
_document: &SvgDocument,
_tool_data: &DocumentToolData,
_data: &mut Self::ToolData,
_input: &InputPreprocessor,
_responses: &mut VecDeque<Message>,
) -> Self {
use SelectMessage::*;
use SelectToolFsmState::*;
if let ToolMessage::Select(event) = event {
match (self, event) {
(Ready, MouseMove) => self,
}
} else {
self
}
}
}

View File

@@ -0,0 +1,175 @@
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{message_prelude::*, SvgDocument};
use document_core::{layers::style, Operation};
#[derive(Default)]
pub struct Shape {
fsm_state: ShapeToolFsmState,
data: ShapeToolData,
}
#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug)]
pub enum ShapeMessage {
Undo,
DragStart,
DragStop,
MouseMove,
Abort,
Center,
UnCenter,
LockAspectRatio,
UnlockAspectRatio,
}
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);
}
fn actions(&self) -> ActionList {
use ShapeToolFsmState::*;
match self.fsm_state {
Ready => actions!(ShapeMessageDiscriminant; Undo, DragStart, Center, UnCenter, LockAspectRatio, UnlockAspectRatio),
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Center, UnCenter, LockAspectRatio, UnlockAspectRatio, MouseMove, Abort),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShapeToolFsmState {
Ready,
Dragging,
}
impl Default for ShapeToolFsmState {
fn default() -> Self {
ShapeToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct ShapeToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
constrain_to_square: bool,
center_around_cursor: bool,
sides: u8,
}
impl Fsm for ShapeToolFsmState {
type ToolData = ShapeToolData;
fn transition(self, event: ToolMessage, _document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, responses: &mut VecDeque<Message>) -> Self {
use ShapeMessage::*;
use ShapeToolFsmState::*;
if let ToolMessage::Shape(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
data.sides = 6;
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
Dragging
}
(Dragging, MouseMove) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data));
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
responses.push_back(Operation::ClearWorkingFolder.into());
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
responses.push_back(make_operation(data, tool_data));
responses.push_back(Operation::CommitTransaction.into());
}
Ready
}
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
Ready
}
(Ready, LockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, true, Ready),
(Ready, UnlockAspectRatio) => update_state_no_op(&mut data.constrain_to_square, false, Ready),
(Dragging, LockAspectRatio) => update_state(|data| &mut data.constrain_to_square, true, tool_data, data, responses, Dragging),
(Dragging, UnlockAspectRatio) => update_state(|data| &mut data.constrain_to_square, false, tool_data, data, responses, Dragging),
(Ready, Center) => update_state_no_op(&mut data.center_around_cursor, true, Ready),
(Ready, UnCenter) => update_state_no_op(&mut data.center_around_cursor, false, Ready),
(Dragging, Center) => update_state(|data| &mut data.center_around_cursor, true, tool_data, data, responses, Dragging),
(Dragging, UnCenter) => update_state(|data| &mut data.center_around_cursor, false, tool_data, data, responses, Dragging),
_ => self,
}
} else {
self
}
}
}
fn update_state_no_op(state: &mut bool, value: bool, new_state: ShapeToolFsmState) -> ShapeToolFsmState {
*state = value;
new_state
}
fn update_state(
state: fn(&mut ShapeToolData) -> &mut bool,
value: bool,
tool_data: &DocumentToolData,
data: &mut ShapeToolData,
responses: &mut VecDeque<Message>,
new_state: ShapeToolFsmState,
) -> ShapeToolFsmState {
*(state(data)) = value;
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data));
new_state
}
fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData) -> Message {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
let (x0, y0, x1, y1) = if data.constrain_to_square {
let (x_dir, y_dir) = ((x1 - x0).signum(), (y1 - y0).signum());
let max_dist = f64::max((x1 - x0).abs(), (y1 - y0).abs());
if data.center_around_cursor {
(x0 - max_dist * x_dir, y0 - max_dist * y_dir, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
} else {
(x0, y0, x0 + max_dist * x_dir, y0 + max_dist * y_dir)
}
} else {
let (x0, y0) = if data.center_around_cursor {
let delta_x = x1 - x0;
let delta_y = y1 - y0;
(x0 - delta_x, y0 - delta_y)
} else {
(x0, y0)
};
(x0, y0, x1, y1)
};
Operation::AddShape {
path: vec![],
insert_index: -1,
x0,
y0,
x1,
y1,
sides: data.sides,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into()
}