From 96d3ef2650058865c9747f6bd6d106e82c80fc2c Mon Sep 17 00:00:00 2001 From: mfish33 <32677537+mfish33@users.noreply.github.com> Date: Sun, 30 Jan 2022 17:53:37 -0800 Subject: [PATCH] Layout system implementation and applied to tool options bar (#499) * initial layout system with tool options * cargo fmt * cargo fmt again * document bar defined on the backend * cargo fmt * removed RC * cargo fmt * - fix increment behavior - removed hashmap from layout message handler - removed no op message from layoutMessage * cargo fmt * only send documentBar when zoom or rotation is updated * ctrl-0 changes zoom properly * Code review changes Co-authored-by: Keavon Chambers --- Cargo.lock | 12 + editor/Cargo.toml | 1 + editor/src/communication/dispatcher.rs | 3 + editor/src/communication/message.rs | 2 + .../src/document/document_message_handler.rs | 154 +++++++++- .../src/document/movement_message_handler.rs | 3 + editor/src/document/portfolio_message.rs | 1 + .../src/document/portfolio_message_handler.rs | 7 + editor/src/frontend/frontend_message.rs | 8 +- editor/src/layout/layout_message.rs | 25 ++ editor/src/layout/layout_message_handler.rs | 88 ++++++ editor/src/layout/mod.rs | 5 + editor/src/layout/widgets.rs | 283 ++++++++++++++++++ editor/src/lib.rs | 2 + editor/src/misc/macros.rs | 2 +- editor/src/viewport_tools/mod.rs | 1 - editor/src/viewport_tools/tool.rs | 77 +---- editor/src/viewport_tools/tool_message.rs | 5 - .../viewport_tools/tool_message_handler.rs | 14 +- editor/src/viewport_tools/tool_options.rs | 40 --- editor/src/viewport_tools/tools/crop.rs | 3 + editor/src/viewport_tools/tools/ellipse.rs | 7 +- editor/src/viewport_tools/tools/eyedropper.rs | 7 +- editor/src/viewport_tools/tools/fill.rs | 7 +- editor/src/viewport_tools/tools/freehand.rs | 55 +++- editor/src/viewport_tools/tools/line.rs | 55 +++- editor/src/viewport_tools/tools/navigate.rs | 7 +- editor/src/viewport_tools/tools/path.rs | 7 +- editor/src/viewport_tools/tools/pen.rs | 55 +++- editor/src/viewport_tools/tools/rectangle.rs | 7 +- editor/src/viewport_tools/tools/select.rs | 178 ++++++++++- editor/src/viewport_tools/tools/shape.rs | 57 +++- editor/src/viewport_tools/tools/text.rs | 55 +++- frontend/src/components/panels/Document.vue | 108 ++----- .../src/components/widgets/WidgetLayout.vue | 50 ++++ frontend/src/components/widgets/WidgetRow.vue | 66 ++++ .../src/components/widgets/WidgetSection.vue | 56 ++++ .../components/widgets/inputs/NumberInput.vue | 10 +- .../widgets/options/ToolOptions.vue | 188 ------------ frontend/src/dispatcher/js-messages.ts | 86 +++++- frontend/src/state/dialog.ts | 3 +- frontend/src/utilities/widgets.ts | 4 - frontend/wasm/src/api.rs | 75 +---- frontend/wasm/src/type_translators.rs | 10 - 44 files changed, 1357 insertions(+), 532 deletions(-) create mode 100644 editor/src/layout/layout_message.rs create mode 100644 editor/src/layout/layout_message_handler.rs create mode 100644 editor/src/layout/mod.rs create mode 100644 editor/src/layout/widgets.rs delete mode 100644 editor/src/viewport_tools/tool_options.rs create mode 100644 frontend/src/components/widgets/WidgetLayout.vue create mode 100644 frontend/src/components/widgets/WidgetRow.vue create mode 100644 frontend/src/components/widgets/WidgetSection.vue delete mode 100644 frontend/src/components/widgets/options/ToolOptions.vue diff --git a/Cargo.lock b/Cargo.lock index 895343d81..d23ffa490 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,6 +62,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -95,6 +106,7 @@ name = "graphite-editor" version = "0.0.0" dependencies = [ "bitflags", + "derivative", "env_logger", "glam", "graphite-graphene", diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 17097fdba..02ccfd8e5 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -24,6 +24,7 @@ kurbo = { git = "https://github.com/linebender/kurbo.git", features = [ "serde", ] } remain = "0.2.2" +derivative = "2.2.0" [dependencies.graphene] path = "../graphene" diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 1bd55b630..9a7b86017 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -1,6 +1,7 @@ use crate::document::PortfolioMessageHandler; use crate::global::GlobalMessageHandler; use crate::input::{InputMapperMessageHandler, InputPreprocessorMessageHandler}; +use crate::layout::layout_message_handler::LayoutMessageHandler; use crate::message_prelude::*; use crate::viewport_tools::tool_message_handler::ToolMessageHandler; @@ -19,6 +20,7 @@ struct DispatcherMessageHandlers { global_message_handler: GlobalMessageHandler, input_mapper_message_handler: InputMapperMessageHandler, input_preprocessor_message_handler: InputPreprocessorMessageHandler, + layout_message_handler: LayoutMessageHandler, portfolio_message_handler: PortfolioMessageHandler, tool_message_handler: ToolMessageHandler, } @@ -76,6 +78,7 @@ impl Dispatcher { InputPreprocessor(message) => { self.message_handlers.input_preprocessor_message_handler.process_action(message, (), &mut self.message_queue); } + Layout(message) => self.message_handlers.layout_message_handler.process_action(message, (), &mut self.message_queue), Portfolio(message) => { self.message_handlers .portfolio_message_handler diff --git a/editor/src/communication/message.rs b/editor/src/communication/message.rs index 30c426c0c..87b93318d 100644 --- a/editor/src/communication/message.rs +++ b/editor/src/communication/message.rs @@ -31,6 +31,8 @@ pub enum Message { #[child] InputPreprocessor(InputPreprocessorMessage), #[child] + Layout(LayoutMessage), + #[child] Portfolio(PortfolioMessage), #[child] Tool(ToolMessage), diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index 3cf5feab5..d57760343 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -7,6 +7,10 @@ use crate::consts::{ ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_EXPORT_SUFFIX, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR, }; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{ + IconButton, LayoutRow, NumberInput, NumberInputIncrementBehavior, OptionalInput, PopoverButton, PropertyHolder, RadioEntryData, RadioInput, Separator, SeparatorDirection, SeparatorType, Widget, + WidgetCallback, WidgetHolder, WidgetLayout, +}; use crate::message_prelude::*; use crate::EditorError; @@ -459,6 +463,154 @@ impl DocumentMessageHandler { } } +impl PropertyHolder for DocumentMessageHandler { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::OptionalInput(OptionalInput { + checked: self.snapping_enabled, + icon: "Snapping".into(), + tooltip: "Snapping".into(), + on_update: WidgetCallback::new(|updated_optional_input| DocumentMessage::SetSnapping { snap: updated_optional_input.checked }.into()), + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Snapping".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::OptionalInput(OptionalInput { + checked: true, + icon: "Grid".into(), + tooltip: "Grid".into(), + on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(318) }.into()), + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Grid".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::OptionalInput(OptionalInput { + checked: self.overlays_visible, + icon: "Overlays".into(), + tooltip: "Overlays".into(), + on_update: WidgetCallback::new(|updated_optional_input| { + DocumentMessage::SetOverlaysVisibility { + visible: updated_optional_input.checked, + } + .into() + }), + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Overlays".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Unrelated, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::RadioInput(RadioInput { + selected_index: if self.view_mode == ViewMode::Normal { 0 } else { 1 }, + entries: vec![ + RadioEntryData { + value: "normal".into(), + icon: "ViewModeNormal".into(), + tooltip: "View Mode: Normal".into(), + on_update: WidgetCallback::new(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Normal }.into()), + ..RadioEntryData::default() + }, + RadioEntryData { + value: "outline".into(), + icon: "ViewModeOutline".into(), + tooltip: "View Mode: Outline".into(), + on_update: WidgetCallback::new(|_| DocumentMessage::SetViewMode { view_mode: ViewMode::Outline }.into()), + ..RadioEntryData::default() + }, + RadioEntryData { + value: "pixels".into(), + icon: "ViewModePixels".into(), + tooltip: "View Mode: Pixels".into(), + on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(320) }.into()), + ..RadioEntryData::default() + }, + ], + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "View Mode".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Section, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + unit: "°".into(), + value: self.movement_handler.tilt / (std::f64::consts::PI / 180.), + increment_factor: 15., + on_update: WidgetCallback::new(|number_input| { + MovementMessage::SetCanvasRotation { + angle_radians: number_input.value * (std::f64::consts::PI / 180.), + } + .into() + }), + ..NumberInput::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Section, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "ZoomIn".into(), + tooltip: "Zoom In".into(), + on_update: WidgetCallback::new(|_| MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "ZoomOut".into(), + tooltip: "Zoom Out".into(), + on_update: WidgetCallback::new(|_| MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + size: 24, + icon: "ZoomReset".into(), + tooltip: "Zoom to 100%".into(), + on_update: WidgetCallback::new(|_| MovementMessage::SetCanvasZoom { zoom_factor: 1. }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + separator_type: SeparatorType::Related, + direction: SeparatorDirection::Horizontal, + })), + WidgetHolder::new(Widget::NumberInput(NumberInput { + unit: "%".into(), + value: self.movement_handler.zoom * 100., + min: Some(0.000001), + max: Some(1000000.), + on_update: WidgetCallback::new(|number_input| { + MovementMessage::SetCanvasZoom { + zoom_factor: number_input.value / 100., + } + .into() + }), + increment_behavior: NumberInputIncrementBehavior::Callback, + increment_callback_decrease: WidgetCallback::new(|_| MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }.into()), + increment_callback_increase: WidgetCallback::new(|_| MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }.into()), + ..NumberInput::default() + })), + ], + }]) + } +} + impl MessageHandler for DocumentMessageHandler { #[remain::check] fn process_action(&mut self, message: DocumentMessage, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { @@ -674,7 +826,7 @@ impl MessageHandler for Docum responses.extend([LayerChanged { affected_layer_path }.into(), DocumentStructureChanged.into()]); } GroupSelectedLayers => { - let mut new_folder_path: Vec = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec(); + let mut new_folder_path = self.graphene_document.shallowest_common_folder(self.selected_layers()).unwrap_or(&[]).to_vec(); // Required for grouping parent folders with their own children if !new_folder_path.is_empty() && self.selected_layers_contains(&new_folder_path) { diff --git a/editor/src/document/movement_message_handler.rs b/editor/src/document/movement_message_handler.rs index 97c81d056..1c71f1d9d 100644 --- a/editor/src/document/movement_message_handler.rs +++ b/editor/src/document/movement_message_handler.rs @@ -156,6 +156,7 @@ impl MessageHandler { @@ -245,12 +246,14 @@ impl MessageHandler { self.zoom = zoom_factor.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); responses.push_back(FrontendMessage::UpdateCanvasZoom { factor: self.snapped_scale() }.into()); responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); + responses.push_back(PortfolioMessage::UpdateDocumentBar.into()); self.create_document_transform(&ipp.viewport_bounds, responses); } TransformCanvasEnd => { diff --git a/editor/src/document/portfolio_message.rs b/editor/src/document/portfolio_message.rs index 46b01ed42..6a988d35b 100644 --- a/editor/src/document/portfolio_message.rs +++ b/editor/src/document/portfolio_message.rs @@ -60,5 +60,6 @@ pub enum PortfolioMessage { SelectDocument { document_id: u64, }, + UpdateDocumentBar, UpdateOpenDocumentsList, } diff --git a/editor/src/document/portfolio_message_handler.rs b/editor/src/document/portfolio_message_handler.rs index 38ad14bbb..092de83cc 100644 --- a/editor/src/document/portfolio_message_handler.rs +++ b/editor/src/document/portfolio_message_handler.rs @@ -3,6 +3,8 @@ use super::DocumentMessageHandler; use crate::consts::{DEFAULT_DOCUMENT_NAME, GRAPHITE_DOCUMENT_VERSION}; use crate::frontend::utility_types::FrontendDocumentDetails; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::layout_message::LayoutTarget; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use graphene::Operation as DocumentOperation; @@ -369,6 +371,11 @@ impl MessageHandler for Port responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into()); } responses.push_back(ToolMessage::DocumentIsDirty.into()); + responses.push_back(PortfolioMessage::UpdateDocumentBar.into()); + } + UpdateDocumentBar => { + let active_document = self.active_document(); + active_document.register_properties(responses, LayoutTarget::DocumentBar) } UpdateOpenDocumentsList => { // Send the list of document tab names diff --git a/editor/src/frontend/frontend_message.rs b/editor/src/frontend/frontend_message.rs index 5e9969330..7b986d5a6 100644 --- a/editor/src/frontend/frontend_message.rs +++ b/editor/src/frontend/frontend_message.rs @@ -1,8 +1,9 @@ use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon}; use crate::document::layer_panel::{LayerPanelEntry, RawBuffer}; +use crate::layout::layout_message::LayoutTarget; +use crate::layout::widgets::SubLayout; use crate::message_prelude::*; use crate::misc::HintData; -use crate::viewport_tools::tool_options::ToolOptions; use crate::Color; use serde::{Deserialize, Serialize}; @@ -15,6 +16,7 @@ pub enum FrontendMessage { DisplayConfirmationToCloseAllDocuments, DisplayConfirmationToCloseDocument { document_id: u64 }, DisplayDialogAboutGraphite, + DisplayDialogComingSoon { issue: Option }, DisplayDialogError { title: String, description: String }, DisplayDialogPanic { panic_info: String, title: String, description: String }, DisplayDocumentLayerTreeStructure { data_buffer: RawBuffer }, @@ -30,11 +32,12 @@ pub enum FrontendMessage { // Update prefix: give the frontend a new value or state for it to use UpdateActiveDocument { document_id: u64 }, - UpdateActiveTool { tool_name: String, tool_options: Option }, + UpdateActiveTool { tool_name: String }, UpdateCanvasRotation { angle_radians: f64 }, UpdateCanvasZoom { factor: f64 }, UpdateDocumentArtboards { svg: String }, UpdateDocumentArtwork { svg: String }, + UpdateDocumentBarLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateDocumentLayer { data: LayerPanelEntry }, UpdateDocumentOverlays { svg: String }, UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 }, @@ -42,5 +45,6 @@ pub enum FrontendMessage { UpdateInputHints { hint_data: HintData }, UpdateMouseCursor { cursor: MouseCursorIcon }, UpdateOpenDocumentsList { open_documents: Vec }, + UpdateToolOptionsLayout { layout_target: LayoutTarget, layout: SubLayout }, UpdateWorkingColors { primary: Color, secondary: Color }, } diff --git a/editor/src/layout/layout_message.rs b/editor/src/layout/layout_message.rs new file mode 100644 index 000000000..400a5f369 --- /dev/null +++ b/editor/src/layout/layout_message.rs @@ -0,0 +1,25 @@ +use super::widgets::WidgetLayout; +use crate::message_prelude::*; + +use serde::{Deserialize, Serialize}; + +#[remain::sorted] +#[impl_message(Message, Layout)] +#[derive(PartialEq, Clone, Deserialize, Serialize, Debug)] +pub enum LayoutMessage { + SendLayout { layout: WidgetLayout, layout_target: LayoutTarget }, + UpdateLayout { layout_target: LayoutTarget, widget_id: u64, value: serde_json::Value }, +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Deserialize, Serialize, Debug, Hash, Eq, Copy)] +#[repr(u8)] +pub enum LayoutTarget { + DocumentBar, + ToolOptions, + + // KEEP THIS ENUM LAST + // This is a marker that is used to define an array that is used to hold widgets + #[remain::unsorted] + LayoutTargetLength, +} diff --git a/editor/src/layout/layout_message_handler.rs b/editor/src/layout/layout_message_handler.rs new file mode 100644 index 000000000..7bdd72ebc --- /dev/null +++ b/editor/src/layout/layout_message_handler.rs @@ -0,0 +1,88 @@ +use super::layout_message::LayoutTarget; +use super::widgets::WidgetLayout; +use crate::layout::widgets::Widget; +use crate::message_prelude::*; + +use serde_json::Value; +use std::collections::VecDeque; + +#[derive(Debug, Clone, Default)] +pub struct LayoutMessageHandler { + layouts: [WidgetLayout; LayoutTarget::LayoutTargetLength as usize], +} + +impl LayoutMessageHandler { + fn send_layout(&self, layout_target: LayoutTarget, responses: &mut VecDeque) { + let widget_layout = &self.layouts[layout_target as usize]; + let message = match layout_target { + LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { + layout_target, + layout: widget_layout.layout.clone(), + }, + LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { + layout_target, + layout: widget_layout.layout.clone(), + }, + LayoutTarget::LayoutTargetLength => panic!("`LayoutTargetLength` is not a valid Layout Target and is used for array indexing"), + }; + responses.push_back(message.into()); + } +} + +impl MessageHandler for LayoutMessageHandler { + fn process_action(&mut self, action: LayoutMessage, _data: (), responses: &mut std::collections::VecDeque) { + use LayoutMessage::*; + match action { + SendLayout { layout, layout_target } => { + self.layouts[layout_target as usize] = layout; + + self.send_layout(layout_target, responses); + } + UpdateLayout { layout_target, widget_id, value } => { + let layout = &mut self.layouts[layout_target as usize]; + let widget_holder = layout.iter_mut().find(|widget| widget.widget_id == widget_id).expect("Received invalid widget_id from the frontend"); + match &mut widget_holder.widget { + Widget::NumberInput(number_input) => match value { + Value::Number(num) => { + let update_value = num.as_f64().unwrap(); + number_input.value = update_value; + let callback_message = (number_input.on_update.callback)(number_input); + responses.push_back(callback_message); + } + Value::String(str) => match str.as_str() { + "Increment" => responses.push_back((number_input.increment_callback_increase.callback)(number_input)), + "Decrement" => responses.push_back((number_input.increment_callback_decrease.callback)(number_input)), + _ => { + panic!("Invalid string found when updating `NumberInput`") + } + }, + _ => panic!("Invalid type found when updating `NumberInput`"), + }, + Widget::Separator(_) => {} + Widget::IconButton(icon_button) => { + let callback_message = (icon_button.on_update.callback)(icon_button); + responses.push_back(callback_message); + } + Widget::PopoverButton(_) => {} + Widget::OptionalInput(optional_input) => { + let update_value = value.as_bool().expect("OptionalInput update was not of type: bool"); + optional_input.checked = update_value; + let callback_message = (optional_input.on_update.callback)(optional_input); + responses.push_back(callback_message); + } + Widget::RadioInput(radio_input) => { + let update_value = value.as_u64().expect("OptionalInput update was not of type: u64"); + radio_input.selected_index = update_value as u32; + let callback_message = (radio_input.entries[update_value as usize].on_update.callback)(&()); + responses.push_back(callback_message); + } + }; + self.send_layout(layout_target, responses); + } + } + } + + fn actions(&self) -> crate::message_prelude::ActionList { + actions!() + } +} diff --git a/editor/src/layout/mod.rs b/editor/src/layout/mod.rs new file mode 100644 index 000000000..86e8a31e9 --- /dev/null +++ b/editor/src/layout/mod.rs @@ -0,0 +1,5 @@ +pub mod layout_message; +pub mod layout_message_handler; +pub mod widgets; + +pub use layout_message::{LayoutMessage, LayoutMessageDiscriminant}; diff --git a/editor/src/layout/widgets.rs b/editor/src/layout/widgets.rs new file mode 100644 index 000000000..95ec267de --- /dev/null +++ b/editor/src/layout/widgets.rs @@ -0,0 +1,283 @@ +use super::layout_message::LayoutTarget; +use crate::message_prelude::*; + +use derivative::*; +use serde::{Deserialize, Serialize}; + +pub trait PropertyHolder { + fn properties(&self) -> WidgetLayout { + WidgetLayout::default() + } + + fn register_properties(&self, responses: &mut VecDeque, layout_target: LayoutTarget) { + responses.push_back( + LayoutMessage::SendLayout { + layout: self.properties(), + layout_target, + } + .into(), + ) + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] +pub struct WidgetLayout { + pub layout: SubLayout, +} + +impl WidgetLayout { + pub fn new(layout: SubLayout) -> Self { + Self { layout } + } + + pub fn iter(&self) -> WidgetIter<'_> { + WidgetIter { + stack: self.layout.iter().collect(), + current_slice: None, + } + } + + pub fn iter_mut(&mut self) -> WidgetIterMut<'_> { + WidgetIterMut { + stack: self.layout.iter_mut().collect(), + current_slice: None, + } + } +} + +pub type SubLayout = Vec; + +#[remain::sorted] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum LayoutRow { + Row { name: String, widgets: Vec }, + Section { name: String, layout: SubLayout }, +} + +impl LayoutRow { + pub fn widgets(&self) -> Vec { + match &self { + Self::Row { name: _, widgets } => widgets.to_vec(), + Self::Section { name: _, layout } => layout.iter().flat_map(|row| row.widgets()).collect(), + } + } +} + +#[derive(Debug, Default)] +pub struct WidgetIter<'a> { + pub stack: Vec<&'a LayoutRow>, + pub current_slice: Option<&'a [WidgetHolder]>, +} + +impl<'a> Iterator for WidgetIter<'a> { + type Item = &'a WidgetHolder; + + fn next(&mut self) -> Option { + if let Some(item) = self.current_slice.map(|slice| slice.first()).flatten() { + self.current_slice = Some(&self.current_slice.unwrap()[1..]); + return Some(item); + } + + match self.stack.pop() { + Some(LayoutRow::Row { name: _, widgets }) => { + self.current_slice = Some(widgets); + self.next() + } + Some(LayoutRow::Section { name: _, layout }) => { + for layout_row in layout { + self.stack.push(layout_row); + } + self.next() + } + None => None, + } + } +} + +#[derive(Debug, Default)] +pub struct WidgetIterMut<'a> { + pub stack: Vec<&'a mut LayoutRow>, + pub current_slice: Option<&'a mut [WidgetHolder]>, +} + +impl<'a> Iterator for WidgetIterMut<'a> { + type Item = &'a mut WidgetHolder; + + fn next(&mut self) -> Option { + if let Some((first, rest)) = self.current_slice.take().map(|slice| slice.split_first_mut()).flatten() { + self.current_slice = Some(rest); + return Some(first); + }; + + match self.stack.pop() { + Some(LayoutRow::Row { name: _, widgets }) => { + self.current_slice = Some(widgets); + self.next() + } + Some(LayoutRow::Section { name: _, layout }) => { + for layout_row in layout { + self.stack.push(layout_row); + } + self.next() + } + None => None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct WidgetHolder { + pub widget_id: u64, + pub widget: Widget, +} + +impl WidgetHolder { + pub fn new(widget: Widget) -> Self { + Self { widget_id: generate_uuid(), widget } + } +} + +#[derive(Clone)] +pub struct WidgetCallback { + pub callback: fn(&T) -> Message, +} + +impl WidgetCallback { + pub fn new(callback: fn(&T) -> Message) -> Self { + Self { callback } + } +} + +impl Default for WidgetCallback { + fn default() -> Self { + Self { callback: |_| Message::NoOp } + } +} + +#[remain::sorted] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Widget { + IconButton(IconButton), + NumberInput(NumberInput), + OptionalInput(OptionalInput), + PopoverButton(PopoverButton), + RadioInput(RadioInput), + Separator(Separator), +} + +#[derive(Clone, Serialize, Deserialize, Derivative)] +#[derivative(Debug, PartialEq, Default)] +pub struct NumberInput { + pub value: f64, + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, + pub min: Option, + pub max: Option, + #[serde(rename = "isInteger")] + pub is_integer: bool, + #[serde(rename = "incrementBehavior")] + pub increment_behavior: NumberInputIncrementBehavior, + #[serde(rename = "incrementFactor")] + #[derivative(Default(value = "1."))] + pub increment_factor: f64, + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub increment_callback_increase: WidgetCallback, + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub increment_callback_decrease: WidgetCallback, + pub label: String, + pub unit: String, +} + +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +pub enum NumberInputIncrementBehavior { + Add, + Multiply, + Callback, +} + +impl Default for NumberInputIncrementBehavior { + fn default() -> Self { + Self::Add + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Separator { + pub direction: SeparatorDirection, + + #[serde(rename = "type")] + pub separator_type: SeparatorType, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SeparatorDirection { + Horizontal, + Vertical, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SeparatorType { + Related, + Unrelated, + Section, + List, +} + +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct IconButton { + pub icon: String, + #[serde(rename = "title")] + pub tooltip: String, + pub size: u32, + #[serde(rename = "gapAfter")] + pub gap_after: bool, + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, +} + +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct OptionalInput { + pub checked: bool, + pub icon: String, + #[serde(rename = "title")] + pub tooltip: String, + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback, +} + +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct PopoverButton { + pub title: String, + pub text: String, +} + +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct RadioInput { + pub entries: Vec, + + // This uses `u32` instead of `usize` since it will be serialized as a normal JS number + // TODO(mfish33): Replace with usize when using native UI + #[serde(rename = "selectedIndex")] + pub selected_index: u32, +} + +#[derive(Clone, Serialize, Deserialize, Derivative, Default)] +#[derivative(Debug, PartialEq)] +pub struct RadioEntryData { + pub value: String, + pub label: String, + pub icon: String, + pub tooltip: String, + #[serde(skip)] + #[derivative(Debug = "ignore", PartialEq = "ignore")] + pub on_update: WidgetCallback<()>, +} diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 1e04b002a..5e8d5317e 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -8,6 +8,7 @@ pub mod document; pub mod frontend; pub mod global; pub mod input; +pub mod layout; pub mod viewport_tools; #[doc(inline)] @@ -67,6 +68,7 @@ pub mod message_prelude { pub use crate::frontend::{FrontendMessage, FrontendMessageDiscriminant}; pub use crate::global::{GlobalMessage, GlobalMessageDiscriminant}; pub use crate::input::{InputMapperMessage, InputMapperMessageDiscriminant, InputPreprocessorMessage, InputPreprocessorMessageDiscriminant}; + pub use crate::layout::{LayoutMessage, LayoutMessageDiscriminant}; pub use crate::misc::derivable_custom_traits::{ToDiscriminant, TransitiveChild}; pub use crate::viewport_tools::tool_message::{ToolMessage, ToolMessageDiscriminant}; pub use crate::viewport_tools::tools::crop::{CropMessage, CropMessageDiscriminant}; diff --git a/editor/src/misc/macros.rs b/editor/src/misc/macros.rs index 539c12452..174e6d320 100644 --- a/editor/src/misc/macros.rs +++ b/editor/src/misc/macros.rs @@ -41,7 +41,7 @@ macro_rules! count_args { /// ``` macro_rules! gen_tools_hash_map { ($($enum_variant:ident => $struct_path:ty),* $(,)?) => {{ - let mut hash_map: ::std::collections::HashMap<$crate::viewport_tools::tool::ToolType, ::std::boxed::Box $crate::message_prelude::MessageHandler<$crate::viewport_tools::tool_message::ToolMessage,$crate::viewport_tools::tool::ToolActionHandlerData<'a>>>> = ::std::collections::HashMap::with_capacity(count_args!($(($enum_variant)),*)); + let mut hash_map: ::std::collections::HashMap<$crate::viewport_tools::tool::ToolType, ::std::boxed::Box<$crate::viewport_tools::tool::Tool>> = ::std::collections::HashMap::with_capacity(count_args!($(($enum_variant)),*)); $(hash_map.insert($crate::viewport_tools::tool::ToolType::$enum_variant, ::std::boxed::Box::new(<$struct_path>::default()));)* hash_map diff --git a/editor/src/viewport_tools/mod.rs b/editor/src/viewport_tools/mod.rs index c245ee587..2ef65d58a 100644 --- a/editor/src/viewport_tools/mod.rs +++ b/editor/src/viewport_tools/mod.rs @@ -2,5 +2,4 @@ pub mod snapping; pub mod tool; pub mod tool_message; pub mod tool_message_handler; -pub mod tool_options; pub mod tools; diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index 39fabf765..df0e6d0c9 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -1,8 +1,8 @@ -use super::tool_options::{SelectAppendMode, ShapeType, ToolOptions}; use super::tools::*; use crate::communication::message_handler::MessageHandler; use crate::document::DocumentMessageHandler; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use graphene::color::Color; @@ -15,6 +15,7 @@ pub type ToolActionHandlerData<'a> = (&'a DocumentMessageHandler, &'a DocumentTo pub trait Fsm { type ToolData; + type ToolOptions; #[must_use] fn transition( @@ -23,6 +24,7 @@ pub trait Fsm { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, messages: &mut VecDeque, ) -> Self; @@ -35,14 +37,16 @@ pub trait Fsm { pub struct DocumentToolData { pub primary_color: Color, pub secondary_color: Color, - pub tool_options: HashMap, } -type SubToolMessageHandler = dyn for<'a> MessageHandler>; +pub trait ToolCommon: for<'a> MessageHandler> + PropertyHolder {} +impl ToolCommon for T where T: for<'a> MessageHandler> + PropertyHolder {} + +type Tool = dyn ToolCommon; pub struct ToolData { pub active_tool_type: ToolType, - pub tools: HashMap>, + pub tools: HashMap>, } impl fmt::Debug for ToolData { @@ -52,10 +56,11 @@ impl fmt::Debug for ToolData { } impl ToolData { - pub fn active_tool_mut(&mut self) -> &mut Box { + 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 { + + pub fn active_tool(&self) -> &Tool { self.tools.get(&self.active_tool_type).map(|x| x.as_ref()).expect("The active tool is not initialized") } } @@ -98,7 +103,6 @@ impl Default for ToolFsmState { document_tool_data: DocumentToolData { primary_color: Color::BLACK, secondary_color: Color::WHITE, - tool_options: default_tool_options(), }, } } @@ -114,35 +118,6 @@ impl ToolFsmState { } } -fn default_tool_options() -> HashMap { - let tool_init = |tool: ToolType| (tool, tool.default_options()); - [ - tool_init(ToolType::Select), - tool_init(ToolType::Crop), - tool_init(ToolType::Navigate), - tool_init(ToolType::Eyedropper), - tool_init(ToolType::Text), - tool_init(ToolType::Fill), - tool_init(ToolType::Gradient), - tool_init(ToolType::Brush), - tool_init(ToolType::Heal), - tool_init(ToolType::Clone), - tool_init(ToolType::Patch), - tool_init(ToolType::BlurSharpen), - tool_init(ToolType::Relight), - tool_init(ToolType::Path), - tool_init(ToolType::Pen), - tool_init(ToolType::Freehand), - tool_init(ToolType::Spline), - tool_init(ToolType::Line), - tool_init(ToolType::Rectangle), - tool_init(ToolType::Ellipse), - tool_init(ToolType::Shape), - ] - .into_iter() - .collect() -} - #[repr(usize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ToolType { @@ -201,36 +176,6 @@ impl fmt::Display for ToolType { } } -impl ToolType { - fn default_options(&self) -> ToolOptions { - match self { - ToolType::Select => ToolOptions::Select { append_mode: SelectAppendMode::New }, - ToolType::Crop => ToolOptions::Crop {}, - ToolType::Navigate => ToolOptions::Navigate {}, - ToolType::Eyedropper => ToolOptions::Eyedropper {}, - ToolType::Text => ToolOptions::Text { font_size: 14 }, - ToolType::Fill => ToolOptions::Fill {}, - ToolType::Gradient => ToolOptions::Gradient {}, - ToolType::Brush => ToolOptions::Brush {}, - ToolType::Heal => ToolOptions::Heal {}, - ToolType::Clone => ToolOptions::Clone {}, - ToolType::Patch => ToolOptions::Patch {}, - ToolType::BlurSharpen => ToolOptions::BlurSharpen {}, - ToolType::Relight => ToolOptions::Relight {}, - ToolType::Path => ToolOptions::Path {}, - ToolType::Pen => ToolOptions::Pen { weight: 5 }, - ToolType::Freehand => ToolOptions::Freehand { weight: 5 }, - ToolType::Spline => ToolOptions::Spline {}, - ToolType::Line => ToolOptions::Line { weight: 5 }, - ToolType::Rectangle => ToolOptions::Rectangle {}, - ToolType::Ellipse => ToolOptions::Ellipse {}, - ToolType::Shape => ToolOptions::Shape { - shape_type: ShapeType::Polygon { vertices: 6 }, - }, - } - } -} - pub enum StandardToolMessageType { Abort, DocumentIsDirty, diff --git a/editor/src/viewport_tools/tool_message.rs b/editor/src/viewport_tools/tool_message.rs index 24578d8d4..95809e131 100644 --- a/editor/src/viewport_tools/tool_message.rs +++ b/editor/src/viewport_tools/tool_message.rs @@ -1,5 +1,4 @@ use super::tool::ToolType; -use super::tool_options::ToolOptions; use crate::message_prelude::*; use graphene::color::Color; @@ -92,10 +91,6 @@ pub enum ToolMessage { SelectSecondaryColor { color: Color, }, - SetToolOptions { - tool_type: ToolType, - tool_options: ToolOptions, - }, SwapColors, UpdateCursor, UpdateHints, diff --git a/editor/src/viewport_tools/tool_message_handler.rs b/editor/src/viewport_tools/tool_message_handler.rs index c16cb7ad5..96d4ad090 100644 --- a/editor/src/viewport_tools/tool_message_handler.rs +++ b/editor/src/viewport_tools/tool_message_handler.rs @@ -1,6 +1,7 @@ use super::tool::{message_to_tool_type, standard_tool_message, update_working_colors, StandardToolMessageType, ToolFsmState}; use crate::document::DocumentMessageHandler; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::layout_message::LayoutTarget; use crate::message_prelude::*; use graphene::color::Color; @@ -60,8 +61,10 @@ impl MessageHandler { // Send the DocumentIsDirty message to the active tool's sub-tool message handler @@ -90,11 +93,6 @@ impl MessageHandler { - let document_data = &mut self.tool_state.document_tool_data; - - document_data.tool_options.insert(tool_type, tool_options); - } SwapColors => { let document_data = &mut self.tool_state.document_tool_data; @@ -123,7 +121,7 @@ impl MessageHandler ActionList { - let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool, SetToolOptions); + let mut list = actions!(ToolMessageDiscriminant; ResetColors, SwapColors, ActivateTool); list.extend(self.tool_state.tool_data.active_tool().actions()); list diff --git a/editor/src/viewport_tools/tool_options.rs b/editor/src/viewport_tools/tool_options.rs deleted file mode 100644 index 7da44712e..000000000 --- a/editor/src/viewport_tools/tool_options.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)] -pub enum ToolOptions { - Select { append_mode: SelectAppendMode }, - Crop {}, - Navigate {}, - Eyedropper {}, - Text { font_size: u32 }, - Fill {}, - Gradient {}, - Brush {}, - Heal {}, - Clone {}, - Patch {}, - BlurSharpen {}, - Relight {}, - Path {}, - Pen { weight: u32 }, - Freehand { weight: u32 }, - Spline {}, - Line { weight: u32 }, - Rectangle {}, - Ellipse {}, - Shape { shape_type: ShapeType }, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)] -pub enum SelectAppendMode { - New, - Add, - Subtract, - Intersect, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)] -pub enum ShapeType { - Star { vertices: u32 }, - Polygon { vertices: u32 }, -} diff --git a/editor/src/viewport_tools/tools/crop.rs b/editor/src/viewport_tools/tools/crop.rs index b5af28ad1..618c7fa53 100644 --- a/editor/src/viewport_tools/tools/crop.rs +++ b/editor/src/viewport_tools/tools/crop.rs @@ -1,3 +1,4 @@ +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use crate::viewport_tools::tool::ToolActionHandlerData; @@ -25,3 +26,5 @@ impl<'a> MessageHandler> for Crop { advertise_actions!(); } + +impl PropertyHolder for Crop {} diff --git a/editor/src/viewport_tools/tools/ellipse.rs b/editor/src/viewport_tools/tools/ellipse.rs index a2f84cd14..6e7886992 100644 --- a/editor/src/viewport_tools/tools/ellipse.rs +++ b/editor/src/viewport_tools/tools/ellipse.rs @@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; @@ -36,6 +37,8 @@ pub enum EllipseMessage { }, } +impl PropertyHolder for Ellipse {} + impl<'a> MessageHandler> for Ellipse { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -48,7 +51,7 @@ impl<'a> MessageHandler> for Ellipse { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -86,6 +89,7 @@ struct EllipseToolData { impl Fsm for EllipseToolFsmState { type ToolData = EllipseToolData; + type ToolOptions = (); fn transition( self, @@ -93,6 +97,7 @@ impl Fsm for EllipseToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { diff --git a/editor/src/viewport_tools/tools/eyedropper.rs b/editor/src/viewport_tools/tools/eyedropper.rs index de3f65233..468a3babb 100644 --- a/editor/src/viewport_tools/tools/eyedropper.rs +++ b/editor/src/viewport_tools/tools/eyedropper.rs @@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::MouseMotion; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; @@ -32,6 +33,8 @@ pub enum EyedropperMessage { RightMouseDown, } +impl PropertyHolder for Eyedropper {} + impl<'a> MessageHandler> for Eyedropper { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -44,7 +47,7 @@ impl<'a> MessageHandler> for Eyedropper { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -72,6 +75,7 @@ struct EyedropperToolData {} impl Fsm for EyedropperToolFsmState { type ToolData = EyedropperToolData; + type ToolOptions = (); fn transition( self, @@ -79,6 +83,7 @@ impl Fsm for EyedropperToolFsmState { document: &DocumentMessageHandler, _tool_data: &DocumentToolData, _data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { diff --git a/editor/src/viewport_tools/tools/fill.rs b/editor/src/viewport_tools/tools/fill.rs index fdeb7c75d..aa4456ad7 100644 --- a/editor/src/viewport_tools/tools/fill.rs +++ b/editor/src/viewport_tools/tools/fill.rs @@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::MouseMotion; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; @@ -32,6 +33,8 @@ pub enum FillMessage { RightMouseDown, } +impl PropertyHolder for Fill {} + impl<'a> MessageHandler> for Fill { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -44,7 +47,7 @@ impl<'a> MessageHandler> for Fill { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -72,6 +75,7 @@ struct FillToolData {} impl Fsm for FillToolFsmState { type ToolData = FillToolData; + type ToolOptions = (); fn transition( self, @@ -79,6 +83,7 @@ impl Fsm for FillToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, _data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { diff --git a/editor/src/viewport_tools/tools/freehand.rs b/editor/src/viewport_tools/tools/freehand.rs index 3b80f90bc..3bf96b9be 100644 --- a/editor/src/viewport_tools/tools/freehand.rs +++ b/editor/src/viewport_tools/tools/freehand.rs @@ -2,10 +2,10 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::MouseMotion; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo}; -use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; -use crate::viewport_tools::tool_options::ToolOptions; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use graphene::layers::style; use graphene::Operation; @@ -17,6 +17,17 @@ use serde::{Deserialize, Serialize}; pub struct Freehand { fsm_state: FreehandToolFsmState, data: FreehandToolData, + options: FreehandOptions, +} + +pub struct FreehandOptions { + line_weight: u32, +} + +impl Default for FreehandOptions { + fn default() -> Self { + Self { line_weight: 5 } + } } #[remain::sorted] @@ -31,6 +42,13 @@ pub enum FreehandMessage { DragStart, DragStop, PointerMove, + UpdateOptions(FreehandMessageOptionsUpdate), +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum FreehandMessageOptionsUpdate { + LineWeight(u32), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -39,6 +57,23 @@ enum FreehandToolFsmState { Drawing, } +impl PropertyHolder for Freehand { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { + unit: " px".into(), + label: "Weight".into(), + value: self.options.line_weight as f64, + is_integer: true, + min: Some(1.), + on_update: WidgetCallback::new(|number_input| FreehandMessage::UpdateOptions(FreehandMessageOptionsUpdate::LineWeight(number_input.value as u32)).into()), + ..NumberInput::default() + }))], + }]) + } +} + impl<'a> MessageHandler> for Freehand { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -51,7 +86,14 @@ impl<'a> MessageHandler> for Freehand { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if let ToolMessage::Freehand(FreehandMessage::UpdateOptions(action)) = action { + match action { + FreehandMessageOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + } + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -84,6 +126,7 @@ struct FreehandToolData { impl Fsm for FreehandToolFsmState { type ToolData = FreehandToolData; + type ToolOptions = FreehandOptions; fn transition( self, @@ -91,6 +134,7 @@ impl Fsm for FreehandToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { @@ -110,10 +154,7 @@ impl Fsm for FreehandToolFsmState { data.points.push(pos); - data.weight = match tool_data.tool_options.get(&ToolType::Freehand) { - Some(&ToolOptions::Freehand { weight }) => weight, - _ => 5, - }; + data.weight = tool_options.line_weight; responses.push_back(make_operation(data, tool_data)); diff --git a/editor/src/viewport_tools/tools/line.rs b/editor/src/viewport_tools/tools/line.rs index 7f4f9adb5..377ebebc4 100644 --- a/editor/src/viewport_tools/tools/line.rs +++ b/editor/src/viewport_tools/tools/line.rs @@ -4,11 +4,11 @@ use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::mouse::ViewportPosition; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::snapping::SnapHandler; -use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; -use crate::viewport_tools::tool_options::ToolOptions; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use graphene::layers::style; use graphene::Operation; @@ -20,6 +20,17 @@ use serde::{Deserialize, Serialize}; pub struct Line { fsm_state: LineToolFsmState, data: LineToolData, + options: LineOptions, +} + +pub struct LineOptions { + line_weight: u32, +} + +impl Default for LineOptions { + fn default() -> Self { + Self { line_weight: 5 } + } } #[remain::sorted] @@ -38,6 +49,30 @@ pub enum LineMessage { lock_angle: Key, snap_angle: Key, }, + UpdateOptions(LineOptionsUpdate), +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum LineOptionsUpdate { + LineWeight(u32), +} + +impl PropertyHolder for Line { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { + unit: " px".into(), + label: "Weight".into(), + value: self.options.line_weight as f64, + is_integer: true, + min: Some(0.), + on_update: WidgetCallback::new(|number_input| LineMessage::UpdateOptions(LineOptionsUpdate::LineWeight(number_input.value as u32)).into()), + ..NumberInput::default() + }))], + }]) + } } impl<'a> MessageHandler> for Line { @@ -52,7 +87,14 @@ impl<'a> MessageHandler> for Line { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if let ToolMessage::Line(LineMessage::UpdateOptions(action)) = action { + match action { + LineOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + } + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -95,6 +137,7 @@ struct LineToolData { impl Fsm for LineToolFsmState { type ToolData = LineToolData; + type ToolOptions = LineOptions; fn transition( self, @@ -102,6 +145,7 @@ impl Fsm for LineToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { @@ -118,10 +162,7 @@ impl Fsm for LineToolFsmState { data.path = Some(vec![generate_uuid()]); responses.push_back(DocumentMessage::DeselectAllLayers.into()); - data.weight = match tool_data.tool_options.get(&ToolType::Line) { - Some(&ToolOptions::Line { weight }) => weight, - _ => 5, - }; + data.weight = tool_options.line_weight; responses.push_back( Operation::AddLine { diff --git a/editor/src/viewport_tools/tools/navigate.rs b/editor/src/viewport_tools/tools/navigate.rs index 995982c32..021ea6233 100644 --- a/editor/src/viewport_tools/tools/navigate.rs +++ b/editor/src/viewport_tools/tools/navigate.rs @@ -2,6 +2,7 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; @@ -37,6 +38,8 @@ pub enum NavigateMessage { ZoomCanvasBegin, } +impl PropertyHolder for Navigate {} + impl<'a> MessageHandler> for Navigate { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -49,7 +52,7 @@ impl<'a> MessageHandler> for Navigate { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -89,6 +92,7 @@ struct NavigateToolData { impl Fsm for NavigateToolFsmState { type ToolData = NavigateToolData; + type ToolOptions = (); fn transition( self, @@ -96,6 +100,7 @@ impl Fsm for NavigateToolFsmState { _document: &DocumentMessageHandler, _tool_data: &DocumentToolData, data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, messages: &mut VecDeque, ) -> Self { diff --git a/editor/src/viewport_tools/tools/path.rs b/editor/src/viewport_tools/tools/path.rs index 9c0c201ff..9520989d4 100644 --- a/editor/src/viewport_tools/tools/path.rs +++ b/editor/src/viewport_tools/tools/path.rs @@ -4,6 +4,7 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; @@ -38,6 +39,8 @@ pub enum PathMessage { PointerMove, } +impl PropertyHolder for Path {} + impl<'a> MessageHandler> for Path { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -50,7 +53,7 @@ impl<'a> MessageHandler> for Path { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -105,6 +108,7 @@ struct PathToolSelection { impl Fsm for PathToolFsmState { type ToolData = PathToolData; + type ToolOptions = (); fn transition( self, @@ -112,6 +116,7 @@ impl Fsm for PathToolFsmState { document: &DocumentMessageHandler, _tool_data: &DocumentToolData, data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { diff --git a/editor/src/viewport_tools/tools/pen.rs b/editor/src/viewport_tools/tools/pen.rs index 2cc84fa85..5e1db78e6 100644 --- a/editor/src/viewport_tools/tools/pen.rs +++ b/editor/src/viewport_tools/tools/pen.rs @@ -2,11 +2,11 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::snapping::SnapHandler; -use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; -use crate::viewport_tools::tool_options::ToolOptions; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use graphene::layers::style; use graphene::Operation; @@ -18,6 +18,17 @@ use serde::{Deserialize, Serialize}; pub struct Pen { fsm_state: PenToolFsmState, data: PenToolData, + options: PenOptions, +} + +pub struct PenOptions { + line_weight: u32, +} + +impl Default for PenOptions { + fn default() -> Self { + Self { line_weight: 5 } + } } #[remain::sorted] @@ -34,6 +45,7 @@ pub enum PenMessage { DragStop, PointerMove, Undo, + UpdateOptions(PenOptionsUpdate), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -42,6 +54,29 @@ enum PenToolFsmState { Drawing, } +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum PenOptionsUpdate { + LineWeight(u32), +} + +impl PropertyHolder for Pen { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { + unit: " px".into(), + label: "Weight".into(), + value: self.options.line_weight as f64, + is_integer: true, + min: Some(0.), + on_update: WidgetCallback::new(|number_input| PenMessage::UpdateOptions(PenOptionsUpdate::LineWeight(number_input.value as u32)).into()), + ..NumberInput::default() + }))], + }]) + } +} + impl<'a> MessageHandler> for Pen { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -54,7 +89,14 @@ impl<'a> MessageHandler> for Pen { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if let ToolMessage::Pen(PenMessage::UpdateOptions(action)) = action { + match action { + PenOptionsUpdate::LineWeight(line_weight) => self.options.line_weight = line_weight, + } + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -89,6 +131,7 @@ struct PenToolData { impl Fsm for PenToolFsmState { type ToolData = PenToolData; + type ToolOptions = PenOptions; fn transition( self, @@ -96,6 +139,7 @@ impl Fsm for PenToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { @@ -119,10 +163,7 @@ impl Fsm for PenToolFsmState { data.points.push(pos); data.next_point = pos; - data.weight = match tool_data.tool_options.get(&ToolType::Pen) { - Some(&ToolOptions::Pen { weight }) => weight, - _ => 5, - }; + data.weight = tool_options.line_weight; responses.push_back(make_operation(data, tool_data, true)); diff --git a/editor/src/viewport_tools/tools/rectangle.rs b/editor/src/viewport_tools/tools/rectangle.rs index c67f250ff..21e602c10 100644 --- a/editor/src/viewport_tools/tools/rectangle.rs +++ b/editor/src/viewport_tools/tools/rectangle.rs @@ -3,6 +3,7 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::PropertyHolder; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; @@ -36,6 +37,8 @@ pub enum RectangleMessage { }, } +impl PropertyHolder for Rectangle {} + impl<'a> MessageHandler> for Rectangle { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -48,7 +51,7 @@ impl<'a> MessageHandler> for Rectangle { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -85,6 +88,7 @@ struct RectangleToolData { impl Fsm for RectangleToolFsmState { type ToolData = RectangleToolData; + type ToolOptions = (); fn transition( self, @@ -92,6 +96,7 @@ impl Fsm for RectangleToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { diff --git a/editor/src/viewport_tools/tools/select.rs b/editor/src/viewport_tools/tools/select.rs index cb0af2217..06b17dd99 100644 --- a/editor/src/viewport_tools/tools/select.rs +++ b/editor/src/viewport_tools/tools/select.rs @@ -5,6 +5,7 @@ use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::mouse::ViewportPosition; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{IconButton, LayoutRow, PopoverButton, PropertyHolder, Separator, SeparatorDirection, SeparatorType, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::snapping::SnapHandler; @@ -51,6 +52,179 @@ pub enum SelectMessage { }, } +impl PropertyHolder for Select { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![ + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "AlignLeft".into(), + tooltip: "Align Left".into(), + size: 24, + on_update: WidgetCallback::new(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::X, + aggregate: AlignAggregate::Min, + } + .into() + }), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "AlignHorizontalCenter".into(), + tooltip: "Align Horizontal Center".into(), + size: 24, + on_update: WidgetCallback::new(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::X, + aggregate: AlignAggregate::Center, + } + .into() + }), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "AlignRight".into(), + tooltip: "Align Right".into(), + size: 24, + on_update: WidgetCallback::new(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::X, + aggregate: AlignAggregate::Max, + } + .into() + }), + ..IconButton::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Unrelated, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "AlignTop".into(), + tooltip: "Align Top".into(), + size: 24, + on_update: WidgetCallback::new(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::Y, + aggregate: AlignAggregate::Min, + } + .into() + }), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "AlignVerticalCenter".into(), + tooltip: "Align Vertical Center".into(), + size: 24, + on_update: WidgetCallback::new(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::Y, + aggregate: AlignAggregate::Center, + } + .into() + }), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "AlignBottom".into(), + tooltip: "Align Bottom".into(), + size: 24, + on_update: WidgetCallback::new(|_| { + DocumentMessage::AlignSelectedLayers { + axis: AlignAxis::Y, + aggregate: AlignAggregate::Max, + } + .into() + }), + ..IconButton::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Related, + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Align".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Section, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "FlipHorizontal".into(), + tooltip: "Flip Horizontal".into(), + size: 24, + on_update: WidgetCallback::new(|_| SelectMessage::FlipHorizontal.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "FlipVertical".into(), + tooltip: "Flip Vertical".into(), + size: 24, + on_update: WidgetCallback::new(|_| SelectMessage::FlipVertical.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Related, + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Flip".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Section, + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "BooleanUnion".into(), + tooltip: "Boolean Union".into(), + size: 24, + on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "BooleanSubtractFront".into(), + tooltip: "Boolean Subtract Front".into(), + size: 24, + on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "BooleanSubtractBack".into(), + tooltip: "Boolean Subtract Back".into(), + size: 24, + on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "BooleanIntersect".into(), + tooltip: "Boolean Intersect".into(), + size: 24, + on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::IconButton(IconButton { + icon: "BooleanDifference".into(), + tooltip: "Boolean Difference".into(), + size: 24, + on_update: WidgetCallback::new(|_| FrontendMessage::DisplayDialogComingSoon { issue: Some(197) }.into()), + ..IconButton::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Related, + })), + WidgetHolder::new(Widget::PopoverButton(PopoverButton { + title: "Boolean".into(), + text: "The contents of this popover menu are coming soon".into(), + })), + ], + }]) + } +} + impl<'a> MessageHandler> for Select { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { if action == ToolMessage::UpdateHints { @@ -63,7 +237,7 @@ impl<'a> MessageHandler> for Select { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -141,6 +315,7 @@ fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] { impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; + type ToolOptions = (); fn transition( self, @@ -148,6 +323,7 @@ impl Fsm for SelectToolFsmState { document: &DocumentMessageHandler, _tool_data: &DocumentToolData, data: &mut Self::ToolData, + _tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { diff --git a/editor/src/viewport_tools/tools/shape.rs b/editor/src/viewport_tools/tools/shape.rs index 1da0331ba..a406db1e0 100644 --- a/editor/src/viewport_tools/tools/shape.rs +++ b/editor/src/viewport_tools/tools/shape.rs @@ -3,10 +3,10 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; -use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; -use crate::viewport_tools::tool_options::{ShapeType, ToolOptions}; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use graphene::layers::style; use graphene::Operation; @@ -18,6 +18,17 @@ use serde::{Deserialize, Serialize}; pub struct Shape { fsm_state: ShapeToolFsmState, data: ShapeToolData, + options: ShapeOptions, +} + +pub struct ShapeOptions { + vertices: u8, +} + +impl Default for ShapeOptions { + fn default() -> Self { + Self { vertices: 6 } + } } #[remain::sorted] @@ -35,6 +46,30 @@ pub enum ShapeMessage { center: Key, lock_ratio: Key, }, + UpdateOptions(ShapeOptionsUpdate), +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum ShapeOptionsUpdate { + Vertices(u8), +} + +impl PropertyHolder for Shape { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { + label: "Sides".into(), + value: self.options.vertices as f64, + is_integer: true, + min: Some(3.), + max: Some(256.), + on_update: WidgetCallback::new(|number_input| ShapeMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value as u8)).into()), + ..NumberInput::default() + }))], + }]) + } } impl<'a> MessageHandler> for Shape { @@ -49,7 +84,14 @@ impl<'a> MessageHandler> for Shape { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if let ToolMessage::Shape(ShapeMessage::UpdateOptions(action)) = action { + match action { + ShapeOptionsUpdate::Vertices(vertices) => self.options.vertices = vertices, + } + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -87,6 +129,7 @@ struct ShapeToolData { impl Fsm for ShapeToolFsmState { type ToolData = ShapeToolData; + type ToolOptions = ShapeOptions; fn transition( self, @@ -94,6 +137,7 @@ impl Fsm for ShapeToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { @@ -109,12 +153,7 @@ impl Fsm for ShapeToolFsmState { responses.push_back(DocumentMessage::StartTransaction.into()); shape_data.path = Some(vec![generate_uuid()]); responses.push_back(DocumentMessage::DeselectAllLayers.into()); - data.sides = match tool_data.tool_options.get(&ToolType::Shape) { - Some(&ToolOptions::Shape { - shape_type: ShapeType::Polygon { vertices }, - }) => vertices as u8, - _ => 6, - }; + data.sides = tool_options.vertices; responses.push_back( Operation::AddNgon { diff --git a/editor/src/viewport_tools/tools/text.rs b/editor/src/viewport_tools/tools/text.rs index 91bd2fc6d..5b62c7fdf 100644 --- a/editor/src/viewport_tools/tools/text.rs +++ b/editor/src/viewport_tools/tools/text.rs @@ -3,10 +3,10 @@ use crate::document::DocumentMessageHandler; use crate::frontend::utility_types::MouseCursorIcon; use crate::input::keyboard::{Key, MouseMotion}; use crate::input::InputPreprocessorMessageHandler; +use crate::layout::widgets::{LayoutRow, NumberInput, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::message_prelude::*; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; -use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; -use crate::viewport_tools::tool_options::ToolOptions; +use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use glam::{DAffine2, DVec2}; use graphene::intersection::Quad; @@ -19,6 +19,17 @@ use serde::{Deserialize, Serialize}; pub struct Text { fsm_state: TextToolFsmState, data: TextToolData, + options: TextOptions, +} + +pub struct TextOptions { + font_size: u32, +} + +impl Default for TextOptions { + fn default() -> Self { + Self { font_size: 14 } + } } #[remain::sorted] @@ -41,6 +52,30 @@ pub enum TextMessage { UpdateBounds { new_text: String, }, + UpdateOptions(TextOptionsUpdate), +} + +#[remain::sorted] +#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] +pub enum TextOptionsUpdate { + FontSize(u32), +} + +impl PropertyHolder for Text { + fn properties(&self) -> WidgetLayout { + WidgetLayout::new(vec![LayoutRow::Row { + name: "".into(), + widgets: vec![WidgetHolder::new(Widget::NumberInput(NumberInput { + unit: " px".into(), + label: "Font Size".into(), + value: self.options.font_size as f64, + is_integer: true, + min: Some(1.), + on_update: WidgetCallback::new(|number_input| TextMessage::UpdateOptions(TextOptionsUpdate::FontSize(number_input.value as u32)).into()), + ..NumberInput::default() + }))], + }]) + } } impl<'a> MessageHandler> for Text { @@ -55,7 +90,14 @@ impl<'a> MessageHandler> for Text { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, data.2, responses); + if let ToolMessage::Text(TextMessage::UpdateOptions(action)) = action { + match action { + TextOptionsUpdate::FontSize(font_size) => self.options.font_size = font_size, + } + return; + } + + let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &self.options, data.2, responses); if self.fsm_state != new_state { self.fsm_state = new_state; @@ -137,6 +179,7 @@ fn update_overlays(document: &DocumentMessageHandler, data: &mut TextToolData, r impl Fsm for TextToolFsmState { type ToolData = TextToolData; + type ToolOptions = TextOptions; fn transition( self, @@ -144,6 +187,7 @@ impl Fsm for TextToolFsmState { document: &DocumentMessageHandler, tool_data: &DocumentToolData, data: &mut Self::ToolData, + tool_options: &Self::ToolOptions, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) -> Self { @@ -200,10 +244,7 @@ impl Fsm for TextToolFsmState { // Creating new text else if state == TextToolFsmState::Ready { let transform = DAffine2::from_translation(input.mouse.position).to_cols_array(); - let font_size = match tool_data.tool_options.get(&ToolType::Text) { - Some(&ToolOptions::Text { font_size }) => font_size, - _ => 14, - }; + let font_size = tool_options.font_size; data.path = vec![generate_uuid()]; responses.push_back( diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index a180ce803..21cba17e5 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -6,67 +6,12 @@ - + - - - -

Snapping

-

The contents of this popover menu are coming soon

-
- - - - - -

Grid

-

The contents of this popover menu are coming soon

-
- - - - - -

Overlays

-

The contents of this popover menu are coming soon

-
- - - - - -

View Mode

-

The contents of this popover menu are coming soon

-
- - - - - - - - - - - - - - -
+ @@ -292,6 +237,9 @@ import { ToolName, UpdateDocumentArtboards, UpdateMouseCursor, + UpdateToolOptionsLayout, + defaultWidgetLayout, + UpdateDocumentBarLayout, TriggerTextCommit, DisplayRemoveEditableTextbox, DisplayEditableTextbox, @@ -300,31 +248,19 @@ import { import LayoutCol from "@/components/layout/LayoutCol.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue"; import IconButton from "@/components/widgets/buttons/IconButton.vue"; -import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue"; import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue"; import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue"; -import NumberInput from "@/components/widgets/inputs/NumberInput.vue"; -import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue"; -import RadioInput, { RadioEntries } from "@/components/widgets/inputs/RadioInput.vue"; +import { RadioEntries } from "@/components/widgets/inputs/RadioInput.vue"; import ShelfItemInput from "@/components/widgets/inputs/ShelfItemInput.vue"; import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue"; -import ToolOptions from "@/components/widgets/options/ToolOptions.vue"; import CanvasRuler from "@/components/widgets/rulers/CanvasRuler.vue"; import PersistentScrollbar from "@/components/widgets/scrollbars/PersistentScrollbar.vue"; import Separator from "@/components/widgets/separators/Separator.vue"; +import WidgetLayout from "@/components/widgets/WidgetLayout.vue"; export default defineComponent({ inject: ["editor", "dialog"], methods: { - setSnapping(snap: boolean) { - this.editor.instance.set_snapping(snap); - }, - setOverlaysVisibility(visible: boolean) { - this.editor.instance.set_overlays_visibility(visible); - }, - setViewMode(newViewMode: string) { - this.editor.instance.set_view_mode(newViewMode); - }, viewportResize() { const canvas = this.$refs.canvas as HTMLElement; // Get the width and height rounded up to the nearest even number because resizing is centered and dividing an odd number by 2 for centering causes antialiasing @@ -336,18 +272,6 @@ export default defineComponent({ this.canvasSvgWidth = `${width}px`; this.canvasSvgHeight = `${height}px`; }, - setCanvasZoom(newZoom: number) { - this.editor.instance.set_canvas_zoom(newZoom / 100); - }, - increaseCanvasZoom() { - this.editor.instance.increase_canvas_zoom(); - }, - decreaseCanvasZoom() { - this.editor.instance.decrease_canvas_zoom(); - }, - setRotation(newRotation: number) { - this.editor.instance.set_rotation(newRotation * (Math.PI / 180)); - }, translateCanvasX(newValue: number) { const delta = newValue - this.scrollbarPos.x; this.scrollbarPos.x = newValue; @@ -440,7 +364,6 @@ export default defineComponent({ this.editor.dispatcher.subscribeJsMessage(UpdateActiveTool, (updateActiveTool) => { this.activeTool = updateActiveTool.tool_name; - this.activeToolOptions = updateActiveTool.tool_options; }); this.editor.dispatcher.subscribeJsMessage(UpdateCanvasZoom, (updateCanvasZoom) => { @@ -481,6 +404,14 @@ export default defineComponent({ ); }); + this.editor.dispatcher.subscribeJsMessage(UpdateToolOptionsLayout, (updateToolOptionsLayout) => { + this.toolOptionsLayout = updateToolOptionsLayout; + }); + + this.editor.dispatcher.subscribeJsMessage(UpdateDocumentBarLayout, (updateDocumentBarLayout) => { + this.documentBarLayout = updateDocumentBarLayout; + }); + window.addEventListener("resize", this.viewportResize); window.addEventListener("DOMContentLoaded", this.viewportResize); }, @@ -506,7 +437,8 @@ export default defineComponent({ canvasSvgHeight: "100%", canvasCursor: "default", activeTool: "Select" as ToolName, - activeToolOptions: {}, + toolOptionsLayout: defaultWidgetLayout(), + documentBarLayout: defaultWidgetLayout(), documentModeEntries, viewModeEntries, documentModeSelectionIndex: 0, @@ -534,12 +466,8 @@ export default defineComponent({ PersistentScrollbar, CanvasRuler, IconButton, - PopoverButton, - RadioInput, - NumberInput, DropdownInput, - OptionalInput, - ToolOptions, + WidgetLayout, }, }); diff --git a/frontend/src/components/widgets/WidgetLayout.vue b/frontend/src/components/widgets/WidgetLayout.vue new file mode 100644 index 000000000..d3a2c4815 --- /dev/null +++ b/frontend/src/components/widgets/WidgetLayout.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/components/widgets/WidgetRow.vue b/frontend/src/components/widgets/WidgetRow.vue new file mode 100644 index 000000000..7a385db30 --- /dev/null +++ b/frontend/src/components/widgets/WidgetRow.vue @@ -0,0 +1,66 @@ + + + + + + diff --git a/frontend/src/components/widgets/WidgetSection.vue b/frontend/src/components/widgets/WidgetSection.vue new file mode 100644 index 000000000..0ce946c75 --- /dev/null +++ b/frontend/src/components/widgets/WidgetSection.vue @@ -0,0 +1,56 @@ + + + + + + + diff --git a/frontend/src/components/widgets/inputs/NumberInput.vue b/frontend/src/components/widgets/inputs/NumberInput.vue index 7420c8d9c..39c231e8d 100644 --- a/frontend/src/components/widgets/inputs/NumberInput.vue +++ b/frontend/src/components/widgets/inputs/NumberInput.vue @@ -167,7 +167,7 @@ export default defineComponent({ }, data() { return { - text: `${this.value}${this.unit}`, + text: this.generateText(this.value), editing: false, id: `${Math.random()}`.substring(2), }; @@ -230,9 +230,9 @@ export default defineComponent({ if (typeof this.min === "number" && !Number.isNaN(this.min)) sanitized = Math.max(sanitized, this.min); if (typeof this.max === "number" && !Number.isNaN(this.max)) sanitized = Math.min(sanitized, this.max); if (!invalid) this.$emit("update:value", sanitized); - this.setText(sanitized); + this.text = this.generateText(sanitized); }, - setText(value: number) { + generateText(value: number): string { // Find the amount of digits on the left side of the decimal // 10.25 == 2 // 1.23 == 1 @@ -240,7 +240,7 @@ export default defineComponent({ const leftSideDigits = Math.max(Math.floor(value).toString().length, 0) * Math.sign(value); const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0); const displayValue = Math.round(value * roundingPower) / roundingPower; - this.text = `${displayValue}${this.unit}`; + return `${displayValue}${this.unit}`; }, }, watch: { @@ -254,7 +254,7 @@ export default defineComponent({ let sanitized = newValue; if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min); if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max); - this.setText(sanitized); + this.text = this.generateText(sanitized); }, }, mounted() { diff --git a/frontend/src/components/widgets/options/ToolOptions.vue b/frontend/src/components/widgets/options/ToolOptions.vue deleted file mode 100644 index e1923071a..000000000 --- a/frontend/src/components/widgets/options/ToolOptions.vue +++ /dev/null @@ -1,188 +0,0 @@ - - - - - diff --git a/frontend/src/dispatcher/js-messages.ts b/frontend/src/dispatcher/js-messages.ts index 2fd26b994..c1152293e 100644 --- a/frontend/src/dispatcher/js-messages.ts +++ b/frontend/src/dispatcher/js-messages.ts @@ -137,8 +137,6 @@ export type ToolName = export class UpdateActiveTool extends JsMessage { readonly tool_name!: ToolName; - - readonly tool_options!: object; } export class UpdateActiveDocument extends JsMessage { @@ -386,6 +384,87 @@ export class TriggerIndexedDbRemoveDocument extends JsMessage { document_id!: string; } +export interface WidgetLayout { + layout_target: unknown; + layout: LayoutRow[]; +} + +export function defaultWidgetLayout(): WidgetLayout { + return { + layout: [], + layout_target: null, + }; +} + +export type LayoutRow = WidgetRow | WidgetSection; + +export type WidgetRow = { name: string; widgets: Widget[] }; +export function isWidgetRow(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetRow { + return Boolean((layoutRow as WidgetRow).widgets); +} + +export type WidgetSection = { name: string; layout: LayoutRow[] }; +export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetSection { + return Boolean((layoutRow as WidgetSection).layout); +} + +export type WidgetKind = "NumberInput" | "Separator" | "IconButton" | "PopoverButton" | "OptionalInput" | "RadioInput"; + +export interface Widget { + kind: WidgetKind; + widget_id: BigInt; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + props: any; +} + +export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout { + layout_target!: unknown; + + @Transform(({ value }) => createWidgetLayout(value)) + layout!: LayoutRow[]; +} + +export class UpdateDocumentBarLayout extends JsMessage { + layout_target!: unknown; + + @Transform(({ value }) => createWidgetLayout(value)) + layout!: LayoutRow[]; +} + +// Unpacking rust types to more usable type in the frontend +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createWidgetLayout(widgetLayout: any[]): LayoutRow[] { + return widgetLayout.map((rowOrSection) => { + if (rowOrSection.Row) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => { + const { widget_id } = widgetHolder; + const kind = Object.keys(widgetHolder.widget)[0]; + const props = widgetHolder.widget[kind]; + + return { widget_id, kind, props }; + }); + + return { + name: rowOrSection.Row.name, + widgets, + }; + } + if (rowOrSection.Section) { + return { + name: rowOrSection.Section.name, + layout: createWidgetLayout(rowOrSection.Section), + }; + } + + throw new Error("Layout row type does not exist"); + }); +} + +export class DisplayDialogComingSoon extends JsMessage { + issue: number | undefined; +} + export class TriggerTextCommit extends JsMessage {} // Any is used since the type of the object should be known from the rust side @@ -421,5 +500,8 @@ export const messageConstructors: Record = { TriggerIndexedDbRemoveDocument, TriggerTextCommit, UpdateDocumentArtboards, + UpdateToolOptionsLayout, + DisplayDialogComingSoon, + UpdateDocumentBarLayout, } as const; export type JsMessageType = keyof typeof messageConstructors; diff --git a/frontend/src/state/dialog.ts b/frontend/src/state/dialog.ts index 880314891..9703f9014 100644 --- a/frontend/src/state/dialog.ts +++ b/frontend/src/state/dialog.ts @@ -1,6 +1,6 @@ import { reactive, readonly } from "vue"; -import { DisplayDialogAboutGraphite } from "@/dispatcher/js-messages"; +import { DisplayDialogAboutGraphite, DisplayDialogComingSoon } from "@/dispatcher/js-messages"; import { EditorState } from "@/state/wasm-loader"; import { IconName } from "@/utilities/icons"; import { stripIndents } from "@/utilities/strip-indents"; @@ -107,6 +107,7 @@ export function createDialogState(editor: EditorState) { // Run on creation editor.dispatcher.subscribeJsMessage(DisplayDialogAboutGraphite, () => onAboutHandler()); + editor.dispatcher.subscribeJsMessage(DisplayDialogComingSoon, (displayDialogComingSoon) => comingSoon(displayDialogComingSoon.issue)); return { state: readonly(state), diff --git a/frontend/src/utilities/widgets.ts b/frontend/src/utilities/widgets.ts index aa6607626..4175c203e 100644 --- a/frontend/src/utilities/widgets.ts +++ b/frontend/src/utilities/widgets.ts @@ -1,9 +1,5 @@ import { IconName, IconSize } from "@/utilities/icons"; -export type Widgets = TextButtonWidget | IconButtonWidget | SeparatorWidget | PopoverButtonWidget | NumberInputWidget; -export type WidgetRow = Widgets[]; -export type WidgetLayout = WidgetRow[]; - // Text Button export interface TextButtonWidget { kind: "TextButton"; diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index 9a277e1c3..3e66c6144 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -3,7 +3,7 @@ // on the dispatcher messaging system and more complex Rust data types. use crate::helpers::Error; -use crate::type_translators::{translate_blend_mode, translate_key, translate_tool_type, translate_view_mode}; +use crate::type_translators::{translate_blend_mode, translate_key, translate_tool_type}; use crate::{EDITOR_HAS_CRASHED, EDITOR_INSTANCES}; use editor::consts::{FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION}; @@ -12,14 +12,13 @@ use editor::input::mouse::{EditorMouseState, ScrollDelta, ViewportBounds}; use editor::message_prelude::*; use editor::misc::EditorError; use editor::viewport_tools::tool::ToolType; -use editor::viewport_tools::tool_options::ToolOptions; use editor::viewport_tools::tools; use editor::Color; use editor::Editor; use editor::LayerId; use serde::Serialize; -use serde_wasm_bindgen; +use serde_wasm_bindgen::{self, from_value}; use std::sync::atomic::Ordering; use wasm_bindgen::prelude::*; @@ -105,19 +104,15 @@ impl JsEditorHandle { } } - /// Update the options for a given tool - pub fn set_tool_options(&self, tool: String, options: &JsValue) -> Result<(), JsValue> { - match serde_wasm_bindgen::from_value::(options.clone()) { - Ok(tool_options) => match translate_tool_type(&tool) { - Some(tool_type) => { - let message = ToolMessage::SetToolOptions { tool_type, tool_options }; - self.dispatch(message); - - Ok(()) - } - None => Err(Error::new(&format!("Couldn't set options for {} because it was not recognized as a valid tool", tool)).into()), - }, - Err(err) => Err(Error::new(&format!("Invalid JSON for ToolOptions: {}", err)).into()), + /// Update layout of a given UI + pub fn update_layout(&self, layout_target: JsValue, widget_id: u64, value: JsValue) -> Result<(), JsValue> { + match (from_value(layout_target), from_value(value)) { + (Ok(layout_target), Ok(value)) => { + let message = LayoutMessage::UpdateLayout { layout_target, widget_id, value }; + self.dispatch(message); + Ok(()) + } + _ => Err(Error::new("Could not update UI").into()), } } @@ -217,7 +212,6 @@ impl JsEditorHandle { self.dispatch(message); } - #[wasm_bindgen] pub fn request_about_graphite_dialog(&self) { let message = PortfolioMessage::RequestAboutGraphiteDialog; self.dispatch(message); @@ -225,7 +219,6 @@ impl JsEditorHandle { /// Send new bounds when document panel viewports get resized or moved within the editor /// [left, top, right, bottom]... - #[wasm_bindgen] pub fn bounds_of_viewports(&self, bounds_of_viewports: &[f64]) { let chunked: Vec<_> = bounds_of_viewports.chunks(4).map(ViewportBounds::from_slice).collect(); @@ -454,52 +447,6 @@ impl JsEditorHandle { self.dispatch(message); } - /// Set snapping on or off - pub fn set_snapping(&self, snap: bool) { - let message = DocumentMessage::SetSnapping { snap }; - self.dispatch(message); - } - - /// Set display of overlays on or off - pub fn set_overlays_visibility(&self, visible: bool) { - let message = DocumentMessage::SetOverlaysVisibility { visible }; - self.dispatch(message); - } - - /// Set the view mode to change the way layers are drawn in the viewport - pub fn set_view_mode(&self, view_mode: String) -> Result<(), JsValue> { - if let Some(view_mode) = translate_view_mode(view_mode.as_str()) { - self.dispatch(DocumentMessage::SetViewMode { view_mode }); - Ok(()) - } else { - Err(Error::new("Invalid view mode").into()) - } - } - - /// Sets the zoom to the value - pub fn set_canvas_zoom(&self, zoom_factor: f64) { - let message = MovementMessage::SetCanvasZoom { zoom_factor }; - self.dispatch(message); - } - - /// Zoom in to the next step - pub fn increase_canvas_zoom(&self) { - let message = MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }; - self.dispatch(message); - } - - /// Zoom out to the next step - pub fn decrease_canvas_zoom(&self) { - let message = MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }; - self.dispatch(message); - } - - /// Sets the rotation to the new value (in radians) - pub fn set_rotation(&self, angle_radians: f64) { - let message = MovementMessage::SetCanvasRotation { angle_radians }; - self.dispatch(message); - } - /// Translates document (in viewport coords) pub fn translate_canvas(&self, delta_x: f64, delta_y: f64) { let message = MovementMessage::TranslateCanvas { delta: (delta_x, delta_y).into() }; diff --git a/frontend/wasm/src/type_translators.rs b/frontend/wasm/src/type_translators.rs index 250eab3d5..ae34aecbb 100644 --- a/frontend/wasm/src/type_translators.rs +++ b/frontend/wasm/src/type_translators.rs @@ -3,7 +3,6 @@ use crate::helpers::match_string_to_enum; use editor::input::keyboard::Key; use editor::viewport_tools::tool::ToolType; use graphene::layers::blend_mode::BlendMode; -use graphene::layers::style::ViewMode; pub fn translate_tool_type(name: &str) -> Option { use ToolType::*; @@ -130,12 +129,3 @@ pub fn translate_key(name: &str) -> Key { _ => UnknownKey, } } - -pub fn translate_view_mode(name: &str) -> Option { - Some(match name { - "Normal" => ViewMode::Normal, - "Outline" => ViewMode::Outline, - "Pixels" => ViewMode::Pixels, - _ => return None, - }) -}