Restructure project directories (#333)

`/client/web` -> `/frontend`
`/client/cli` -> *delete for now*
`/client/native` -> *delete for now*
`/core/editor` -> `/editor`
`/core/document` -> `/graphene`
`/core/renderer` -> `/charcoal`
`/core/proc-macro` -> `/proc-macros` *(now plural)*
This commit is contained in:
Keavon Chambers
2021-08-07 05:17:18 -07:00
parent 434695d578
commit 53ad105f57
239 changed files with 197 additions and 224 deletions

24
editor/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "graphite-editor"
version = "0.1.0"
authors = ["Graphite Authors <contact@graphite.design>"]
edition = "2018"
readme = "../README.md"
homepage = "https://www.graphite.design"
repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0"
[dependencies]
log = "0.4"
bitflags = "1.2.1"
thiserror = "1.0.24"
serde = { version = "1.0", features = ["derive"] }
graphite-proc-macros = { path = "../proc-macros" }
glam = { version="0.17", features = ["serde"] }
[dependencies.graphene]
path = "../graphene"
package = "graphite-graphene"
[dev-dependencies]
env_logger = "0.8.4"

View File

@@ -0,0 +1,326 @@
use crate::{frontend::FrontendMessageHandler, message_prelude::*, Callback, EditorError};
pub use crate::document::DocumentsMessageHandler;
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,
documents_message_handler: DocumentsMessageHandler,
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::Documents(DocumentsMessage::Document(DocumentMessage::RenderDocument))
| Message::Frontend(FrontendMessage::UpdateCanvas { .. })
| Message::Frontend(FrontendMessage::SetCanvasZoom { .. })
| Message::Frontend(FrontendMessage::SetCanvasRotation { .. })
| Message::Documents(DocumentsMessage::Document(DocumentMessage::DispatchOperation { .. }))
) || MessageDiscriminant::from(&message).local_name().ends_with("MouseMove"))
{
log::trace!("Message: {:?}", message);
//log::trace!("Hints:{:?}", self.input_mapper.hints(self.collect_actions()));
}
match message {
NoOp => (),
Documents(message) => self.documents_message_handler.process_action(message, &self.input_preprocessor, &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.documents_message_handler.active_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.documents_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(),
documents_message_handler: DocumentsMessageHandler::default(),
tool_message_handler: ToolMessageHandler::default(),
messages: VecDeque::new(),
}
}
}
#[cfg(test)]
mod test {
use crate::{document::DocumentMessageHandler, message_prelude::*, misc::test_utils::EditorTestUtils, Editor};
use graphene::{color::Color, Operation};
use log::info;
fn init_logger() {
let _ = env_logger::builder().is_test(true).try_init();
}
/// Create an editor instance with three layers
/// 1. A red rectangle
/// 2. A blue shape
/// 3. A green ellipse
fn create_editor_with_three_layers() -> Editor {
let mut editor = Editor::new(Box::new(|e| {
info!("Got frontend message: {:?}", e);
}));
editor.select_primary_color(Color::RED);
editor.draw_rect(100, 200, 300, 400);
editor.select_primary_color(Color::BLUE);
editor.draw_shape(10, 1200, 1300, 400);
editor.select_primary_color(Color::GREEN);
editor.draw_ellipse(104, 1200, 1300, 400);
editor
}
#[test]
/// - create rect, shape and ellipse
/// - copy
/// - paste
/// - assert that ellipse was copied
fn copy_paste_single_layer() {
init_logger();
let mut editor = create_editor_with_three_layers();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
assert_eq!(layers_before_copy.len(), 3);
assert_eq!(layers_after_copy.len(), 4);
// Existing layers are unaffected
for i in 0..=2 {
assert_eq!(layers_before_copy[i], layers_after_copy[i]);
}
// The ellipse was copied
assert_eq!(layers_before_copy[2], layers_after_copy[3]);
}
#[test]
/// - create rect, shape and ellipse
/// - select shape
/// - copy
/// - paste
/// - assert that shape was copied
fn copy_paste_single_layer_from_middle() {
init_logger();
let mut editor = create_editor_with_three_layers();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let shape_id = document_before_copy.root.as_folder().unwrap().layer_ids[1];
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![shape_id]])).unwrap();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
assert_eq!(layers_before_copy.len(), 3);
assert_eq!(layers_after_copy.len(), 4);
// Existing layers are unaffected
for i in 0..=2 {
assert_eq!(layers_before_copy[i], layers_after_copy[i]);
}
// The shape was copied
assert_eq!(layers_before_copy[1], layers_after_copy[3]);
}
#[test]
fn copy_paste_folder() {
init_logger();
let mut editor = create_editor_with_three_layers();
const FOLDER_INDEX: usize = 3;
const ELLIPSE_INDEX: usize = 2;
const SHAPE_INDEX: usize = 1;
const RECT_INDEX: usize = 0;
const LINE_INDEX: usize = 0;
const PEN_INDEX: usize = 1;
editor.handle_message(DocumentMessage::AddFolder(vec![])).unwrap();
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().document.clone();
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];
// TODO: This adding of a Line and Pen should be rewritten using the corresponding functions in EditorTestUtils.
// This has not been done yet as the line and pen tool are not yet able to add layers to the currently selected folder
editor
.handle_message(Operation::AddLine {
path: vec![folder_id, LINE_INDEX as u64],
insert_index: 0,
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
style: Default::default(),
})
.unwrap();
editor
.handle_message(Operation::AddPen {
path: vec![folder_id, PEN_INDEX as u64],
insert_index: 0,
transform: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
style: Default::default(),
points: vec![(10.0, 20.0), (30.0, 40.0)],
})
.unwrap();
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![folder_id]])).unwrap();
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
assert_eq!(layers_before_copy.len(), 4);
assert_eq!(layers_after_copy.len(), 5);
let rect_before_copy = &layers_before_copy[RECT_INDEX];
let ellipse_before_copy = &layers_before_copy[ELLIPSE_INDEX];
let shape_before_copy = &layers_before_copy[SHAPE_INDEX];
let folder_before_copy = &layers_before_copy[FOLDER_INDEX];
let line_before_copy = folder_before_copy.as_folder().unwrap().layers()[LINE_INDEX].clone();
let pen_before_copy = folder_before_copy.as_folder().unwrap().layers()[PEN_INDEX].clone();
assert_eq!(&layers_after_copy[0], rect_before_copy);
assert_eq!(&layers_after_copy[1], shape_before_copy);
assert_eq!(&layers_after_copy[2], ellipse_before_copy);
assert_eq!(&layers_after_copy[3], folder_before_copy);
assert_eq!(&layers_after_copy[4], folder_before_copy);
// Check the layers inside the two folders
let first_folder_layers_after_copy = layers_after_copy[3].as_folder().unwrap().layers();
let second_folder_layers_after_copy = layers_after_copy[4].as_folder().unwrap().layers();
assert_eq!(first_folder_layers_after_copy.len(), 2);
assert_eq!(second_folder_layers_after_copy.len(), 2);
assert_eq!(first_folder_layers_after_copy[0], line_before_copy);
assert_eq!(first_folder_layers_after_copy[1], pen_before_copy);
assert_eq!(second_folder_layers_after_copy[0], line_before_copy);
assert_eq!(second_folder_layers_after_copy[1], pen_before_copy);
}
#[test]
/// - create rect, shape and ellipse
/// - select ellipse and rect
/// - copy
/// - delete
/// - create another rect
/// - paste
/// - paste
fn copy_paste_deleted_layers() {
init_logger();
let mut editor = create_editor_with_three_layers();
const ELLIPSE_INDEX: usize = 2;
const SHAPE_INDEX: usize = 1;
const RECT_INDEX: usize = 0;
let document_before_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let rect_id = document_before_copy.root.as_folder().unwrap().layer_ids[RECT_INDEX];
let ellipse_id = document_before_copy.root.as_folder().unwrap().layer_ids[ELLIPSE_INDEX];
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![rect_id], vec![ellipse_id]])).unwrap();
editor.handle_message(DocumentsMessage::CopySelectedLayers).unwrap();
editor.handle_message(DocumentMessage::DeleteSelectedLayers).unwrap();
editor.draw_rect(0, 800, 12, 200);
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
editor.handle_message(DocumentsMessage::PasteLayers { path: vec![], insert_index: -1 }).unwrap();
let document_after_copy = editor.dispatcher.documents_message_handler.active_document().document.clone();
let layers_before_copy = document_before_copy.root.as_folder().unwrap().layers();
let layers_after_copy = document_after_copy.root.as_folder().unwrap().layers();
assert_eq!(layers_before_copy.len(), 3);
assert_eq!(layers_after_copy.len(), 6);
let rect_before_copy = &layers_before_copy[RECT_INDEX];
let ellipse_before_copy = &layers_before_copy[ELLIPSE_INDEX];
assert_eq!(layers_after_copy[0], layers_before_copy[SHAPE_INDEX]);
assert_eq!(&layers_after_copy[2], rect_before_copy);
assert_eq!(&layers_after_copy[3], ellipse_before_copy);
assert_eq!(&layers_after_copy[4], rect_before_copy);
assert_eq!(&layers_after_copy[5], ellipse_before_copy);
}
#[test]
/// - create rect, shape and ellipse
/// - select ellipse and rect
/// - move them down and back up again
fn move_seletion() {
init_logger();
let mut editor = create_editor_with_three_layers();
let verify_order = |handler: &mut DocumentMessageHandler| (handler.all_layers_sorted(), handler.non_selected_layers_sorted(), handler.selected_layers_sorted());
editor.handle_message(DocumentMessage::SelectLayers(vec![vec![0], vec![2]])).unwrap();
editor.handle_message(DocumentMessage::ReorderSelectedLayers(1)).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
editor.handle_message(DocumentMessage::ReorderSelectedLayers(-1)).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
assert_eq!(all, selected.into_iter().chain(non_selected.into_iter()).collect::<Vec<_>>());
editor.handle_message(DocumentMessage::ReorderSelectedLayers(i32::MAX)).unwrap();
let (all, non_selected, selected) = verify_order(&mut editor.dispatcher.documents_message_handler.active_document_mut());
assert_eq!(all, non_selected.into_iter().chain(selected.into_iter()).collect::<Vec<_>>());
}
}

View File

@@ -0,0 +1,55 @@
use crate::message_prelude::*;
use graphite_proc_macros::*;
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
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(Clone, Debug, PartialEq)]
pub enum Message {
NoOp,
#[child]
Documents(DocumentsMessage),
#[child]
Global(GlobalMessage),
#[child]
Tool(ToolMessage),
#[child]
Frontend(FrontendMessage),
#[child]
InputPreprocessor(InputPreprocessorMessage),
#[child]
InputMapper(InputMapperMessage),
}
impl Message {
/// Returns the byte representation of the message.
///
/// # Safety
/// This function reads from uninitialized memory!!!
/// Only use if you know what you are doing
unsafe fn as_slice(&self) -> &[u8] {
core::slice::from_raw_parts(self as *const Message as *const u8, std::mem::size_of::<Message>())
}
/// Returns a pseudo hash that should uniquely identify the message.
/// This is needed because `Hash` is not implemented for f64s
///
/// # Safety
/// This function reads from uninitialized memory but the generated value should be fine.
pub fn pseudo_hash(&self) -> u64 {
let mut s = DefaultHasher::new();
unsafe { self.as_slice() }.hash(&mut s);
s.finish()
}
}

View File

@@ -0,0 +1,36 @@
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
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;
}
pub fn generate_hash<'a>(messages: impl IntoIterator<Item = &'a Message>, ipp: &InputPreprocessor, document_hash: u64) -> u64 {
let mut s = DefaultHasher::new();
document_hash.hash(&mut s);
ipp.hash(&mut s);
for message in messages {
message.pseudo_hash().hash(&mut s);
}
s.finish()
}

16
editor/src/consts.rs Normal file
View File

@@ -0,0 +1,16 @@
pub const PLUS_KEY_ZOOM_RATE: f64 = 1.25;
pub const MINUS_KEY_ZOOM_RATE: f64 = 0.8;
pub const VIEWPORT_ZOOM_SCALE_MIN: f64 = 0.000_001;
pub const VIEWPORT_ZOOM_SCALE_MAX: f64 = 1_000_000.;
pub const VIEWPORT_SCROLL_RATE: f64 = 0.6;
pub const WHEEL_ZOOM_RATE: f64 = 1. / 600.;
pub const MOUSE_ZOOM_RATE: f64 = 1. / 400.;
pub const ROTATE_SNAP_INTERVAL: f64 = 15.;
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
pub const SELECTION_TOLERANCE: f64 = 1.0;

View File

@@ -0,0 +1,501 @@
pub use super::layer_panel::*;
use crate::{frontend::layer_panel::*, EditorError};
use glam::{DAffine2, DVec2};
use graphene::{document::Document as InternalDocument, LayerId};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use graphene::layers::BlendMode;
use graphene::{DocumentResponse, Operation as DocumentOperation};
use log::warn;
use std::collections::VecDeque;
use super::movement_handler::{MovementMessage, MovementMessageHandler};
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum FlipAxis {
X,
Y,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAxis {
X,
Y,
}
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum AlignAggregate {
Min,
Max,
Center,
Average,
}
#[derive(Clone, Debug)]
pub struct DocumentMessageHandler {
pub document: InternalDocument,
pub document_backup: Option<InternalDocument>,
pub name: String,
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
movement_handler: MovementMessageHandler,
}
impl Default for DocumentMessageHandler {
fn default() -> Self {
Self {
document: InternalDocument::default(),
document_backup: None,
name: String::from("Untitled Document"),
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
}
}
}
#[impl_message(Message, DocumentsMessage, Document)]
#[derive(PartialEq, Clone, Debug)]
pub enum DocumentMessage {
#[child]
Movement(MovementMessage),
DispatchOperation(Box<DocumentOperation>),
SelectLayers(Vec<Vec<LayerId>>),
SelectAllLayers,
DeselectAllLayers,
DeleteLayer(Vec<LayerId>),
DeleteSelectedLayers,
DuplicateSelectedLayers,
SetBlendModeForSelectedLayers(BlendMode),
SetOpacityForSelectedLayers(f64),
AddFolder(Vec<LayerId>),
RenameLayer(Vec<LayerId>, String),
ToggleLayerVisibility(Vec<LayerId>),
FlipSelectedLayers(FlipAxis),
ToggleLayerExpansion(Vec<LayerId>),
FolderChanged(Vec<LayerId>),
StartTransaction,
RollbackTransaction,
AbortTransaction,
CommitTransaction,
ExportDocument,
RenderDocument,
Undo,
NudgeSelectedLayers(f64, f64),
AlignSelectedLayers(AlignAxis, AlignAggregate),
MoveSelectedLayersTo {
path: Vec<LayerId>,
insert_index: isize,
},
ReorderSelectedLayers(i32), // relative_position,
}
impl From<DocumentOperation> for DocumentMessage {
fn from(operation: DocumentOperation) -> DocumentMessage {
Self::DispatchOperation(Box::new(operation))
}
}
impl From<DocumentOperation> for Message {
fn from(operation: DocumentOperation) -> Message {
DocumentMessage::DispatchOperation(Box::new(operation)).into()
}
}
impl DocumentMessageHandler {
pub fn active_document(&self) -> &DocumentMessageHandler {
self
}
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
self
}
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
}
fn handle_folder_changed(&mut self, path: Vec<LayerId>) -> Option<Message> {
let _ = self.document.render_root();
self.layer_data(&path).expanded.then(|| {
let children = self.layer_panel(path.as_slice()).expect("The provided Path was not valid");
FrontendMessage::ExpandFolder { path, children }.into()
})
}
fn clear_selection(&mut self) {
self.layer_data.values_mut().for_each(|layer_data| layer_data.selected = false);
}
fn select_layer(&mut self, path: &[LayerId]) -> Option<Message> {
self.layer_data(path).selected = true;
// TODO: Add deduplication
(!path.is_empty()).then(|| self.handle_folder_changed(path[..path.len() - 1].to_vec())).flatten()
}
pub fn layerdata(&self, path: &[LayerId]) -> &LayerData {
self.layer_data.get(path).expect("Layerdata does not exist")
}
pub fn layerdata_mut(&mut self, path: &[LayerId]) -> &mut LayerData {
self.layer_data.entry(path.to_vec()).or_insert_with(|| LayerData::new(true))
}
pub fn selected_layers(&self) -> impl Iterator<Item = &Vec<LayerId>> {
self.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path))
}
/// Returns the paths to all layers in order, optionally including only selected or non-selected layers.
fn layers_sorted(&self, selected: Option<bool>) -> Vec<Vec<LayerId>> {
// Compute the indices for each layer to be able to sort them
let mut layers_with_indices: Vec<(Vec<LayerId>, Vec<usize>)> = self
.layer_data
.iter()
// 'path.len() > 0' filters out root layer since it has no indices
.filter_map(|(path, data)| (!path.is_empty() && (data.selected == selected.unwrap_or(data.selected))).then(|| path.clone()))
.filter_map(|path| {
// Currently it is possible that layer_data contains layers that are don't actually exist (has been partially fixed in #281)
// and thus indices_for_path can return an error. We currently skip these layers and log a warning.
// Once this problem is solved this code can be simplified
match self.document.indices_for_path(&path) {
Err(err) => {
warn!("layers_sorted: Could not get indices for the layer {:?}: {:?}", path, err);
None
}
Ok(indices) => Some((path, indices)),
}
})
.collect();
layers_with_indices.sort_by_key(|(_, indices)| indices.clone());
layers_with_indices.into_iter().map(|(path, _)| path).collect()
}
/// Returns the paths to all layers in order
pub fn all_layers_sorted(&self) -> Vec<Vec<LayerId>> {
self.layers_sorted(None)
}
/// Returns the paths to all selected layers in order
pub fn selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
self.layers_sorted(Some(true))
}
/// Returns the paths to all non_selected layers in order
#[allow(dead_code)] // used for test cases
pub fn non_selected_layers_sorted(&self) -> Vec<Vec<LayerId>> {
self.layers_sorted(Some(false))
}
pub fn with_name(name: String) -> Self {
Self {
document: InternalDocument::default(),
document_backup: None,
name,
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
movement_handler: MovementMessageHandler::default(),
}
}
pub fn layer_data(&mut self, path: &[LayerId]) -> &mut LayerData {
layer_data(&mut self.layer_data, path)
}
pub fn backup(&mut self) {
self.document_backup = Some(self.document.clone())
}
pub fn rollback(&mut self) -> Result<(), EditorError> {
self.backup();
self.reset()
}
pub fn reset(&mut self) -> Result<(), EditorError> {
match self.document_backup.take() {
Some(backup) => {
self.document = backup;
Ok(())
}
None => Err(EditorError::NoTransactionInProgress),
}
}
pub fn layer_panel_entry(&mut self, path: Vec<LayerId>) -> Result<LayerPanelEntry, EditorError> {
self.document.render_root();
let data: LayerData = *layer_data(&mut self.layer_data, &path);
let layer = self.document.layer(&path)?;
let entry = layer_panel_entry(&data, self.document.multiply_transforms(&path).unwrap(), layer, path);
Ok(entry)
}
/// Returns a list of `LayerPanelEntry`s intended for display purposes. These don't contain
/// any actual data, but ratfolderch as visibility and names of the layers.
pub fn layer_panel(&mut self, path: &[LayerId]) -> Result<Vec<LayerPanelEntry>, EditorError> {
let folder = self.document.folder(path)?;
let paths: Vec<Vec<LayerId>> = folder.layer_ids.iter().map(|id| [path, &[*id]].concat()).collect();
let data: Vec<LayerData> = paths.iter().map(|path| *layer_data(&mut self.layer_data, path)).collect();
let folder = self.document.folder(path)?;
let entries = folder
.layers()
.iter()
.zip(paths.iter().zip(data))
.rev()
.map(|(layer, (path, data))| {
layer_panel_entry(
&data,
self.document.generate_transform_across_scope(path, Some(self.document.root.transform.inverse())).unwrap(),
layer,
path.to_vec(),
)
})
.collect();
Ok(entries)
}
}
impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHandler {
fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
match message {
Movement(message) => self.movement_handler.process_action(message, (layer_data(&mut self.layer_data, &[]), &self.document, ipp), responses),
DeleteLayer(path) => responses.push_back(DocumentOperation::DeleteLayer { path }.into()),
AddFolder(path) => responses.push_back(DocumentOperation::AddFolder { path }.into()),
StartTransaction => self.backup(),
RollbackTransaction => {
self.rollback().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
}
AbortTransaction => {
self.reset().unwrap_or_else(|e| log::warn!("{}", e));
responses.extend([DocumentMessage::RenderDocument.into(), self.handle_folder_changed(vec![]).unwrap()]);
}
CommitTransaction => self.document_backup = None,
ExportDocument => {
let bbox = self.document.visible_layers_bounding_box().unwrap_or([DVec2::ZERO, ipp.viewport_size.as_f64()]);
let size = bbox[1] - bbox[0];
responses.push_back(
FrontendMessage::ExportDocument {
document: format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}">{}{}</svg>"#,
bbox[0].x,
bbox[0].y,
size.x,
size.y,
"\n",
self.document.render_root()
),
}
.into(),
)
}
SetBlendModeForSelectedLayers(blend_mode) => {
let active_document = self;
for path in active_document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())) {
responses.push_back(DocumentOperation::SetLayerBlendMode { path, blend_mode }.into());
}
}
SetOpacityForSelectedLayers(opacity) => {
let opacity = opacity.clamp(0., 1.);
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::SetLayerOpacity { path, opacity }.into());
}
}
ToggleLayerVisibility(path) => {
responses.push_back(DocumentOperation::ToggleVisibility { path }.into());
}
ToggleLayerExpansion(path) => {
self.layer_data(&path).expanded ^= true;
responses.extend(self.handle_folder_changed(path));
}
DeleteSelectedLayers => {
for path in self.selected_layers().cloned() {
responses.push_back(DocumentOperation::DeleteLayer { path }.into())
}
}
DuplicateSelectedLayers => {
for path in self.selected_layers_sorted() {
responses.push_back(DocumentOperation::DuplicateLayer { path }.into())
}
}
SelectLayers(paths) => {
self.clear_selection();
for path in paths {
responses.extend(self.select_layer(&path));
}
// TODO: Correctly update layer panel in clear_selection instead of here
responses.extend(self.handle_folder_changed(Vec::new()));
}
SelectAllLayers => {
let all_layer_paths = self.layer_data.keys().filter(|path| !path.is_empty()).cloned().collect::<Vec<_>>();
for path in all_layer_paths {
responses.extend(self.select_layer(&path));
}
}
DeselectAllLayers => {
self.clear_selection();
let children = self.layer_panel(&[]).expect("The provided Path was not valid");
responses.push_back(FrontendMessage::ExpandFolder { path: vec![], children }.into());
}
Undo => {
// this is a temporary fix and will be addressed by #123
if let Some(id) = self.document.root.as_folder().unwrap().list_layers().last() {
responses.push_back(DocumentOperation::DeleteLayer { path: vec![*id] }.into())
}
}
FolderChanged(path) => responses.extend(self.handle_folder_changed(path)),
DispatchOperation(op) => match self.document.handle_operation(&op) {
Ok(Some(mut document_responses)) => {
let canvas_dirty = self.filter_document_responses(&mut document_responses);
responses.extend(
document_responses
.into_iter()
.map(|response| match response {
DocumentResponse::FolderChanged { path } => self.handle_folder_changed(path),
DocumentResponse::DeletedLayer { path } => {
self.layer_data.remove(&path);
None
}
DocumentResponse::LayerChanged { path } => Some(
FrontendMessage::UpdateLayer {
path: path.clone(),
data: self.layer_panel_entry(path).unwrap(),
}
.into(),
),
DocumentResponse::CreatedLayer { path } => self.select_layer(&path),
DocumentResponse::DocumentChanged => unreachable!(),
})
.flatten(),
);
if canvas_dirty {
responses.push_back(RenderDocument.into())
}
}
Err(e) => log::error!("DocumentError: {:?}", e),
Ok(_) => (),
},
RenderDocument => responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.document.render_root(),
}
.into(),
),
NudgeSelectedLayers(x, y) => {
for path in self.selected_layers().cloned() {
let operation = DocumentOperation::TransformLayerInViewport {
path,
transform: DAffine2::from_translation((x, y).into()).to_cols_array(),
};
responses.push_back(operation.into());
}
}
MoveSelectedLayersTo { path, insert_index } => {
responses.push_back(DocumentsMessage::CopySelectedLayers.into());
responses.push_back(DocumentMessage::DeleteSelectedLayers.into());
responses.push_back(DocumentsMessage::PasteLayers { path, insert_index }.into());
}
ReorderSelectedLayers(relative_position) => {
let all_layer_paths = self.all_layers_sorted();
let selected_layers = self.selected_layers_sorted();
if let Some(pivot) = match relative_position.signum() {
-1 => selected_layers.first(),
1 => selected_layers.last(),
_ => unreachable!(),
} {
if let Some(pos) = all_layer_paths.iter().position(|path| path == pivot) {
let max = all_layer_paths.len() as i64 - 1;
let insert_pos = (pos as i64 + relative_position as i64).clamp(0, max) as usize;
let insert = all_layer_paths.get(insert_pos);
if let Some(insert_path) = insert {
let (id, path) = insert_path.split_last().expect("Can't move the root folder");
if let Some(folder) = self.document.layer(path).ok().map(|layer| layer.as_folder().ok()).flatten() {
let selected: Vec<_> = selected_layers
.iter()
.filter(|layer| layer.starts_with(path) && layer.len() == path.len() + 1)
.map(|x| x.last().unwrap())
.collect();
let non_selected: Vec<_> = folder.layer_ids.iter().filter(|id| selected.iter().all(|x| x != id)).collect();
let offset = if relative_position < 0 || non_selected.is_empty() { 0 } else { 1 };
let fallback = offset * (non_selected.len());
let insert_index = non_selected.iter().position(|x| *x == id).map(|x| x + offset).unwrap_or(fallback) as isize;
responses.push_back(DocumentMessage::MoveSelectedLayersTo { path: path.to_vec(), insert_index }.into())
}
}
}
}
}
FlipSelectedLayers(axis) => {
let scale = match axis {
FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.),
};
if let Some([min, max]) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) {
let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center);
for path in self.selected_layers() {
responses.push_back(
DocumentOperation::TransformLayerInScope {
path: path.clone(),
transform: DAffine2::from_scale(scale).to_cols_array(),
scope: bbox_trans.to_cols_array(),
}
.into(),
);
}
}
}
AlignSelectedLayers(axis, aggregate) => {
let (paths, boxes): (Vec<_>, Vec<_>) = self.selected_layers().filter_map(|path| self.document.viewport_bounding_box(path).ok()?.map(|b| (path, b))).unzip();
let axis = match axis {
AlignAxis::X => DVec2::X,
AlignAxis::Y => DVec2::Y,
};
let lerp = |bbox: &[DVec2; 2]| bbox[0].lerp(bbox[1], 0.5);
if let Some(combined_box) = self.document.combined_viewport_bounding_box(self.selected_layers().map(|x| x.as_slice())) {
let aggregated = match aggregate {
AlignAggregate::Min => combined_box[0],
AlignAggregate::Max => combined_box[1],
AlignAggregate::Center => lerp(&combined_box),
AlignAggregate::Average => boxes.iter().map(|b| lerp(b)).reduce(|a, b| a + b).map(|b| b / boxes.len() as f64).unwrap(),
};
for (path, bbox) in paths.into_iter().zip(boxes) {
let center = match aggregate {
AlignAggregate::Min => bbox[0],
AlignAggregate::Max => bbox[1],
_ => lerp(&bbox),
};
let translation = (aggregated - center) * axis;
responses.push_back(
DocumentOperation::TransformLayerInViewport {
path: path.clone(),
transform: DAffine2::from_translation(translation).to_cols_array(),
}
.into(),
);
}
}
}
RenameLayer(path, name) => responses.push_back(DocumentOperation::RenameLayer { path, name }.into()),
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(DocumentMessageDiscriminant;
Undo,
SelectAllLayers,
DeselectAllLayers,
RenderDocument,
ExportDocument,
);
if self.layer_data.values().any(|data| data.selected) {
let select = actions!(DocumentMessageDiscriminant;
DeleteSelectedLayers,
DuplicateSelectedLayers,
NudgeSelectedLayers,
ReorderSelectedLayers,
);
common.extend(select);
}
common.extend(self.movement_handler.actions());
common
}
}

View File

@@ -0,0 +1,253 @@
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use graphene::layers::Layer;
use graphene::{LayerId, Operation as DocumentOperation};
use log::warn;
use std::collections::VecDeque;
use super::DocumentMessageHandler;
#[impl_message(Message, Documents)]
#[derive(PartialEq, Clone, Debug)]
pub enum DocumentsMessage {
CopySelectedLayers,
PasteLayers {
path: Vec<LayerId>,
insert_index: isize,
},
SelectDocument(usize),
CloseDocument(usize),
#[child]
Document(DocumentMessage),
CloseActiveDocumentWithConfirmation,
CloseAllDocumentsWithConfirmation,
CloseAllDocuments,
NewDocument,
GetOpenDocumentsList,
NextDocument,
PrevDocument,
}
#[derive(Debug, Clone)]
pub struct DocumentsMessageHandler {
documents: Vec<DocumentMessageHandler>,
active_document_index: usize,
copy_buffer: Vec<Layer>,
}
impl DocumentsMessageHandler {
pub fn active_document(&self) -> &DocumentMessageHandler {
&self.documents[self.active_document_index]
}
pub fn active_document_mut(&mut self) -> &mut DocumentMessageHandler {
&mut self.documents[self.active_document_index]
}
}
impl Default for DocumentsMessageHandler {
fn default() -> Self {
Self {
documents: vec![DocumentMessageHandler::default()],
active_document_index: 0,
copy_buffer: vec![],
}
}
}
impl MessageHandler<DocumentsMessage, &InputPreprocessor> for DocumentsMessageHandler {
fn process_action(&mut self, message: DocumentsMessage, ipp: &InputPreprocessor, responses: &mut VecDeque<Message>) {
use DocumentMessage::*;
use DocumentsMessage::*;
match message {
Document(message) => self.active_document_mut().process_action(message, ipp, responses),
SelectDocument(id) => {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
self.active_document_index = id;
responses.push_back(
FrontendMessage::SetActiveDocument {
document_index: self.active_document_index,
}
.into(),
);
responses.push_back(RenderDocument.into());
}
CloseActiveDocumentWithConfirmation => {
responses.push_back(
FrontendMessage::DisplayConfirmationToCloseDocument {
document_index: self.active_document_index,
}
.into(),
);
}
CloseAllDocumentsWithConfirmation => {
responses.push_back(FrontendMessage::DisplayConfirmationToCloseAllDocuments.into());
}
CloseAllDocuments => {
// Empty the list of internal document data
self.documents.clear();
// Create a new blank document
responses.push_back(NewDocument.into());
}
CloseDocument(id) => {
assert!(id < self.documents.len(), "Tried to select a document that was not initialized");
// Remove doc from the backend store; use `id` as client tabs and backend documents will be in sync
self.documents.remove(id);
// Send the new list of document tab names
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
// Last tab was closed, so create a new blank tab
if self.documents.is_empty() {
self.active_document_index = 0;
responses.push_back(NewDocument.into());
}
// The currently selected doc is being closed
else if id == self.active_document_index {
// The currently selected tab was the rightmost tab
if id == self.documents.len() {
self.active_document_index -= 1;
}
let lp = self.active_document_mut().layer_panel(&[]).expect("Could not get panel for active doc");
responses.push_back(FrontendMessage::ExpandFolder { path: Vec::new(), children: lp }.into());
responses.push_back(
FrontendMessage::SetActiveDocument {
document_index: self.active_document_index,
}
.into(),
);
responses.push_back(
FrontendMessage::UpdateCanvas {
document: self.active_document_mut().document.render_root(),
}
.into(),
);
}
// Active doc will move one space to the left
else if id < self.active_document_index {
self.active_document_index -= 1;
responses.push_back(
FrontendMessage::SetActiveDocument {
document_index: self.active_document_index,
}
.into(),
);
}
}
NewDocument => {
let digits = ('0'..='9').collect::<Vec<char>>();
let mut doc_title_numbers = self
.documents
.iter()
.map(|d| {
if d.name.ends_with(digits.as_slice()) {
let (_, number) = d.name.split_at(17);
number.trim().parse::<usize>().unwrap()
} else {
1
}
})
.collect::<Vec<usize>>();
doc_title_numbers.sort_unstable();
let mut new_doc_title_num = 1;
while new_doc_title_num <= self.documents.len() {
if new_doc_title_num != doc_title_numbers[new_doc_title_num - 1] {
break;
}
new_doc_title_num += 1;
}
let name = match new_doc_title_num {
1 => "Untitled Document".to_string(),
_ => format!("Untitled Document {}", new_doc_title_num),
};
self.active_document_index = self.documents.len();
let new_document = DocumentMessageHandler::with_name(name);
self.documents.push(new_document);
// Send the new list of document tab names
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
responses.push_back(
FrontendMessage::ExpandFolder {
path: Vec::new(),
children: Vec::new(),
}
.into(),
);
responses.push_back(SelectDocument(self.active_document_index).into());
}
GetOpenDocumentsList => {
// Send the list of document tab names
let open_documents = self.documents.iter().map(|doc| doc.name.clone()).collect();
responses.push_back(FrontendMessage::UpdateOpenDocumentsList { open_documents }.into());
}
NextDocument => {
let id = (self.active_document_index + 1) % self.documents.len();
responses.push_back(SelectDocument(id).into());
}
PrevDocument => {
let id = (self.active_document_index + self.documents.len() - 1) % self.documents.len();
responses.push_back(SelectDocument(id).into());
}
CopySelectedLayers => {
let paths = self.active_document().selected_layers_sorted();
self.copy_buffer.clear();
for path in paths {
match self.active_document().document.layer(&path).map(|t| t.clone()) {
Ok(layer) => {
self.copy_buffer.push(layer);
}
Err(e) => warn!("Could not access selected layer {:?}: {:?}", path, e),
}
}
}
PasteLayers { path, insert_index } => {
let paste = |layer: &Layer, responses: &mut VecDeque<_>| {
log::trace!("pasting into folder {:?} as index: {}", path, insert_index);
responses.push_back(
DocumentOperation::PasteLayer {
layer: layer.clone(),
path: path.clone(),
insert_index,
}
.into(),
)
};
if insert_index == -1 {
for layer in self.copy_buffer.iter() {
paste(layer, responses)
}
} else {
for layer in self.copy_buffer.iter().rev() {
paste(layer, responses)
}
}
}
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(DocumentsMessageDiscriminant;
NewDocument,
CloseActiveDocumentWithConfirmation,
CloseAllDocumentsWithConfirmation,
CloseAllDocuments,
NextDocument,
PrevDocument,
PasteLayers,
);
if self.active_document().layer_data.values().any(|data| data.selected) {
let select = actions!(DocumentsMessageDiscriminant;
CopySelectedLayers,
);
common.extend(select);
}
common.extend(self.active_document().actions());
common
}
}

View File

@@ -0,0 +1,94 @@
use crate::{consts::ROTATE_SNAP_INTERVAL, frontend::layer_panel::*};
use glam::{DAffine2, DVec2};
use graphene::{
layers::{Layer, LayerData as DocumentLayerData},
LayerId,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
pub struct LayerData {
pub selected: bool,
pub expanded: bool,
pub translation: DVec2,
pub rotation: f64,
pub snap_rotate: bool,
pub scale: f64,
}
impl LayerData {
pub fn new(expanded: bool) -> LayerData {
LayerData {
selected: false,
expanded,
translation: DVec2::ZERO,
rotation: 0.,
snap_rotate: false,
scale: 1.,
}
}
pub fn snapped_angle(&self) -> f64 {
let increment_radians: f64 = ROTATE_SNAP_INTERVAL.to_radians();
if self.snap_rotate {
(self.rotation / increment_radians).round() * increment_radians
} else {
self.rotation
}
}
pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 {
// TODO: replace with DAffine2::from_scale_angle_translation and fix the errors
let offset_transform = DAffine2::from_translation(offset);
let scale_transform = DAffine2::from_scale(DVec2::new(self.scale, self.scale));
let angle_transform = DAffine2::from_angle(self.snapped_angle());
let translation_transform = DAffine2::from_translation(self.translation);
scale_transform * offset_transform * angle_transform * translation_transform
}
}
pub fn layer_data<'a>(layer_data: &'a mut HashMap<Vec<LayerId>, LayerData>, path: &[LayerId]) -> &'a mut LayerData {
if !layer_data.contains_key(path) {
layer_data.insert(path.to_vec(), LayerData::new(false));
}
layer_data.get_mut(path).unwrap()
}
pub fn layer_panel_entry(layer_data: &LayerData, transform: DAffine2, layer: &Layer, path: Vec<LayerId>) -> LayerPanelEntry {
let layer_type: LayerType = (&layer.data).into();
let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type));
let arr = layer.data.bounding_box(transform).unwrap_or([DVec2::ZERO, DVec2::ZERO]);
let arr = arr.iter().map(|x| (*x).into()).collect::<Vec<(f64, f64)>>();
let mut thumbnail = String::new();
layer.data.clone().render(&mut thumbnail, &mut vec![transform]);
let transform = transform.to_cols_array().iter().map(ToString::to_string).collect::<Vec<_>>().join(",");
let thumbnail = if let [(x_min, y_min), (x_max, y_max)] = arr.as_slice() {
format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{} {} {} {}"><g transform="matrix({})">{}</g></svg>"#,
x_min,
y_min,
x_max - x_min,
y_max - y_min,
transform,
thumbnail,
)
} else {
String::new()
};
// LayerIds are sent as (u32, u32) because jsond does not support u64s
let path = path.iter().map(|id| ((id >> 32) as u32, (id << 32 >> 32) as u32)).collect::<Vec<_>>();
LayerPanelEntry {
name,
visible: layer.visible,
blend_mode: layer.blend_mode,
opacity: layer.opacity,
layer_type: (&layer.data).into(),
layer_data: *layer_data,
path,
thumbnail,
}
}

View File

@@ -0,0 +1,14 @@
mod document_file;
mod document_message_handler;
mod layer_panel;
mod movement_handler;
#[doc(inline)]
pub use document_file::LayerData;
#[doc(inline)]
pub use document_file::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler, FlipAxis};
#[doc(inline)]
pub use document_message_handler::{DocumentsMessage, DocumentsMessageDiscriminant, DocumentsMessageHandler};
#[doc(inline)]
pub use movement_handler::{MovementMessage, MovementMessageDiscriminant};

View File

@@ -0,0 +1,214 @@
pub use super::layer_panel::*;
use super::LayerData;
use crate::message_prelude::*;
use crate::{
consts::{MOUSE_ZOOM_RATE, VIEWPORT_SCROLL_RATE, VIEWPORT_ZOOM_SCALE_MAX, VIEWPORT_ZOOM_SCALE_MIN, WHEEL_ZOOM_RATE},
input::{mouse::ViewportPosition, InputPreprocessor},
};
use glam::DVec2;
use graphene::document::Document;
use graphene::Operation as DocumentOperation;
use std::collections::VecDeque;
#[impl_message(Message, DocumentMessage, Movement)]
#[derive(PartialEq, Clone, Debug)]
pub enum MovementMessage {
MouseMove,
TranslateCanvasBegin,
WheelCanvasTranslate { use_y_as_x: bool },
RotateCanvasBegin { snap: bool },
EnableSnapping,
DisableSnapping,
ZoomCanvasBegin,
TranslateCanvasEnd,
SetCanvasZoom(f64),
MultiplyCanvasZoom(f64),
WheelCanvasZoom,
SetCanvasRotation(f64),
ZoomCanvasToFitAll,
}
#[derive(Debug, Clone, Hash, Default, PartialEq)]
pub struct MovementMessageHandler {
translating: bool,
rotating: bool,
zooming: bool,
snapping: bool,
mouse_pos: ViewportPosition,
}
impl MovementMessageHandler {
fn create_document_transform_from_layerdata(&self, layerdata: &LayerData, viewport_size: &ViewportPosition, responses: &mut VecDeque<Message>) {
let half_viewport = viewport_size.as_f64() / 2.;
let scaled_half_viewport = half_viewport / layerdata.scale;
responses.push_back(
DocumentOperation::SetLayerTransform {
path: vec![],
transform: layerdata.calculate_offset_transform(scaled_half_viewport).to_cols_array(),
}
.into(),
);
}
}
impl MessageHandler<MovementMessage, (&mut LayerData, &Document, &InputPreprocessor)> for MovementMessageHandler {
fn process_action(&mut self, message: MovementMessage, data: (&mut LayerData, &Document, &InputPreprocessor), responses: &mut VecDeque<Message>) {
let (layerdata, document, ipp) = data;
use MovementMessage::*;
match message {
TranslateCanvasBegin => {
self.translating = true;
self.mouse_pos = ipp.mouse.position;
}
RotateCanvasBegin { snap } => {
self.rotating = true;
self.snapping = snap;
layerdata.snap_rotate = snap;
self.mouse_pos = ipp.mouse.position;
}
EnableSnapping => self.snapping = true,
DisableSnapping => self.snapping = false,
ZoomCanvasBegin => {
self.zooming = true;
self.mouse_pos = ipp.mouse.position;
}
TranslateCanvasEnd => {
layerdata.rotation = layerdata.snapped_angle();
layerdata.snap_rotate = false;
self.translating = false;
self.rotating = false;
self.zooming = false;
}
MouseMove => {
if self.translating {
let delta = ipp.mouse.position.as_f64() - self.mouse_pos.as_f64();
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
layerdata.translation += transformed_delta;
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
if self.rotating {
let half_viewport = ipp.viewport_size.as_f64() / 2.;
let rotation = {
let start_vec = self.mouse_pos.as_f64() - half_viewport;
let end_vec = ipp.mouse.position.as_f64() - half_viewport;
start_vec.angle_between(end_vec)
};
let snapping = self.snapping;
layerdata.rotation += rotation;
layerdata.snap_rotate = snapping;
responses.push_back(
FrontendMessage::SetCanvasRotation {
new_radians: layerdata.snapped_angle(),
}
.into(),
);
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
if self.zooming {
let difference = self.mouse_pos.y as f64 - ipp.mouse.position.y as f64;
let amount = 1. + difference * MOUSE_ZOOM_RATE;
let new = (layerdata.scale * amount).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
self.mouse_pos = ipp.mouse.position;
}
SetCanvasZoom(new) => {
layerdata.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
MultiplyCanvasZoom(multiplier) => {
let new = (layerdata.scale * multiplier).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
WheelCanvasZoom => {
let scroll = ipp.mouse.scroll_delta.scroll_delta();
let mouse = ipp.mouse.position.as_f64();
let viewport_size = ipp.viewport_size.as_f64();
let mut zoom_factor = 1. + scroll.abs() * WHEEL_ZOOM_RATE;
if ipp.mouse.scroll_delta.y > 0 {
zoom_factor = 1. / zoom_factor
};
let new_viewport_size = viewport_size * (1. / zoom_factor);
let delta_size = viewport_size - new_viewport_size;
let mouse_percent = mouse / viewport_size;
let delta = (delta_size * -2.) * (mouse_percent - DVec2::splat(0.5));
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
let new = (layerdata.scale * zoom_factor).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX);
layerdata.scale = new;
layerdata.translation += transformed_delta;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
WheelCanvasTranslate { use_y_as_x } => {
let delta = match use_y_as_x {
false => -ipp.mouse.scroll_delta.as_dvec2(),
true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(),
} * VIEWPORT_SCROLL_RATE;
let transformed_delta = document.root.transform.inverse().transform_vector2(delta);
layerdata.translation += transformed_delta;
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
SetCanvasRotation(new) => {
layerdata.rotation = new;
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: new }.into());
}
ZoomCanvasToFitAll => {
if let Some([pos1, pos2]) = document.visible_layers_bounding_box() {
let pos1 = document.root.transform.inverse().transform_point2(pos1);
let pos2 = document.root.transform.inverse().transform_point2(pos2);
let v1 = document.root.transform.inverse().transform_point2(DVec2::ZERO);
let v2 = document.root.transform.inverse().transform_point2(ipp.viewport_size.as_f64());
let center = v1.lerp(v2, 0.5) - pos1.lerp(pos2, 0.5);
let size = (pos2 - pos1) / (v2 - v1);
let size = 1. / size;
let new_scale = size.min_element();
layerdata.translation += center;
layerdata.scale *= new_scale;
responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: layerdata.scale }.into());
self.create_document_transform_from_layerdata(layerdata, &ipp.viewport_size, responses);
}
}
}
}
fn actions(&self) -> ActionList {
let mut common = actions!(MovementMessageDiscriminant;
MouseMove,
TranslateCanvasEnd,
TranslateCanvasBegin,
RotateCanvasBegin,
ZoomCanvasBegin,
SetCanvasZoom,
MultiplyCanvasZoom,
SetCanvasRotation,
WheelCanvasZoom,
WheelCanvasTranslate,
ZoomCanvasToFitAll,
);
if self.rotating {
let snapping = actions!(MovementMessageDiscriminant;
EnableSnapping,
DisableSnapping,
);
common.extend(snapping);
}
common
}
}

View File

@@ -0,0 +1,54 @@
use crate::frontend::layer_panel::LayerPanelEntry;
use crate::message_prelude::*;
use crate::Color;
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 },
SetActiveDocument { document_index: usize },
UpdateOpenDocumentsList { open_documents: Vec<String> },
DisplayConfirmationToCloseDocument { document_index: usize },
DisplayConfirmationToCloseAllDocuments,
UpdateCanvas { document: String },
UpdateLayer { path: Vec<LayerId>, data: LayerPanelEntry },
ExportDocument { document: String },
EnableTextInput,
DisableTextInput,
UpdateWorkingColors { primary: Color, secondary: Color },
SetCanvasZoom { new_zoom: f64 },
SetCanvasRotation { new_radians: f64 },
}
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>) {
(self.callback)(message)
}
advertise_actions!(
FrontendMessageDiscriminant;
CollapseFolder,
ExpandFolder,
SetActiveTool,
UpdateCanvas,
EnableTextInput,
DisableTextInput,
SetCanvasZoom,
SetCanvasRotation,
);
}

View File

@@ -0,0 +1,44 @@
use crate::document::LayerData;
use graphene::layers::{BlendMode, LayerDataType};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct LayerPanelEntry {
pub name: String,
pub visible: bool,
pub blend_mode: BlendMode,
pub opacity: f64,
pub layer_type: LayerType,
pub layer_data: LayerData,
// TODO: Instead of turning the u64 into (u32, u32)s here, do that in the WASM translation layer
pub path: Vec<(u32, u32)>,
pub thumbnail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum LayerType {
Folder,
Shape,
}
impl fmt::Display for LayerType {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
LayerType::Folder => "Folder",
LayerType::Shape => "Shape",
};
formatter.write_str(name)
}
}
impl From<&LayerDataType> for LayerType {
fn from(data: &LayerDataType) -> Self {
use LayerDataType::*;
match data {
Folder(_) => LayerType::Folder,
Shape(_) => LayerType::Shape,
}
}
}

View File

@@ -0,0 +1,4 @@
pub mod frontend_message_handler;
pub mod layer_panel;
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, Hash)]
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);
}

3
editor/src/global/mod.rs Normal file
View File

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

View File

@@ -0,0 +1,315 @@
use super::{
keyboard::{Key, KeyStates, NUMBER_OF_KEYS},
InputPreprocessor,
};
use crate::consts::{MINUS_KEY_ZOOM_RATE, PLUS_KEY_ZOOM_RATE};
use crate::message_prelude::*;
use crate::tool::ToolType;
use std::fmt::Write;
const NUDGE_AMOUNT: f64 = 1.;
const SHIFT_NUDGE_AMOUNT: f64 = 10.;
#[impl_message(Message, InputMapper)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum InputMapperMessage {
PointerMove,
MouseScroll,
KeyUp(Key),
#[child]
KeyDown(Key),
}
#[derive(PartialEq, Clone, Debug)]
struct MappingEntry {
trigger: InputMapperMessage,
modifiers: KeyStates,
action: Message,
}
#[derive(Debug, Clone)]
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)
}
const fn new() -> Self {
Self(Vec::new())
}
fn key_array() -> [Self; NUMBER_OF_KEYS] {
const DEFAULT: KeyMappingEntries = KeyMappingEntries::new();
[DEFAULT; NUMBER_OF_KEYS]
}
}
impl Default for KeyMappingEntries {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
struct Mapping {
key_up: [KeyMappingEntries; NUMBER_OF_KEYS],
key_down: [KeyMappingEntries; NUMBER_OF_KEYS],
pointer_move: KeyMappingEntries,
mouse_scroll: 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()}]
}};
{action=$action:expr, triggers=[$($m:ident),* $(,)?]} => {{
&[
MappingEntry {trigger:InputMapperMessage::PointerMove, action: $action.into(), modifiers: modifiers!()},
$(
MappingEntry {trigger:InputMapperMessage::KeyDown(Key::$m), action: $action.into(), modifiers: modifiers!()},
MappingEntry {trigger:InputMapperMessage::KeyUp(Key::$m), action: $action.into(), modifiers: modifiers!()},
)*
]
}};
}
macro_rules! mapping {
//[$(<action=$action:expr; message=$key:expr; $(modifiers=[$($m:ident),* $(,)?];)?>)*] => {{
[$($entry:expr),* $(,)?] => {{
let mut key_up = KeyMappingEntries::key_array();
let mut key_down = KeyMappingEntries::key_array();
let mut pointer_move: KeyMappingEntries = Default::default();
let mut mouse_scroll: KeyMappingEntries = Default::default();
$(
for entry in $entry {
let arr = match entry.trigger {
InputMapperMessage::KeyDown(key) => &mut key_down[key as usize],
InputMapperMessage::KeyUp(key) => &mut key_up[key as usize],
InputMapperMessage::PointerMove => &mut pointer_move,
InputMapperMessage::MouseScroll => &mut mouse_scroll,
};
arr.push(entry.clone());
}
)*
(key_up, key_down, pointer_move, mouse_scroll)
}};
}
impl Default for Mapping {
fn default() -> Self {
use Key::*;
let mappings = mapping![
entry! {action=DocumentsMessage::PasteLayers{path: vec![], insert_index: -1}, key_down=KeyV, modifiers=[KeyControl]},
entry! {action=MovementMessage::EnableSnapping, key_down=KeyShift},
entry! {action=MovementMessage::DisableSnapping, key_up=KeyShift},
// Select
entry! {action=SelectMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=SelectMessage::DragStart, key_down=Lmb},
entry! {action=SelectMessage::DragStop, key_up=Lmb},
entry! {action=SelectMessage::Abort, key_down=Rmb},
entry! {action=SelectMessage::Abort, key_down=KeyEscape},
// Eyedropper
entry! {action=EyedropperMessage::LeftMouseDown, key_down=Lmb},
entry! {action=EyedropperMessage::RightMouseDown, key_down=Rmb},
// Rectangle
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::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
// Ellipse
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::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
// Shape
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::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]},
// Line
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::Redraw{center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift}, triggers=[KeyAlt, KeyShift, KeyControl]},
// Pen
entry! {action=PenMessage::PointerMove, 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},
// Fill
entry! {action=FillMessage::MouseDown, key_down=Lmb},
// Tool Actions
entry! {action=ToolMessage::SelectTool(ToolType::Fill), key_down=KeyF},
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::Pen), key_down=KeyP},
entry! {action=ToolMessage::SelectTool(ToolType::Shape), key_down=KeyY},
entry! {action=ToolMessage::SelectTool(ToolType::Eyedropper), key_down=KeyI},
entry! {action=ToolMessage::ResetColors, key_down=KeyX, modifiers=[KeyShift, KeyControl]},
entry! {action=ToolMessage::SwapColors, key_down=KeyX, modifiers=[KeyShift]},
// Document Actions
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},
entry! {action=DocumentMessage::ExportDocument, key_down=KeyE, modifiers=[KeyControl]},
entry! {action=MovementMessage::MouseMove, message=InputMapperMessage::PointerMove},
entry! {action=MovementMessage::RotateCanvasBegin{snap:false}, key_down=Mmb, modifiers=[KeyControl]},
entry! {action=MovementMessage::RotateCanvasBegin{snap:true}, key_down=Mmb, modifiers=[KeyControl, KeyShift]},
entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]},
entry! {action=MovementMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]},
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb},
entry! {action=MovementMessage::TranslateCanvasEnd, key_up=Mmb},
entry! {action=MovementMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyPlus, modifiers=[KeyControl]},
entry! {action=MovementMessage::MultiplyCanvasZoom(PLUS_KEY_ZOOM_RATE), key_down=KeyEquals, modifiers=[KeyControl]},
entry! {action=MovementMessage::MultiplyCanvasZoom(MINUS_KEY_ZOOM_RATE), key_down=KeyMinus, modifiers=[KeyControl]},
entry! {action=MovementMessage::SetCanvasZoom(1.), key_down=Key1, modifiers=[KeyControl]},
entry! {action=MovementMessage::SetCanvasZoom(2.), key_down=Key2, modifiers=[KeyControl]},
entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]},
entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: true}, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]},
entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: false}, message=InputMapperMessage::MouseScroll},
entry! {action=DocumentsMessage::NewDocument, key_down=KeyN, modifiers=[KeyControl]},
entry! {action=DocumentsMessage::NextDocument, key_down=KeyTab, modifiers=[KeyControl]},
entry! {action=DocumentsMessage::PrevDocument, key_down=KeyTab, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentsMessage::CloseAllDocumentsWithConfirmation, key_down=KeyW, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentsMessage::CloseActiveDocumentWithConfirmation, key_down=KeyW, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DuplicateSelectedLayers, key_down=KeyD, modifiers=[KeyControl]},
entry! {action=DocumentsMessage::CopySelectedLayers, key_down=KeyC, modifiers=[KeyControl]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift, KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers(0., -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, SHIFT_NUDGE_AMOUNT), key_down=KeyArrowDown, modifiers=[KeyShift, KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, SHIFT_NUDGE_AMOUNT), key_down=KeyArrowDown, modifiers=[KeyShift, KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers(0., SHIFT_NUDGE_AMOUNT), key_down=KeyArrowDown, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowLeft, modifiers=[KeyShift, KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, SHIFT_NUDGE_AMOUNT), key_down=KeyArrowLeft, modifiers=[KeyShift, KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-SHIFT_NUDGE_AMOUNT, 0.), key_down=KeyArrowLeft, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, -SHIFT_NUDGE_AMOUNT), key_down=KeyArrowRight, modifiers=[KeyShift, KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, SHIFT_NUDGE_AMOUNT), key_down=KeyArrowRight, modifiers=[KeyShift, KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers(SHIFT_NUDGE_AMOUNT, 0.), key_down=KeyArrowRight, modifiers=[KeyShift]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-NUDGE_AMOUNT, -NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers(NUDGE_AMOUNT, -NUDGE_AMOUNT), key_down=KeyArrowUp, modifiers=[KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers(0., -NUDGE_AMOUNT), key_down=KeyArrowUp},
entry! {action=DocumentMessage::NudgeSelectedLayers(-NUDGE_AMOUNT, NUDGE_AMOUNT), key_down=KeyArrowDown, modifiers=[KeyArrowLeft]},
entry! {action=DocumentMessage::NudgeSelectedLayers(NUDGE_AMOUNT, NUDGE_AMOUNT), key_down=KeyArrowDown, modifiers=[KeyArrowRight]},
entry! {action=DocumentMessage::NudgeSelectedLayers(0., NUDGE_AMOUNT), key_down=KeyArrowDown},
entry! {action=DocumentMessage::NudgeSelectedLayers(-NUDGE_AMOUNT, -NUDGE_AMOUNT), key_down=KeyArrowLeft, modifiers=[KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-NUDGE_AMOUNT, NUDGE_AMOUNT), key_down=KeyArrowLeft, modifiers=[KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers(-NUDGE_AMOUNT, 0.), key_down=KeyArrowLeft},
entry! {action=DocumentMessage::NudgeSelectedLayers(NUDGE_AMOUNT, -NUDGE_AMOUNT), key_down=KeyArrowRight, modifiers=[KeyArrowUp]},
entry! {action=DocumentMessage::NudgeSelectedLayers(NUDGE_AMOUNT, NUDGE_AMOUNT), key_down=KeyArrowRight, modifiers=[KeyArrowDown]},
entry! {action=DocumentMessage::NudgeSelectedLayers(NUDGE_AMOUNT, 0.), key_down=KeyArrowRight},
entry! {action=DocumentMessage::ReorderSelectedLayers(i32::MAX), key_down=KeyRightCurlyBracket, modifiers=[KeyControl]}, // TODO: Use KeyRightBracket with ctrl+shift modifiers once input system is fixed
entry! {action=DocumentMessage::ReorderSelectedLayers(1), key_down=KeyRightBracket, modifiers=[KeyControl]},
entry! {action=DocumentMessage::ReorderSelectedLayers(-1), key_down=KeyLeftBracket, modifiers=[KeyControl]},
entry! {action=DocumentMessage::ReorderSelectedLayers(i32::MIN), key_down=KeyLeftCurlyBracket, modifiers=[KeyControl]}, // TODO: Use KeyLeftBracket with ctrl+shift modifiers once input system is fixed
// Global Actions
entry! {action=GlobalMessage::LogInfo, key_down=Key1},
entry! {action=GlobalMessage::LogDebug, key_down=Key2},
entry! {action=GlobalMessage::LogTrace, key_down=Key3},
];
let (mut key_up, mut key_down, mut pointer_move, mut mouse_scroll) = mappings;
let sort = |list: &mut KeyMappingEntries| list.0.sort_by(|u, v| v.modifiers.ones().cmp(&u.modifiers.ones()));
for list in [&mut key_up, &mut key_down] {
for sublist in list {
sort(sublist);
}
}
sort(&mut pointer_move);
sort(&mut mouse_scroll);
Self {
key_up,
key_down,
pointer_move,
mouse_scroll,
}
}
}
impl Mapping {
fn match_message(&self, message: InputMapperMessage, keys: &KeyStates, actions: ActionList) -> Option<Message> {
use InputMapperMessage::*;
let list = match message {
KeyDown(key) => &self.key_down[key as usize],
KeyUp(key) => &self.key_up[key as usize],
PointerMove => &self.pointer_move,
MouseScroll => &self.mouse_scroll,
};
list.match_mapping(keys, actions)
}
}
#[derive(Debug, Default)]
pub struct InputMapper {
mapping: Mapping,
}
impl InputMapper {
pub fn hints(&self, actions: ActionList) -> String {
let mut output = String::new();
let mut actions = actions
.into_iter()
.flatten()
.filter(|a| !matches!(*a, MessageDiscriminant::Tool(ToolMessageDiscriminant::SelectTool) | MessageDiscriminant::Global(_)));
self.mapping
.key_down
.iter()
.enumerate()
.filter_map(|(i, m)| {
let ma = m.0.iter().find_map(|m| actions.find_map(|a| (a == m.action.to_discriminant()).then(|| m.action.to_discriminant())));
ma.map(|a| unsafe { (std::mem::transmute_copy::<usize, Key>(&i), a) })
})
.for_each(|(k, a)| {
let _ = write!(output, "{}: {}, ", k.to_discriminant().local_name(), a.local_name().split('.').last().unwrap());
});
output.replace("Key", "")
}
}
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,198 @@
use std::usize;
use super::keyboard::{Key, KeyStates};
use super::mouse::{MouseKeys, MouseState, ScrollDelta, ViewportPosition};
use crate::message_prelude::*;
use bitflags::bitflags;
#[doc(inline)]
pub use graphene::DocumentResponse;
#[impl_message(Message, InputPreprocessor)]
#[derive(PartialEq, Clone, Debug)]
pub enum InputPreprocessorMessage {
MouseDown(MouseState, ModifierKeys),
MouseUp(MouseState, ModifierKeys),
MouseMove(ViewportPosition, ModifierKeys),
MouseScroll(ScrollDelta, ModifierKeys),
KeyUp(Key, ModifierKeys),
KeyDown(Key, ModifierKeys),
ViewportResize(ViewportPosition),
}
bitflags! {
#[derive(Default)]
#[repr(transparent)]
pub struct ModifierKeys: u8 {
const CONTROL = 0b0000_0001;
const SHIFT = 0b0000_0010;
const ALT = 0b0000_0100;
}
}
#[derive(Debug, Default, Hash)]
pub struct InputPreprocessor {
pub keyboard: KeyStates,
pub mouse: MouseState,
pub viewport_size: ViewportPosition,
}
enum KeyPosition {
Pressed,
Released,
}
impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessor {
fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque<Message>) {
match message {
InputPreprocessorMessage::MouseMove(pos, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.mouse.position = pos;
responses.push_back(InputMapperMessage::PointerMove.into());
}
InputPreprocessorMessage::MouseScroll(delta, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.mouse.scroll_delta = delta;
responses.push_back(InputMapperMessage::MouseScroll.into());
}
InputPreprocessorMessage::MouseDown(state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
responses.push_back(self.translate_mouse_event(state, KeyPosition::Pressed));
}
InputPreprocessorMessage::MouseUp(state, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
responses.push_back(self.translate_mouse_event(state, KeyPosition::Released));
}
InputPreprocessorMessage::KeyDown(key, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.keyboard.set(key as usize);
responses.push_back(InputMapperMessage::KeyDown(key).into());
}
InputPreprocessorMessage::KeyUp(key, modifier_keys) => {
self.handle_modifier_keys(modifier_keys, responses);
self.keyboard.unset(key as usize);
responses.push_back(InputMapperMessage::KeyUp(key).into());
}
InputPreprocessorMessage::ViewportResize(size) => {
responses.push_back(
graphene::Operation::TransformLayer {
path: vec![],
transform: glam::DAffine2::from_translation((size.as_f64() - self.viewport_size.as_f64()) / 2.).to_cols_array(),
}
.into(),
);
self.viewport_size = size;
}
};
}
// 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(),
}
}
fn handle_modifier_keys(&mut self, modifier_keys: ModifierKeys, responses: &mut VecDeque<Message>) {
self.handle_modifier_key(Key::KeyControl, modifier_keys.contains(ModifierKeys::CONTROL), responses);
self.handle_modifier_key(Key::KeyShift, modifier_keys.contains(ModifierKeys::SHIFT), responses);
self.handle_modifier_key(Key::KeyAlt, modifier_keys.contains(ModifierKeys::ALT), responses);
}
fn handle_modifier_key(&mut self, key: Key, key_is_down: bool, responses: &mut VecDeque<Message>) {
let key_was_down = self.keyboard.get(key as usize);
if key_was_down && !key_is_down {
self.keyboard.unset(key as usize);
responses.push_back(InputMapperMessage::KeyUp(key).into());
} else if !key_was_down && key_is_down {
self.keyboard.set(key as usize);
responses.push_back(InputMapperMessage::KeyDown(key).into());
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn process_action_mouse_move_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessor::default();
let message = InputPreprocessorMessage::MouseMove((4, 809).into(), ModifierKeys::ALT);
let mut responses = VecDeque::new();
input_preprocessor.process_action(message, (), &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyAlt as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyAlt).into()));
}
#[test]
fn process_action_mouse_down_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessor::default();
let message = InputPreprocessorMessage::MouseDown(MouseState::new(), ModifierKeys::CONTROL);
let mut responses = VecDeque::new();
input_preprocessor.process_action(message, (), &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyControl).into()));
}
#[test]
fn process_action_mouse_up_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessor::default();
let message = InputPreprocessorMessage::MouseUp(MouseState::new(), ModifierKeys::SHIFT);
let mut responses = VecDeque::new();
input_preprocessor.process_action(message, (), &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyDown(Key::KeyShift).into()));
}
#[test]
fn process_action_key_down_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessor::default();
input_preprocessor.keyboard.set(Key::KeyControl as usize);
let message = InputPreprocessorMessage::KeyDown(Key::KeyA, ModifierKeys::empty());
let mut responses = VecDeque::new();
input_preprocessor.process_action(message, (), &mut responses);
assert!(!input_preprocessor.keyboard.get(Key::KeyControl as usize));
assert_eq!(responses.pop_front(), Some(InputMapperMessage::KeyUp(Key::KeyControl).into()));
}
#[test]
fn process_action_key_up_handle_modifier_keys() {
let mut input_preprocessor = InputPreprocessor::default();
let message = InputPreprocessorMessage::KeyUp(Key::KeyS, ModifierKeys::CONTROL | ModifierKeys::SHIFT);
let mut responses = VecDeque::new();
input_preprocessor.process_action(message, (), &mut responses);
assert!(input_preprocessor.keyboard.get(Key::KeyControl as usize));
assert!(input_preprocessor.keyboard.get(Key::KeyShift as usize));
assert!(responses.contains(&InputMapperMessage::KeyDown(Key::KeyControl).into()));
assert!(responses.contains(&InputMapperMessage::KeyDown(Key::KeyControl).into()));
}
}

View File

@@ -0,0 +1,189 @@
use crate::message_prelude::*;
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 = u128;
// base 2 logarithm of the storage type used to represents how many bits you need to fully address every bit in that storage type
const STORAGE_SIZE: u32 = (std::mem::size_of::<StorageType>() * 8).trailing_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>;
#[impl_message(Message, InputMapperMessage, KeyDown)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
UnknownKey,
// MouseKeys
Lmb,
Rmb,
Mmb,
// Keyboard keys
KeyA,
KeyB,
KeyC,
KeyD,
KeyE,
KeyF,
KeyG,
KeyH,
KeyI,
KeyJ,
KeyK,
KeyL,
KeyM,
KeyN,
KeyO,
KeyP,
KeyQ,
KeyR,
KeyS,
KeyT,
KeyU,
KeyV,
KeyW,
KeyX,
KeyY,
KeyZ,
Key0,
Key1,
Key2,
Key3,
Key4,
Key5,
Key6,
Key7,
Key8,
Key9,
KeyEnter,
KeyEquals,
KeyMinus,
KeyPlus,
KeyShift,
KeyControl,
KeyDelete,
KeyBackspace,
KeyAlt,
KeyEscape,
KeyTab,
KeyArrowUp,
KeyArrowDown,
KeyArrowLeft,
KeyArrowRight,
KeyLeftBracket,
KeyRightBracket,
KeyLeftCurlyBracket,
KeyRightCurlyBracket,
// 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 get(&self, bitvector_index: usize) -> bool {
let (offset, bit) = Self::convert_index(bitvector_index);
(self.0[offset] & bit) != 0
}
pub fn is_empty(&self) -> bool {
let mut result = 0;
for storage in self.0.iter() {
result |= storage;
}
result == 0
}
pub fn ones(&self) -> u32 {
let mut result = 0;
for storage in self.0.iter() {
result += storage.count_ones();
}
result
}
}
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));

9
editor/src/input/mod.rs Normal file
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, ModifierKeys},
};

62
editor/src/input/mouse.rs Normal file
View File

@@ -0,0 +1,62 @@
use bitflags::bitflags;
use glam::DVec2;
// origin is top left
pub type ViewportPosition = glam::UVec2;
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct ScrollDelta {
pub x: i32,
pub y: i32,
pub z: i32,
}
impl ScrollDelta {
pub fn new(x: i32, y: i32, z: i32) -> ScrollDelta {
ScrollDelta { x, y, z }
}
pub fn as_dvec2(&self) -> DVec2 {
DVec2::new(self.x as f64, self.y as f64)
}
pub fn scroll_delta(&self) -> f64 {
let (dx, dy) = (self.x, self.y);
dy.signum() as f64 * ((dy * dy + i32::min(dy.abs(), dx.abs()).pow(2)) as f64).sqrt()
}
}
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)]
pub struct MouseState {
pub position: ViewportPosition,
pub mouse_keys: MouseKeys,
pub scroll_delta: ScrollDelta,
}
impl MouseState {
pub fn new() -> MouseState {
Self::default()
}
pub fn from_pos(x: u32, y: u32) -> MouseState {
MouseState {
position: (x, y).into(),
mouse_keys: MouseKeys::default(),
scroll_delta: ScrollDelta::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,
scroll_delta: ScrollDelta::default(),
}
}
}
bitflags! {
#[derive(Default)]
#[repr(transparent)]
pub struct MouseKeys: u8 {
const LEFT = 0b0000_0001;
const RIGHT = 0b0000_0010;
const MIDDLE = 0b0000_0100;
}
}

77
editor/src/lib.rs Normal file
View File

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

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;
}

35
editor/src/misc/error.rs Normal file
View File

@@ -0,0 +1,35 @@
use crate::Color;
use graphene::DocumentError;
use thiserror::Error;
/// The error type used by the Graphite editor.
#[derive(Clone, Debug, Error)]
pub enum EditorError {
#[error("Failed to execute operation: {0}")]
InvalidOperation(String),
#[error("{0}")]
Misc(String),
#[error("Tried to construct an invalid color {0:?}")]
Color(String),
#[error("The requested tool does not exist")]
UnknownTool,
#[error("The operation caused a document error {0:?}")]
Document(String),
#[error("A Rollback was initated but no transaction was in progress")]
NoTransactionInProgress,
}
macro_rules! derive_from {
($type:ty, $kind:ident) => {
impl From<$type> for EditorError {
fn from(error: $type) -> Self {
EditorError::$kind(format!("{:?}", error))
}
}
};
}
derive_from!(&str, Misc);
derive_from!(String, Misc);
derive_from!(Color, Color);
derive_from!(DocumentError, Document);

138
editor/src/misc/macros.rs Normal file
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),*)
}
}
}

8
editor/src/misc/mod.rs Normal file
View File

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

View File

@@ -0,0 +1,85 @@
use crate::{
input::{
mouse::{MouseKeys, MouseState, ScrollDelta},
InputPreprocessorMessage, ModifierKeys,
},
message_prelude::{Message, ToolMessage},
tool::ToolType,
Editor,
};
use graphene::color::Color;
/// A set of utility functions to make the writing of editor test more declarative
pub trait EditorTestUtils {
fn draw_rect(&mut self, x1: u32, y1: u32, x2: u32, y2: u32);
fn draw_shape(&mut self, x1: u32, y1: u32, x2: u32, y2: u32);
fn draw_ellipse(&mut self, x1: u32, y1: u32, x2: u32, y2: u32);
/// Select given tool and drag it from (x1, y1) to (x2, y2)
fn drag_tool(&mut self, typ: ToolType, x1: u32, y1: u32, x2: u32, y2: u32);
fn move_mouse(&mut self, x: u32, y: u32);
fn mousedown(&mut self, state: MouseState);
fn mouseup(&mut self, state: MouseState);
fn lmb_mousedown(&mut self, x: u32, y: u32);
fn input(&mut self, message: InputPreprocessorMessage);
fn select_tool(&mut self, typ: ToolType);
fn select_primary_color(&mut self, color: Color);
}
impl EditorTestUtils for Editor {
fn draw_rect(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
self.drag_tool(ToolType::Rectangle, x1, y1, x2, y2);
}
fn draw_shape(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
self.drag_tool(ToolType::Shape, x1, y1, x2, y2);
}
fn draw_ellipse(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
self.drag_tool(ToolType::Ellipse, x1, y1, x2, y2);
}
fn drag_tool(&mut self, typ: ToolType, x1: u32, y1: u32, x2: u32, y2: u32) {
self.select_tool(typ);
self.move_mouse(x1, y1);
self.lmb_mousedown(x1, y1);
self.move_mouse(x2, y2);
self.mouseup(MouseState {
position: (x2, y2).into(),
mouse_keys: MouseKeys::empty(),
scroll_delta: ScrollDelta::default(),
});
}
fn move_mouse(&mut self, x: u32, y: u32) {
self.input(InputPreprocessorMessage::MouseMove((x, y).into(), ModifierKeys::default()));
}
fn mousedown(&mut self, state: MouseState) {
self.input(InputPreprocessorMessage::MouseDown(state, ModifierKeys::default()));
}
fn mouseup(&mut self, state: MouseState) {
self.handle_message(InputPreprocessorMessage::MouseUp(state, ModifierKeys::default())).unwrap()
}
fn lmb_mousedown(&mut self, x: u32, y: u32) {
self.mousedown(MouseState {
position: (x, y).into(),
mouse_keys: MouseKeys::LEFT,
scroll_delta: ScrollDelta::default(),
})
}
fn input(&mut self, message: InputPreprocessorMessage) {
self.handle_message(Message::InputPreprocessor(message)).unwrap();
}
fn select_tool(&mut self, typ: ToolType) {
self.handle_message(Message::Tool(ToolMessage::SelectTool(typ))).unwrap();
}
fn select_primary_color(&mut self, color: Color) {
self.handle_message(Message::Tool(ToolMessage::SelectPrimaryColor(color))).unwrap();
}
}

195
editor/src/tool/mod.rs Normal file
View File

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

View File

@@ -0,0 +1,126 @@
use crate::message_prelude::*;
use graphene::color::Color;
use crate::input::InputPreprocessor;
use crate::{
document::DocumentMessageHandler,
tool::{tool_options::ToolOptions, DocumentToolData, ToolFsmState, ToolType},
};
use std::collections::VecDeque;
#[impl_message(Message, Tool)]
#[derive(PartialEq, Clone, Debug)]
pub enum ToolMessage {
SelectTool(ToolType),
SelectPrimaryColor(Color),
SelectSecondaryColor(Color),
SwapColors,
ResetColors,
SetToolOptions(ToolType, ToolOptions),
#[child]
Fill(FillMessage),
#[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, (&DocumentMessageHandler, &InputPreprocessor)> for ToolMessageHandler {
fn process_action(&mut self, message: ToolMessage, data: (&DocumentMessageHandler, &InputPreprocessor), responses: &mut VecDeque<Message>) {
let (document, input) = data;
use ToolMessage::*;
match message {
SelectPrimaryColor(c) => {
self.tool_state.document_tool_data.primary_color = c;
update_working_colors(&self.tool_state.document_tool_data, responses);
}
SelectSecondaryColor(c) => {
self.tool_state.document_tool_data.secondary_color = c;
update_working_colors(&self.tool_state.document_tool_data, responses);
}
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);
update_working_colors(doc_data, responses);
}
ResetColors => {
let doc_data = &mut self.tool_state.document_tool_data;
doc_data.primary_color = Color::BLACK;
doc_data.secondary_color = Color::WHITE;
update_working_colors(doc_data, responses);
}
SetToolOptions(tool_type, tool_options) => {
self.tool_state.document_tool_data.tool_options.insert(tool_type, tool_options);
}
message => {
let tool_type = match message {
Fill(_) => ToolType::Fill,
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, SetToolOptions);
list.extend(self.tool_state.tool_data.active_tool().actions());
list
}
}
fn update_working_colors(doc_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
responses.push_back(
FrontendMessage::UpdateWorkingColors {
primary: doc_data.primary_color,
secondary: doc_data.secondary_color,
}
.into(),
);
}

View File

@@ -0,0 +1,23 @@
use serde::{Deserialize, Serialize};
// TODO: Rename this `ToolOption` to not be plural in a separate commit (together with `enum LayerDataTypes`)
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum ToolOptions {
Select { append_mode: SelectAppendMode },
Ellipse,
Shape { shape_type: ShapeType },
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum SelectAppendMode {
New,
Add,
Subtract,
Intersect,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum ShapeType {
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, Hash)]
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,119 @@
use crate::input::keyboard::Key;
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::DAffine2;
use graphene::{layers::style, Operation};
use super::resize::*;
#[derive(Default)]
pub struct Ellipse {
fsm_state: EllipseToolFsmState,
data: EllipseToolData,
}
#[impl_message(Message, ToolMessage, Ellipse)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum EllipseMessage {
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
Abort,
}
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; DragStart),
Dragging => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
#[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 {
sides: u8,
data: Resize,
}
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use EllipseMessage::*;
use EllipseToolFsmState::*;
if let ToolMessage::Ellipse(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.drag_start = input.mouse.position;
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddEllipse {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Dragging
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Dragging, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.path = None;
Ready
}
(Dragging, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.path = None;
Ready
}
_ => self,
}
} else {
self
}
}
}

View File

@@ -0,0 +1,39 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::message_prelude::*;
use crate::tool::{ToolActionHandlerData, ToolMessage};
use glam::DVec2;
use graphene::layers::LayerDataType;
use graphene::Quad;
#[derive(Default)]
pub struct Eyedropper;
#[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum EyedropperMessage {
LeftMouseDown,
RightMouseDown,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
let mouse_pos = data.2.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos.as_f64() - tolerance, mouse_pos.as_f64() + tolerance]);
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
if let Ok(layer) = data.0.document.layer(path) {
if let LayerDataType::Shape(s) = &layer.data {
s.style.fill().and_then(|fill| {
fill.color().map(|color| match action {
ToolMessage::Eyedropper(EyedropperMessage::LeftMouseDown) => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
ToolMessage::Eyedropper(EyedropperMessage::RightMouseDown) => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
_ => {}
})
});
}
}
}
}
advertise_actions!(EyedropperMessageDiscriminant; LeftMouseDown, RightMouseDown);
}

View File

@@ -0,0 +1,33 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
use glam::DVec2;
use graphene::{Operation, Quad};
#[derive(Default)]
pub struct Fill;
#[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum FillMessage {
MouseDown,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
fn process_action(&mut self, _action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
let mouse_pos = data.2.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos.as_f64() - tolerance, mouse_pos.as_f64() + tolerance]);
if let Some(path) = data.0.document.intersects_quad_root(quad).last() {
responses.push_back(
Operation::FillLayer {
path: path.to_vec(),
color: data.1.primary_color,
}
.into(),
);
}
}
advertise_actions!(FillMessageDiscriminant; MouseDown);
}

View File

@@ -0,0 +1,161 @@
use std::f64::consts::PI;
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::input::keyboard::Key;
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::{DAffine2, DVec2};
use graphene::{layers::style, Operation};
#[derive(Default)]
pub struct Line {
fsm_state: LineToolFsmState,
data: LineToolData,
}
#[impl_message(Message, ToolMessage, Line)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum LineMessage {
DragStart,
DragStop,
Redraw { center: Key, lock_angle: Key, snap_angle: Key },
Abort,
}
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),
Dragging => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
}
}
}
#[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,
path: Option<Vec<LayerId>>,
}
impl Fsm for LineToolFsmState {
type ToolData = LineToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
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;
responses.push_back(DocumentMessage::StartTransaction.into());
data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddLine {
path: data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), None),
}
.into(),
);
Dragging
}
(Dragging, Redraw { center, snap_angle, lock_angle }) => {
data.drag_current = input.mouse.position;
let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect();
responses.push_back(generate_transform(data, values[0], values[1], values[2]));
Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;
// TODO; introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
data.path = None;
Ready
}
(Dragging, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
data.path = None;
Ready
}
_ => self,
}
} else {
self
}
}
}
fn generate_transform(data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {
let mut start = data.drag_start.as_f64();
let stop = data.drag_current.as_f64();
let dir = stop - start;
let mut angle = -dir.angle_between(DVec2::X);
if lock {
angle = data.angle
};
if snap {
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
}
data.angle = angle;
let mut scale = dir.length();
if lock {
let angle_vec = DVec2::new(angle.cos(), angle.sin());
scale = dir.dot(angle_vec);
}
if center {
start -= dir / 2.;
}
Operation::SetLayerTransformInViewport {
path: data.path.clone().unwrap(),
transform: glam::DAffine2::from_scale_angle_translation(DVec2::splat(scale), angle, start).to_cols_array(),
}
.into()
}

View File

@@ -0,0 +1,15 @@
// already implemented
pub mod ellipse;
pub mod fill;
pub mod line;
pub mod pen;
pub mod rectangle;
pub mod resize;
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, Hash)]
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, Hash)]
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,147 @@
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::DAffine2;
use graphene::{layers::style, Operation};
#[derive(Default)]
pub struct Pen {
fsm_state: PenToolFsmState,
data: PenToolData,
}
#[impl_message(Message, ToolMessage, Pen)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum PenMessage {
Undo,
DragStart,
DragStop,
PointerMove,
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, PointerMove, Confirm, Abort),
}
}
}
impl Default for PenToolFsmState {
fn default() -> Self {
PenToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct PenToolData {
points: Vec<DAffine2>,
next_point: DAffine2,
path: Option<Vec<LayerId>>,
}
impl Fsm for PenToolFsmState {
type ToolData = PenToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let transform = document.document.root.transform;
let pos = transform.inverse() * DAffine2::from_translation(input.mouse.position.as_f64());
use PenMessage::*;
use PenToolFsmState::*;
if let ToolMessage::Pen(event) = event {
match (self, event) {
(Ready, DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
data.points.push(pos);
data.next_point = pos;
Dragging
}
(Dragging, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.points.last() != Some(&pos) {
data.points.push(pos);
data.next_point = pos;
}
responses.extend(make_operation(data, tool_data, true));
Dragging
}
(Dragging, PointerMove) => {
data.next_point = pos;
responses.extend(make_operation(data, tool_data, true));
Dragging
}
(Dragging, Confirm) => {
if data.points.len() >= 2 {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.extend(make_operation(data, tool_data, false));
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}
data.path = None;
data.points.clear();
Ready
}
(Dragging, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
data.points.clear();
data.path = None;
Ready
}
_ => self,
}
} else {
self
}
}
}
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> [Message; 2] {
let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.translation.x, p.translation.y)).collect();
if show_preview {
points.push((data.next_point.translation.x, data.next_point.translation.y))
}
[
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into(),
Operation::AddPen {
path: data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
points,
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, 5.)), Some(style::Fill::none())),
}
.into(),
]
}

View File

@@ -0,0 +1,119 @@
use crate::input::keyboard::Key;
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::DAffine2;
use graphene::{layers::style, Operation};
use super::resize::*;
#[derive(Default)]
pub struct Rectangle {
fsm_state: RectangleToolFsmState,
data: RectangleToolData,
}
#[impl_message(Message, ToolMessage, Rectangle)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum RectangleMessage {
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
Abort,
}
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),
Dragging => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
#[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 {
sides: u8,
data: Resize,
}
impl Fsm for RectangleToolFsmState {
type ToolData = RectangleToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use RectangleMessage::*;
use RectangleToolFsmState::*;
if let ToolMessage::Rectangle(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.drag_start = input.mouse.position;
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddRect {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Dragging
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Dragging, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.path = None;
Ready
}
(Dragging, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.path = None;
Ready
}
_ => self,
}
} else {
self
}
}
}

View File

@@ -0,0 +1,34 @@
use crate::input::keyboard::Key;
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::message_prelude::*;
use glam::{DAffine2, Vec2Swizzles};
use graphene::Operation;
#[derive(Clone, Debug, Default)]
pub struct Resize {
pub drag_start: ViewportPosition,
pub path: Option<Vec<LayerId>>,
}
impl Resize {
pub fn calculate_transform(&self, center: Key, lock_ratio: Key, ipp: &InputPreprocessor) -> Option<Message> {
let mut start = self.drag_start.as_f64();
let stop = ipp.mouse.position.as_f64();
let mut size = stop - start;
if ipp.keyboard.get(lock_ratio as usize) {
size = size.abs().max(size.abs().yx()) * size.signum();
}
if ipp.keyboard.get(center as usize) {
start -= size;
size *= 2.;
}
self.path.clone().map(|path| {
Operation::SetLayerTransformInViewport {
path,
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
}
.into()
})
}
}

View File

@@ -0,0 +1,196 @@
use graphene::color::Color;
use graphene::layers::style;
use graphene::layers::style::Fill;
use graphene::layers::style::Stroke;
use graphene::Operation;
use graphene::Quad;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::{
consts::SELECTION_TOLERANCE,
document::{AlignAggregate, AlignAxis, DocumentMessageHandler, FlipAxis},
message_prelude::*,
};
#[derive(Default)]
pub struct Select {
fsm_state: SelectToolFsmState,
data: SelectToolData,
}
#[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, Hash)]
pub enum SelectMessage {
DragStart,
DragStop,
MouseMove,
Abort,
Align(AlignAxis, AlignAggregate),
FlipHorizontal,
FlipVertical,
}
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);
}
fn actions(&self) -> ActionList {
use SelectToolFsmState::*;
match self.fsm_state {
Ready => actions!(SelectMessageDiscriminant; DragStart),
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove),
DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SelectToolFsmState {
Ready,
Dragging,
DrawingBox,
}
impl Default for SelectToolFsmState {
fn default() -> Self {
SelectToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct SelectToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
box_id: Option<Vec<LayerId>>,
}
impl SelectToolData {
fn selection_quad(&self) -> Quad {
let bbox = self.selection_box();
Quad::from_box(bbox)
}
fn selection_box(&self) -> [DVec2; 2] {
if self.drag_current == self.drag_start {
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
[self.drag_start.as_f64() - tolerance, self.drag_start.as_f64() + tolerance]
} else {
[self.drag_start.as_f64(), self.drag_current.as_f64()]
}
}
}
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
_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, DragStart) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
let mut selected: Vec<_> = document.selected_layers().cloned().collect();
let quad = data.selection_quad();
let intersection = document.document.intersects_quad_root(quad);
// If no layer is currently selected and the user clicks on a shape, select that.
if selected.is_empty() {
if let Some(layer) = intersection.last() {
selected.push(layer.clone());
responses.push_back(DocumentMessage::SelectLayers(selected.clone()).into());
}
}
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
// Otherwise enter the box select mode
if selected.iter().any(|path| intersection.contains(path)) {
data.layers_dragging = selected;
Dragging
} else {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.box_id = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(
Operation::AddBoundingBox {
path: data.box_id.clone().unwrap(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x00, 0xA8, 0xFF), 1.0)), Some(Fill::none())),
}
.into(),
);
DrawingBox
}
}
(Dragging, MouseMove) => {
for path in data.layers_dragging.iter() {
responses.push_back(
Operation::TransformLayerInViewport {
path: path.clone(),
transform: DAffine2::from_translation(input.mouse.position.as_f64() - data.drag_current.as_f64()).to_cols_array(),
}
.into(),
);
}
data.drag_current = input.mouse.position;
Dragging
}
(DrawingBox, MouseMove) => {
data.drag_current = input.mouse.position;
let start = data.drag_start.as_f64();
let size = data.drag_current.as_f64() - start;
responses.push_back(
Operation::SetLayerTransformInViewport {
path: data.box_id.clone().unwrap(),
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
}
.into(),
);
DrawingBox
}
(Dragging, DragStop) => Ready,
(DrawingBox, Abort) => {
responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into());
Ready
}
(DrawingBox, DragStop) => {
let quad = data.selection_quad();
responses.push_back(DocumentMessage::SelectLayers(document.document.intersects_quad_root(quad)).into());
responses.push_back(Operation::DeleteLayer { path: data.box_id.take().unwrap() }.into());
Ready
}
(_, Align(axis, aggregate)) => {
responses.push_back(DocumentMessage::AlignSelectedLayers(axis, aggregate).into());
self
}
(_, FlipHorizontal) => {
responses.push_back(DocumentMessage::FlipSelectedLayers(FlipAxis::X).into());
self
}
(_, FlipVertical) => {
responses.push_back(DocumentMessage::FlipSelectedLayers(FlipAxis::Y).into());
self
}
_ => self,
}
} else {
self
}
}
}

View File

@@ -0,0 +1,126 @@
use crate::input::keyboard::Key;
use crate::input::InputPreprocessor;
use crate::tool::{DocumentToolData, Fsm, ShapeType, ToolActionHandlerData, ToolOptions, ToolType};
use crate::{document::DocumentMessageHandler, message_prelude::*};
use glam::DAffine2;
use graphene::{layers::style, Operation};
use super::resize::*;
#[derive(Default)]
pub struct Shape {
fsm_state: ShapeToolFsmState,
data: ShapeToolData,
}
#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug, Hash)]
pub enum ShapeMessage {
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
Abort,
}
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; DragStart),
Dragging => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
#[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 {
sides: u8,
data: Resize,
}
impl Fsm for ShapeToolFsmState {
type ToolData = ShapeToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessor,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use ShapeMessage::*;
use ShapeToolFsmState::*;
if let ToolMessage::Shape(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.drag_start = input.mouse.position;
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_hash(&*responses, input, document.document.hash())]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.sides = match tool_data.tool_options.get(&ToolType::Shape) {
Some(&ToolOptions::Shape {
shape_type: ShapeType::Polygon { vertices },
}) => vertices as u8,
_ => 6,
};
responses.push_back(
Operation::AddShape {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
sides: data.sides,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Dragging
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Dragging, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.path = None;
Ready
}
(Dragging, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.path = None;
Ready
}
_ => self,
}
} else {
self
}
}
}