diff --git a/Cargo.lock b/Cargo.lock index bd92c6cff..e6a513ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/client/web/src/response-handler.ts b/client/web/src/response-handler.ts index d0f016fda..72b52a61b 100644 --- a/client/web/src/response-handler.ts +++ b/client/web/src/response-handler.ts @@ -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; diff --git a/client/web/wasm/src/document.rs b/client/web/wasm/src/document.rs index f0187490b..0eb77588f 100644 --- a/client/web/wasm/src/document.rs +++ b/client/web/wasm/src/document.rs @@ -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) -> 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) -> 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) -> Result<(), JsValue> { #[wasm_bindgen] pub fn toggle_layer_expansion(path: Vec) -> 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) -> Result<(), JsValue> { #[wasm_bindgen] pub fn rename_layer(path: Vec, 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) -> 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) -> 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) -> Result<(), JsValue> { + EDITOR_STATE.with(|editor| editor.borrow_mut().handle_message(DocumentMessage::AddFolder(path))).map_err(convert_error) } diff --git a/client/web/wasm/src/lib.rs b/client/web/wasm/src/lib.rs index bcb3c4501..acf5fee8f 100644 --- a/client/web/wasm/src/lib.rs +++ b/client/web/wasm/src/lib.rs @@ -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 = 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] diff --git a/client/web/wasm/src/window.rs b/client/web/wasm/src/window.rs index d7ff356e0..96352918b 100644 --- a/client/web/wasm/src/window.rs +++ b/client/web/wasm/src/window.rs @@ -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) { diff --git a/client/web/wasm/src/wrappers.rs b/client/web/wasm/src/wrappers.rs index 47cd7494d..2dbd8393d 100644 --- a/client/web/wasm/src/wrappers.rs +++ b/client/web/wasm/src/wrappers.rs @@ -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 { }) } -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, diff --git a/core/document/src/color.rs b/core/document/src/color.rs index 448cef7d5..42ea1ba9a 100644 --- a/core/document/src/color.rs +++ b/core/document/src/color.rs @@ -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, diff --git a/core/document/src/document.rs b/core/document/src/document.rs index 0216f0b04..8e8011fca 100644 --- a/core/document/src/document.rs +++ b/core/document/src/document.rs @@ -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>, 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 = 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 = 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 = 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) } } diff --git a/core/document/src/layers/style/mod.rs b/core/document/src/layers/style/mod.rs index 6b241e13a..d54e3d684 100644 --- a/core/document/src/layers/style/mod.rs +++ b/core/document/src/layers/style/mod.rs @@ -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) } } diff --git a/core/document/src/operation.rs b/core/document/src/operation.rs index 725127d55..dcdeea497 100644 --- a/core/document/src/operation.rs +++ b/core/document/src/operation.rs @@ -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 { diff --git a/core/document/src/response.rs b/core/document/src/response.rs index 2c53f9c75..5d30ce608 100644 --- a/core/document/src/response.rs +++ b/core/document/src/response.rs @@ -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, } -#[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 { diff --git a/core/editor/Cargo.toml b/core/editor/Cargo.toml index 9a14cbc57..fe90d2043 100644 --- a/core/editor/Cargo.toml +++ b/core/editor/Cargo.toml @@ -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" diff --git a/core/editor/src/communication/dispatcher.rs b/core/editor/src/communication/dispatcher.rs new file mode 100644 index 000000000..abe9e376d --- /dev/null +++ b/core/editor/src/communication/dispatcher.rs @@ -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, +} + +impl Dispatcher { + pub fn handle_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(), + } + } +} diff --git a/core/editor/src/communication/message.rs b/core/editor/src/communication/message.rs new file mode 100644 index 000000000..5f1663f0b --- /dev/null +++ b/core/editor/src/communication/message.rs @@ -0,0 +1,30 @@ +use crate::message_prelude::*; +use graphite_proc_macros::*; + +pub trait AsMessage: TransitiveChild +where + Self::TopParent: TransitiveChild + AsMessage, +{ + fn local_name(self) -> String; + fn global_name(self) -> String { + >::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), +} diff --git a/core/editor/src/communication/mod.rs b/core/editor/src/communication/mod.rs new file mode 100644 index 000000000..d1a2b7608 --- /dev/null +++ b/core/editor/src/communication/mod.rs @@ -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>; + +// TODO: Add Send + Sync requirement +// Use something like rw locks for synchronization +pub trait MessageHandlerData {} + +pub trait MessageHandler +where + A::Discriminant: AsMessage, + ::TopParent: TransitiveChild::TopParent, TopParent = ::TopParent> + AsMessage, +{ + /// Return true if the Action is consumed. + fn process_action(&mut self, action: A, data: T, responses: &mut VecDeque); + fn actions(&self) -> ActionList; +} diff --git a/core/editor/src/dispatcher/document_event_handler.rs b/core/editor/src/dispatcher/document_event_handler.rs deleted file mode 100644 index c2504096a..000000000 --- a/core/editor/src/dispatcher/document_event_handler.rs +++ /dev/null @@ -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, responses: &mut Vec, operations: &mut Vec) {} -} diff --git a/core/editor/src/dispatcher/events.rs b/core/editor/src/dispatcher/events.rs deleted file mode 100644 index 75405424d..000000000 --- a/core/editor/src/dispatcher/events.rs +++ /dev/null @@ -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), - ToggleLayerVisibility(Vec), - ToggleLayerExpansion(Vec), - DeleteLayer(Vec), - AddLayer(Vec), - RenameLayer(Vec, 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 for Response { - fn from(response: ToolResponse) -> Self { - Response::Tool(response) - } -} - -impl From 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); - -impl Deref for Trace { - type Target = Vec; - - 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; - } -} diff --git a/core/editor/src/dispatcher/global_event_handler.rs b/core/editor/src/dispatcher/global_event_handler.rs deleted file mode 100644 index ec7144725..000000000 --- a/core/editor/src/dispatcher/global_event_handler.rs +++ /dev/null @@ -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, responses: &mut Vec, operations: &mut Vec) -> bool { - false - } -} diff --git a/core/editor/src/dispatcher/mod.rs b/core/editor/src/dispatcher/mod.rs deleted file mode 100644 index 6483b1062..000000000 --- a/core/editor/src/dispatcher/mod.rs +++ /dev/null @@ -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; -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>(&self, document: &mut Document, operations: I) -> Vec { - 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>, EditorError> { - Ok(document.handle_operation(operation)?) - } - - pub fn dispatch_responses, I: IntoIterator>(&self, responses: I) { - for response in responses { - self.dispatch_response(response); - } - } - - pub fn dispatch_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 } - } -} diff --git a/core/editor/src/document/document_file.rs b/core/editor/src/document/document_file.rs new file mode 100644 index 000000000..56f41eafe --- /dev/null +++ b/core/editor/src/document/document_file.rs @@ -0,0 +1,7 @@ +use document_core::document::Document as InteralDocument; + +#[derive(Clone, Debug, Default)] +pub struct Document { + pub document: InteralDocument, + pub name: String, +} diff --git a/core/editor/src/document/document_message_handler.rs b/core/editor/src/document/document_message_handler.rs new file mode 100644 index 000000000..b81ca2499 --- /dev/null +++ b/core/editor/src/document/document_message_handler.rs @@ -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), + DeleteLayer(Vec), + AddFolder(Vec), + RenameLayer(Vec, String), + ToggleLayerVisibility(Vec), + ToggleLayerExpansion(Vec), + SelectDocument(usize), + RenderDocument, + Undo, +} + +impl From for DocumentMessage { + fn from(operation: DocumentOperation) -> DocumentMessage { + Self::DispatchOperation(operation) + } +} +impl From for Message { + fn from(operation: DocumentOperation) -> Message { + DocumentMessage::DispatchOperation(operation).into() + } +} + +#[derive(Debug, Clone)] +pub struct DocumentMessageHandler { + documents: Vec, + 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) -> 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 for DocumentMessageHandler { + fn process_action(&mut self, message: DocumentMessage, _data: (), responses: &mut VecDeque) { + 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); +} diff --git a/core/editor/src/document/mod.rs b/core/editor/src/document/mod.rs new file mode 100644 index 000000000..5ded8ad5a --- /dev/null +++ b/core/editor/src/document/mod.rs @@ -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}; diff --git a/core/editor/src/frontend/frontend_message_handler.rs b/core/editor/src/frontend/frontend_message_handler.rs new file mode 100644 index 000000000..3964d611f --- /dev/null +++ b/core/editor/src/frontend/frontend_message_handler.rs @@ -0,0 +1,59 @@ +use crate::message_prelude::*; +use document_core::{response::LayerPanelEntry, DocumentResponse, LayerId}; +use serde::{Deserialize, Serialize}; + +pub type Callback = Box; + +#[impl_message(Message, Frontend)] +#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] +pub enum FrontendMessage { + CollapseFolder { path: Vec }, + ExpandFolder { path: Vec, children: Vec }, + SetActiveTool { tool_name: String }, + UpdateCanvas { document: String }, + EnableTextInput, + DisableTextInput, +} + +impl From for Message { + fn from(response: DocumentResponse) -> Self { + let frontend: FrontendMessage = response.into(); + frontend.into() + } +} +impl From 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 for FrontendMessageHandler { + fn process_action(&mut self, message: FrontendMessage, _data: (), _responses: &mut VecDeque) { + log::trace!("Sending {} Response", message.to_discriminant().global_name()); + (self.callback)(message) + } + advertise_actions!( + FrontendMessageDiscriminant; + + CollapseFolder, + ExpandFolder, + SetActiveTool, + UpdateCanvas, + EnableTextInput, + DisableTextInput, + ); +} diff --git a/core/editor/src/frontend/mod.rs b/core/editor/src/frontend/mod.rs new file mode 100644 index 000000000..15fcc7a00 --- /dev/null +++ b/core/editor/src/frontend/mod.rs @@ -0,0 +1,3 @@ +pub mod frontend_message_handler; + +pub use frontend_message_handler::{Callback, FrontendMessage, FrontendMessageDiscriminant, FrontendMessageHandler}; diff --git a/core/editor/src/global/global_message_handler.rs b/core/editor/src/global/global_message_handler.rs new file mode 100644 index 000000000..2cbc066ad --- /dev/null +++ b/core/editor/src/global/global_message_handler.rs @@ -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 for GlobalMessageHandler { + fn process_action(&mut self, message: GlobalMessage, _data: (), _responses: &mut VecDeque) { + 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); +} diff --git a/core/editor/src/global/mod.rs b/core/editor/src/global/mod.rs new file mode 100644 index 000000000..6ba0e00ce --- /dev/null +++ b/core/editor/src/global/mod.rs @@ -0,0 +1,3 @@ +pub mod global_message_handler; + +pub use global_message_handler::{GlobalMessage, GlobalMessageDiscriminant, GlobalMessageHandler}; diff --git a/core/editor/src/hint.rs b/core/editor/src/hint.rs deleted file mode 100644 index eb87eca90..000000000 --- a/core/editor/src/hint.rs +++ /dev/null @@ -1,5 +0,0 @@ -use std::collections::HashMap; - -pub trait Hint { - fn hints(&self) -> HashMap; -} diff --git a/core/editor/src/input/input_mapper.rs b/core/editor/src/input/input_mapper.rs new file mode 100644 index 000000000..035b6ef2d --- /dev/null +++ b/core/editor/src/input/input_mapper.rs @@ -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); + +impl KeyMappingEntries { + fn match_mapping(&self, keys: &KeyStates, actions: ActionList) -> Option { + 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 { + //[$()*] => {{ + [$($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 { + 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 for InputMapper { + fn process_action(&mut self, message: InputMapperMessage, data: (&InputPreprocessor, ActionList), responses: &mut VecDeque) { + let (input, actions) = data; + if let Some(message) = self.mapping.match_message(message, &input.keyboard, actions) { + responses.push_back(message); + } + } + advertise_actions!(); +} diff --git a/core/editor/src/input/input_preprocessor.rs b/core/editor/src/input/input_preprocessor.rs new file mode 100644 index 000000000..c5e345655 --- /dev/null +++ b/core/editor/src/input/input_preprocessor.rs @@ -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 for InputPreprocessor { + fn process_action(&mut self, message: InputPreprocessorMessage, _data: (), responses: &mut VecDeque) { + 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(), + } + } +} diff --git a/core/editor/src/input/keyboard.rs b/core/editor/src/input/keyboard.rs new file mode 100644 index 000000000..1011b2935 --- /dev/null +++ b/core/editor/src/input/keyboard.rs @@ -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::() as u32 * 8 + 2 - std::mem::size_of::().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; + +#[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([StorageType; LENGTH]); + +use std::{ + fmt::{Display, Formatter}, + ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign}, + usize, +}; + +impl BitVector { + #[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 Default for BitVector { + fn default() -> Self { + Self::new() + } +} + +impl Display for BitVector { + 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 $op for BitVector { + 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 $op for &BitVector { + type Output = BitVector; + fn $func(self, right: Self) -> Self::Output { + let mut result = BitVector::::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 $op for BitVector { + 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)); diff --git a/core/editor/src/input/mod.rs b/core/editor/src/input/mod.rs new file mode 100644 index 000000000..3e68eafc6 --- /dev/null +++ b/core/editor/src/input/mod.rs @@ -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}, +}; diff --git a/core/editor/src/input/mouse.rs b/core/editor/src/input/mouse.rs new file mode 100644 index 000000000..29db0af66 --- /dev/null +++ b/core/editor/src/input/mouse.rs @@ -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; + } +} diff --git a/core/editor/src/lib.rs b/core/editor/src/lib.rs index 477aec81f..8fbdf00b9 100644 --- a/core/editor/src/lib.rs +++ b/core/editor/src/lib.rs @@ -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>(&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; +} diff --git a/core/editor/src/macros.rs b/core/editor/src/macros.rs deleted file mode 100644 index 884cf3a45..000000000 --- a/core/editor/src/macros.rs +++ /dev/null @@ -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> = 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> = ::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) - ),* - } - }; -} diff --git a/core/editor/src/misc/derivable_custom_traits.rs b/core/editor/src/misc/derivable_custom_traits.rs new file mode 100644 index 000000000..7396f317f --- /dev/null +++ b/core/editor/src/misc/derivable_custom_traits.rs @@ -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; +} + +pub trait ToDiscriminant { + type Discriminant; + + fn to_discriminant(&self) -> Self::Discriminant; +} + +pub trait TransitiveChild: Into + Into { + type TopParent; + type Parent; +} diff --git a/core/editor/src/error.rs b/core/editor/src/misc/error.rs similarity index 86% rename from core/editor/src/error.rs rename to core/editor/src/misc/error.rs index acf55b628..df5e0a8fb 100644 --- a/core/editor/src/error.rs +++ b/core/editor/src/misc/error.rs @@ -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); diff --git a/core/editor/src/misc/macros.rs b/core/editor/src/misc/macros.rs new file mode 100644 index 000000000..34e2dd8b5 --- /dev/null +++ b/core/editor/src/misc/macros.rs @@ -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> = 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 $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),*) + } + } +} diff --git a/core/editor/src/misc/mod.rs b/core/editor/src/misc/mod.rs new file mode 100644 index 000000000..ad0b8560a --- /dev/null +++ b/core/editor/src/misc/mod.rs @@ -0,0 +1,7 @@ +#[macro_use] +pub mod macros; +pub mod derivable_custom_traits; +mod error; + +pub use error::EditorError; +pub use macros::*; diff --git a/core/editor/src/tools/mod.rs b/core/editor/src/tool/mod.rs similarity index 60% rename from core/editor/src/tools/mod.rs rename to core/editor/src/tool/mod.rs index a828d6efb..f334cf041 100644 --- a/core/editor/src/tools/mod.rs +++ b/core/editor/src/tool/mod.rs @@ -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, Vec); +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, operations: &mut Vec) -> Self; + + fn transition(self, message: ToolMessage, document: &SvgDocument, tool_data: &DocumentToolData, data: &mut Self::ToolData, input: &InputPreprocessor, messages: &mut VecDeque) -> 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>, tool_settings: HashMap, } -impl ToolData { - pub fn active_tool(&mut self) -> Result<&mut Box, EditorError> { - self.tools.get_mut(&self.active_tool_type).ok_or(EditorError::UnknownTool) +type SubToolMessageHandler = dyn for<'a> MessageHandler>; +pub struct ToolData { + pub active_tool_type: ToolType, + pub tools: HashMap>, +} + +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 { + 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 }, -} diff --git a/core/editor/src/tool/tool_message_handler.rs b/core/editor/src/tool/tool_message_handler.rs new file mode 100644 index 000000000..76339bae8 --- /dev/null +++ b/core/editor/src/tool/tool_message_handler.rs @@ -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 for ToolMessageHandler { + fn process_action(&mut self, message: ToolMessage, data: (&SvgDocument, &InputPreprocessor), responses: &mut VecDeque) { + 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 + } +} diff --git a/core/editor/src/tool/tool_settings.rs b/core/editor/src/tool/tool_settings.rs new file mode 100644 index 000000000..8233db3e3 --- /dev/null +++ b/core/editor/src/tool/tool_settings.rs @@ -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 }, +} diff --git a/core/editor/src/tool/tools/crop.rs b/core/editor/src/tool/tools/crop.rs new file mode 100644 index 000000000..94cb62384 --- /dev/null +++ b/core/editor/src/tool/tools/crop.rs @@ -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> for Crop { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses); + } + advertise_actions!(); +} diff --git a/core/editor/src/tool/tools/ellipse.rs b/core/editor/src/tool/tools/ellipse.rs new file mode 100644 index 000000000..dc16f0eb3 --- /dev/null +++ b/core/editor/src/tool/tools/ellipse.rs @@ -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> for Ellipse { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + 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) -> 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, + 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() +} diff --git a/core/editor/src/tool/tools/eyedropper.rs b/core/editor/src/tool/tools/eyedropper.rs new file mode 100644 index 000000000..59dbaa53e --- /dev/null +++ b/core/editor/src/tool/tools/eyedropper.rs @@ -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> for Eyedropper { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses); + } + advertise_actions!(); +} diff --git a/core/editor/src/tool/tools/line.rs b/core/editor/src/tool/tools/line.rs new file mode 100644 index 000000000..27d392645 --- /dev/null +++ b/core/editor/src/tool/tools/line.rs @@ -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> for Line { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + 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) -> 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, + 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() +} diff --git a/core/editor/src/tool/tools/mod.rs b/core/editor/src/tool/tools/mod.rs new file mode 100644 index 000000000..f57a31c18 --- /dev/null +++ b/core/editor/src/tool/tools/mod.rs @@ -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; diff --git a/core/editor/src/tool/tools/navigate.rs b/core/editor/src/tool/tools/navigate.rs new file mode 100644 index 000000000..48ce60b5e --- /dev/null +++ b/core/editor/src/tool/tools/navigate.rs @@ -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> for Navigate { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses); + } + advertise_actions!(); +} diff --git a/core/editor/src/tool/tools/path.rs b/core/editor/src/tool/tools/path.rs new file mode 100644 index 000000000..f60e62c2b --- /dev/null +++ b/core/editor/src/tool/tools/path.rs @@ -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> for Path { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses); + } + advertise_actions!(); +} diff --git a/core/editor/src/tool/tools/pen.rs b/core/editor/src/tool/tools/pen.rs new file mode 100644 index 000000000..eae68b8c9 --- /dev/null +++ b/core/editor/src/tool/tools/pen.rs @@ -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> for Pen { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + 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, + 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) -> 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() +} diff --git a/core/editor/src/tool/tools/rectangle.rs b/core/editor/src/tool/tools/rectangle.rs new file mode 100644 index 000000000..0b3c1aa3b --- /dev/null +++ b/core/editor/src/tool/tools/rectangle.rs @@ -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> for Rectangle { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + 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) -> 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, + 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() +} diff --git a/core/editor/src/tool/tools/select.rs b/core/editor/src/tool/tools/select.rs new file mode 100644 index 000000000..1df1166c7 --- /dev/null +++ b/core/editor/src/tool/tools/select.rs @@ -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> for Select { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + 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, + ) -> Self { + use SelectMessage::*; + use SelectToolFsmState::*; + if let ToolMessage::Select(event) = event { + match (self, event) { + (Ready, MouseMove) => self, + } + } else { + self + } + } +} diff --git a/core/editor/src/tool/tools/shape.rs b/core/editor/src/tool/tools/shape.rs new file mode 100644 index 000000000..3e929b490 --- /dev/null +++ b/core/editor/src/tool/tools/shape.rs @@ -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> for Shape { + fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { + 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) -> 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, + 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() +} diff --git a/core/editor/src/tools/crop.rs b/core/editor/src/tools/crop.rs deleted file mode 100644 index 2661e9299..000000000 --- a/core/editor/src/tools/crop.rs +++ /dev/null @@ -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, Vec) { - todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data) - } -} diff --git a/core/editor/src/tools/ellipse.rs b/core/editor/src/tools/ellipse.rs deleted file mode 100644 index d62277a1a..000000000 --- a/core/editor/src/tools/ellipse.rs +++ /dev/null @@ -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, Vec) { - 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, operations: &mut Vec) -> 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))), - } - } -} diff --git a/core/editor/src/tools/eyedropper.rs b/core/editor/src/tools/eyedropper.rs deleted file mode 100644 index bad3125bf..000000000 --- a/core/editor/src/tools/eyedropper.rs +++ /dev/null @@ -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, Vec) { - todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data) - } -} diff --git a/core/editor/src/tools/line.rs b/core/editor/src/tools/line.rs deleted file mode 100644 index 3c3a52411..000000000 --- a/core/editor/src/tools/line.rs +++ /dev/null @@ -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, Vec) { - 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, operations: &mut Vec) -> 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), - } -} diff --git a/core/editor/src/tools/navigate.rs b/core/editor/src/tools/navigate.rs deleted file mode 100644 index b2d88a70d..000000000 --- a/core/editor/src/tools/navigate.rs +++ /dev/null @@ -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, Vec) { - todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data) - } -} diff --git a/core/editor/src/tools/path.rs b/core/editor/src/tools/path.rs deleted file mode 100644 index 063697176..000000000 --- a/core/editor/src/tools/path.rs +++ /dev/null @@ -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, Vec) { - todo!("{}::handle_input {:?} {:?} {:?}", module_path!(), event, document, tool_data) - } -} diff --git a/core/editor/src/tools/pen.rs b/core/editor/src/tools/pen.rs deleted file mode 100644 index bcf687d11..000000000 --- a/core/editor/src/tools/pen.rs +++ /dev/null @@ -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, Vec) { - 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, - 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, operations: &mut Vec) -> 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())), - } -} diff --git a/core/editor/src/tools/rectangle.rs b/core/editor/src/tools/rectangle.rs deleted file mode 100644 index 351183cb5..000000000 --- a/core/editor/src/tools/rectangle.rs +++ /dev/null @@ -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, Vec) { - 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, operations: &mut Vec) -> 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))), - } -} diff --git a/core/editor/src/tools/select.rs b/core/editor/src/tools/select.rs deleted file mode 100644 index d45e859b1..000000000 --- a/core/editor/src/tools/select.rs +++ /dev/null @@ -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, Vec) { - 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, _operations: &mut Vec) -> 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, - } - } -} diff --git a/core/editor/src/tools/shape.rs b/core/editor/src/tools/shape.rs deleted file mode 100644 index 9dfda0d4f..000000000 --- a/core/editor/src/tools/shape.rs +++ /dev/null @@ -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, Vec) { - 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, operations: &mut Vec) -> 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))), - } -} diff --git a/core/editor/src/workspace/mod.rs b/core/editor/src/workspace/mod.rs deleted file mode 100644 index c792f232f..000000000 --- a/core/editor/src/workspace/mod.rs +++ /dev/null @@ -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, - 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, - pub active: PanelId, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum LayoutDirection { - Horizontal, - Vertical, -} diff --git a/core/proc-macro/Cargo.toml b/core/proc-macro/Cargo.toml index b5d6b3165..1de098f93 100644 --- a/core/proc-macro/Cargo.toml +++ b/core/proc-macro/Cargo.toml @@ -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] diff --git a/core/proc-macro/src/as_message.rs b/core/proc-macro/src/as_message.rs new file mode 100644 index 000000000..cad7ddbb7 --- /dev/null +++ b/core/proc-macro/src/as_message.rs @@ -0,0 +1,55 @@ +use proc_macro2::{Span, TokenStream}; +use syn::{Data, DeriveInput}; + +pub fn derive_as_message_impl(input_item: TokenStream) -> syn::Result { + let input = syn::parse2::(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) +} diff --git a/core/proc-macro/src/combined_message_attrs.rs b/core/proc-macro/src/combined_message_attrs.rs new file mode 100644 index 000000000..27d1da2a0 --- /dev/null +++ b/core/proc-macro/src/combined_message_attrs.rs @@ -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 { + 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 { + Ok(Self { + parent: input.parse()?, + _comma2: input.parse()?, + variant: input.parse()?, + }) + } +} + +pub fn combined_message_attrs_impl(attr: TokenStream, input_item: TokenStream) -> syn::Result { + if attr.is_empty() { + return top_level_impl(input_item); + } + + let mut input = syn::parse2::(input_item)?; + + let (parent_is_top, parent, variant) = match syn::parse2::(attr.clone()) { + Ok(x) => (false, x.parent, x.variant), + Err(_) => { + let x = syn::parse2::(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 { + let mut input = syn::parse2::(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; + } + }) +} diff --git a/core/proc-macro/src/discriminant.rs b/core/proc-macro/src/discriminant.rs new file mode 100644 index 000000000..655010ec9 --- /dev/null +++ b/core/proc-macro/src/discriminant.rs @@ -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 { + let input = syn::parse2::(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::(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::(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::>(); + + 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::>(); + + 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) +} diff --git a/core/proc-macro/src/structs.rs b/core/proc-macro/src/helper_structs.rs similarity index 70% rename from core/proc-macro/src/structs.rs rename to core/proc-macro/src/helper_structs.rs index c87e3cf98..ecec648cd 100644 --- a/core/proc-macro/src/structs.rs +++ b/core/proc-macro/src/helper_structs.rs @@ -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, +} + +impl Parse for IdentList { + fn parse(input: ParseStream) -> syn::Result { + 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 { + pub paren_token: Paren, + pub first: F, + pub sep: Token![,], + pub second: S, +} + +impl Parse for Pair +where + F: Parse, + S: Parse, +{ + fn parse(input: ParseStream) -> syn::Result { + 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 { + 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(pub Vec); + +impl Parse for SimpleCommaDelimeted { + fn parse(input: ParseStream) -> syn::Result { + let punct = Punctuated::::parse_terminated(input)?; + Ok(Self(punct.into_iter().collect())) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/proc-macro/src/helpers.rs b/core/proc-macro/src/helpers.rs index d64d3358d..5f0fe1b5e 100644 --- a/core/proc-macro/src/helpers.rs +++ b/core/proc-macro/src/helpers.rs @@ -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(iter: impl Iterator>) -> syn::Re }) } +/// Creates an ident at the call site +pub fn call_site_ident>(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 = 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"); } } diff --git a/core/proc-macro/src/hint.rs b/core/proc-macro/src/hint.rs new file mode 100644 index 000000000..fd52b4f45 --- /dev/null +++ b/core/proc-macro/src/hint.rs @@ -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, Vec)> { + fold_error_iter( + attrs + .iter() + .filter(|a| a.path.get_ident().map_or(false, |i| i == "hint")) + .map(|attr| syn::parse2::(attr.tokens.clone())), + ) + .and_then(|v: Vec| { + 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 { + let input = syn::parse2::(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::>(); + + 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, Vec)>| { + let (keys, values): (Vec>, Vec>) = hints.into_iter().unzip(); + let cap: Vec = keys.iter().map(|v| v.len()).collect(); + + quote::quote! { + impl Hint for #ident { + fn hints(&self) -> ::std::collections::HashMap { + 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 { + let mut hm = ::std::collections::HashMap::with_capacity(#cap); + #( + hm.insert(#keys.to_string(), #values.to_string()); + )* + hm + } + } + } + }) + } + } +} diff --git a/core/proc-macro/src/lib.rs b/core/proc-macro/src/lib.rs index fd2de8d9b..e8068aee8 100644 --- a/core/proc-macro/src/lib.rs +++ b/core/proc-macro/src/lib.rs @@ -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, Vec)> { - fold_error_iter( - attrs - .iter() - .filter(|a| a.path.get_ident().map_or(false, |i| i == "hint")) - .map(|attr| syn::parse2::(attr.tokens.clone())), - ) - .and_then(|v: Vec| { - 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 `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 { - let input = syn::parse2::(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(, )]` (**required**): declare the parent type (``) +/// and a function (``, 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::>(); - - 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, Vec)>| { - let (keys, values): (Vec>, Vec>) = hints.into_iter().unzip(); - let cap: Vec = keys.iter().map(|v| v.len()).collect(); - - quote::quote! { - impl Hint for #ident { - fn hints(&self) -> ::std::collections::HashMap { - 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 { - 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(, )]`, +/// where `` is the parent message type and `` 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(, , )]`, where the first `` is the top parent message type, the secont `` is the parent message type +/// and `` 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 { /// # 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 diff --git a/core/proc-macro/src/transitive_child.rs b/core/proc-macro/src/transitive_child.rs new file mode 100644 index 000000000..6f4ac71a6 --- /dev/null +++ b/core/proc-macro/src/transitive_child.rs @@ -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 { + let input = syn::parse2::(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::>(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 } + }) +}