diff --git a/client/web/.eslintrc.js b/client/web/.eslintrc.js index 879500821..04749764d 100644 --- a/client/web/.eslintrc.js +++ b/client/web/.eslintrc.js @@ -21,7 +21,7 @@ module.exports = { }, }, rules: { - indent: ["error", "tab"], + indent: ["error", "tab", { SwitchCase: 1 }], quotes: ["error", "double"], "linebreak-style": ["error", "unix"], "eol-last": ["error", "always"], @@ -29,7 +29,7 @@ module.exports = { "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", "max-len": ["error", { code: 200, tabWidth: 4 }], "@typescript-eslint/camelcase": "off", - camelcase: ["error", { ignoreImports: true, ignoreDestructuring: true }], + camelcase: ["error", { allow: ["^(?:[a-z]+_)*[a-z]+$"] }], "prettier-vue/prettier": [ "error", { diff --git a/client/web/src/components/panels/Document.vue b/client/web/src/components/panels/Document.vue index 4c9887c71..31fee8210 100644 --- a/client/web/src/components/panels/Document.vue +++ b/client/web/src/components/panels/Document.vue @@ -180,7 +180,7 @@ diff --git a/client/web/src/response-handler.ts b/client/web/src/response-handler.ts index 45be02395..b9e6c2465 100644 --- a/client/web/src/response-handler.ts +++ b/client/web/src/response-handler.ts @@ -1,4 +1,4 @@ -type ResponseCallback = (responseData: string) => void; +type ResponseCallback = (responseData: Response) => void; type ResponseMap = { [response: string]: ResponseCallback | undefined; }; @@ -9,10 +9,10 @@ declare global { } export enum ResponseType { - "Tool::UpdateCanvas" = "Tool::UpdateCanvas", - "Document::ExpandFolder" = "Document::ExpandFolder", - "Document::CollapseFolder" = "Document::CollapseFolder", - "Tool::SetActiveTool" = "Tool::SetActiveTool", + UpdateCanvas = "UpdateCanvas", + ExpandFolder = "ExpandFolder", + CollapseFolder = "CollapseFolder", + SetActiveTool = "SetActiveTool", } export function attachResponseHandlerToPage() { @@ -24,12 +24,82 @@ export function registerResponseHandler(responseType: ResponseType, callback: Re } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function handleResponse(responseType: ResponseType, responseData: any) { - const callback = window.responseMap[responseType]; +function parseResponse(origin: string, responseType: string, data: any): Response { + type OriginNames = "Document" | "Tool"; - if (callback) { - callback(responseData); + const originHandlers = { + Document: () => { + switch (responseType) { + case "DocumentChanged": + return (data.Document.DocumentChanged as DocumentChanged) as Response; + case "CollapseFolder": + return (data.Document.CollapseFolder as CollapseFolder) as Response; + case "ExpandFolder": + return (data.Document.ExpandFolder as ExpandFolder) as Response; + default: + return undefined; + } + }, + Tool: () => { + switch (responseType) { + case "SetActiveTool": + return (data.Tool.SetActiveTool as SetActiveTool) as Response; + case "UpdateCanvas": + return (data.Tool.UpdateCanvas as UpdateCanvas) as Response; + default: + return undefined; + } + }, + }; + + // TODO: Optional chaining would be nice here when we can upgrade to Webpack 5: https://github.com/webpack/webpack/issues/10227 + // const response = originHandlers[origin as OriginNames]?.(); + const response = originHandlers[origin as OriginNames] && originHandlers[origin as OriginNames](); + if (!response) throw new Error("ResponseType not recognized."); + return response; +} + +// 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); + + 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.`); } else { - console.error(`Received a Response of type "${responseType}" but no handler was registered for it from the client.`); + console.error(`Received a Response of type "${responseIdentifier}" but but was not able to parse the data.`); } } + +export type Response = SetActiveTool | UpdateCanvas | DocumentChanged | CollapseFolder | ExpandFolder; + +export interface SetActiveTool { + tool_name: string; +} +export interface UpdateCanvas { + document: string; +} +export type DocumentChanged = {}; +export interface CollapseFolder { + path: Array; +} +export interface ExpandFolder { + path: Array; + children: Array; +} + +export interface LayerPanelEntry { + name: string; + visible: boolean; + layer_type: LayerType; + collapsed: boolean; + path: Array; +} + +export enum LayerType { + Folder, + Shape, +} diff --git a/client/web/wasm/src/document.rs b/client/web/wasm/src/document.rs index 58c08985a..f0187490b 100644 --- a/client/web/wasm/src/document.rs +++ b/client/web/wasm/src/document.rs @@ -1,7 +1,7 @@ use crate::shims::Error; use crate::wrappers::{translate_key, translate_tool, Color}; use crate::EDITOR_STATE; -use editor_core::events; +use editor_core::{events, LayerId}; use wasm_bindgen::prelude::*; fn convert_error(err: editor_core::EditorError) -> JsValue { @@ -32,7 +32,14 @@ mod mouse_state { (false, 1) => Event::LmbUp(state), (false, 2) => Event::RmbUp(state), (false, 4) => Event::MmbUp(state), - _ => panic!("two buttons where modified at the same time. modification: {:#010b}", diff), + (down, _) => { + log::warn!("two buttons where modified at the same time. Modification: {:#010b}", diff); + if down { + Event::AmbiguousMouseDown(state) + } else { + Event::AmbiguousMouseUp(state) + } + } } } } @@ -118,3 +125,45 @@ pub fn swap_colors() -> Result<(), JsValue> { pub fn reset_colors() -> Result<(), JsValue> { EDITOR_STATE.with(|editor| editor.borrow_mut().handle_event(events::Event::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) +} + +/// 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))) + .map_err(convert_error) +} + +/// Toggle expansions state of a layer from the layer list +#[wasm_bindgen] +pub fn toggle_layer_expansion(path: Vec) -> Result<(), JsValue> { + EDITOR_STATE + .with(|editor| editor.borrow_mut().handle_event(events::Event::ToggleLayerExpansion(path))) + .map_err(convert_error) +} + +/// Renames a layer from the layer list +#[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))) + .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) +} + +/// 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) +} diff --git a/core/document/src/document.rs b/core/document/src/document.rs index 7e5799f46..4a128d26f 100644 --- a/core/document/src/document.rs +++ b/core/document/src/document.rs @@ -1,6 +1,6 @@ use crate::{ layers::{self, Folder, Layer, LayerData, LayerDataTypes, Line, PolyLine, Rect, Shape}, - response::{LayerPanelEntry, LayerType}, + response::LayerPanelEntry, DocumentError, DocumentResponse, LayerId, Operation, }; @@ -172,16 +172,12 @@ impl Document { /// any actual data, but rather metadata such as visibility and names of the layers. pub fn layer_panel(&self, path: &[LayerId]) -> Result, DocumentError> { let folder = self.document_folder(path)?; - let l_type = |layer: &LayerDataTypes| match layer { - LayerDataTypes::Folder(_) => LayerType::Folder, - _ => LayerType::Shape, - }; - let translate = |layer: &Layer| LayerPanelEntry { - name: layer.name.clone().unwrap_or_else(|| String::from("UnnamedFolder")), - visible: layer.visible, - layer_type: l_type(&layer.data), - }; - let entries = folder.layers().iter().map(|layer| translate(layer)).collect(); + let entries = folder + .layers() + .iter() + .zip(folder.layer_ids.iter()) + .map(|(layer, id)| LayerPanelEntry::from_layer(layer, [path, &[*id]].concat())) + .collect(); Ok(entries) } diff --git a/core/document/src/layers/folder.rs b/core/document/src/layers/folder.rs index 71896e2ca..e47978afc 100644 --- a/core/document/src/layers/folder.rs +++ b/core/document/src/layers/folder.rs @@ -9,6 +9,7 @@ pub struct Folder { next_assignment_id: LayerId, pub layer_ids: Vec, layers: Vec, + pub collapsed: bool, } impl LayerData for Folder { @@ -87,6 +88,7 @@ impl Default for Folder { layer_ids: vec![], layers: vec![], next_assignment_id: 0, + collapsed: false, } } } diff --git a/core/document/src/response.rs b/core/document/src/response.rs index 2aa0a5917..31d85eba5 100644 --- a/core/document/src/response.rs +++ b/core/document/src/response.rs @@ -1,4 +1,7 @@ -use crate::LayerId; +use crate::{ + layers::{Layer, LayerDataTypes}, + LayerId, +}; use serde::{Deserialize, Serialize}; use std::fmt; @@ -7,12 +10,19 @@ pub struct LayerPanelEntry { pub name: String, pub visible: bool, pub layer_type: LayerType, + pub collapsed: bool, + pub path: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum LayerType { Folder, Shape, + Circle, + Rect, + Line, + PolyLine, + Ellipse, } impl fmt::Display for LayerType { @@ -20,12 +30,47 @@ impl fmt::Display for LayerType { let name = match self { LayerType::Folder => "folder", LayerType::Shape => "shape", + LayerType::Rect => "rect", + LayerType::Line => "line", + LayerType::Circle => "circle", + LayerType::PolyLine => "poly line", + LayerType::Ellipse => "ellipse", }; formatter.write_str(name) } } +impl From<&LayerDataTypes> for LayerType { + fn from(data: &LayerDataTypes) -> Self { + use LayerDataTypes::*; + match data { + Folder(_) => LayerType::Folder, + Shape(_) => LayerType::Shape, + Circle(_) => LayerType::Circle, + Rect(_) => LayerType::Rect, + Line(_) => LayerType::Line, + PolyLine(_) => LayerType::PolyLine, + Ellipse(_) => LayerType::Ellipse, + } + } +} + +impl LayerPanelEntry { + pub fn from_layer(layer: &Layer, path: Vec) -> Self { + let layer_type: LayerType = (&layer.data).into(); + let name = layer.name.clone().unwrap_or_else(|| format!("Unnamed {}", layer_type)); + let collapsed = if let LayerDataTypes::Folder(f) = &layer.data { f.collapsed } else { true }; + Self { + name, + visible: layer.visible, + layer_type, + collapsed, + path, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[repr(C)] // TODO - Make Copy when possible diff --git a/core/document/src/shape_points.rs b/core/document/src/shape_points.rs index 3ff34122f..21258394b 100644 --- a/core/document/src/shape_points.rs +++ b/core/document/src/shape_points.rs @@ -1,7 +1,6 @@ use std::{fmt, ops::Add}; use kurbo::{PathEl, Point, Vec2}; -use log::info; #[derive(Debug, Clone, Copy, PartialEq)] pub struct ShapePoints { @@ -47,7 +46,6 @@ impl std::fmt::Display for ShapePoints { let sine = theta.sin(); Vec2::new(v.x * cosine - v.y * sine, v.x * sine + v.y * cosine) } - info!("sides{}", self.sides); for i in 0..self.sides { let radians = self.apothem_offset_angle() * ((i * 2 + (self.sides % 2)) as f64); let offset = rotate(&self.extent, radians); diff --git a/core/editor/src/dispatcher/document_event_handler.rs b/core/editor/src/dispatcher/document_event_handler.rs new file mode 100644 index 000000000..c2504096a --- /dev/null +++ b/core/editor/src/dispatcher/document_event_handler.rs @@ -0,0 +1,9 @@ +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 index e6428e503..8fde796f4 100644 --- a/core/editor/src/dispatcher/events.rs +++ b/core/editor/src/dispatcher/events.rs @@ -2,6 +2,7 @@ use crate::tools::ToolType; use crate::Color; use bitflags::bitflags; +use document_core::LayerId; use serde::{Deserialize, Serialize}; #[doc(inline)] @@ -18,8 +19,16 @@ 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), @@ -34,6 +43,7 @@ pub enum Event { #[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 }, } diff --git a/core/editor/src/dispatcher/global_event_handler.rs b/core/editor/src/dispatcher/global_event_handler.rs new file mode 100644 index 000000000..ec7144725 --- /dev/null +++ b/core/editor/src/dispatcher/global_event_handler.rs @@ -0,0 +1,15 @@ +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 index d275e4570..84cced139 100644 --- a/core/editor/src/dispatcher/mod.rs +++ b/core/editor/src/dispatcher/mod.rs @@ -90,6 +90,7 @@ impl Dispatcher { _ => (), } } + _ => todo!("Implement layer handling"), } let (mut tool_responses, operations) = editor_state diff --git a/core/editor/src/workspace/mod.rs b/core/editor/src/workspace/mod.rs index eedb8a832..c792f232f 100644 --- a/core/editor/src/workspace/mod.rs +++ b/core/editor/src/workspace/mod.rs @@ -1,5 +1,8 @@ -pub type PanelId = usize; +use serde::{Deserialize, Serialize}; +pub type PanelId = u32; + +#[derive(Debug, Default, Serialize, Deserialize)] pub struct Workspace { pub hovered_panel: PanelId, pub root: PanelGroup, @@ -15,14 +18,20 @@ impl Workspace { // add panel / panel group // delete panel / panel group // move panel / panel group - // get_serialized_layout() } +#[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 { @@ -32,16 +41,19 @@ impl PanelGroup { } } +#[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,