mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
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:
parent
56c1110800
commit
a596ce0104
72 changed files with 3028 additions and 1856 deletions
56
Cargo.lock
generated
56
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
73
core/editor/src/communication/dispatcher.rs
Normal file
73
core/editor/src/communication/dispatcher.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
30
core/editor/src/communication/message.rs
Normal file
30
core/editor/src/communication/message.rs
Normal 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),
|
||||
}
|
||||
23
core/editor/src/communication/mod.rs
Normal file
23
core/editor/src/communication/mod.rs
Normal 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;
|
||||
}
|
||||
|
|
@ -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>) {}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
7
core/editor/src/document/document_file.rs
Normal file
7
core/editor/src/document/document_file.rs
Normal 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,
|
||||
}
|
||||
100
core/editor/src/document/document_message_handler.rs
Normal file
100
core/editor/src/document/document_message_handler.rs
Normal 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);
|
||||
}
|
||||
8
core/editor/src/document/mod.rs
Normal file
8
core/editor/src/document/mod.rs
Normal 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};
|
||||
59
core/editor/src/frontend/frontend_message_handler.rs
Normal file
59
core/editor/src/frontend/frontend_message_handler.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
3
core/editor/src/frontend/mod.rs
Normal file
3
core/editor/src/frontend/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod frontend_message_handler;
|
||||
|
||||
pub use frontend_message_handler::{Callback, FrontendMessage, FrontendMessageDiscriminant, FrontendMessageHandler};
|
||||
40
core/editor/src/global/global_message_handler.rs
Normal file
40
core/editor/src/global/global_message_handler.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
use crate::message_prelude::*;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[impl_message(Message, Global)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum GlobalMessage {
|
||||
LogInfo,
|
||||
LogDebug,
|
||||
LogTrace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GlobalMessageHandler {}
|
||||
|
||||
impl GlobalMessageHandler {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageHandler<GlobalMessage, ()> for GlobalMessageHandler {
|
||||
fn process_action(&mut self, message: GlobalMessage, _data: (), _responses: &mut VecDeque<Message>) {
|
||||
use GlobalMessage::*;
|
||||
match message {
|
||||
LogInfo => {
|
||||
log::set_max_level(log::LevelFilter::Info);
|
||||
log::info!("set log verbosity to info");
|
||||
}
|
||||
LogDebug => {
|
||||
log::set_max_level(log::LevelFilter::Debug);
|
||||
log::info!("set log verbosity to debug");
|
||||
}
|
||||
LogTrace => {
|
||||
log::set_max_level(log::LevelFilter::Trace);
|
||||
log::info!("set log verbosity to trace");
|
||||
}
|
||||
}
|
||||
}
|
||||
advertise_actions!(GlobalMessageDiscriminant; LogInfo, LogDebug, LogTrace);
|
||||
}
|
||||
3
core/editor/src/global/mod.rs
Normal file
3
core/editor/src/global/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod global_message_handler;
|
||||
|
||||
pub use global_message_handler::{GlobalMessage, GlobalMessageDiscriminant, GlobalMessageHandler};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
pub trait Hint {
|
||||
fn hints(&self) -> HashMap<String, String>;
|
||||
}
|
||||
191
core/editor/src/input/input_mapper.rs
Normal file
191
core/editor/src/input/input_mapper.rs
Normal 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!();
|
||||
}
|
||||
74
core/editor/src/input/input_preprocessor.rs
Normal file
74
core/editor/src/input/input_preprocessor.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
143
core/editor/src/input/keyboard.rs
Normal file
143
core/editor/src/input/keyboard.rs
Normal 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));
|
||||
9
core/editor/src/input/mod.rs
Normal file
9
core/editor/src/input/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod input_mapper;
|
||||
pub mod input_preprocessor;
|
||||
pub mod keyboard;
|
||||
pub mod mouse;
|
||||
|
||||
pub use {
|
||||
input_mapper::{InputMapper, InputMapperMessage, InputMapperMessageDiscriminant},
|
||||
input_preprocessor::{InputPreprocessor, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant},
|
||||
};
|
||||
48
core/editor/src/input/mouse.rs
Normal file
48
core/editor/src/input/mouse.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
),*
|
||||
}
|
||||
};
|
||||
}
|
||||
18
core/editor/src/misc/derivable_custom_traits.rs
Normal file
18
core/editor/src/misc/derivable_custom_traits.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//! Traits that can be derived using macros from `graphite-proc-macros`
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub trait Hint {
|
||||
fn hints(&self) -> HashMap<String, String>;
|
||||
}
|
||||
|
||||
pub trait ToDiscriminant {
|
||||
type Discriminant;
|
||||
|
||||
fn to_discriminant(&self) -> Self::Discriminant;
|
||||
}
|
||||
|
||||
pub trait TransitiveChild: Into<Self::Parent> + Into<Self::TopParent> {
|
||||
type TopParent;
|
||||
type Parent;
|
||||
}
|
||||
|
|
@ -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);
|
||||
138
core/editor/src/misc/macros.rs
Normal file
138
core/editor/src/misc/macros.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/// Counts args in the macro invocation by adding `+ 1` for every arg.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// let x = count_args!(("example1"), (10), (25));
|
||||
/// assert_eq!(x, 3);
|
||||
/// ```
|
||||
/// expands to
|
||||
/// ```ignore
|
||||
/// let x = 0 + 1 + 1 + 1;
|
||||
/// assert_eq!(x, 3);
|
||||
/// ```
|
||||
macro_rules! count_args {
|
||||
(@one $($t:tt)*) => { 1 };
|
||||
($(($($x:tt)*)),*$(,)?) => {
|
||||
0 $(+ count_args!(@one $($x)*))*
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a [`std::collections::HashMap`] for `ToolState`'s `tools` variable.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// let tools = gen_tools_hash_map! {
|
||||
/// Select => select::Select,
|
||||
/// Crop => crop::Crop,
|
||||
/// };
|
||||
/// ```
|
||||
/// expands to
|
||||
/// ```ignore
|
||||
/// let tools = {
|
||||
/// let mut hash_map: std::collections::HashMap<crate::tool::ToolType, Box<dyn crate::tool::Tool>> = std::collections::HashMap::with_capacity(count_args!(/* Macro args */));
|
||||
///
|
||||
/// hash_map.insert(crate::tool::ToolType::Select, Box::new(select::Select::default()));
|
||||
/// hash_map.insert(crate::tool::ToolType::Crop, Box::new(crop::Crop::default()));
|
||||
///
|
||||
/// hash_map
|
||||
/// };
|
||||
/// ```
|
||||
macro_rules! gen_tools_hash_map {
|
||||
($($enum_variant:ident => $struct_path:ty),* $(,)?) => {{
|
||||
let mut hash_map: ::std::collections::HashMap<$crate::tool::ToolType, ::std::boxed::Box<dyn for<'a> $crate::message_prelude::MessageHandler<$crate::tool::tool_messages::ToolMessage,$crate::tool::ToolActionHandlerData<'a>>>> = ::std::collections::HashMap::with_capacity(count_args!($(($enum_variant)),*));
|
||||
$(hash_map.insert($crate::tool::ToolType::$enum_variant, ::std::boxed::Box::new(<$struct_path>::default()));)*
|
||||
|
||||
hash_map
|
||||
}};
|
||||
}
|
||||
|
||||
/// Creates a string representation of an enum value that exactly matches the given name of each enum variant
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// enum E {
|
||||
/// A(u8),
|
||||
/// B
|
||||
/// }
|
||||
///
|
||||
/// // this line is important
|
||||
/// use E::*;
|
||||
///
|
||||
/// let a = E::A(7);
|
||||
/// let s = match_variant_name!(match (a) { A, B });
|
||||
/// ```
|
||||
///
|
||||
/// expands to
|
||||
///
|
||||
/// ```ignore
|
||||
/// // ...
|
||||
///
|
||||
/// let s = match a {
|
||||
/// A { .. } => "A",
|
||||
/// B { .. } => "B"
|
||||
/// };
|
||||
/// ```
|
||||
macro_rules! match_variant_name {
|
||||
(match ($e:expr) { $($v:ident),* $(,)? }) => {
|
||||
match $e {
|
||||
$(
|
||||
$v { .. } => stringify!($v)
|
||||
),*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Syntax sugar for initializing an `ActionList`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// actions!(DocumentMessage::Undo, DocumentMessage::Redo);
|
||||
/// ```
|
||||
///
|
||||
/// expands to:
|
||||
/// ```ignore
|
||||
/// vec![vec![DocumentMessage::Undo, DocumentMessage::Redo]];
|
||||
/// ```
|
||||
///
|
||||
/// and
|
||||
/// ```ignore
|
||||
/// actions!(DocumentMessage; Undo, Redo);
|
||||
/// ```
|
||||
///
|
||||
/// expands to:
|
||||
/// ```ignore
|
||||
/// vec![vec![DocumentMessage::Undo, DocumentMessage::Redo]];
|
||||
/// ```
|
||||
///
|
||||
macro_rules! actions {
|
||||
($($v:expr),* $(,)?) => {{
|
||||
vec![$(vec![$v.into()]),*]
|
||||
}};
|
||||
($name:ident; $($v:ident),* $(,)?) => {{
|
||||
vec![vec![$(($name::$v).into()),*]]
|
||||
}};
|
||||
}
|
||||
|
||||
/// Does the same thing as the `actions!` macro but wraps everything in:
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn actions(&self) -> ActionList {
|
||||
/// actions!(…)
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! advertise_actions {
|
||||
($($v:expr),* $(,)?) => {
|
||||
fn actions(&self) -> $crate::communication::ActionList {
|
||||
actions!($($v),*)
|
||||
}
|
||||
};
|
||||
($name:ident; $($v:ident),* $(,)?) => {
|
||||
fn actions(&self) -> $crate::communication::ActionList {
|
||||
actions!($name; $($v),*)
|
||||
}
|
||||
}
|
||||
}
|
||||
7
core/editor/src/misc/mod.rs
Normal file
7
core/editor/src/misc/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#[macro_use]
|
||||
pub mod macros;
|
||||
pub mod derivable_custom_traits;
|
||||
mod error;
|
||||
|
||||
pub use error::EditorError;
|
||||
pub use macros::*;
|
||||
|
|
@ -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 },
|
||||
}
|
||||
101
core/editor/src/tool/tool_message_handler.rs
Normal file
101
core/editor/src/tool/tool_message_handler.rs
Normal 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
|
||||
}
|
||||
}
|
||||
20
core/editor/src/tool/tool_settings.rs
Normal file
20
core/editor/src/tool/tool_settings.rs
Normal 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 },
|
||||
}
|
||||
18
core/editor/src/tool/tools/crop.rs
Normal file
18
core/editor/src/tool/tools/crop.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use crate::message_prelude::*;
|
||||
use crate::tool::ToolActionHandlerData;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Crop;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Crop)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
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!();
|
||||
}
|
||||
173
core/editor/src/tool/tools/ellipse.rs
Normal file
173
core/editor/src/tool/tools/ellipse.rs
Normal 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()
|
||||
}
|
||||
18
core/editor/src/tool/tools/eyedropper.rs
Normal file
18
core/editor/src/tool/tools/eyedropper.rs
Normal 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!();
|
||||
}
|
||||
184
core/editor/src/tool/tools/line.rs
Normal file
184
core/editor/src/tool/tools/line.rs
Normal 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()
|
||||
}
|
||||
13
core/editor/src/tool/tools/mod.rs
Normal file
13
core/editor/src/tool/tools/mod.rs
Normal 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;
|
||||
18
core/editor/src/tool/tools/navigate.rs
Normal file
18
core/editor/src/tool/tools/navigate.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use crate::message_prelude::*;
|
||||
use crate::tool::ToolActionHandlerData;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Navigate;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Navigate)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum NavigateMessage {
|
||||
MouseMove,
|
||||
}
|
||||
|
||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
|
||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
|
||||
}
|
||||
advertise_actions!();
|
||||
}
|
||||
18
core/editor/src/tool/tools/path.rs
Normal file
18
core/editor/src/tool/tools/path.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use crate::message_prelude::*;
|
||||
use crate::tool::ToolActionHandlerData;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Path;
|
||||
|
||||
#[impl_message(Message, ToolMessage, Path)]
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
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!();
|
||||
}
|
||||
130
core/editor/src/tool/tools/pen.rs
Normal file
130
core/editor/src/tool/tools/pen.rs
Normal 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()
|
||||
}
|
||||
171
core/editor/src/tool/tools/rectangle.rs
Normal file
171
core/editor/src/tool/tools/rectangle.rs
Normal 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()
|
||||
}
|
||||
60
core/editor/src/tool/tools/select.rs
Normal file
60
core/editor/src/tool/tools/select.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
175
core/editor/src/tool/tools/shape.rs
Normal file
175
core/editor/src/tool/tools/shape.rs
Normal 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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
|
@ -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))),
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))),
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
55
core/proc-macro/src/as_message.rs
Normal file
55
core/proc-macro/src/as_message.rs
Normal 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)
|
||||
}
|
||||
124
core/proc-macro/src/combined_message_attrs.rs
Normal file
124
core/proc-macro/src/combined_message_attrs.rs
Normal 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;
|
||||
}
|
||||
})
|
||||
}
|
||||
139
core/proc-macro/src/discriminant.rs
Normal file
139
core/proc-macro/src/discriminant.rs
Normal 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)
|
||||
}
|
||||
|
|
@ -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::*;
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
85
core/proc-macro/src/hint.rs
Normal file
85
core/proc-macro/src/hint.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
54
core/proc-macro/src/transitive_child.rs
Normal file
54
core/proc-macro/src/transitive_child.rs
Normal 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 }
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue