mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-16 06:38:03 +08:00
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:
326
editor/src/communication/dispatcher.rs
Normal file
326
editor/src/communication/dispatcher.rs
Normal 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<_>>());
|
||||
}
|
||||
}
|
||||
55
editor/src/communication/message.rs
Normal file
55
editor/src/communication/message.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
36
editor/src/communication/mod.rs
Normal file
36
editor/src/communication/mod.rs
Normal 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
16
editor/src/consts.rs
Normal 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;
|
||||
501
editor/src/document/document_file.rs
Normal file
501
editor/src/document/document_file.rs
Normal 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
|
||||
}
|
||||
}
|
||||
253
editor/src/document/document_message_handler.rs
Normal file
253
editor/src/document/document_message_handler.rs
Normal 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
|
||||
}
|
||||
}
|
||||
94
editor/src/document/layer_panel.rs
Normal file
94
editor/src/document/layer_panel.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
14
editor/src/document/mod.rs
Normal file
14
editor/src/document/mod.rs
Normal 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};
|
||||
214
editor/src/document/movement_handler.rs
Normal file
214
editor/src/document/movement_handler.rs
Normal 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
|
||||
}
|
||||
}
|
||||
54
editor/src/frontend/frontend_message_handler.rs
Normal file
54
editor/src/frontend/frontend_message_handler.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
44
editor/src/frontend/layer_panel.rs
Normal file
44
editor/src/frontend/layer_panel.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
4
editor/src/frontend/mod.rs
Normal file
4
editor/src/frontend/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod frontend_message_handler;
|
||||
pub mod layer_panel;
|
||||
|
||||
pub use frontend_message_handler::{Callback, FrontendMessage, FrontendMessageDiscriminant, FrontendMessageHandler};
|
||||
40
editor/src/global/global_message_handler.rs
Normal file
40
editor/src/global/global_message_handler.rs
Normal 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
3
editor/src/global/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod global_message_handler;
|
||||
|
||||
pub use global_message_handler::{GlobalMessage, GlobalMessageDiscriminant, GlobalMessageHandler};
|
||||
315
editor/src/input/input_mapper.rs
Normal file
315
editor/src/input/input_mapper.rs
Normal 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!();
|
||||
}
|
||||
198
editor/src/input/input_preprocessor.rs
Normal file
198
editor/src/input/input_preprocessor.rs
Normal 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()));
|
||||
}
|
||||
}
|
||||
189
editor/src/input/keyboard.rs
Normal file
189
editor/src/input/keyboard.rs
Normal 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
9
editor/src/input/mod.rs
Normal 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
62
editor/src/input/mouse.rs
Normal 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
77
editor/src/lib.rs
Normal 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;
|
||||
}
|
||||
18
editor/src/misc/derivable_custom_traits.rs
Normal file
18
editor/src/misc/derivable_custom_traits.rs
Normal 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
35
editor/src/misc/error.rs
Normal 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
138
editor/src/misc/macros.rs
Normal 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
8
editor/src/misc/mod.rs
Normal 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::*;
|
||||
85
editor/src/misc/test_utils.rs
Normal file
85
editor/src/misc/test_utils.rs
Normal 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
195
editor/src/tool/mod.rs
Normal 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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
126
editor/src/tool/tool_message_handler.rs
Normal file
126
editor/src/tool/tool_message_handler.rs
Normal 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(),
|
||||
);
|
||||
}
|
||||
23
editor/src/tool/tool_options.rs
Normal file
23
editor/src/tool/tool_options.rs
Normal 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 },
|
||||
}
|
||||
18
editor/src/tool/tools/crop.rs
Normal file
18
editor/src/tool/tools/crop.rs
Normal 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!();
|
||||
}
|
||||
119
editor/src/tool/tools/ellipse.rs
Normal file
119
editor/src/tool/tools/ellipse.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
editor/src/tool/tools/eyedropper.rs
Normal file
39
editor/src/tool/tools/eyedropper.rs
Normal 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);
|
||||
}
|
||||
33
editor/src/tool/tools/fill.rs
Normal file
33
editor/src/tool/tools/fill.rs
Normal 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);
|
||||
}
|
||||
161
editor/src/tool/tools/line.rs
Normal file
161
editor/src/tool/tools/line.rs
Normal 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()
|
||||
}
|
||||
15
editor/src/tool/tools/mod.rs
Normal file
15
editor/src/tool/tools/mod.rs
Normal 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;
|
||||
18
editor/src/tool/tools/navigate.rs
Normal file
18
editor/src/tool/tools/navigate.rs
Normal 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!();
|
||||
}
|
||||
18
editor/src/tool/tools/path.rs
Normal file
18
editor/src/tool/tools/path.rs
Normal 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!();
|
||||
}
|
||||
147
editor/src/tool/tools/pen.rs
Normal file
147
editor/src/tool/tools/pen.rs
Normal 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(),
|
||||
]
|
||||
}
|
||||
119
editor/src/tool/tools/rectangle.rs
Normal file
119
editor/src/tool/tools/rectangle.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
34
editor/src/tool/tools/resize.rs
Normal file
34
editor/src/tool/tools/resize.rs
Normal 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()
|
||||
})
|
||||
}
|
||||
}
|
||||
196
editor/src/tool/tools/select.rs
Normal file
196
editor/src/tool/tools/select.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
126
editor/src/tool/tools/shape.rs
Normal file
126
editor/src/tool/tools/shape.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user