Major overhaul of input and communication systems

* Add input manager

* WIP lifetime hell

* Hell yeah, dark lifetime magic

* Replace events with actions in tools

* Fix borrow in GlobalEventHandler

* Fix typo in response-handler

* Distribute dispatch structure

* Add translation from events to actions

* Port default key actions to input_mapper

* Split actions macro

* Add handling for Ambiguous Mouse events

* Fix warnings and clippy lints

* Add actions macro

* WIP rework

* Add AsMessage macro

* Add implementation for derived enums

* Add macro implementation for top level message

* Add #[child] attribute to indicate derivation

* Replace some mentions of Actions and Responses with Message

* It compiles !!!

* Add functionality to some message handlers

* Add document rendering

* ICE

* Rework the previous code while keeping basic functionality

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

* Add workaround for ICE

* Fix cyclic reference in document.rs

* Make derive_transitive_child a bit more powerful

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

* Adapt frontend message format

* Make responses use VecDeque

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

* Move traits to sensible location

* Are we rectangle yet?

* Simplify, improve & document `derive_discriminant`

* Change `child` to `sub_discriminant`

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

* Add docs for `derive_transitive_child`

* Finish docs and improve macros

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

* Remove logging call

* Move files around and cleanup structure

* Fix proc macro doc tests

* Improve actions_fn!() macro

* Add ellipse tool

* Pass populated actions list to the input mapper

* Add KeyState bitvector

* Merge mouse buttons into "keyboard"

* Add macro for initialization of key mapper table

* Add syntactic sugar for the macro

* Implement mapping function

* Translate the remaining tools

* Fix shape tool

* Add keybindings for line and pen tool

* Fix modifiers

* Cleanup

* Add doc comments for the actions macro

* Fix formatting

* Rename MouseMove to PointerMove

* Add keybinds for tools

* Apply review suggestions

* Rename KeyMappings -> KeyMappingEntries

* Apply review changes

Co-authored-by: T0mstone <realt0mstone@gmail.com>
Co-authored-by: Paul Kupper <kupper.pa@gmail.com>
This commit is contained in:
TrueDoctor 2021-05-23 01:26:24 +02:00 committed by GitHub
parent 56c1110800
commit a596ce0104
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 3028 additions and 1856 deletions

56
Cargo.lock generated
View file

@ -101,9 +101,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "js-sys"
version = "0.3.50"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062"
dependencies = [
"wasm-bindgen",
]
@ -164,18 +164,18 @@ checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
[[package]]
name = "serde"
version = "1.0.125"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.125"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
"proc-macro2",
"quote",
@ -195,9 +195,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.68"
version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
dependencies = [
"proc-macro2",
"quote",
@ -226,15 +226,15 @@ dependencies = [
[[package]]
name = "unicode-xid"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "wasm-bindgen"
version = "0.2.73"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd"
dependencies = [
"cfg-if 1.0.0",
"serde",
@ -244,9 +244,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.73"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900"
dependencies = [
"bumpalo",
"lazy_static",
@ -259,9 +259,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.23"
version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea"
checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
@ -271,9 +271,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.73"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -281,9 +281,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.73"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97"
dependencies = [
"proc-macro2",
"quote",
@ -294,15 +294,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.73"
version = "0.2.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e972e914de63aa53bd84865e54f5c761bd274d48e5be3a6329a662c0386aa67a"
checksum = "8cab416a9b970464c2882ed92d55b0c33046b08e0bdc9d59b3b718acd4e1bae8"
dependencies = [
"console_error_panic_hook",
"js-sys",
@ -314,9 +314,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.23"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6153a8f9bf24588e9f25c87223414fff124049f68d3a442a0f0eab4768a8b6"
checksum = "dd4543fc6cf3541ef0d98bf720104cc6bd856d7eba449fd2aa365ef4fed0e782"
dependencies = [
"proc-macro2",
"quote",
@ -324,9 +324,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.50"
version = "0.3.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582"
dependencies = [
"js-sys",
"wasm-bindgen",

View file

@ -26,55 +26,35 @@ export function registerResponseHandler(responseType: ResponseType, callback: Re
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function handleResponse(responseIdentifier: string, responseData: any) {
const [origin, responesType] = responseIdentifier.split("::", 2);
const callback = window.responseMap[responesType];
const data = parseResponse(origin, responesType, responseData);
export function handleResponse(responseType: string, responseData: any) {
const callback = window.responseMap[responseType];
const data = parseResponse(responseType, responseData);
if (callback && data) {
callback(data);
} else if (data) {
console.error(`Received a Response of type "${responseIdentifier}" but no handler was registered for it from the client.`);
console.error(`Received a Response of type "${responseType}" but no handler was registered for it from the client.`);
} else {
console.error(`Received a Response of type "${responseIdentifier}" but but was not able to parse the data.`);
console.error(`Received a Response of type "${responseType}" but but was not able to parse the data.`);
}
}
enum OriginNames {
Document = "Document",
Tool = "Tool",
}
function parseResponse(origin: string, responseType: string, data: any): Response {
const response = (() => {
switch (origin) {
case OriginNames.Document:
switch (responseType) {
case "DocumentChanged":
return newDocumentChanged(data.Document.DocumentChanged);
case "CollapseFolder":
return newCollapseFolder(data.Document.CollapseFolder);
case "ExpandFolder":
return newExpandFolder(data.Document.ExpandFolder);
default:
return undefined;
}
case OriginNames.Tool:
switch (responseType) {
case "SetActiveTool":
return newSetActiveTool(data.Tool.SetActiveTool);
case "UpdateCanvas":
return newUpdateCanvas(data.Tool.UpdateCanvas);
default:
return undefined;
}
default:
return undefined;
}
})();
if (!response) throw new Error(`Unrecognized origin/responseType pair: ${origin}, ${responseType}`);
return response;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parseResponse(responseType: string, data: any): Response {
switch (responseType) {
case "DocumentChanged":
return newDocumentChanged(data.DocumentChanged);
case "CollapseFolder":
return newCollapseFolder(data.CollapseFolder);
case "ExpandFolder":
return newExpandFolder(data.ExpandFolder);
case "SetActiveTool":
return newSetActiveTool(data.SetActiveTool);
case "UpdateCanvas":
return newUpdateCanvas(data.UpdateCanvas);
default:
throw new Error(`Unrecognized origin/responseType pair: ${origin}, ${responseType}`);
}
}
export type Response = SetActiveTool | UpdateCanvas | DocumentChanged | CollapseFolder | ExpandFolder;

View file

@ -1,54 +1,22 @@
use crate::shims::Error;
use crate::wrappers::{translate_key, translate_tool, Color};
use crate::EDITOR_STATE;
use editor_core::{events, LayerId};
use editor_core::message_prelude::*;
use editor_core::{
input::mouse::{MouseState, ViewportPosition},
LayerId,
};
use wasm_bindgen::prelude::*;
fn convert_error(err: editor_core::EditorError) -> JsValue {
Error::new(&err.to_string()).into()
}
mod mouse_state {
pub(super) type MouseKeys = u8;
use editor_core::events::{self, Event, MouseState, ViewportPosition};
static mut MOUSE_STATE: MouseKeys = 0;
pub(super) fn translate_mouse_down(mod_keys: MouseKeys, position: ViewportPosition) -> Event {
translate_mouse_event(mod_keys, position, true)
}
pub(super) fn translate_mouse_up(mod_keys: MouseKeys, position: ViewportPosition) -> Event {
translate_mouse_event(mod_keys, position, false)
}
fn translate_mouse_event(mod_keys: MouseKeys, position: ViewportPosition, down: bool) -> Event {
let diff = unsafe { MOUSE_STATE } ^ mod_keys;
unsafe { MOUSE_STATE = mod_keys };
let mouse_keys = events::MouseKeys::from_bits(mod_keys).expect("invalid modifier keys");
let state = MouseState { position, mouse_keys };
match (down, diff) {
(true, 1) => Event::LmbDown(state),
(true, 2) => Event::RmbDown(state),
(true, 4) => Event::MmbDown(state),
(false, 1) => Event::LmbUp(state),
(false, 2) => Event::RmbUp(state),
(false, 4) => Event::MmbUp(state),
(down, _) => {
log::warn!("two buttons where modified at the same time. Modification: {:#010b}", diff);
if down {
Event::AmbiguousMouseDown(state)
} else {
Event::AmbiguousMouseUp(state)
}
}
}
}
}
/// Modify the currently selected tool in the document state store
#[wasm_bindgen]
pub fn select_tool(tool: String) -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| match translate_tool(&tool) {
Some(tool) => editor.borrow_mut().handle_event(events::Event::SelectTool(tool)).map_err(convert_error),
Some(tool) => editor.borrow_mut().handle_message(ToolMessage::SelectTool(tool)).map_err(convert_error),
None => Err(Error::new(&format!("Couldn't select {} because it was not recognized as a valid tool", tool)).into()),
})
}
@ -58,26 +26,24 @@ pub fn select_tool(tool: String) -> Result<(), JsValue> {
#[wasm_bindgen]
pub fn on_mouse_move(x: u32, y: u32) -> Result<(), JsValue> {
// TODO: Convert these screenspace viewport coordinates to canvas coordinates based on the current zoom and pan
let ev = events::Event::MouseMove(events::ViewportPosition { x, y });
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(ev)).map_err(convert_error)
let ev = InputPreprocessorMessage::MouseMove(ViewportPosition { x, y });
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
}
/// A mouse button depressed within screenspace the bounds of the viewport
#[wasm_bindgen]
pub fn on_mouse_down(x: u32, y: u32, mouse_keys: u8) -> Result<(), JsValue> {
// TODO: Convert these screenspace viewport coordinates to canvas coordinates based on the current zoom and pan
let pos = events::ViewportPosition { x, y };
let ev = mouse_state::translate_mouse_down(mouse_keys, pos);
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(ev)).map_err(convert_error)
let pos = ViewportPosition { x, y };
let ev = InputPreprocessorMessage::MouseDown(MouseState::from_u8_pos(mouse_keys, pos));
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
}
/// A mouse button released
#[wasm_bindgen]
pub fn on_mouse_up(x: u32, y: u32, mouse_keys: u8) -> Result<(), JsValue> {
// TODO: Convert these screenspace viewport coordinates to canvas coordinates based on the current zoom and pan
let pos = events::ViewportPosition { x, y };
let ev = mouse_state::translate_mouse_up(mouse_keys, pos);
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(ev)).map_err(convert_error)
let pos = ViewportPosition { x, y };
let ev = InputPreprocessorMessage::MouseUp(MouseState::from_u8_pos(mouse_keys, pos));
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
}
/// A keyboard button depressed within screenspace the bounds of the viewport
@ -85,8 +51,8 @@ pub fn on_mouse_up(x: u32, y: u32, mouse_keys: u8) -> Result<(), JsValue> {
pub fn on_key_down(name: String) -> Result<(), JsValue> {
let key = translate_key(&name);
log::trace!("key down {:?}, name: {}", key, name);
let ev = events::Event::KeyDown(key);
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(ev)).map_err(convert_error)
let ev = InputPreprocessorMessage::KeyDown(key);
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
}
/// A keyboard button released
@ -94,15 +60,15 @@ pub fn on_key_down(name: String) -> Result<(), JsValue> {
pub fn on_key_up(name: String) -> Result<(), JsValue> {
let key = translate_key(&name);
log::trace!("key up {:?}, name: {}", key, name);
let ev = events::Event::KeyUp(key);
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(ev)).map_err(convert_error)
let ev = InputPreprocessorMessage::KeyUp(key);
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ev)).map_err(convert_error)
}
/// Update primary color
#[wasm_bindgen]
pub fn update_primary_color(primary_color: Color) -> Result<(), JsValue> {
EDITOR_STATE
.with(|editor| editor.borrow_mut().handle_event(events::Event::SelectPrimaryColor(primary_color.inner())))
.with(|editor| editor.borrow_mut().handle_message(ToolMessage::SelectPrimaryColor(primary_color.inner())))
.map_err(convert_error)
}
@ -110,33 +76,35 @@ pub fn update_primary_color(primary_color: Color) -> Result<(), JsValue> {
#[wasm_bindgen]
pub fn update_secondary_color(secondary_color: Color) -> Result<(), JsValue> {
EDITOR_STATE
.with(|editor| editor.borrow_mut().handle_event(events::Event::SelectSecondaryColor(secondary_color.inner())))
.with(|editor| editor.borrow_mut().handle_message(ToolMessage::SelectSecondaryColor(secondary_color.inner())))
.map_err(convert_error)
}
/// Swap primary and secondary color
#[wasm_bindgen]
pub fn swap_colors() -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(events::Event::SwapColors)).map_err(convert_error)
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ToolMessage::SwapColors)).map_err(convert_error)
}
/// Reset primary and secondary colors to their defaults
#[wasm_bindgen]
pub fn reset_colors() -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(events::Event::ResetColors)).map_err(convert_error)
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(ToolMessage::ResetColors)).map_err(convert_error)
}
/// Select a layer from the layer list
#[wasm_bindgen]
pub fn select_layer(path: Vec<LayerId>) -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(events::Event::SelectLayer(path))).map_err(convert_error)
EDITOR_STATE
.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::SelectLayer(path)))
.map_err(convert_error)
}
/// Toggle visibility of a layer from the layer list
#[wasm_bindgen]
pub fn toggle_layer_visibility(path: Vec<LayerId>) -> Result<(), JsValue> {
EDITOR_STATE
.with(|editor| editor.borrow_mut().handle_event(events::Event::ToggleLayerVisibility(path)))
.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::ToggleLayerVisibility(path)))
.map_err(convert_error)
}
@ -144,7 +112,7 @@ pub fn toggle_layer_visibility(path: Vec<LayerId>) -> Result<(), JsValue> {
#[wasm_bindgen]
pub fn toggle_layer_expansion(path: Vec<LayerId>) -> Result<(), JsValue> {
EDITOR_STATE
.with(|editor| editor.borrow_mut().handle_event(events::Event::ToggleLayerExpansion(path)))
.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::ToggleLayerExpansion(path)))
.map_err(convert_error)
}
@ -152,18 +120,20 @@ pub fn toggle_layer_expansion(path: Vec<LayerId>) -> Result<(), JsValue> {
#[wasm_bindgen]
pub fn rename_layer(path: Vec<LayerId>, new_name: String) -> Result<(), JsValue> {
EDITOR_STATE
.with(|editor| editor.borrow_mut().handle_event(events::Event::RenameLayer(path, new_name)))
.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::RenameLayer(path, new_name)))
.map_err(convert_error)
}
/// Deletes a layer from the layer list
#[wasm_bindgen]
pub fn delete_layer(path: Vec<LayerId>) -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(events::Event::DeleteLayer(path))).map_err(convert_error)
EDITOR_STATE
.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::DeleteLayer(path)))
.map_err(convert_error)
}
/// Requests the backend to add a layer to the layer list
#[wasm_bindgen]
pub fn add_layer(path: Vec<LayerId>) -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(events::Event::AddLayer(path))).map_err(convert_error)
pub fn add_folder(path: Vec<LayerId>) -> Result<(), JsValue> {
EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::AddFolder(path))).map_err(convert_error)
}

View file

@ -4,11 +4,10 @@ pub mod utils;
pub mod window;
pub mod wrappers;
use editor_core::{events::Response, Editor};
use editor_core::{message_prelude::*, Editor};
use std::cell::RefCell;
use utils::WasmLog;
use wasm_bindgen::prelude::*;
use wrappers::WasmResponse;
// the thread_local macro provides a way to initialize static variables with non-constant functions
thread_local! { pub static EDITOR_STATE: RefCell<Editor> = RefCell::new(Editor::new(Box::new(handle_response))) }
@ -21,19 +20,20 @@ pub fn init() {
log::set_max_level(log::LevelFilter::Debug);
}
fn handle_response(response: Response) {
let response_type = response.to_string();
fn handle_response(response: FrontendMessage) {
let response_type = response.to_discriminant().local_name();
send_response(response_type, response);
}
fn send_response(response_type: String, response_data: Response) {
let response_data = JsValue::from_serde(&WasmResponse::new(response_data)).expect("Failed to serialize response");
handleResponse(response_type, response_data);
fn send_response(response_type: String, response_data: FrontendMessage) {
let response_data = JsValue::from_serde(&response_data).expect("Failed to serialize response");
let _ = handleResponse(response_type, response_data).map_err(|error| log::error!("javascript threw an error: {:?}", error));
}
#[wasm_bindgen(module = "/../src/response-handler.ts")]
extern "C" {
fn handleResponse(responseType: String, responseData: JsValue);
#[wasm_bindgen(catch)]
fn handleResponse(responseType: String, responseData: JsValue) -> Result<(), JsValue>;
}
#[wasm_bindgen]

View file

@ -20,7 +20,7 @@ pub fn get_active_document() -> DocumentId {
todo!("get_active_document")
}
use editor_core::workspace::PanelId;
type PanelId = u32;
/// Notify the editor that the mouse hovers above a panel
#[wasm_bindgen]
pub fn panel_hover_enter(panel_id: PanelId) {

View file

@ -1,9 +1,7 @@
use crate::shims::Error;
use editor_core::events;
use editor_core::tools::{SelectAppendMode, ToolType};
use editor_core::input::keyboard::Key;
use editor_core::tool::{SelectAppendMode, ToolType};
use editor_core::Color as InnerColor;
use events::Response;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
@ -26,15 +24,6 @@ impl Color {
}
}
#[derive(Serialize, Deserialize)]
pub struct WasmResponse(Response);
impl WasmResponse {
pub fn new(response: Response) -> Self {
Self(response)
}
}
macro_rules! match_string_to_enum {
(match ($e:expr) {$($var:ident),* $(,)?}) => {
match $e {
@ -85,8 +74,9 @@ pub fn translate_append_mode(name: &str) -> Option<SelectAppendMode> {
})
}
pub fn translate_key(name: &str) -> events::Key {
use events::Key::*;
pub fn translate_key(name: &str) -> Key {
log::trace!("pressed key: {}", name);
use Key::*;
match name {
"e" => KeyE,
"v" => KeyV,
@ -109,6 +99,7 @@ pub fn translate_key(name: &str) -> events::Key {
"9" => Key9,
"Enter" => KeyEnter,
"Shift" => KeyShift,
"CapsLock" => KeyCaps,
"Control" => KeyControl,
"Alt" => KeyAlt,
"Escape" => KeyEscape,

View file

@ -55,7 +55,7 @@ impl Color {
pub fn components(&self) -> (f32, f32, f32, f32) {
(self.red, self.green, self.blue, self.alpha)
}
pub fn to_hex(&self) -> String {
pub fn as_hex(&self) -> String {
format!(
"{:02X?}{:02X?}{:02X?}{:02X?}",
(self.r() * 255.) as u8,

View file

@ -184,10 +184,9 @@ impl Document {
/// Mutate the document by applying the `operation` to it. If the operation necessitates a
/// reaction from the frontend, responses may be returned.
pub fn handle_operation(&mut self, operation: Operation) -> Result<Option<Vec<DocumentResponse>>, DocumentError> {
self.work_operations.push(operation.clone());
let responses = match operation {
let responses = match &operation {
Operation::AddCircle { path, insert_index, cx, cy, r, style } => {
self.add_layer(&path, Layer::new(LayerDataTypes::Circle(layers::Circle::new((cx, cy), r, style))), insert_index)?;
self.add_layer(&path, Layer::new(LayerDataTypes::Circle(layers::Circle::new((*cx, *cy), *r, *style))), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged])
}
@ -201,7 +200,7 @@ impl Document {
rot,
style,
} => {
self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new((cx, cy), (rx, ry), rot, style))), insert_index)?;
self.add_layer(&path, Layer::new(LayerDataTypes::Ellipse(layers::Ellipse::new((*cx, *cy), (*rx, *ry), *rot, *style))), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged])
}
@ -214,7 +213,7 @@ impl Document {
y1,
style,
} => {
self.add_layer(&path, Layer::new(LayerDataTypes::Rect(Rect::new((x0, y0), (x1, y1), style))), insert_index)?;
self.add_layer(&path, Layer::new(LayerDataTypes::Rect(Rect::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged])
}
@ -227,14 +226,14 @@ impl Document {
y1,
style,
} => {
self.add_layer(&path, Layer::new(LayerDataTypes::Line(Line::new((x0, y0), (x1, y1), style))), insert_index)?;
self.add_layer(&path, Layer::new(LayerDataTypes::Line(Line::new((*x0, *y0), (*x1, *y1), *style))), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged])
}
Operation::AddPen { path, insert_index, points, style } => {
let points: Vec<kurbo::Point> = points.into_iter().map(|it| it.into()).collect();
let polyline = PolyLine::new(points, style);
self.add_layer(&path, Layer::new(LayerDataTypes::PolyLine(polyline)), insert_index)?;
let points: Vec<kurbo::Point> = points.iter().map(|&it| it.into()).collect();
let polyline = PolyLine::new(points, *style);
self.add_layer(&path, Layer::new(LayerDataTypes::PolyLine(polyline)), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged])
}
Operation::AddShape {
@ -247,24 +246,27 @@ impl Document {
sides,
style,
} => {
let s = Shape::new((x0, y0), (x1, y1), sides, style);
self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s)), insert_index)?;
let s = Shape::new((*x0, *y0), (*x1, *y1), *sides, *style);
self.add_layer(&path, Layer::new(LayerDataTypes::Shape(s)), *insert_index)?;
Some(vec![DocumentResponse::DocumentChanged])
}
Operation::DeleteLayer { path } => {
self.delete(&path)?;
Some(vec![DocumentResponse::DocumentChanged])
let (path, _) = split_path(path.as_slice()).unwrap_or_else(|_| (&[], 0));
let children = self.layer_panel(path)?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::ExpandFolder { path: path.to_vec(), children }])
}
Operation::AddFolder { path } => {
self.set_layer(&path, Layer::new(LayerDataTypes::Folder(Folder::default())))?;
Some(vec![DocumentResponse::DocumentChanged])
let children = self.layer_panel(path.as_slice())?;
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::ExpandFolder { path: path.clone(), children }])
}
Operation::MountWorkingFolder { path } => {
self.work_mount_path = path.clone();
self.work_operations.clear();
self.work_mount_path = path;
self.work = Folder::default();
self.work_mounted = true;
None
@ -286,18 +288,18 @@ impl Document {
let mut path: Vec<LayerId> = vec![];
std::mem::swap(&mut path, &mut self.work_mount_path);
std::mem::swap(&mut ops, &mut self.work_operations);
let len = ops.len() - 1;
self.work_mounted = false;
self.work_mount_path = vec![];
self.work = Folder::default();
let mut responses = vec![];
for operation in ops.into_iter().take(len) {
for operation in ops.into_iter() {
if let Some(mut op_responses) = self.handle_operation(operation)? {
responses.append(&mut op_responses);
}
}
let children = self.layer_panel(path.as_slice())?;
// TODO: Return `responses` and add deduplication in the future
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::ExpandFolder { path, children }])
}
Operation::ToggleVisibility { path } => {
@ -306,9 +308,15 @@ impl Document {
layer.cache_dirty = true;
});
let children = self.layer_panel(&path.as_slice()[..path.len() - 1])?;
Some(vec![DocumentResponse::ExpandFolder { path: vec![], children }])
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::ExpandFolder { path: vec![], children }])
}
};
if !matches!(
operation,
Operation::CommitTransaction | Operation::MountWorkingFolder { .. } | Operation::DiscardWorkingFolder | Operation::ClearWorkingFolder
) {
self.work_operations.push(operation);
}
Ok(responses)
}
}

View file

@ -15,7 +15,7 @@ impl Fill {
}
pub fn render(&self) -> String {
match self.color {
Some(c) => format!("fill: #{};", c.to_hex()),
Some(c) => format!("fill: #{};", c.as_hex()),
None => "fill: none;".to_string(),
}
}
@ -33,7 +33,7 @@ impl Stroke {
Self { color, width }
}
pub fn render(&self) -> String {
format!("stroke: #{};stroke-width:{};", self.color.to_hex(), self.width)
format!("stroke: #{};stroke-width:{};", self.color.as_hex(), self.width)
}
}

View file

@ -2,6 +2,7 @@ use crate::{layers::style, LayerId};
use serde::{Deserialize, Serialize};
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum Operation {
AddCircle {

View file

@ -5,7 +5,7 @@ use crate::{
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct LayerPanelEntry {
pub name: String,
pub visible: bool,
@ -14,7 +14,7 @@ pub struct LayerPanelEntry {
pub path: Vec<LayerId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum LayerType {
Folder,
Shape,
@ -71,7 +71,7 @@ impl LayerPanelEntry {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[repr(C)]
// TODO - Make Copy when possible
pub enum DocumentResponse {

View file

@ -13,11 +13,8 @@ log = "0.4"
bitflags = "1.2.1"
thiserror = "1.0.24"
serde = { version = "1.0", features = ["derive"] }
graphite-proc-macros = {path = "../proc-macro"}
[dependencies.document-core]
path = "../document"
package = "graphite-document-core"
[dependencies.proc-macros]
path = "../proc-macro"
package = "graphite-proc-macros"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,63 +1,79 @@
mod crop;
mod ellipse;
mod eyedropper;
mod line;
mod navigate;
mod path;
mod pen;
mod rectangle;
mod select;
mod shape;
pub mod tool_message_handler;
pub mod tool_settings;
pub mod tools;
use crate::events::{Event, ModKeys, MouseState, ToolResponse, Trace, TracePoint};
use crate::Color;
use crate::Document;
use crate::EditorError;
use document_core::Operation;
use std::{collections::HashMap, fmt};
use crate::input::InputPreprocessor;
use crate::message_prelude::*;
use crate::SvgDocument;
use crate::{
communication::{message::Message, MessageHandler},
Color,
};
use std::collections::VecDeque;
use std::{
collections::HashMap,
fmt::{self, Debug},
};
pub use tool_message_handler::ToolMessageHandler;
use tool_settings::ToolSettings;
pub use tool_settings::*;
use tools::*;
pub trait Tool {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>);
pub mod tool_messages {
pub use super::tool_message_handler::{ToolMessage, ToolMessageDiscriminant};
pub use super::tools::ellipse::{EllipseMessage, EllipseMessageDiscriminant};
pub use super::tools::rectangle::{RectangleMessage, RectangleMessageDiscriminant};
}
pub type ToolActionHandlerData<'a> = (&'a SvgDocument, &'a DocumentToolData, &'a InputPreprocessor);
pub trait Fsm {
type ToolData;
fn transition(self, event: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self;
fn transition(self, message: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, messages: &mut VecDeque<Message>) -> Self;
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct DocumentToolData {
pub mouse_state: MouseState,
pub mod_keys: ModKeys,
pub primary_color: Color,
pub secondary_color: Color,
}
pub struct ToolData {
pub active_tool_type: ToolType,
pub tools: HashMap<ToolType, Box<dyn Tool>>,
tool_settings: HashMap<ToolType, ToolSettings>,
}
impl ToolData {
pub fn active_tool(&mut self) -> Result<&mut Box<dyn Tool>, EditorError> {
self.tools.get_mut(&self.active_tool_type).ok_or(EditorError::UnknownTool)
type SubToolMessageHandler = dyn for<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>>;
pub struct ToolData {
pub active_tool_type: ToolType,
pub tools: HashMap<ToolType, Box<SubToolMessageHandler>>,
}
impl fmt::Debug for ToolData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ToolData").field("active_tool_type", &self.active_tool_type).field("tool_settings", &"[…]").finish()
}
}
impl ToolData {
pub fn active_tool_mut(&mut self) -> &mut Box<SubToolMessageHandler> {
self.tools.get_mut(&self.active_tool_type).expect("The active tool is not initialized")
}
pub fn active_tool(&self) -> &SubToolMessageHandler {
self.tools.get(&self.active_tool_type).map(|x| x.as_ref()).expect("The active tool is not initialized")
}
}
#[derive(Debug)]
pub struct ToolFsmState {
pub document_tool_data: DocumentToolData,
pub tool_data: ToolData,
pub trace: Trace,
}
impl Default for ToolFsmState {
fn default() -> Self {
ToolFsmState {
trace: Trace::new(),
tool_data: ToolData {
active_tool_type: ToolType::Select,
tools: gen_tools_hash_map! {
Rectangle => rectangle::Rectangle,
Select => select::Select,
Crop => crop::Crop,
Navigate => navigate::Navigate,
@ -65,17 +81,14 @@ impl Default for ToolFsmState {
Path => path::Path,
Pen => pen::Pen,
Line => line::Line,
Rectangle => rectangle::Rectangle,
Ellipse => ellipse::Ellipse,
Shape => shape::Shape,
Ellipse => ellipse::Ellipse,
},
tool_settings: default_tool_settings(),
},
document_tool_data: DocumentToolData {
mouse_state: MouseState::default(),
mod_keys: ModKeys::default(),
primary_color: Color::BLACK,
secondary_color: Color::WHITE,
tool_settings: default_tool_settings(),
},
}
}
@ -86,13 +99,6 @@ impl ToolFsmState {
Self::default()
}
pub fn record_trace_point(&mut self) {
self.trace.push(TracePoint {
mouse_state: self.document_tool_data.mouse_state,
mod_keys: self.document_tool_data.mod_keys,
})
}
pub fn swap_colors(&mut self) {
std::mem::swap(&mut self.document_tool_data.primary_color, &mut self.document_tool_data.secondary_color);
}
@ -178,24 +184,3 @@ impl ToolType {
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum ToolSettings {
Select { append_mode: SelectAppendMode },
Ellipse,
Shape { shape: Shape },
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum SelectAppendMode {
New,
Add,
Subtract,
Intersect,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Shape {
Star { vertices: u32 },
Polygon { vertices: u32 },
}

View file

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

View file

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

View file

@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Crop;
#[impl_message(Message, ToolMessage, Crop)]
#[derive(PartialEq, Clone, Debug)]
pub enum CropMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Crop {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
}
advertise_actions!();
}

View file

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

View file

@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Eyedropper;
#[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Clone, Debug)]
pub enum EyedropperMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
}
advertise_actions!();
}

View file

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

View file

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

View file

@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Navigate;
#[impl_message(Message, ToolMessage, Navigate)]
#[derive(PartialEq, Clone, Debug)]
pub enum NavigateMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
}
advertise_actions!();
}

View file

@ -0,0 +1,18 @@
use crate::message_prelude::*;
use crate::tool::ToolActionHandlerData;
#[derive(Default)]
pub struct Path;
#[impl_message(Message, ToolMessage, Path)]
#[derive(PartialEq, Clone, Debug)]
pub enum PathMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
}
advertise_actions!();
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,169 +0,0 @@
use crate::events::{Event, ToolResponse};
use crate::events::{Key, ViewportPosition};
use crate::tools::{Fsm, Tool};
use crate::Document;
use document_core::layers::style;
use document_core::Operation;
use super::DocumentToolData;
#[derive(Default)]
pub struct Ellipse {
fsm_state: EllipseToolFsmState,
data: EllipseToolData,
}
impl Tool for Ellipse {
fn handle_input(&mut self, event: &Event, document: &Document, tool_data: &DocumentToolData) -> (Vec<ToolResponse>, Vec<Operation>) {
let mut responses = Vec::new();
let mut operations = Vec::new();
self.fsm_state = self.fsm_state.transition(event, document, tool_data, &mut self.data, &mut responses, &mut operations);
(responses, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EllipseToolFsmState {
Ready,
LmbDown,
}
impl Default for EllipseToolFsmState {
fn default() -> Self {
EllipseToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
constrain_to_circle: bool,
center_around_cursor: bool,
}
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
fn transition(self, event: &Event, document: &Document, tool_data: &DocumentToolData, data: &mut Self::ToolData, _responses: &mut Vec<ToolResponse>, operations: &mut Vec<Operation>) -> Self {
match (self, event) {
(EllipseToolFsmState::Ready, Event::LmbDown(mouse_state)) => {
data.drag_start = mouse_state.position;
data.drag_current = mouse_state.position;
operations.push(Operation::MountWorkingFolder { path: vec![] });
EllipseToolFsmState::LmbDown
}
(EllipseToolFsmState::Ready, Event::KeyDown(Key::KeyZ)) => {
if let Some(id) = document.root.list_layers().last() {
operations.push(Operation::DeleteLayer { path: vec![*id] })
}
EllipseToolFsmState::Ready
}
(EllipseToolFsmState::LmbDown, Event::MouseMove(mouse_state)) => {
data.drag_current = *mouse_state;
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
EllipseToolFsmState::LmbDown
}
(EllipseToolFsmState::LmbDown, Event::LmbUp(mouse_state)) => {
data.drag_current = mouse_state.position;
operations.push(Operation::ClearWorkingFolder);
// TODO - introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.drag_start != data.drag_current {
operations.push(make_operation(data, tool_data));
operations.push(Operation::CommitTransaction);
}
EllipseToolFsmState::Ready
}
// TODO - simplify with or_patterns when rust 1.53.0 is stable (https://github.com/rust-lang/rust/issues/54883)
(EllipseToolFsmState::LmbDown, Event::KeyUp(Key::KeyEscape)) | (EllipseToolFsmState::LmbDown, Event::RmbDown(_)) => {
operations.push(Operation::DiscardWorkingFolder);
EllipseToolFsmState::Ready
}
(state, Event::KeyDown(Key::KeyShift)) => {
data.constrain_to_circle = true;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyShift)) => {
data.constrain_to_circle = false;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyDown(Key::KeyAlt)) => {
data.center_around_cursor = true;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
(state, Event::KeyUp(Key::KeyAlt)) => {
data.center_around_cursor = false;
if state == EllipseToolFsmState::LmbDown {
operations.push(Operation::ClearWorkingFolder);
operations.push(make_operation(data, tool_data));
}
self
}
_ => self,
}
}
}
fn make_operation(data: &EllipseToolData, tool_data: &DocumentToolData) -> Operation {
let x0 = data.drag_start.x as f64;
let y0 = data.drag_start.y as f64;
let x1 = data.drag_current.x as f64;
let y1 = data.drag_current.y as f64;
if data.constrain_to_circle {
let (cx, cy, r) = if data.center_around_cursor {
(x0, y0, f64::hypot(x1 - x0, y1 - y0))
} else {
let diameter = f64::max((x1 - x0).abs(), (y1 - y0).abs());
let (x2, y2) = (x0 + (x1 - x0).signum() * diameter, y0 + (y1 - y0).signum() * diameter);
((x0 + x2) * 0.5, (y0 + y2) * 0.5, diameter * 0.5)
};
Operation::AddCircle {
path: vec![],
insert_index: -1,
cx,
cy,
r,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
} else {
let (cx, cy, r_scale) = if data.center_around_cursor { (x0, y0, 1.0) } else { ((x0 + x1) * 0.5, (y0 + y1) * 0.5, 0.5) };
let (rx, ry) = ((x1 - x0).abs() * r_scale, (y1 - y0).abs() * r_scale);
Operation::AddEllipse {
path: vec![],
insert_index: -1,
cx,
cy,
rx,
ry,
rot: 0.0,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0.26"
syn = "1.0.68"
syn = { version = "1.0.68", features = ["full"] }
quote = "1.0.9"
[dev-dependencies.editor-core]

View file

@ -0,0 +1,55 @@
use proc_macro2::{Span, TokenStream};
use syn::{Data, DeriveInput};
pub fn derive_as_message_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
let data = match input.data {
Data::Enum(data) => data,
_ => return Err(syn::Error::new(Span::call_site(), "Tried to derive AsMessage for non-enum")),
};
let input_type = input.ident;
let (globs, names) = data
.variants
.iter()
.map(|var| {
let var_name = &var.ident;
let var_name_s = var.ident.to_string();
if var.attrs.iter().any(|a| a.path.is_ident("child")) {
(
quote::quote! {
#input_type::#var_name(child)
},
quote::quote! {
format!("{}.{}", #var_name_s, child.local_name())
},
)
} else {
(
quote::quote! {
#input_type::#var_name { .. }
},
quote::quote! {
#var_name_s.to_string()
},
)
}
})
.unzip::<_, _, Vec<_>, Vec<_>>();
let res = quote::quote! {
impl AsMessage for #input_type {
fn local_name(self) -> String {
match self {
#(
#globs => #names
),*
}
}
}
};
Ok(res)
}

View file

@ -0,0 +1,124 @@
use crate::helpers::call_site_ident;
use proc_macro2::Ident;
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::Token;
use syn::{ItemEnum, TypePath};
struct MessageArgs {
pub _top_parent: TypePath,
pub _comma1: Token![,],
pub parent: TypePath,
pub _comma2: Token![,],
pub variant: Ident,
}
impl Parse for MessageArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
_top_parent: input.parse()?,
_comma1: input.parse()?,
parent: input.parse()?,
_comma2: input.parse()?,
variant: input.parse()?,
})
}
}
struct TopLevelMessageArgs {
pub parent: TypePath,
pub _comma2: Token![,],
pub variant: Ident,
}
impl Parse for TopLevelMessageArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
parent: input.parse()?,
_comma2: input.parse()?,
variant: input.parse()?,
})
}
}
pub fn combined_message_attrs_impl(attr: TokenStream, input_item: TokenStream) -> syn::Result<TokenStream> {
if attr.is_empty() {
return top_level_impl(input_item);
}
let mut input = syn::parse2::<ItemEnum>(input_item)?;
let (parent_is_top, parent, variant) = match syn::parse2::<MessageArgs>(attr.clone()) {
Ok(x) => (false, x.parent, x.variant),
Err(_) => {
let x = syn::parse2::<TopLevelMessageArgs>(attr)?;
(true, x.parent, x.variant)
}
};
let parent_discriminant = quote::quote! {
<#parent as ToDiscriminant>::Discriminant
};
input.attrs.push(syn::parse_quote! { #[derive(ToDiscriminant, TransitiveChild)] });
input.attrs.push(syn::parse_quote! { #[parent(#parent, #parent::#variant)] });
if parent_is_top {
input.attrs.push(syn::parse_quote! { #[parent_is_top] });
}
input
.attrs
.push(syn::parse_quote! { #[discriminant_attr(derive(Debug, Copy, Clone, PartialEq, Eq, Hash, AsMessage, TransitiveChild))] });
input
.attrs
.push(syn::parse_quote! { #[discriminant_attr(parent(#parent_discriminant, #parent_discriminant::#variant))] });
if parent_is_top {
input.attrs.push(syn::parse_quote! { #[discriminant_attr(parent_is_top)] });
}
for var in &mut input.variants {
if let Some(attr) = var.attrs.iter_mut().find(|a| a.path.is_ident("child")) {
let last_segment = attr.path.segments.last_mut().unwrap();
last_segment.ident = call_site_ident("sub_discriminant");
var.attrs.push(syn::parse_quote! {
#[discriminant_attr(child)]
});
}
}
Ok(input.into_token_stream())
}
fn top_level_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let mut input = syn::parse2::<ItemEnum>(input_item)?;
input.attrs.push(syn::parse_quote! { #[derive(ToDiscriminant)] });
input.attrs.push(syn::parse_quote! { #[discriminant_attr(derive(Debug, Copy, Clone, PartialEq, Eq, Hash, AsMessage))] });
for var in &mut input.variants {
if let Some(attr) = var.attrs.iter_mut().find(|a| a.path.is_ident("child")) {
let last_segment = attr.path.segments.last_mut().unwrap();
last_segment.ident = call_site_ident("sub_discriminant");
var.attrs.push(syn::parse_quote! {
#[discriminant_attr(child)]
});
}
}
let input_type = &input.ident;
let discriminant = call_site_ident(format!("{}Discriminant", input_type));
Ok(quote::quote! {
#input
impl TransitiveChild for #input_type {
type TopParent = Self;
type Parent = Self;
}
impl TransitiveChild for #discriminant {
type TopParent = Self;
type Parent = Self;
}
})
}

View file

@ -0,0 +1,139 @@
use crate::helper_structs::ParenthesizedTokens;
use crate::helpers::call_site_ident;
use proc_macro2::{Ident, Span, TokenStream};
use syn::spanned::Spanned;
use syn::{Attribute, Data, DeriveInput, Field, Fields, ItemEnum};
pub fn derive_discriminant_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
let mut data = match input.data {
Data::Enum(data) => data,
_ => return Err(syn::Error::new(Span::call_site(), "Tried to derive a discriminant for non-enum")),
};
let mut is_sub_discriminant = vec![];
let mut attr_errs = vec![];
for var in &mut data.variants {
if var.attrs.iter().any(|a| a.path.is_ident("sub_discriminant")) {
match var.fields.len() {
1 => {
let Field { ty, .. } = var.fields.iter_mut().next().unwrap();
*ty = syn::parse_quote! {
<#ty as ToDiscriminant>::Discriminant
};
is_sub_discriminant.push(true);
}
n => unimplemented!("#[sub_discriminant] on variants with {} fields is not supported (for now)", n),
}
} else {
var.fields = Fields::Unit;
is_sub_discriminant.push(false);
}
let mut retain = vec![];
for (i, a) in var.attrs.iter_mut().enumerate() {
if a.path.is_ident("discriminant_attr") {
match syn::parse2::<ParenthesizedTokens>(a.tokens.clone()) {
Ok(ParenthesizedTokens { tokens, .. }) => {
let attr: Attribute = syn::parse_quote! {
#[#tokens]
};
*a = attr;
retain.push(i);
}
Err(e) => {
attr_errs.push(syn::Error::new(a.span(), e));
}
}
}
}
var.attrs = var.attrs.iter().enumerate().filter_map(|(i, x)| retain.contains(&i).then(|| x.clone())).collect();
}
let attrs = input
.attrs
.iter()
.cloned()
.filter_map(|a| {
let a_span = a.span();
a.path
.is_ident("discriminant_attr")
.then(|| match syn::parse2::<ParenthesizedTokens>(a.tokens) {
Ok(ParenthesizedTokens { tokens, .. }) => {
let attr: Attribute = syn::parse_quote! {
#[#tokens]
};
Some(attr)
}
Err(e) => {
attr_errs.push(syn::Error::new(a_span, e));
None
}
})
.and_then(|opt| opt)
})
.collect::<Vec<Attribute>>();
if !attr_errs.is_empty() {
return Err(attr_errs
.into_iter()
.reduce(|mut l, r| {
l.combine(r);
l
})
.unwrap());
}
let discriminant = ItemEnum {
attrs,
vis: input.vis,
enum_token: data.enum_token,
ident: call_site_ident(format!("{}Discriminant", input.ident)),
generics: input.generics,
brace_token: data.brace_token,
variants: data.variants,
};
let input_type = &input.ident;
let discriminant_type = &discriminant.ident;
let variant = &discriminant.variants.iter().map(|var| &var.ident).collect::<Vec<&Ident>>();
let (pattern, value) = is_sub_discriminant
.into_iter()
.map(|b| {
(
if b {
quote::quote! { (x) }
} else {
quote::quote! { { .. } }
},
b.then(|| quote::quote! { (x.to_discriminant()) }).unwrap_or_default(),
)
})
.unzip::<_, _, Vec<_>, Vec<_>>();
let res = quote::quote! {
#discriminant
impl ToDiscriminant for #input_type {
type Discriminant = #discriminant_type;
fn to_discriminant(&self) -> #discriminant_type {
match self {
#(
#input_type::#variant #pattern => #discriminant_type::#variant #value
),*
}
}
}
impl From<&#input_type> for #discriminant_type {
fn from(x: &#input_type) -> #discriminant_type {
x.to_discriminant()
}
}
};
Ok(res)
}

View file

@ -1,10 +1,24 @@
use proc_macro2::Ident;
use proc_macro2::{Ident, TokenStream};
use std::collections::HashMap;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::Paren;
use syn::{parenthesized, LitStr, Token};
pub struct IdentList {
pub parts: Punctuated<Ident, Token![,]>,
}
impl Parse for IdentList {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let _paren_token = parenthesized!(content in input);
Ok(Self {
parts: Punctuated::parse_terminated(&content)?,
})
}
}
/// Parses `("some text")`
pub struct AttrInnerSingleString {
_paren_token: Paren,
@ -80,6 +94,55 @@ impl AttrInnerKeyStringMap {
}
}
/// Parses `(left, right)`
pub struct Pair<F, S> {
pub paren_token: Paren,
pub first: F,
pub sep: Token![,],
pub second: S,
}
impl<F, S> Parse for Pair<F, S>
where
F: Parse,
S: Parse,
{
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let paren_token = parenthesized!(content in input);
Ok(Self {
paren_token,
first: content.parse()?,
sep: content.parse()?,
second: content.parse()?,
})
}
}
/// parses `(...)`
pub struct ParenthesizedTokens {
pub paren: Paren,
pub tokens: TokenStream,
}
impl Parse for ParenthesizedTokens {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let paren = parenthesized!(content in input);
Ok(Self { paren, tokens: content.parse()? })
}
}
/// parses a comma-delimeted list of `T`s with optional trailing comma
pub struct SimpleCommaDelimeted<T>(pub Vec<T>);
impl<T: Parse> Parse for SimpleCommaDelimeted<T> {
fn parse(input: ParseStream) -> syn::Result<Self> {
let punct = Punctuated::<T, Token![,]>::parse_terminated(input)?;
Ok(Self(punct.into_iter().collect()))
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -1,4 +1,4 @@
use proc_macro2::Ident;
use proc_macro2::{Ident, Span};
use syn::punctuated::Punctuated;
use syn::{Path, PathArguments, PathSegment, Token};
@ -19,8 +19,13 @@ pub fn fold_error_iter<T>(iter: impl Iterator<Item = syn::Result<T>>) -> syn::Re
})
}
/// Creates an ident at the call site
pub fn call_site_ident<S: AsRef<str>>(s: S) -> Ident {
Ident::new(s.as_ref(), Span::call_site())
}
/// Creates the path `left::right` from the idents `left` and `right`
pub fn two_path(left_ident: Ident, right_ident: Ident) -> Path {
pub fn two_segment_path(left_ident: Ident, right_ident: Ident) -> Path {
let mut segments: Punctuated<PathSegment, Token![::]> = Punctuated::new();
segments.push(PathSegment {
ident: left_ident,
@ -57,6 +62,6 @@ mod tests {
#[test]
fn test_two_path() {
let _span = quote::quote! { "" }.span();
assert_eq!(two_path(Ident::new("a", _span), Ident::new("b", _span)).to_token_stream().to_string(), "a :: b");
assert_eq!(two_segment_path(Ident::new("a", _span), Ident::new("b", _span)).to_token_stream().to_string(), "a :: b");
}
}

View file

@ -0,0 +1,85 @@
use crate::helper_structs::AttrInnerKeyStringMap;
use crate::helpers::{fold_error_iter, two_segment_path};
use proc_macro2::{Span, TokenStream as TokenStream2};
use syn::{Attribute, Data, DeriveInput, LitStr, Variant};
fn parse_hint_helper_attrs(attrs: &[Attribute]) -> syn::Result<(Vec<LitStr>, Vec<LitStr>)> {
fold_error_iter(
attrs
.iter()
.filter(|a| a.path.get_ident().map_or(false, |i| i == "hint"))
.map(|attr| syn::parse2::<AttrInnerKeyStringMap>(attr.tokens.clone())),
)
.and_then(|v: Vec<AttrInnerKeyStringMap>| {
fold_error_iter(AttrInnerKeyStringMap::multi_into_iter(v).map(|(k, mut v)| match v.len() {
0 => panic!("internal error: a key without values was somehow inserted into the hashmap"),
1 => {
let single_val = v.pop().unwrap();
Ok((LitStr::new(&k.to_string(), Span::call_site()), single_val))
}
_ => {
// the first value is ok, the other ones should error
let after_first = v.into_iter().skip(1);
// this call to fold_error_iter will always return Err with a combined error
fold_error_iter(after_first.map(|lit| Err(syn::Error::new(lit.span(), format!("value for key {} was already given", k))))).map(|_: Vec<()>| unreachable!())
}
}))
})
.map(|v| v.into_iter().unzip())
}
pub fn derive_hint_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> {
let input = syn::parse2::<DeriveInput>(input_item)?;
let ident = input.ident;
match input.data {
Data::Enum(data) => {
let variants = data.variants.iter().map(|var: &Variant| two_segment_path(ident.clone(), var.ident.clone())).collect::<Vec<_>>();
let hint_result = fold_error_iter(data.variants.into_iter().map(|var: Variant| parse_hint_helper_attrs(&var.attrs)));
hint_result.map(|hints: Vec<(Vec<LitStr>, Vec<LitStr>)>| {
let (keys, values): (Vec<Vec<LitStr>>, Vec<Vec<LitStr>>) = hints.into_iter().unzip();
let cap: Vec<usize> = keys.iter().map(|v| v.len()).collect();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
match self {
#(
#variants { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
)*
}
}
}
}
})
}
Data::Struct(_) | Data::Union(_) => {
let hint_result = parse_hint_helper_attrs(&input.attrs);
hint_result.map(|(keys, values)| {
let cap = keys.len();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
}
}
})
}
}
}

View file

@ -1,91 +1,215 @@
mod as_message;
mod combined_message_attrs;
mod discriminant;
mod helper_structs;
mod helpers;
mod structs;
mod hint;
mod transitive_child;
use crate::helpers::{fold_error_iter, two_path};
use crate::structs::{AttrInnerKeyStringMap, AttrInnerSingleString};
use crate::as_message::derive_as_message_impl;
use crate::combined_message_attrs::combined_message_attrs_impl;
use crate::discriminant::derive_discriminant_impl;
use crate::helper_structs::AttrInnerSingleString;
use crate::hint::derive_hint_impl;
use crate::transitive_child::derive_transitive_child_impl;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use syn::{parse_macro_input, Attribute, Data, DeriveInput, LitStr, Variant};
use syn::parse_macro_input;
fn parse_hint_helper_attrs(attrs: &[Attribute]) -> syn::Result<(Vec<LitStr>, Vec<LitStr>)> {
fold_error_iter(
attrs
.iter()
.filter(|a| a.path.get_ident().map_or(false, |i| i == "hint"))
.map(|attr| syn::parse2::<AttrInnerKeyStringMap>(attr.tokens.clone())),
)
.and_then(|v: Vec<AttrInnerKeyStringMap>| {
fold_error_iter(AttrInnerKeyStringMap::multi_into_iter(v).map(|(k, mut v)| match v.len() {
0 => panic!("internal error: a key without values was somehow inserted into the hashmap"),
1 => {
let single_val = v.pop().unwrap();
Ok((LitStr::new(&k.to_string(), Span::call_site()), single_val))
}
_ => {
// the first value is ok, the other ones should error
let after_first = v.into_iter().skip(1);
// this call to fold_error_iter will always return Err with a combined error
fold_error_iter(after_first.map(|lit| Err(syn::Error::new(lit.span(), format!("value for key {} was already given", k))))).map(|_: Vec<()>| unreachable!())
}
}))
})
.map(|v| v.into_iter().unzip())
/// Derive the `ToDiscriminant` trait and create a `<Type Name>Discriminant` enum
///
/// This derive macro is enum-only.
///
/// The discriminant enum is a copy of the input enum with all fields of every variant removed.\
/// *) The exception to that rule is the `#[child]` attribute
///
/// # Helper attributes
/// - `#[sub_discriminant]`: only usable on variants with a single field; instead of no fields, the discriminant of the single field will be included in the discriminant,
/// acting as a sub-discriminant.
/// - `#[discriminant_attr(…)]`: usable on the enum itself or on any variant; applies `#[…]` in its place on the discriminant.
///
/// # Attributes on the Discriminant
/// All attributes on variants and the type itself are cleared when constructing the discriminant.
/// If the discriminant is supposed to also have an attribute, you must double it with `#[discriminant_attr(…)]`
///
/// # Example
/// ```
/// # use graphite_proc_macros::ToDiscriminant;
/// # use editor_core::misc::derivable_custom_traits::ToDiscriminant;
/// # use std::ffi::OsString;
///
/// #[derive(ToDiscriminant)]
/// #[discriminant_attr(derive(Debug, Eq, PartialEq))]
/// pub enum EnumA {
/// A(u8),
/// #[sub_discriminant]
/// B(EnumB)
/// }
///
/// #[derive(ToDiscriminant)]
/// #[discriminant_attr(derive(Debug, Eq, PartialEq))]
/// #[discriminant_attr(repr(u8))]
/// pub enum EnumB {
/// Foo(u8),
/// Bar(String),
/// #[cfg(feature = "some-feature")]
/// #[discriminant_attr(cfg(feature = "some-feature"))]
/// WindowsBar(OsString)
/// }
///
/// let a = EnumA::A(1);
/// assert_eq!(a.to_discriminant(), EnumADiscriminant::A);
/// let b = EnumA::B(EnumB::Bar("bar".to_string()));
/// assert_eq!(b.to_discriminant(), EnumADiscriminant::B(EnumBDiscriminant::Bar));
/// ```
#[proc_macro_derive(ToDiscriminant, attributes(sub_discriminant, discriminant_attr))]
pub fn derive_discriminant(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_discriminant_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
fn derive_hint_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> {
let input = syn::parse2::<DeriveInput>(input_item)?;
/// Derive the `TransitiveChild` trait and generate `From` impls to convert into the parent, as well as the top parent type
///
/// This macro cannot be invoked on the top parent (which has no parent but itself). Instead, implement `TransitiveChild` manually
/// like in the example.
///
/// # Helper Attributes
/// - `#[parent(<Type>, <Expr>)]` (**required**): declare the parent type (`<Type>`)
/// and a function (`<Expr>`, has to evaluate to a single arg function) for converting a value of this type to the parent type
/// - `#[parent_is_top]`: Denote that the parent type has no further parent type (this is required because otherwise the `From` impls for parent and top parent would overlap)
///
/// # Example
/// ```
/// # use graphite_proc_macros::TransitiveChild;
/// # use editor_core::misc::derivable_custom_traits::TransitiveChild;
///
/// #[derive(Debug, Eq, PartialEq)]
/// struct A { u: u8, b: B };
///
/// impl A {
/// pub fn from_b(b: B) -> Self {
/// Self { u: 7, b }
/// }
/// }
///
/// impl TransitiveChild for A {
/// type Parent = Self;
/// type TopParent = Self;
/// }
///
/// #[derive(TransitiveChild, Debug, Eq, PartialEq)]
/// #[parent(A, A::from_b)]
/// #[parent_is_top]
/// enum B {
/// Foo,
/// Bar,
/// Child(C)
/// }
///
/// #[derive(TransitiveChild, Debug, Eq, PartialEq)]
/// #[parent(B, B::Child)]
/// struct C(D);
///
/// #[derive(TransitiveChild, Debug, Eq, PartialEq)]
/// #[parent(C, C)]
/// struct D;
///
/// let d = D;
/// assert_eq!(A::from(d), A { u: 7, b: B::Child(C(D)) });
/// ```
#[proc_macro_derive(TransitiveChild, attributes(parent, parent_is_top))]
pub fn derive_transitive_child(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_transitive_child_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
let ident = input.ident;
/// Derive the `AsMessage` trait
///
/// # Helper Attributes
/// - `#[child]`: only on tuple variants with a single field; Denote that the message path should continue inside the variant
///
/// # Example
/// See also [`TransitiveChild`]
/// ```
/// # use graphite_proc_macros::{TransitiveChild, AsMessage};
/// # use editor_core::misc::derivable_custom_traits::TransitiveChild;
/// # use editor_core::message_prelude::*;
///
/// #[derive(AsMessage)]
/// pub enum TopMessage {
/// A(u8),
/// B(u16),
/// #[child]
/// C(MessageC),
/// #[child]
/// D(MessageD)
/// }
///
/// impl TransitiveChild for TopMessage {
/// type Parent = Self;
/// type TopParent = Self;
/// }
///
/// #[derive(TransitiveChild, AsMessage, Copy, Clone)]
/// #[parent(TopMessage, TopMessage::C)]
/// #[parent_is_top]
/// pub enum MessageC {
/// X1,
/// X2
/// }
///
/// #[derive(TransitiveChild, AsMessage, Copy, Clone)]
/// #[parent(TopMessage, TopMessage::D)]
/// #[parent_is_top]
/// pub enum MessageD {
/// Y1,
/// #[child]
/// Y2(MessageE)
/// }
///
/// #[derive(TransitiveChild, AsMessage, Copy, Clone)]
/// #[parent(MessageD, MessageD::Y2)]
/// pub enum MessageE {
/// Alpha,
/// Beta
/// }
///
/// let c = MessageC::X1;
/// assert_eq!(c.local_name(), "X1");
/// assert_eq!(c.global_name(), "C.X1");
/// let d = MessageD::Y2(MessageE::Alpha);
/// assert_eq!(d.local_name(), "Y2.Alpha");
/// assert_eq!(d.global_name(), "D.Y2.Alpha");
/// let e = MessageE::Beta;
/// assert_eq!(e.local_name(), "Beta");
/// assert_eq!(e.global_name(), "D.Y2.Beta");
/// ```
#[proc_macro_derive(AsMessage, attributes(child))]
pub fn derive_message(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_as_message_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
match input.data {
Data::Enum(data) => {
let variants = data.variants.iter().map(|var: &Variant| two_path(ident.clone(), var.ident.clone())).collect::<Vec<_>>();
let hint_result = fold_error_iter(data.variants.into_iter().map(|var: Variant| parse_hint_helper_attrs(&var.attrs)));
hint_result.map(|hints: Vec<(Vec<LitStr>, Vec<LitStr>)>| {
let (keys, values): (Vec<Vec<LitStr>>, Vec<Vec<LitStr>>) = hints.into_iter().unzip();
let cap: Vec<usize> = keys.iter().map(|v| v.len()).collect();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
match self {
#(
#variants { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
)*
}
}
}
}
})
}
Data::Struct(_) | Data::Union(_) => {
let hint_result = parse_hint_helper_attrs(&input.attrs);
hint_result.map(|(keys, values)| {
let cap = keys.len();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
}
}
})
}
}
/// This macro is basically an abbreviation for the usual [`ToDiscriminant`], [`TransitiveChild`] and [`AsMessage`] invokations
///
/// This macro is enum-only.
///
/// Also note that all three of those derives have to be in scope.
///
/// # Usage
/// There are three possible argument syntaxes you can use:
/// 1. no arguments: this is for the top-level message enum. It derives `ToDiscriminant`, `AsMessage` on the discriminant, and implements `TransitiveChild` on both
/// (the parent and top parent being the respective types themselves).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// 2. two arguments: this is for message enums whose direct parent is the top level message enum. The syntax is `#[impl_message(<Type>, <Ident>)]`,
/// where `<Type>` is the parent message type and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both (adding `#[parent_is_top]` to both).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// 3. three arguments: this is for all other message enums that are transitive children of the top level message enum. The syntax is
/// `#[impl_message(<Type>, <Type>, <Ident>)]`, where the first `<Type>` is the top parent message type, the secont `<Type>` is the parent message type
/// and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both.
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// **This third option will likely change in the future**
#[proc_macro_attribute]
pub fn impl_message(attr: TokenStream, input_item: TokenStream) -> TokenStream {
TokenStream::from(combined_message_attrs_impl(attr.into(), input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// Derive the `Hint` trait
@ -93,7 +217,7 @@ fn derive_hint_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> {
/// # Example
/// ```
/// # use graphite_proc_macros::Hint;
/// # use editor_core::hint::Hint;
/// # use editor_core::misc::derivable_custom_traits::Hint;
///
/// #[derive(Hint)]
/// pub enum StateMachine {
@ -152,6 +276,7 @@ pub fn edge(attr: TokenStream, item: TokenStream) -> TokenStream {
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::TokenStream as TokenStream2;
fn ts_assert_eq(l: TokenStream2, r: TokenStream2) {
// not sure if this is the best way of doing things but if two TokenStreams are equal, their `to_string` is also equal

View file

@ -0,0 +1,54 @@
use crate::helper_structs::Pair;
use proc_macro2::{Span, TokenStream};
use syn::{Attribute, DeriveInput, Expr, Type};
pub fn derive_transitive_child_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
let Attribute { tokens, .. } = input
.attrs
.iter()
.find(|a| a.path.is_ident("parent"))
.ok_or_else(|| syn::Error::new(Span::call_site(), format!("tried to derive TransitiveChild without a #[parent] attribute (on {})", input.ident)))?;
let parent_is_top = input.attrs.iter().any(|a| a.path.is_ident("parent_is_top"));
let Pair {
first: parent_type,
second: to_parent,
..
} = syn::parse2::<Pair<Type, Expr>>(tokens.clone())?;
let top_parent_type: Type = syn::parse_quote! { <#parent_type as TransitiveChild>::TopParent };
let input_type = &input.ident;
let trait_impl = quote::quote! {
impl TransitiveChild for #input_type {
type Parent = #parent_type;
type TopParent = #top_parent_type;
}
};
let from_for_parent = quote::quote! {
impl From<#input_type> for #parent_type {
fn from(x: #input_type) -> #parent_type {
(#to_parent)(x)
}
}
};
let from_for_top = quote::quote! {
impl From<#input_type> for #top_parent_type {
fn from(x: #input_type) -> #top_parent_type {
#top_parent_type::from((#to_parent)(x))
}
}
};
Ok(if parent_is_top {
quote::quote! { #trait_impl #from_for_parent }
} else {
quote::quote! { #trait_impl #from_for_parent #from_for_top }
})
}