From 743803ce048084eb60dc3b08e0a2cd1eb81aa702 Mon Sep 17 00:00:00 2001 From: Dhruv Date: Sat, 15 Jul 2023 15:07:18 +0530 Subject: [PATCH] Add Snapping Options to the Snap Dropdown Menu (#1321) * [wip]feat: add snapping options * [wip]fix: use svelte component for optionsWidget * fix: use apt PopoverButton types * refactor: minor formatting improvements * Fix popover layout * [wip]feat: attempt implementing CheckboxInputData struct * fix: use correct Checkbox struct 's default method * fix: revert adding CheckboxInputData struct - This reverts commit 2a481887fc89a94a459ef57ba4ab3024d3b60aa1. * feat: use checkboxes for snapping options * feat: add label to dropdown checkbox elements * fix: separate Snap dropdown menu elements - move each element into separate row * [wip]feat: modularize snapping states - maintain individual snapping states for document * fix: snapping checkboxes' behavior - checkboxes now update internal snapping state * refactor: update snap states individually - this prevents out-of-sync states - enables reusing existing snap state object * feat: snap to boxes and nodes conditionally * [wip]feat: attempt to invert checkbox on update - attempt implementing mutable WidgetCallback struct - attempt using above struct to invert checkbox state on update * Fix widget diffing * refactor: remove unused code * feat: align checkboxes consistently with labels * feat: use separators to stylize snapping menu - removes need for custom CSS and label property - ensures consistency across the application * refactor: remove unneeded css --------- Co-authored-by: hypercube <0hypercube@gmail.com>, TrueDoctor --- .../messages/layout/layout_message_handler.rs | 4 + .../layout/utility_types/layout_widget.rs | 12 +++ .../utility_types/widgets/button_widgets.rs | 4 + .../portfolio/document/document_message.rs | 4 +- .../document/document_message_handler.rs | 101 ++++++++++++++++-- .../document_node_types.rs | 2 +- .../portfolio/document/utility_types/misc.rs | 33 ++++++ .../tool/common_functionality/snapping.rs | 16 ++- .../components/widgets/WidgetLayout.svelte | 2 +- .../src/components/widgets/WidgetRow.svelte | 9 +- .../widgets/buttons/PopoverButton.svelte | 3 +- frontend/src/wasm-communication/messages.ts | 55 ++++++---- 12 files changed, 204 insertions(+), 41 deletions(-) diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index cd02b8ff..cc4a77c2 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -31,6 +31,10 @@ impl LayoutMessageHandler { widget_path.push(index); return Some((widget, widget_path)); } + + if let Widget::PopoverButton(popover) = &widget.widget { + stack.extend(popover.options_widget.iter().enumerate().map(|(child, val)| ([widget_path.as_slice(), &[index, child]].concat(), val))); + } } } // A section contains more LayoutGroups which we add to the stack. diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 086f3d18..bb565df6 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -241,6 +241,12 @@ impl<'a> Iterator for WidgetIter<'a> { fn next(&mut self) -> Option { if let Some(item) = self.current_slice.and_then(|slice| slice.first()) { self.current_slice = Some(&self.current_slice.unwrap()[1..]); + + if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = item { + self.stack.extend(p.options_widget.iter()); + return self.next(); + } + return Some(item); } @@ -276,6 +282,12 @@ impl<'a> Iterator for WidgetIterMut<'a> { fn next(&mut self) -> Option { if let Some((first, rest)) = self.current_slice.take().and_then(|slice| slice.split_first_mut()) { self.current_slice = Some(rest); + + if let WidgetHolder { widget: Widget::PopoverButton(p), .. } = first { + self.stack.extend(p.options_widget.iter_mut()); + return self.next(); + } + return Some(first); }; diff --git a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs index 640ec8a5..69282351 100644 --- a/editor/src/messages/layout/utility_types/widgets/button_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/button_widgets.rs @@ -1,5 +1,6 @@ use crate::messages::input_mapper::utility_types::misc::ActionKeys; use crate::messages::layout::utility_types::layout_widget::WidgetCallback; +use crate::messages::layout::utility_types::widget_prelude::SubLayout; use crate::messages::portfolio::document::node_graph::FrontendGraphDataType; use graphite_proc_macros::WidgetBuilder; @@ -48,6 +49,9 @@ pub struct PopoverButton { pub tooltip: String, + #[serde(rename = "optionsWidget")] + pub options_widget: SubLayout, + #[serde(skip)] pub tooltip_shortcut: Option, } diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index a609f7d8..a4c94ce7 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -171,7 +171,9 @@ pub enum DocumentMessage { replacement_selected_layers: Vec>, }, SetSnapping { - snap: bool, + snapping_enabled: Option, + bounding_box_snapping: Option, + node_snapping: Option, }, SetViewMode { view_mode: ViewMode, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index a5762dbf..7d59bcb4 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1,5 +1,5 @@ use super::utility_types::error::EditorError; -use super::utility_types::misc::DocumentRenderMode; +use super::utility_types::misc::{DocumentRenderMode, SnappingOptions, SnappingState}; use crate::application::generate_uuid; use crate::consts::{ASYMPTOTIC_EFFECT, DEFAULT_DOCUMENT_NAME, FILE_SAVE_SUFFIX, GRAPHITE_DOCUMENT_VERSION, SCALE_EFFECT, SCROLLBAR_SPACING, VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR}; use crate::messages::frontend::utility_types::ExportBounds; @@ -7,6 +7,7 @@ use crate::messages::frontend::utility_types::FileType; use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; +use crate::messages::layout::utility_types::widget_prelude::{CheckboxInput, TextLabel}; use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton}; use crate::messages::layout::utility_types::widgets::input_widgets::{ DropdownEntryData, DropdownInput, NumberInput, NumberInputIncrementBehavior, NumberInputMode, OptionalInput, RadioEntryData, RadioInput, @@ -48,7 +49,8 @@ pub struct DocumentMessageHandler { pub document_mode: DocumentMode, pub view_mode: ViewMode, - pub snapping_enabled: bool, + #[serde(skip)] + pub snapping_state: SnappingState, pub overlays_visible: bool, #[serde(skip)] @@ -83,7 +85,7 @@ impl Default for DocumentMessageHandler { document_mode: DocumentMode::DesignMode, view_mode: ViewMode::default(), - snapping_enabled: true, + snapping_state: SnappingState::default(), overlays_visible: true, document_undo_history: VecDeque::new(), @@ -845,8 +847,20 @@ impl MessageHandler { - self.snapping_enabled = snap; + SetSnapping { + snapping_enabled, + bounding_box_snapping, + node_snapping, + } => { + if let Some(state) = snapping_enabled { + self.snapping_state.snapping_enabled = state + }; + if let Some(state) = bounding_box_snapping { + self.snapping_state.bounding_box_snapping = state + } + if let Some(state) = node_snapping { + self.snapping_state.node_snapping = state + }; } SetViewMode { view_mode } => { self.view_mode = view_mode; @@ -1553,17 +1567,88 @@ impl DocumentMessageHandler { } pub fn update_document_widgets(&self, responses: &mut VecDeque) { + let snapping_state = self.snapping_state.clone(); let mut widgets = vec![ WidgetHolder::new(Widget::OptionalInput(OptionalInput { - checked: self.snapping_enabled, + checked: snapping_state.snapping_enabled, icon: "Snapping".into(), tooltip: "Snapping".into(), - on_update: WidgetCallback::new(|optional_input: &OptionalInput| DocumentMessage::SetSnapping { snap: optional_input.checked }.into()), + on_update: WidgetCallback::new(move |optional_input: &OptionalInput| { + let snapping_enabled = optional_input.checked; + DocumentMessage::SetSnapping { + snapping_enabled: Some(snapping_enabled), + bounding_box_snapping: Some(snapping_state.bounding_box_snapping), + node_snapping: Some(snapping_state.node_snapping), + } + .into() + }), ..Default::default() })), WidgetHolder::new(Widget::PopoverButton(PopoverButton { header: "Snapping".into(), - text: "Coming soon".into(), + text: "Select the vectors to snap to.".into(), // TODO: check whether this is an apt description + options_widget: vec![ + LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { + tooltip: SnappingOptions::BoundingBoxes.to_string(), + checked: snapping_state.bounding_box_snapping, + on_update: WidgetCallback::new(move |input: &CheckboxInput| { + DocumentMessage::SetSnapping { + snapping_enabled: None, + bounding_box_snapping: Some(input.checked), + node_snapping: None, + } + .into() + }), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Unrelated, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: SnappingOptions::BoundingBoxes.to_string(), + table_align: false, + min_width: 60, + ..Default::default() + })), + // adds appropriate space between row elements + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Vertical, + separator_type: SeparatorType::Related, + })), + ], + }, + LayoutGroup::Row { + widgets: vec![ + WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { + checked: self.snapping_state.node_snapping, + tooltip: SnappingOptions::Points.to_string(), + on_update: WidgetCallback::new(|input: &CheckboxInput| { + DocumentMessage::SetSnapping { + snapping_enabled: None, + bounding_box_snapping: None, + node_snapping: Some(input.checked), + } + .into() + }), + ..Default::default() + })), + WidgetHolder::new(Widget::Separator(Separator { + direction: SeparatorDirection::Horizontal, + separator_type: SeparatorType::Unrelated, + })), + WidgetHolder::new(Widget::TextLabel(TextLabel { + value: SnappingOptions::Points.to_string(), + table_align: false, + min_width: 60, + ..Default::default() + })), + ], + }, + ], + ..Default::default() })), WidgetHolder::new(Widget::Separator(Separator { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index 73416371..bd5adb04 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -1866,7 +1866,7 @@ pub fn collect_node_types() -> Vec { impl DocumentNodeType { /// Generate a [`DocumentNodeImplementation`] from this node type, using a nested network. pub fn generate_implementation(&self) -> DocumentNodeImplementation { - let num_inputs = self.inputs.len(); + // let num_inputs = self.inputs.len(); let inner_network = match &self.identifier { NodeImplementation::DocumentNode(network) => network.clone(), diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 135495b6..67f4044f 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -74,3 +74,36 @@ pub enum DocumentRenderMode<'a> { OnlyBelowLayerInFolder(&'a [LayerId]), LayerCutout(&'a [LayerId], Color), } + +#[derive(Clone, Debug, Serialize, Deserialize)] +/// SnappingState determines the current individual snapping states +pub struct SnappingState { + pub snapping_enabled: bool, + pub bounding_box_snapping: bool, + pub node_snapping: bool, +} + +impl Default for SnappingState { + fn default() -> Self { + Self { + snapping_enabled: true, + bounding_box_snapping: true, + node_snapping: true, + } + } +} + +// TODO: implement icons for SnappingOptions eventually +pub enum SnappingOptions { + BoundingBoxes, + Points, +} + +impl fmt::Display for SnappingOptions { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SnappingOptions::BoundingBoxes => write!(f, "Bounding Boxes"), + SnappingOptions::Points => write!(f, "Points"), + } + } +} diff --git a/editor/src/messages/tool/common_functionality/snapping.rs b/editor/src/messages/tool/common_functionality/snapping.rs index a11310f3..96052ab7 100644 --- a/editor/src/messages/tool/common_functionality/snapping.rs +++ b/editor/src/messages/tool/common_functionality/snapping.rs @@ -216,7 +216,9 @@ impl SnapManager { snap_x: bool, snap_y: bool, ) { - if document_message_handler.snapping_enabled { + let snapping_enabled = document_message_handler.snapping_state.snapping_enabled; + let bounding_box_snapping = document_message_handler.snapping_state.bounding_box_snapping; + if snapping_enabled && bounding_box_snapping { self.snap_x = snap_x; self.snap_y = snap_y; @@ -235,7 +237,9 @@ impl SnapManager { /// /// This should be called after start_snap pub fn add_snap_points(&mut self, document_message_handler: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, snap_points: impl Iterator) { - if document_message_handler.snapping_enabled { + let snapping_enabled = document_message_handler.snapping_state.snapping_enabled; + let node_snapping = document_message_handler.snapping_state.node_snapping; + if snapping_enabled && node_snapping { let snap_points = snap_points.filter(|&pos| pos.x >= 0. && pos.y >= 0. && pos.x < input.viewport_bounds.size().x && pos.y <= input.viewport_bounds.size().y); if let Some(targets) = &mut self.point_targets { targets.extend(snap_points); @@ -259,6 +263,10 @@ impl SnapManager { ) { let Some(vector_data) = &layer.as_vector_data() else { return }; + if !document_message_handler.snapping_state.node_snapping { + return; + }; + let transform = document_message_handler.document_legacy.multiply_transforms(path).unwrap(); let snap_points = vector_data .manipulator_groups() @@ -299,7 +307,7 @@ impl SnapManager { /// Finds the closest snap from an array of layers to the specified snap targets in viewport coords. /// Returns 0 for each axis that there is no snap less than the snap tolerance. pub fn snap_layers(&mut self, responses: &mut VecDeque, document_message_handler: &DocumentMessageHandler, snap_anchors: Vec, mouse_delta: DVec2) -> DVec2 { - if document_message_handler.snapping_enabled { + if document_message_handler.snapping_state.snapping_enabled { self.calculate_snap(snap_anchors.iter().map(move |&snap| mouse_delta + snap), responses) } else { DVec2::ZERO @@ -308,7 +316,7 @@ impl SnapManager { /// Handles snapping of a viewport position, returning another viewport position. pub fn snap_position(&mut self, responses: &mut VecDeque, document_message_handler: &DocumentMessageHandler, position_viewport: DVec2) -> DVec2 { - if document_message_handler.snapping_enabled { + if document_message_handler.snapping_state.snapping_enabled { self.calculate_snap([position_viewport].into_iter(), responses) + position_viewport } else { position_viewport diff --git a/frontend/src/components/widgets/WidgetLayout.svelte b/frontend/src/components/widgets/WidgetLayout.svelte index dcd265ed..deb6e992 100644 --- a/frontend/src/components/widgets/WidgetLayout.svelte +++ b/frontend/src/components/widgets/WidgetLayout.svelte @@ -1,5 +1,5 @@