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

View File

@@ -0,0 +1,73 @@
use crate::{frontend::FrontendMessageHandler, message_prelude::*, Callback, EditorError};
pub use crate::document::DocumentMessageHandler;
pub use crate::input::{InputMapper, InputPreprocessor};
pub use crate::tool::ToolMessageHandler;
use crate::global::GlobalMessageHandler;
use std::collections::VecDeque;
pub struct Dispatcher {
frontend_message_handler: FrontendMessageHandler,
input_preprocessor: InputPreprocessor,
input_mapper: InputMapper,
global_message_handler: GlobalMessageHandler,
tool_message_handler: ToolMessageHandler,
document_message_handler: DocumentMessageHandler,
messages: VecDeque<Message>,
}
impl Dispatcher {
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Result<(), EditorError> {
let message = message.into();
use Message::*;
if !matches!(
message,
Message::InputPreprocessor(_) | Message::InputMapper(_) | Message::Tool(ToolMessage::Rectangle(RectangleMessage::MouseMove))
) {
log::trace!("Message: {}", message.to_discriminant().global_name());
}
match message {
NoOp => (),
Document(message) => self.document_message_handler.process_action(message, (), &mut self.messages),
Global(message) => self.global_message_handler.process_action(message, (), &mut self.messages),
Tool(message) => self
.tool_message_handler
.process_action(message, (&self.document_message_handler.active_document().document, &self.input_preprocessor), &mut self.messages),
Frontend(message) => self.frontend_message_handler.process_action(message, (), &mut self.messages),
InputPreprocessor(message) => self.input_preprocessor.process_action(message, (), &mut self.messages),
InputMapper(message) => {
let actions = self.collect_actions();
self.input_mapper.process_action(message, (&self.input_preprocessor, actions), &mut self.messages)
}
}
if let Some(message) = self.messages.pop_front() {
self.handle_message(message)?;
}
Ok(())
}
pub fn collect_actions(&self) -> ActionList {
//TODO: reduce the number of heap allocations
let mut list = Vec::new();
list.extend(self.frontend_message_handler.actions());
list.extend(self.input_preprocessor.actions());
list.extend(self.input_mapper.actions());
list.extend(self.global_message_handler.actions());
list.extend(self.tool_message_handler.actions());
list.extend(self.document_message_handler.actions());
list
}
pub fn new(callback: Callback) -> Dispatcher {
Dispatcher {
frontend_message_handler: FrontendMessageHandler::new(callback),
input_preprocessor: InputPreprocessor::default(),
global_message_handler: GlobalMessageHandler::new(),
input_mapper: InputMapper::default(),
document_message_handler: DocumentMessageHandler::default(),
tool_message_handler: ToolMessageHandler::default(),
messages: VecDeque::new(),
}
}
}

View File

@@ -0,0 +1,30 @@
use crate::message_prelude::*;
use graphite_proc_macros::*;
pub trait AsMessage: TransitiveChild
where
Self::TopParent: TransitiveChild<Parent = Self::TopParent, TopParent = Self::TopParent> + AsMessage,
{
fn local_name(self) -> String;
fn global_name(self) -> String {
<Self as Into<Self::TopParent>>::into(self).local_name()
}
}
#[impl_message]
#[derive(PartialEq, Clone, Debug)]
pub enum Message {
NoOp,
#[child]
Document(DocumentMessage),
#[child]
Global(GlobalMessage),
#[child]
Tool(ToolMessage),
#[child]
Frontend(FrontendMessage),
#[child]
InputPreprocessor(InputPreprocessorMessage),
#[child]
InputMapper(InputMapperMessage),
}

View File

@@ -0,0 +1,23 @@
pub mod dispatcher;
pub mod message;
use crate::message_prelude::*;
pub use dispatcher::*;
pub use crate::input::InputPreprocessor;
use std::collections::VecDeque;
pub type ActionList = Vec<Vec<MessageDiscriminant>>;
// TODO: Add Send + Sync requirement
// Use something like rw locks for synchronization
pub trait MessageHandlerData {}
pub trait MessageHandler<A: ToDiscriminant, T>
where
A::Discriminant: AsMessage,
<A::Discriminant as TransitiveChild>::TopParent: TransitiveChild<Parent = <A::Discriminant as TransitiveChild>::TopParent, TopParent = <A::Discriminant as TransitiveChild>::TopParent> + AsMessage,
{
/// Return true if the Action is consumed.
fn process_action(&mut self, action: A, data: T, responses: &mut VecDeque<Message>);
fn actions(&self) -> ActionList;
}

View File

@@ -1,9 +0,0 @@
use super::{Event, EventHandler, Operation, Response};
use crate::tools::{DocumentToolData, ToolData};
use crate::Document;
pub struct DocumentEventHandler {}
impl DocumentEventHandler {
fn pre_process_event(&mut self, editor_state: &Document, tool_data: &mut DocumentToolData, events: &mut Vec<Event>, responses: &mut Vec<Response>, operations: &mut Vec<Operation>) {}
}

View File

@@ -1,211 +0,0 @@
use crate::tools::ToolType;
use crate::Color;
use bitflags::bitflags;
use document_core::LayerId;
use serde::{Deserialize, Serialize};
#[doc(inline)]
pub use document_core::DocumentResponse;
use std::{
fmt,
ops::{Deref, DerefMut},
};
#[derive(Debug, Clone)]
#[repr(C)]
pub enum Event {
SelectTool(ToolType),
SelectPrimaryColor(Color),
SelectSecondaryColor(Color),
SelectLayer(Vec<LayerId>),
ToggleLayerVisibility(Vec<LayerId>),
ToggleLayerExpansion(Vec<LayerId>),
DeleteLayer(Vec<LayerId>),
AddLayer(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String),
SwapColors,
ResetColors,
AmbiguousMouseDown(MouseState),
AmbiguousMouseUp(MouseState),
LmbDown(MouseState),
RmbDown(MouseState),
MmbDown(MouseState),
LmbUp(MouseState),
RmbUp(MouseState),
MmbUp(MouseState),
MouseMove(ViewportPosition),
KeyUp(Key),
KeyDown(Key),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[repr(C)]
pub enum ToolResponse {
// These may not have the same names as any of the DocumentResponses
SetActiveTool { tool_name: String },
UpdateCanvas { document: String },
}
impl fmt::Display for ToolResponse {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
use ToolResponse::*;
let name = match_variant_name!(match (self) {
SetActiveTool,
UpdateCanvas,
});
formatter.write_str(name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[repr(C)]
// TODO - Make Copy when possible
pub enum Response {
Tool(ToolResponse),
Document(DocumentResponse),
}
impl From<ToolResponse> for Response {
fn from(response: ToolResponse) -> Self {
Response::Tool(response)
}
}
impl From<DocumentResponse> for Response {
fn from(response: DocumentResponse) -> Self {
Response::Document(response)
}
}
impl fmt::Display for Response {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
use Response::*;
let name = match_variant_name!(match (self) {
Tool,
Document
});
let appendix = match self {
Tool(t) => t.to_string(),
Document(d) => d.to_string(),
};
formatter.write_str(format!("{}::{}", name, appendix).as_str())
}
}
#[derive(Debug, Clone, Default)]
pub struct Trace(Vec<TracePoint>);
impl Deref for Trace {
type Target = Vec<TracePoint>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Trace {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Trace {
pub fn new() -> Self {
Self::default()
}
}
// origin is top left
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub struct ViewportPosition {
pub x: u32,
pub y: u32,
}
impl ViewportPosition {
pub fn distance(&self, other: &Self) -> f64 {
let x_diff = other.x as f64 - self.x as f64;
let y_diff = other.y as f64 - self.y as f64;
f64::sqrt(x_diff * x_diff + y_diff * y_diff)
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub struct TracePoint {
pub mouse_state: MouseState,
pub mod_keys: ModKeys,
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub struct MouseState {
pub position: ViewportPosition,
pub mouse_keys: MouseKeys,
}
impl MouseState {
pub fn new() -> MouseState {
Self::default()
}
pub fn from_pos(x: u32, y: u32) -> MouseState {
MouseState {
position: ViewportPosition { x, y },
mouse_keys: MouseKeys::default(),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Key {
UnknownKey,
KeyR,
KeyM,
KeyE,
KeyL,
KeyP,
KeyV,
KeyX,
KeyZ,
KeyY,
KeyEnter,
Key0,
Key1,
Key2,
Key3,
Key4,
Key5,
Key6,
Key7,
Key8,
Key9,
KeyShift,
KeyControl,
KeyAlt,
KeyEscape,
}
bitflags! {
#[derive(Default)]
#[repr(transparent)]
pub struct ModKeys: u8 {
const CONTROL = 0b0000_0001;
const SHIFT = 0b0000_0010;
const ALT = 0b0000_0100;
}
}
bitflags! {
#[derive(Default)]
#[repr(transparent)]
pub struct MouseKeys: u8 {
const LEFT = 0b0000_0001;
const RIGHT = 0b0000_0010;
const MIDDLE = 0b0000_0100;
}
}

View File

@@ -1,15 +0,0 @@
use super::{input_manager::InputManager, Event, EventHandler, Operation, Response};
use crate::tools::{DocumentToolData, ToolData, ToolSettings};
use document_core::document::Document;
pub struct GlobalEventHandler {}
impl GlobalEventHandler {
fn new(tool_data: ToolData) -> Self {
Self {}
}
fn pre_process_event(&mut self, input: &InputManager, events: &mut Vec<Event>, responses: &mut Vec<Response>, operations: &mut Vec<Operation>) -> bool {
false
}
}

View File

@@ -1,170 +0,0 @@
pub mod events;
use crate::{tools::ToolType, Color, Document, EditorError, EditorState};
use document_core::Operation;
use events::{DocumentResponse, Event, Key, Response, ToolResponse};
pub type Callback = Box<dyn Fn(Response)>;
pub struct Dispatcher {
callback: Callback,
}
impl Dispatcher {
pub fn handle_event(&self, editor_state: &mut EditorState, event: &Event) -> Result<(), EditorError> {
log::trace!("{:?}", event);
match event {
Event::SelectTool(tool_name) => {
editor_state.tool_state.tool_data.active_tool_type = *tool_name;
self.dispatch_response(ToolResponse::SetActiveTool { tool_name: tool_name.to_string() });
}
Event::SelectPrimaryColor(color) => {
editor_state.tool_state.document_tool_data.primary_color = *color;
}
Event::SelectSecondaryColor(color) => {
editor_state.tool_state.document_tool_data.secondary_color = *color;
}
Event::SwapColors => {
editor_state.tool_state.swap_colors();
}
Event::ResetColors => {
editor_state.tool_state.document_tool_data.primary_color = Color::BLACK;
editor_state.tool_state.document_tool_data.secondary_color = Color::WHITE;
}
Event::LmbDown(mouse_state) | Event::RmbDown(mouse_state) | Event::MmbDown(mouse_state) | Event::LmbUp(mouse_state) | Event::RmbUp(mouse_state) | Event::MmbUp(mouse_state) => {
editor_state.tool_state.document_tool_data.mouse_state = *mouse_state;
}
Event::MouseMove(pos) => {
editor_state.tool_state.document_tool_data.mouse_state.position = *pos;
}
Event::ToggleLayerVisibility(path) => {
let document_responses = self.dispatch_operations(&mut editor_state.document, vec![Operation::ToggleVisibility { path: path.clone() }]);
self.dispatch_response(ToolResponse::UpdateCanvas {
document: editor_state.document.render_root(),
});
self.dispatch_responses(document_responses);
}
Event::KeyUp(_key) => (),
Event::KeyDown(key) => {
log::trace!("pressed key {:?}", key);
log::debug!("pressed key {:?}", key);
match key {
Key::Key0 => {
log::set_max_level(log::LevelFilter::Info);
log::debug!("set log verbosity to info");
}
Key::Key1 => {
log::set_max_level(log::LevelFilter::Debug);
log::debug!("set log verbosity to debug");
}
Key::Key2 => {
log::set_max_level(log::LevelFilter::Trace);
log::debug!("set log verbosity to trace");
}
Key::KeyV => {
editor_state.tool_state.tool_data.active_tool_type = ToolType::Select;
self.dispatch_response(ToolResponse::SetActiveTool {
tool_name: ToolType::Select.to_string(),
});
}
Key::KeyL => {
editor_state.tool_state.tool_data.active_tool_type = ToolType::Line;
self.dispatch_response(ToolResponse::SetActiveTool {
tool_name: ToolType::Line.to_string(),
});
}
Key::KeyP => {
editor_state.tool_state.tool_data.active_tool_type = ToolType::Pen;
self.dispatch_response(ToolResponse::SetActiveTool { tool_name: ToolType::Pen.to_string() });
}
Key::KeyM => {
editor_state.tool_state.tool_data.active_tool_type = ToolType::Rectangle;
self.dispatch_response(ToolResponse::SetActiveTool {
tool_name: ToolType::Rectangle.to_string(),
});
}
Key::KeyY => {
editor_state.tool_state.tool_data.active_tool_type = ToolType::Shape;
self.dispatch_response(ToolResponse::SetActiveTool {
tool_name: ToolType::Shape.to_string(),
});
}
Key::KeyE => {
editor_state.tool_state.tool_data.active_tool_type = ToolType::Ellipse;
self.dispatch_response(ToolResponse::SetActiveTool {
tool_name: ToolType::Ellipse.to_string(),
});
}
Key::KeyX => {
editor_state.tool_state.swap_colors();
}
_ => (),
}
}
_ => todo!("Implement layer handling"),
}
let (mut tool_responses, operations) = editor_state
.tool_state
.tool_data
.active_tool()?
.handle_input(event, &editor_state.document, &editor_state.tool_state.document_tool_data);
let mut document_responses = self.dispatch_operations(&mut editor_state.document, operations);
//let changes = document_responses.drain_filter(|x| x == DocumentResponse::DocumentChanged);
let mut canvas_dirty = false;
let mut i = 0;
while i < document_responses.len() {
if matches!(document_responses[i], DocumentResponse::DocumentChanged) {
canvas_dirty = true;
document_responses.remove(i);
} else {
i += 1;
}
}
if canvas_dirty {
tool_responses.push(ToolResponse::UpdateCanvas {
document: editor_state.document.render_root(),
})
}
self.dispatch_responses(tool_responses);
self.dispatch_responses(document_responses);
Ok(())
}
fn dispatch_operations<I: IntoIterator<Item = Operation>>(&self, document: &mut Document, operations: I) -> Vec<DocumentResponse> {
let mut responses = vec![];
for operation in operations {
match self.dispatch_operation(document, operation) {
Ok(Some(mut res)) => {
responses.append(&mut res);
}
Ok(None) => (),
Err(error) => log::error!("{}", error),
}
}
responses
}
fn dispatch_operation(&self, document: &mut Document, operation: Operation) -> Result<Option<Vec<DocumentResponse>>, EditorError> {
Ok(document.handle_operation(operation)?)
}
pub fn dispatch_responses<T: Into<Response>, I: IntoIterator<Item = T>>(&self, responses: I) {
for response in responses {
self.dispatch_response(response);
}
}
pub fn dispatch_response<T: Into<Response>>(&self, response: T) {
let func = &self.callback;
let response: Response = response.into();
log::trace!("Sending {} Response", response);
func(response)
}
pub fn new(callback: Callback) -> Dispatcher {
Dispatcher { callback }
}
}

View File

@@ -0,0 +1,7 @@
use document_core::document::Document as InteralDocument;
#[derive(Clone, Debug, Default)]
pub struct Document {
pub document: InteralDocument,
pub name: String,
}

View File

@@ -0,0 +1,100 @@
use crate::message_prelude::*;
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
use crate::document::Document;
use std::collections::VecDeque;
#[impl_message(Message, Document)]
#[derive(PartialEq, Clone, Debug)]
pub enum DocumentMessage {
DispatchOperation(DocumentOperation),
SelectLayer(Vec<LayerId>),
DeleteLayer(Vec<LayerId>),
AddFolder(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String),
ToggleLayerVisibility(Vec<LayerId>),
ToggleLayerExpansion(Vec<LayerId>),
SelectDocument(usize),
RenderDocument,
Undo,
}
impl From<DocumentOperation> for DocumentMessage {
fn from(operation: DocumentOperation) -> DocumentMessage {
Self::DispatchOperation(operation)
}
}
impl From<DocumentOperation> for Message {
fn from(operation: DocumentOperation) -> Message {
DocumentMessage::DispatchOperation(operation).into()
}
}
#[derive(Debug, Clone)]
pub struct DocumentMessageHandler {
documents: Vec<Document>,
active_document: usize,
}
impl DocumentMessageHandler {
pub fn active_document(&self) -> &Document {
&self.documents[self.active_document]
}
pub fn active_document_mut(&mut self) -> &mut Document {
&mut self.documents[self.active_document]
}
fn filter_document_responses(&self, document_responses: &mut Vec<DocumentResponse>) -> bool {
let len = document_responses.len();
document_responses.retain(|response| !matches!(response, DocumentResponse::DocumentChanged));
document_responses.len() != len
}
}
impl Default for DocumentMessageHandler {
fn default() -> Self {
Self {
documents: vec![Document::default()],
active_document: 0,
}
}
}
impl MessageHandler<DocumentMessage, ()> for DocumentMessageHandler {
fn process_action(&mut self, message: DocumentMessage, _data: (), responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
match message {
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
SelectDocument(id) => {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
self.active_document = id;
}
ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
}
Undo => {
// this is a temporary fix and will be addressed by #123
if let Some(id) = self.active_document().document.root.list_layers().last() {
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
}
}
DispatchOperation(op) => {
if let Ok(Some(mut document_responses)) = self.active_document_mut().document.handle_operation(op) {
let canvas_dirty = self.filter_document_responses(&mut document_responses);
responses.extend(document_responses.into_iter().map(Into::into));
if canvas_dirty {
responses.push_back(RenderDocument.into())
}
}
}
RenderDocument => responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(),
}
.into(),
),
message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()),
}
}
advertise_actions!(DocumentMessageDiscriminant; Undo, RenderDocument);
}

View File

@@ -0,0 +1,8 @@
mod document_file;
mod document_message_handler;
#[doc(inline)]
pub use document_file::Document;
#[doc(inline)]
pub use document_message_handler::{DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler};

View File

@@ -0,0 +1,59 @@
use crate::message_prelude::*;
use document_core::{response::LayerPanelEntry, DocumentResponse, LayerId};
use serde::{Deserialize, Serialize};
pub type Callback = Box<dyn Fn(FrontendMessage)>;
#[impl_message(Message, Frontend)]
#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)]
pub enum FrontendMessage {
CollapseFolder { path: Vec<LayerId> },
ExpandFolder { path: Vec<LayerId>, children: Vec<LayerPanelEntry> },
SetActiveTool { tool_name: String },
UpdateCanvas { document: String },
EnableTextInput,
DisableTextInput,
}
impl From<DocumentResponse> for Message {
fn from(response: DocumentResponse) -> Self {
let frontend: FrontendMessage = response.into();
frontend.into()
}
}
impl From<DocumentResponse> for FrontendMessage {
fn from(response: DocumentResponse) -> Self {
match response {
DocumentResponse::ExpandFolder { path, children } => Self::ExpandFolder { path, children },
DocumentResponse::CollapseFolder { path } => Self::CollapseFolder { path },
_ => unimplemented!("The frontend does not handle {:?}", response),
}
}
}
pub struct FrontendMessageHandler {
callback: crate::Callback,
}
impl FrontendMessageHandler {
pub fn new(callback: Callback) -> Self {
Self { callback }
}
}
impl MessageHandler<FrontendMessage, ()> for FrontendMessageHandler {
fn process_action(&mut self, message: FrontendMessage, _data: (), _responses: &mut VecDeque<Message>) {
log::trace!("Sending {} Response", message.to_discriminant().global_name());
(self.callback)(message)
}
advertise_actions!(
FrontendMessageDiscriminant;
CollapseFolder,
ExpandFolder,
SetActiveTool,
UpdateCanvas,
EnableTextInput,
DisableTextInput,
);
}

View File

@@ -0,0 +1,3 @@
pub mod frontend_message_handler;
pub use frontend_message_handler::{Callback, FrontendMessage, FrontendMessageDiscriminant, FrontendMessageHandler};

View File

@@ -0,0 +1,40 @@
use crate::message_prelude::*;
use std::collections::VecDeque;
#[impl_message(Message, Global)]
#[derive(PartialEq, Clone, Debug)]
pub enum GlobalMessage {
LogInfo,
LogDebug,
LogTrace,
}
#[derive(Debug, Default)]
pub struct GlobalMessageHandler {}
impl GlobalMessageHandler {
pub fn new() -> Self {
Self::default()
}
}
impl MessageHandler<GlobalMessage, ()> for GlobalMessageHandler {
fn process_action(&mut self, message: GlobalMessage, _data: (), _responses: &mut VecDeque<Message>) {
use GlobalMessage::*;
match message {
LogInfo => {
log::set_max_level(log::LevelFilter::Info);
log::info!("set log verbosity to info");
}
LogDebug => {
log::set_max_level(log::LevelFilter::Debug);
log::info!("set log verbosity to debug");
}
LogTrace => {
log::set_max_level(log::LevelFilter::Trace);
log::info!("set log verbosity to trace");
}
}
}
advertise_actions!(GlobalMessageDiscriminant; LogInfo, LogDebug, LogTrace);
}

View File

@@ -0,0 +1,3 @@
pub mod global_message_handler;
pub use global_message_handler::{GlobalMessage, GlobalMessageDiscriminant, GlobalMessageHandler};

View File

@@ -1,5 +0,0 @@
use std::collections::HashMap;
pub trait Hint {
fn hints(&self) -> HashMap<String, String>;
}

View File

@@ -0,0 +1,191 @@
use crate::message_prelude::*;
use crate::tool::ToolType;
use super::{
keyboard::{Key, KeyStates, NUMBER_OF_KEYS},
InputPreprocessor,
};
#[impl_message(Message, InputMapper)]
#[derive(PartialEq, Clone, Debug)]
pub enum InputMapperMessage {
PointerMove,
KeyUp(Key),
KeyDown(Key),
}
#[derive(PartialEq, Clone, Debug)]
struct MappingEntry {
trigger: InputMapperMessage,
modifiers: KeyStates,
action: Message,
}
#[derive(Debug, Clone, Default)]
struct KeyMappingEntries(Vec<MappingEntry>);
impl KeyMappingEntries {
fn match_mapping(&self, keys: &KeyStates, actions: ActionList) -> Option<Message> {
for entry in self.0.iter() {
let all_required_modifiers_pressed = ((*keys & entry.modifiers) ^ entry.modifiers).is_empty();
if all_required_modifiers_pressed && actions.iter().flatten().any(|action| entry.action.to_discriminant() == *action) {
return Some(entry.action.clone());
}
}
None
}
fn push(&mut self, entry: MappingEntry) {
self.0.push(entry)
}
}
#[derive(Debug, Clone)]
struct Mapping {
up: [KeyMappingEntries; NUMBER_OF_KEYS],
down: [KeyMappingEntries; NUMBER_OF_KEYS],
pointer_move: KeyMappingEntries,
}
macro_rules! modifiers {
($($m:ident),*) => {{
#[allow(unused_mut)]
let mut state = KeyStates::new();
$(
state.set(Key::$m as usize);
),*
state
}};
}
macro_rules! entry {
{action=$action:expr, key_down=$key:ident $(, modifiers=[$($m:ident),* $(,)?])?} => {{
entry!{action=$action, message=InputMapperMessage::KeyDown(Key::$key) $(, modifiers=[$($m),*])?}
}};
{action=$action:expr, key_up=$key:ident $(, modifiers=[$($m:ident),* $(,)?])?} => {{
entry!{action=$action, message=InputMapperMessage::KeyUp(Key::$key) $(, modifiers=[$($m),* ])?}
}};
{action=$action:expr, message=$message:expr $(, modifiers=[$($m:ident),* $(,)?])?} => {{
MappingEntry {trigger: $message, modifiers: modifiers!($($($m),*)?), action: $action.into()}
}};
}
macro_rules! mapping {
//[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{
[$($entry:expr),* $(,)?] => {{
let mut up: [KeyMappingEntries; NUMBER_OF_KEYS] = Default::default();
let mut down: [KeyMappingEntries; NUMBER_OF_KEYS] = Default::default();
let mut pointer_move: KeyMappingEntries = Default::default();
$(
let arr = match $entry.trigger {
InputMapperMessage::KeyDown(key) => &mut down[key as usize],
InputMapperMessage::KeyUp(key) => &mut up[key as usize],
InputMapperMessage::PointerMove => &mut pointer_move,
};
arr.push($entry);
)*
(up, down, pointer_move)
}};
}
impl Default for Mapping {
fn default() -> Self {
let (up, down, pointer_move) = mapping![
// Rectangle
entry! {action=RectangleMessage::Center, key_down=KeyAlt},
entry! {action=RectangleMessage::UnCenter, key_up=KeyAlt},
entry! {action=RectangleMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=RectangleMessage::DragStart, key_down=Lmb},
entry! {action=RectangleMessage::DragStop, key_up=Lmb},
entry! {action=RectangleMessage::Abort, key_down=Rmb},
entry! {action=RectangleMessage::Abort, key_down=KeyEscape},
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=RectangleMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=RectangleMessage::UnlockAspectRatio, key_up=KeyCaps},
// Ellipse
entry! {action=EllipseMessage::Center, key_down=KeyAlt},
entry! {action=EllipseMessage::UnCenter, key_up=KeyAlt},
entry! {action=EllipseMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=EllipseMessage::DragStart, key_down=Lmb},
entry! {action=EllipseMessage::DragStop, key_up=Lmb},
entry! {action=EllipseMessage::Abort, key_down=Rmb},
entry! {action=EllipseMessage::Abort, key_down=KeyEscape},
entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=EllipseMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=EllipseMessage::UnlockAspectRatio, key_up=KeyCaps},
// Shape
entry! {action=ShapeMessage::Center, key_down=KeyAlt},
entry! {action=ShapeMessage::UnCenter, key_up=KeyAlt},
entry! {action=ShapeMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=ShapeMessage::DragStart, key_down=Lmb},
entry! {action=ShapeMessage::DragStop, key_up=Lmb},
entry! {action=ShapeMessage::Abort, key_down=Rmb},
entry! {action=ShapeMessage::Abort, key_down=KeyEscape},
entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyShift},
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyShift},
entry! {action=ShapeMessage::LockAspectRatio, key_down=KeyCaps},
entry! {action=ShapeMessage::UnlockAspectRatio, key_up=KeyCaps},
// Line
entry! {action=LineMessage::Center, key_down=KeyAlt},
entry! {action=LineMessage::UnCenter, key_up=KeyAlt},
entry! {action=LineMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=LineMessage::DragStart, key_down=Lmb},
entry! {action=LineMessage::DragStop, key_up=Lmb},
entry! {action=LineMessage::Abort, key_down=Rmb},
entry! {action=LineMessage::Abort, key_down=KeyEscape},
entry! {action=LineMessage::LockAngle, key_down=KeyControl},
entry! {action=LineMessage::UnlockAngle, key_up=KeyControl},
entry! {action=LineMessage::SnapToAngle, key_down=KeyShift},
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyShift},
entry! {action=LineMessage::SnapToAngle, key_down=KeyCaps},
entry! {action=LineMessage::UnSnapToAngle, key_up=KeyCaps},
// Pen
entry! {action=PenMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=PenMessage::DragStart, key_down=Lmb},
entry! {action=PenMessage::DragStop, key_up=Lmb},
entry! {action=PenMessage::Confirm, key_down=Rmb},
entry! {action=PenMessage::Confirm, key_down=KeyEscape},
entry! {action=PenMessage::Confirm, key_down=KeyEnter},
// Document Actions
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
// Tool Actions
entry! {action=ToolMessage::SelectTool(ToolType::Rectangle), key_down=KeyM},
entry! {action=ToolMessage::SelectTool(ToolType::Ellipse), key_down=KeyE},
entry! {action=ToolMessage::SelectTool(ToolType::Select), key_down=KeyV},
entry! {action=ToolMessage::SelectTool(ToolType::Line), key_down=KeyL},
entry! {action=ToolMessage::SelectTool(ToolType::Shape), key_down=KeyY},
entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]},
// Global Actions
entry! {action=GlobalMessage::LogInfo, key_down=Key1},
entry! {action=GlobalMessage::LogDebug, key_down=Key2},
entry! {action=GlobalMessage::LogTrace, key_down=Key3},
];
Self { up, down, pointer_move }
}
}
impl Mapping {
fn match_message(&self, message: InputMapperMessage, keys: &KeyStates, actions: ActionList) -> Option<Message> {
use InputMapperMessage::*;
let list = match message {
KeyDown(key) => &self.down[key as usize],
KeyUp(key) => &self.up[key as usize],
PointerMove => &self.pointer_move,
};
list.match_mapping(keys, actions)
}
}
#[derive(Debug, Default)]
pub struct InputMapper {
mapping: Mapping,
}
impl MessageHandler<InputMapperMessage, (&InputPreprocessor, ActionList)> for InputMapper {
fn process_action(&mut self, message: InputMapperMessage, data: (&InputPreprocessor, ActionList), responses: &mut VecDeque<Message>) {
let (input, actions) = data;
if let Some(message) = self.mapping.match_message(message, &input.keyboard, actions) {
responses.push_back(message);
}
}
advertise_actions!();
}

View File

@@ -0,0 +1,74 @@
use super::keyboard::{Key, KeyStates};
use super::mouse::{MouseKeys, MouseState, ViewportPosition};
use crate::message_prelude::*;
#[doc(inline)]
pub use document_core::DocumentResponse;
#[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug)]
pub enum InputPreprocessorMessage {
MouseDown(MouseState),
MouseUp(MouseState),
MouseMove(ViewportPosition),
KeyUp(Key),
KeyDown(Key),
}
#[derive(Debug, Default)]
pub struct InputPreprocessor {
pub keyboard: KeyStates,
pub mouse: MouseState,
}
enum KeyPosition {
Pressed,
Released,
}
impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor {
fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque<Message>) {
let response = match message {
InputPreprocessorMessage::MouseMove(pos) => {
self.mouse.position = pos;
InputMapperMessage::PointerMove.into()
}
InputPreprocessorMessage::MouseDown(state) => self.translate_mouse_event(state, KeyPosition::Pressed),
InputPreprocessorMessage::MouseUp(state) => self.translate_mouse_event(state, KeyPosition::Released),
InputPreprocessorMessage::KeyDown(key) => {
self.keyboard.set(key as usize);
InputMapperMessage::KeyDown(key).into()
}
InputPreprocessorMessage::KeyUp(key) => {
self.keyboard.unset(key as usize);
InputMapperMessage::KeyUp(key).into()
}
};
responses.push_back(response)
}
// clean user input and if possible reconstruct it
// store the changes in the keyboard if it is a key event
// transform canvas coordinates to document coordinates
advertise_actions!();
}
impl InputPreprocessor {
fn translate_mouse_event(&mut self, new_state: MouseState, position: KeyPosition) -> Message {
// Calculate the difference between the two key states (binary xor)
let diff = self.mouse.mouse_keys ^ new_state.mouse_keys;
self.mouse = new_state;
let key = match diff {
MouseKeys::LEFT => Key::Lmb,
MouseKeys::RIGHT => Key::Rmb,
MouseKeys::MIDDLE => Key::Mmb,
_ => {
log::warn!("The number of buttons modified at the same time was not equal to 1. Modification: {:#010b}", diff);
Key::UnknownKey
}
};
match position {
KeyPosition::Pressed => InputMapperMessage::KeyDown(key).into(),
KeyPosition::Released => InputMapperMessage::KeyUp(key).into(),
}
}
}

View File

@@ -0,0 +1,143 @@
pub const NUMBER_OF_KEYS: usize = Key::NumKeys as usize;
// Edit this to specify the storage type used
// TODO: Increase size of type
pub type StorageType = u8;
const STORAGE_SIZE: u32 = std::mem::size_of::<usize>() as u32 * 8 + 2 - std::mem::size_of::<StorageType>().leading_zeros();
const STORAGE_SIZE_BITS: usize = 1 << STORAGE_SIZE;
const KEY_MASK_STORAGE_LENGTH: usize = (NUMBER_OF_KEYS + STORAGE_SIZE_BITS - 1) >> STORAGE_SIZE;
pub type KeyStates = BitVector<KEY_MASK_STORAGE_LENGTH>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
UnknownKey,
// MouseKeys
Lmb,
Rmb,
Mmb,
// Keyboard keys
KeyR,
KeyM,
KeyE,
KeyL,
KeyP,
KeyV,
KeyX,
KeyZ,
KeyY,
KeyEnter,
Key0,
Key1,
Key2,
Key3,
Key4,
Key5,
Key6,
Key7,
Key8,
Key9,
KeyShift,
KeyCaps,
KeyControl,
KeyAlt,
KeyEscape,
// This has to be the last element in the enum.
NumKeys,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BitVector<const LENGTH: usize>([StorageType; LENGTH]);
use std::{
fmt::{Display, Formatter},
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign},
usize,
};
impl<const LENGTH: usize> BitVector<LENGTH> {
#[inline]
fn convert_index(bitvector_index: usize) -> (usize, StorageType) {
let bit = 1 << (bitvector_index & (STORAGE_SIZE_BITS as StorageType - 1) as usize);
let offset = bitvector_index >> STORAGE_SIZE;
(offset, bit)
}
pub const fn new() -> Self {
Self([0; LENGTH])
}
pub fn set(&mut self, bitvector_index: usize) {
let (offset, bit) = Self::convert_index(bitvector_index);
self.0[offset] |= bit;
}
pub fn unset(&mut self, bitvector_index: usize) {
let (offset, bit) = Self::convert_index(bitvector_index);
self.0[offset] &= !bit;
}
pub fn toggle(&mut self, bitvector_index: usize) {
let (offset, bit) = Self::convert_index(bitvector_index);
self.0[offset] ^= bit;
}
pub fn is_empty(&self) -> bool {
let mut result = 0;
for storage in self.0.iter() {
result |= storage;
}
result == 0
}
}
impl<const LENGTH: usize> Default for BitVector<LENGTH> {
fn default() -> Self {
Self::new()
}
}
impl<const LENGTH: usize> Display for BitVector<LENGTH> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for storage in self.0.iter().rev() {
write!(f, "{:0width$b}", storage, width = STORAGE_SIZE_BITS)?;
}
Ok(())
}
}
macro_rules! bit_ops {
($(($op:ident, $func:ident)),* $(,)?) => {
$(
impl<const LENGTH: usize> $op for BitVector<LENGTH> {
type Output = Self;
fn $func(self, right: Self) -> Self::Output {
let mut result = Self::new();
for ((left, right), new) in self.0.iter().zip(right.0.iter()).zip(result.0.iter_mut()) {
*new = $op::$func(left, right);
}
result
}
}
impl<const LENGTH: usize> $op for &BitVector<LENGTH> {
type Output = BitVector<LENGTH>;
fn $func(self, right: Self) -> Self::Output {
let mut result = BitVector::<LENGTH>::new();
for ((left, right), new) in self.0.iter().zip(right.0.iter()).zip(result.0.iter_mut()) {
*new = $op::$func(left, right);
}
result
}
}
)*
};
}
macro_rules! bit_ops_assign {
($(($op:ident, $func:ident)),* $(,)?) => {
$(impl<const LENGTH: usize> $op for BitVector<LENGTH> {
fn $func(&mut self, right: Self) {
for (left, right) in self.0.iter_mut().zip(right.0.iter()) {
$op::$func(left, right);
}
}
})*
};
}
bit_ops!((BitAnd, bitand), (BitOr, bitor), (BitXor, bitxor));
bit_ops_assign!((BitAndAssign, bitand_assign), (BitOrAssign, bitor_assign), (BitXorAssign, bitxor_assign));

View File

@@ -0,0 +1,9 @@
pub mod input_mapper;
pub mod input_preprocessor;
pub mod keyboard;
pub mod mouse;
pub use {
input_mapper::{InputMapper, InputMapperMessage, InputMapperMessageDiscriminant},
input_preprocessor::{InputPreprocessor, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant},
};

View File

@@ -0,0 +1,48 @@
use bitflags::bitflags;
// origin is top left
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub struct ViewportPosition {
pub x: u32,
pub y: u32,
}
impl ViewportPosition {
pub fn distance(&self, other: &Self) -> f64 {
let x_diff = other.x as i64 - self.x as i64;
let y_diff = other.y as i64 - self.y as i64;
f64::sqrt((x_diff * x_diff + y_diff * y_diff) as f64)
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub struct MouseState {
pub position: ViewportPosition,
pub mouse_keys: MouseKeys,
}
impl MouseState {
pub fn new() -> MouseState {
Self::default()
}
pub fn from_pos(x: u32, y: u32) -> MouseState {
MouseState {
position: ViewportPosition { x, y },
mouse_keys: MouseKeys::default(),
}
}
pub fn from_u8_pos(keys: u8, position: ViewportPosition) -> Self {
let mouse_keys = MouseKeys::from_bits(keys).expect("invalid modifier keys");
Self { position, mouse_keys }
}
}
bitflags! {
#[derive(Default)]
#[repr(transparent)]
pub struct MouseKeys: u8 {
const LEFT = 0b0000_0001;
const RIGHT = 0b0000_0010;
const MIDDLE = 0b0000_0100;
}
}

View File

@@ -1,17 +1,19 @@
// since our policy is tabs, we want to stop clippy from warning about that
#![allow(clippy::tabs_in_doc_comments)]
#[macro_use]
mod macros;
extern crate graphite_proc_macros;
mod dispatcher;
mod error;
pub mod hint;
pub mod tools;
pub mod workspace;
mod communication;
#[macro_use]
pub mod misc;
mod document;
mod frontend;
mod global;
pub mod input;
pub mod tool;
#[doc(inline)]
pub use error::EditorError;
pub use misc::EditorError;
#[doc(inline)]
pub use document_core::color::Color;
@@ -20,41 +22,49 @@ pub use document_core::color::Color;
pub use document_core::LayerId;
#[doc(inline)]
pub use dispatcher::events;
pub use document_core::document::Document as SvgDocument;
#[doc(inline)]
pub use dispatcher::Callback;
use dispatcher::Dispatcher;
use document_core::document::Document;
use tools::ToolFsmState;
use workspace::Workspace;
pub struct EditorState {
tool_state: ToolFsmState,
workspace: Workspace,
document: Document,
}
pub use frontend::Callback;
use communication::dispatcher::Dispatcher;
// TODO: serialize with serde to save the current editor state
pub struct Editor {
state: EditorState,
dispatcher: Dispatcher,
}
use message_prelude::*;
impl Editor {
pub fn new(callback: Callback) -> Self {
Self {
state: EditorState {
tool_state: ToolFsmState::new(),
workspace: Workspace::new(),
document: Document::default(),
},
dispatcher: Dispatcher::new(callback),
}
}
pub fn handle_event(&mut self, event: events::Event) -> Result<(), EditorError> {
self.dispatcher.handle_event(&mut self.state, &event)
pub fn handle_message<T: Into<Message>>(&mut self, message: T) -> Result<(), EditorError> {
self.dispatcher.handle_message(message)
}
}
pub mod message_prelude {
pub use super::communication::message::{AsMessage, Message, MessageDiscriminant};
pub use super::communication::{ActionList, MessageHandler};
pub use super::document::{DocumentMessage, DocumentMessageDiscriminant};
pub use super::frontend::{FrontendMessage, FrontendMessageDiscriminant};
pub use super::global::{GlobalMessage, GlobalMessageDiscriminant};
pub use super::input::{InputMapperMessage, InputMapperMessageDiscriminant, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant};
pub use super::misc::derivable_custom_traits::{ToDiscriminant, TransitiveChild};
pub use super::tool::tool_messages::*;
pub use super::tool::tools::crop::{CropMessage, CropMessageDiscriminant};
pub use super::tool::tools::eyedropper::{EyedropperMessage, EyedropperMessageDiscriminant};
pub use super::tool::tools::line::{LineMessage, LineMessageDiscriminant};
pub use super::tool::tools::navigate::{NavigateMessage, NavigateMessageDiscriminant};
pub use super::tool::tools::path::{PathMessage, PathMessageDiscriminant};
pub use super::tool::tools::pen::{PenMessage, PenMessageDiscriminant};
pub use super::tool::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant};
pub use super::tool::tools::select::{SelectMessage, SelectMessageDiscriminant};
pub use super::tool::tools::shape::{ShapeMessage, ShapeMessageDiscriminant};
pub use graphite_proc_macros::*;
pub use std::collections::VecDeque;
}

View File

@@ -1,86 +0,0 @@
/// Counts args in the macro invocation by adding `+ 1` for every arg.
///
/// # Example
///
/// ```ignore
/// let x = count_args!(("example1"), (10), (25));
/// assert_eq!(x, 3);
/// ```
/// expands to
/// ```ignore
/// let x = 0 + 1 + 1 + 1;
/// assert_eq!(x, 3);
/// ```
macro_rules! count_args {
(@one $($t:tt)*) => { 1 };
($(($($x:tt)*)),*$(,)?) => {
0 $(+ count_args!(@one $($x)*))*
};
}
/// Generates a [`std::collections::HashMap`] for `ToolState`'s `tools` variable.
///
/// # Example
///
/// ```ignore
/// let tools = gen_tools_hash_map! {
/// Select => select::Select,
/// Crop => crop::Crop,
/// };
/// ```
/// expands to
/// ```ignore
/// let tools = {
/// let mut hash_map: std::collections::HashMap<crate::tools::ToolType, Box<dyn crate::tools::Tool>> = std::collections::HashMap::with_capacity(count_args!(/* Macro args */));
///
/// hash_map.insert(crate::tools::ToolType::Select, Box::new(select::Select::default()));
/// hash_map.insert(crate::tools::ToolType::Crop, Box::new(crop::Crop::default()));
///
/// hash_map
/// };
/// ```
macro_rules! gen_tools_hash_map {
($($enum_variant:ident => $struct_path:ty),* $(,)?) => {{
let mut hash_map: ::std::collections::HashMap<$crate::tools::ToolType, ::std::boxed::Box<dyn $crate::tools::Tool>> = ::std::collections::HashMap::with_capacity(count_args!($(($enum_variant)),*));
$(hash_map.insert($crate::tools::ToolType::$enum_variant, ::std::boxed::Box::new(<$struct_path>::default()));)*
hash_map
}};
}
/// Creates a string representation of an enum value that exactly matches the given name of each enum variant
///
/// # Example
///
/// ```ignore
/// enum E {
/// A(u8),
/// B
/// }
///
/// // this line is important
/// use E::*;
///
/// let a = E::A(7);
/// let s = match_variant_name!(match (a) { A, B });
/// ```
///
/// expands to
///
/// ```ignore
/// // ...
///
/// let s = match a {
/// A { .. } => "A",
/// B { .. } => "B"
/// };
/// ```
macro_rules! match_variant_name {
(match ($e:expr) { $($v:ident),* $(,)? }) => {
match $e {
$(
$v { .. } => stringify!($v)
),*
}
};
}

View File

@@ -0,0 +1,18 @@
//! Traits that can be derived using macros from `graphite-proc-macros`
use std::collections::HashMap;
pub trait Hint {
fn hints(&self) -> HashMap<String, String>;
}
pub trait ToDiscriminant {
type Discriminant;
fn to_discriminant(&self) -> Self::Discriminant;
}
pub trait TransitiveChild: Into<Self::Parent> + Into<Self::TopParent> {
type TopParent;
type Parent;
}

View File

@@ -1,4 +1,3 @@
use crate::events::Event;
use crate::Color;
use document_core::DocumentError;
use thiserror::Error;
@@ -8,8 +7,6 @@ use thiserror::Error;
pub enum EditorError {
#[error("Failed to execute operation: {0}")]
InvalidOperation(String),
#[error("Failed to dispatch event: {0}")]
InvalidEvent(String),
#[error("{0}")]
Misc(String),
#[error("Tried to construct an invalid color {0:?}")]
@@ -33,5 +30,4 @@ macro_rules! derive_from {
derive_from!(&str, Misc);
derive_from!(String, Misc);
derive_from!(Color, Color);
derive_from!(Event, InvalidEvent);
derive_from!(DocumentError, Document);

View File

@@ -0,0 +1,138 @@
/// Counts args in the macro invocation by adding `+ 1` for every arg.
///
/// # Example
///
/// ```ignore
/// let x = count_args!(("example1"), (10), (25));
/// assert_eq!(x, 3);
/// ```
/// expands to
/// ```ignore
/// let x = 0 + 1 + 1 + 1;
/// assert_eq!(x, 3);
/// ```
macro_rules! count_args {
(@one $($t:tt)*) => { 1 };
($(($($x:tt)*)),*$(,)?) => {
0 $(+ count_args!(@one $($x)*))*
};
}
/// Generates a [`std::collections::HashMap`] for `ToolState`'s `tools` variable.
///
/// # Example
///
/// ```ignore
/// let tools = gen_tools_hash_map! {
/// Select => select::Select,
/// Crop => crop::Crop,
/// };
/// ```
/// expands to
/// ```ignore
/// let tools = {
/// let mut hash_map: std::collections::HashMap<crate::tool::ToolType, Box<dyn crate::tool::Tool>> = std::collections::HashMap::with_capacity(count_args!(/* Macro args */));
///
/// hash_map.insert(crate::tool::ToolType::Select, Box::new(select::Select::default()));
/// hash_map.insert(crate::tool::ToolType::Crop, Box::new(crop::Crop::default()));
///
/// hash_map
/// };
/// ```
macro_rules! gen_tools_hash_map {
($($enum_variant:ident => $struct_path:ty),* $(,)?) => {{
let mut hash_map: ::std::collections::HashMap<$crate::tool::ToolType, ::std::boxed::Box<dyn for<'a> $crate::message_prelude::MessageHandler<$crate::tool::tool_messages::ToolMessage,$crate::tool::ToolActionHandlerData<'a>>>> = ::std::collections::HashMap::with_capacity(count_args!($(($enum_variant)),*));
$(hash_map.insert($crate::tool::ToolType::$enum_variant, ::std::boxed::Box::new(<$struct_path>::default()));)*
hash_map
}};
}
/// Creates a string representation of an enum value that exactly matches the given name of each enum variant
///
/// # Example
///
/// ```ignore
/// enum E {
/// A(u8),
/// B
/// }
///
/// // this line is important
/// use E::*;
///
/// let a = E::A(7);
/// let s = match_variant_name!(match (a) { A, B });
/// ```
///
/// expands to
///
/// ```ignore
/// // ...
///
/// let s = match a {
/// A { .. } => "A",
/// B { .. } => "B"
/// };
/// ```
macro_rules! match_variant_name {
(match ($e:expr) { $($v:ident),* $(,)? }) => {
match $e {
$(
$v { .. } => stringify!($v)
),*
}
};
}
/// Syntax sugar for initializing an `ActionList`
///
/// # Example
///
/// ```ignore
/// actions!(DocumentMessage::Undo, DocumentMessage::Redo);
/// ```
///
/// expands to:
/// ```ignore
/// vec![vec![DocumentMessage::Undo, DocumentMessage::Redo]];
/// ```
///
/// and
/// ```ignore
/// actions!(DocumentMessage; Undo, Redo);
/// ```
///
/// expands to:
/// ```ignore
/// vec![vec![DocumentMessage::Undo, DocumentMessage::Redo]];
/// ```
///
macro_rules! actions {
($($v:expr),* $(,)?) => {{
vec![$(vec![$v.into()]),*]
}};
($name:ident; $($v:ident),* $(,)?) => {{
vec![vec![$(($name::$v).into()),*]]
}};
}
/// Does the same thing as the `actions!` macro but wraps everything in:
///
/// ```ignore
/// fn actions(&self) -> ActionList {
/// actions!(…)
/// }
/// ```
macro_rules! advertise_actions {
($($v:expr),* $(,)?) => {
fn actions(&self) -> $crate::communication::ActionList {
actions!($($v),*)
}
};
($name:ident; $($v:ident),* $(,)?) => {
fn actions(&self) -> $crate::communication::ActionList {
actions!($name; $($v),*)
}
}
}

View File

@@ -0,0 +1,7 @@
#[macro_use]
pub mod macros;
pub mod derivable_custom_traits;
mod error;
pub use error::EditorError;
pub use macros::*;

View File

@@ -1,63 +1,79 @@
mod crop;
mod ellipse;
mod eyedropper;
mod line;
mod navigate;
mod path;
mod pen;
mod rectangle;
mod select;
mod shape;
pub mod tool_message_handler;
pub mod tool_settings;
pub mod tools;
use crate::events::{Event, ModKeys, MouseState, ToolResponse, Trace, TracePoint};
use crate::Color;
use crate::Document;
use crate::EditorError;
use document_core::Operation;
use std::{collections::HashMap, fmt};
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 trait Tool {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>);
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, event: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self;
fn transition(self, message: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, messages: &mut VecDeque<Message>) -> Self;
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct DocumentToolData {
pub mouse_state: MouseState,
pub mod_keys: ModKeys,
pub primary_color: Color,
pub secondary_color: Color,
}
pub struct ToolData {
pub active_tool_type: ToolType,
pub tools: HashMap<ToolType, Box<dyn Tool>>,
tool_settings: HashMap<ToolType, ToolSettings>,
}
impl ToolData {
pub fn active_tool(&mut self) -> Result<&mut Box<dyn Tool>, EditorError> {
self.tools.get_mut(&self.active_tool_type).ok_or(EditorError::UnknownTool)
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,
pub trace: Trace,
}
impl Default for ToolFsmState {
fn default() -> Self {
ToolFsmState {
trace: Trace::new(),
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,
@@ -65,17 +81,14 @@ impl Default for ToolFsmState {
Path => path::Path,
Pen => pen::Pen,
Line => line::Line,
Rectangle => rectangle::Rectangle,
Ellipse => ellipse::Ellipse,
Shape => shape::Shape,
Ellipse => ellipse::Ellipse,
},
tool_settings: default_tool_settings(),
},
document_tool_data: DocumentToolData {
mouse_state: MouseState::default(),
mod_keys: ModKeys::default(),
primary_color: Color::BLACK,
secondary_color: Color::WHITE,
tool_settings: default_tool_settings(),
},
}
}
@@ -86,13 +99,6 @@ impl ToolFsmState {
Self::default()
}
pub fn record_trace_point(&mut self) {
self.trace.push(TracePoint {
mouse_state: self.document_tool_data.mouse_state,
mod_keys: self.document_tool_data.mod_keys,
})
}
pub fn swap_colors(&mut self) {
std::mem::swap(&mut self.document_tool_data.primary_color, &mut self.document_tool_data.secondary_color);
}
@@ -178,24 +184,3 @@ impl ToolType {
}
}
}
#[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,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()
}

View File

@@ -1,15 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::tools::Tool;
use crate::Document;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Crop;
impl Tool for Crop {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data)
}
}

View File

@@ -1,169 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::events::{Key, ViewportPosition};
use crate::tools::{Fsm, Tool};
use crate::Document;
use document_core::layers::style;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Ellipse {
fsm_state: EllipseToolFsmState,
data: EllipseToolData,
}
impl Tool for Ellipse {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
let mut responses = Vec::new();
let mut operations = Vec::new();
self.fsm_state = self.fsm_state.transition(event, document, tool_data, &mut self.data, &mut responses, &mut operations);
(responses, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EllipseToolFsmState {
Ready,
LmbDown,
}
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: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, _responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self {
match (self, event) {
(EllipseToolFsmState::Ready, Event::LmbDown(mouse_state)) => {
data.drag_start = mouse_state.position;
data.drag_current = mouse_state.position;
operations.push(Operation::MountWorkingFolder { path: vec![] });
EllipseToolFsmState::LmbDown
}
(EllipseToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => {
if let Some(id) = document.root.list_layers().last() {
operations.push(Operation::DeleteLayer { path: vec![*id] })
}
EllipseToolFsmState::Ready
}
(EllipseToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => {
data.drag_current = *mouse_state;
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
EllipseToolFsmState::LmbDown
}
(EllipseToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => {
data.drag_current = mouse_state.position;
operations.push(Operation::ClearWorkingFolder);
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
operations.push(make_operation(data, tool_data));
operations.push(Operation::CommitTransaction);
}
EllipseToolFsmState::Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(EllipseToolFsmState::LmbDown, Event::KeyUp(Key::KeyEscape)) | (EllipseToolFsmState::LmbDown, Event::RmbDown(_)) => {
operations.push(Operation::DiscardWorkingFolder);
EllipseToolFsmState::Ready
}
(state, Event::KeyDown(Key::KeyShift)) => {
data.constrain_to_circle = true;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyShift)) => {
data.constrain_to_circle = false;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyDown(Key::KeyAlt)) => {
data.center_around_cursor = true;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyAlt)) => {
data.center_around_cursor = false;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
_ => self,
}
}
}
fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData) -> Operation {
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))),
}
}
}

View File

@@ -1,15 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::tools::Tool;
use crate::Document;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Eyedropper;
impl Tool for Eyedropper {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data)
}
}

View File

@@ -1,195 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::events::{Key, ViewportPosition};
use crate::tools::{Fsm, Tool};
use crate::Document;
use document_core::layers::style;
use document_core::Operation;
use super::DocumentToolData;
use std::f64::consts::PI;
#[derive(Default)]
pub struct Line {
fsm_state: LineToolFsmState,
data: LineToolData,
}
impl Tool for Line {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
let mut responses = Vec::new();
let mut operations = Vec::new();
self.fsm_state = self.fsm_state.transition(event, document, tool_data, &mut self.data, &mut responses, &mut operations);
(responses, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LineToolFsmState {
Ready,
LmbDown,
}
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: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, _responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self {
match (self, event) {
(LineToolFsmState::Ready, Event::LmbDown(mouse_state)) => {
data.drag_start = mouse_state.position;
data.drag_current = mouse_state.position;
operations.push(Operation::MountWorkingFolder { path: vec![] });
LineToolFsmState::LmbDown
}
(LineToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => {
if let Some(id) = document.root.list_layers().last() {
operations.push(Operation::DeleteLayer { path: vec![*id] })
}
LineToolFsmState::Ready
}
(LineToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => {
data.drag_current = *mouse_state;
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
LineToolFsmState::LmbDown
}
(LineToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => {
data.drag_current = mouse_state.position;
operations.push(Operation::ClearWorkingFolder);
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
operations.push(make_operation(data, tool_data));
operations.push(Operation::CommitTransaction);
}
LineToolFsmState::Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(LineToolFsmState::LmbDown, Event::KeyUp(Key::KeyEscape)) | (LineToolFsmState::LmbDown, Event::RmbDown(_)) => {
operations.push(Operation::DiscardWorkingFolder);
LineToolFsmState::Ready
}
(state, Event::KeyDown(Key::KeyShift)) => {
data.snap_angle = true;
if state == LineToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyShift)) => {
data.snap_angle = false;
if state == LineToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyDown(Key::KeyControl)) => {
data.lock_angle = true;
if state == LineToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyControl)) => {
data.lock_angle = false;
if state == LineToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyDown(Key::KeyAlt)) => {
data.center_around_cursor = true;
if state == LineToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyAlt)) => {
data.center_around_cursor = false;
if state == LineToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
_ => self,
}
}
}
fn make_operation(data: &mut LineToolData, tool_data: &DocumentToolData) -> Operation {
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),
}
}

View File

@@ -1,15 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::tools::Tool;
use crate::Document;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Navigate;
impl Tool for Navigate {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data)
}
}

View File

@@ -1,15 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::tools::Tool;
use crate::Document;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Path;
impl Tool for Path {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data)
}
}

View File

@@ -1,112 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::events::{Key, ViewportPosition};
use crate::tools::{Fsm, Tool};
use crate::Document;
use document_core::layers::style;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Pen {
fsm_state: PenToolFsmState,
data: PenToolData,
}
impl Tool for Pen {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
let mut responses = Vec::new();
let mut operations = Vec::new();
self.fsm_state = self.fsm_state.transition(event, document, tool_data, &mut self.data, &mut responses, &mut operations);
(responses, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PenToolFsmState {
Ready,
LmbDown,
}
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: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, _responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self {
match (self, event) {
(PenToolFsmState::Ready, Event::LmbDown(mouse_state)) => {
operations.push(Operation::MountWorkingFolder { path: vec![] });
data.points.push(mouse_state.position);
data.next_point = mouse_state.position;
PenToolFsmState::LmbDown
}
(PenToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => {
if let Some(id) = document.root.list_layers().last() {
operations.push(Operation::DeleteLayer { path: vec![*id] })
}
PenToolFsmState::Ready
}
(PenToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => {
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.points.last() != Some(&mouse_state.position) {
data.points.push(mouse_state.position);
data.next_point = mouse_state.position;
}
PenToolFsmState::LmbDown
}
(PenToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => {
data.next_point = *mouse_state;
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data, true));
PenToolFsmState::LmbDown
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(PenToolFsmState::LmbDown, Event::KeyDown(Key::KeyEnter)) | (PenToolFsmState::LmbDown, Event::KeyDown(Key::KeyEscape)) | (PenToolFsmState::LmbDown, Event::RmbDown(_)) => {
operations.push(Operation::ClearWorkingFolder);
if data.points.len() >= 2 {
operations.push(make_operation(data, tool_data, false));
operations.push(Operation::CommitTransaction);
} else {
operations.push(Operation::DiscardWorkingFolder);
}
data.points.clear();
PenToolFsmState::Ready
}
_ => self,
}
}
}
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Operation {
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())),
}
}

View File

@@ -1,168 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::events::{Key, ViewportPosition};
use crate::tools::{Fsm, Tool};
use crate::Document;
use document_core::layers::style;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Rectangle {
fsm_state: RectangleToolFsmState,
data: RectangleToolData,
}
impl Tool for Rectangle {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
let mut responses = Vec::new();
let mut operations = Vec::new();
self.fsm_state = self.fsm_state.transition(event, document, tool_data, &mut self.data, &mut responses, &mut operations);
(responses, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RectangleToolFsmState {
Ready,
LmbDown,
}
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: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, _responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self {
match (self, event) {
(RectangleToolFsmState::Ready, Event::LmbDown(mouse_state)) => {
data.drag_start = mouse_state.position;
data.drag_current = mouse_state.position;
operations.push(Operation::MountWorkingFolder { path: vec![] });
RectangleToolFsmState::LmbDown
}
(RectangleToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => {
if let Some(id) = document.root.list_layers().last() {
operations.push(Operation::DeleteLayer { path: vec![*id] })
}
RectangleToolFsmState::Ready
}
(RectangleToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => {
data.drag_current = *mouse_state;
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
RectangleToolFsmState::LmbDown
}
(RectangleToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => {
data.drag_current = mouse_state.position;
operations.push(Operation::ClearWorkingFolder);
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
operations.push(make_operation(data, tool_data));
operations.push(Operation::CommitTransaction);
}
RectangleToolFsmState::Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(RectangleToolFsmState::LmbDown, Event::KeyUp(Key::KeyEscape)) | (RectangleToolFsmState::LmbDown, Event::RmbDown(_)) => {
operations.push(Operation::DiscardWorkingFolder);
RectangleToolFsmState::Ready
}
(state, Event::KeyDown(Key::KeyShift)) => {
data.constrain_to_square = true;
if state == RectangleToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyShift)) => {
data.constrain_to_square = false;
if state == RectangleToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyDown(Key::KeyAlt)) => {
data.center_around_cursor = true;
if state == RectangleToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyAlt)) => {
data.center_around_cursor = false;
if state == RectangleToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
_ => self,
}
}
}
fn make_operation(data: &RectangleToolData, tool_data: &DocumentToolData) -> Operation {
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))),
}
}

View File

@@ -1,58 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::tools::{Fsm, Tool};
use crate::Document;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Select {
fsm_state: SelectToolFsmState,
data: SelectToolData,
}
impl Tool for Select {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
let mut responses = Vec::new();
let mut operations = Vec::new();
self.fsm_state = self.fsm_state.transition(event, document, tool_data, &mut self.data, &mut responses, &mut operations);
(responses, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SelectToolFsmState {
Ready,
LmbDown,
TransformSelected,
}
impl Default for SelectToolFsmState {
fn default() -> Self {
SelectToolFsmState::Ready
}
}
#[derive(Default)]
struct SelectToolData;
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
fn transition(self, event: &Event, _document: &Document, _tool_data: &DocumentToolData, _data: &mut Self::ToolData, _responses: &mut Vec<ToolResponse>, _operations: &mut Vec<Operation>) -> Self {
match (self, event) {
(SelectToolFsmState::Ready, Event::LmbDown(_mouse_state)) => SelectToolFsmState::LmbDown,
(SelectToolFsmState::LmbDown, Event::LmbUp(_mouse_state)) => SelectToolFsmState::Ready,
(SelectToolFsmState::LmbDown, Event::MouseMove(_mouse_state)) => SelectToolFsmState::TransformSelected,
(SelectToolFsmState::TransformSelected, Event::MouseMove(_mouse_state)) => self,
(SelectToolFsmState::TransformSelected, Event::LmbUp(_mouse_state)) => SelectToolFsmState::Ready,
_ => self,
}
}
}

View File

@@ -1,171 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::events::{Key, ViewportPosition};
use crate::tools::{Fsm, Tool};
use crate::Document;
use document_core::layers::style;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Shape {
fsm_state: ShapeToolFsmState,
data: ShapeToolData,
}
impl Tool for Shape {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
let mut responses = Vec::new();
let mut operations = Vec::new();
self.fsm_state = self.fsm_state.transition(event, document, tool_data, &mut self.data, &mut responses, &mut operations);
(responses, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShapeToolFsmState {
Ready,
LmbDown,
}
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: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, _responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self {
match (self, event) {
(ShapeToolFsmState::Ready, Event::LmbDown(mouse_state)) => {
data.drag_start = mouse_state.position;
data.drag_current = mouse_state.position;
data.sides = 6;
operations.push(Operation::MountWorkingFolder { path: vec![] });
ShapeToolFsmState::LmbDown
}
(ShapeToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => {
if let Some(id) = document.root.list_layers().last() {
operations.push(Operation::DeleteLayer { path: vec![*id] })
}
ShapeToolFsmState::Ready
}
(ShapeToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => {
data.drag_current = *mouse_state;
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
ShapeToolFsmState::LmbDown
}
(ShapeToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => {
data.drag_current = mouse_state.position;
operations.push(Operation::ClearWorkingFolder);
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
operations.push(make_operation(data, tool_data));
operations.push(Operation::CommitTransaction);
}
ShapeToolFsmState::Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(ShapeToolFsmState::LmbDown, Event::KeyUp(Key::KeyEscape)) | (ShapeToolFsmState::LmbDown, Event::RmbDown(_)) => {
operations.push(Operation::DiscardWorkingFolder);
ShapeToolFsmState::Ready
}
(state, Event::KeyDown(Key::KeyShift)) => {
data.constrain_to_square = true;
if state == ShapeToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyShift)) => {
data.constrain_to_square = false;
if state == ShapeToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyDown(Key::KeyAlt)) => {
data.center_around_cursor = true;
if state == ShapeToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyAlt)) => {
data.center_around_cursor = false;
if state == ShapeToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
_ => self,
}
}
}
fn make_operation(data: &ShapeToolData, tool_data: &DocumentToolData) -> Operation {
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))),
}
}

View File

@@ -1,60 +0,0 @@
use serde::{Deserialize, Serialize};
pub type PanelId = u32;
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Workspace {
pub hovered_panel: PanelId,
pub root: PanelGroup,
}
impl Workspace {
pub fn new() -> Workspace {
Workspace {
hovered_panel: 0,
root: PanelGroup::new(),
}
}
// add panel / panel group
// delete panel / panel group
// move panel / panel group
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PanelGroup {
pub contents: Vec<Contents>,
pub layout_direction: LayoutDirection,
}
impl Default for PanelGroup {
fn default() -> Self {
Self::new()
}
}
impl PanelGroup {
fn new() -> PanelGroup {
PanelGroup {
contents: vec![],
layout_direction: LayoutDirection::Horizontal,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Contents {
PanelArea(PanelArea),
Group(PanelGroup),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PanelArea {
pub panels: Vec<PanelId>,
pub active: PanelId,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum LayoutDirection {
Horizontal,
Vertical,
}