Skip to content

Commit 49db963

Browse files
4adexKeavon
andauthored
Improve Path tool layer selection behavior using double-click instead of single-click (#2794)
* Improve path tool layer selection behaviour * Fix layer selection behaviour * Fix layer double click selection behaviour * Code review --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent 5b5b369 commit 49db963

File tree

3 files changed

+139
-37
lines changed

3 files changed

+139
-37
lines changed

editor/src/messages/input_mapper/input_mappings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ pub fn input_mappings() -> Mapping {
225225
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
226226
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift, shrink_selection: Alt }),
227227
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift, shrink_selection: Alt }),
228-
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSmoothSharp),
228+
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::DoubleClick { extend_selection: Shift, shrink_selection: Alt }),
229229
entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }),
230230
entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }),
231231
entry!(KeyDown(ArrowRight); modifiers=[ArrowUp], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: -NUDGE_AMOUNT }),

editor/src/messages/tool/common_functionality/shape_editor.rs

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ impl SelectedLayerState {
9696
self.selected_segments.remove(&segment);
9797
}
9898

99+
pub fn deselect_all_points_in_layer(&mut self) {
100+
self.selected_points.clear();
101+
}
102+
103+
pub fn deselect_all_segments_in_layer(&mut self) {
104+
self.selected_segments.clear();
105+
}
106+
99107
pub fn clear_points(&mut self) {
100108
self.selected_points.clear();
101109
}
@@ -388,6 +396,10 @@ impl ClosestSegment {
388396

389397
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
390398
impl ShapeState {
399+
pub fn is_selected_layer(&self, layer: LayerNodeIdentifier) -> bool {
400+
self.selected_shape_state.contains_key(&layer)
401+
}
402+
391403
pub fn is_point_ignored(&self, point: &ManipulatorPointId) -> bool {
392404
(point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors)
393405
}
@@ -637,7 +649,7 @@ impl ShapeState {
637649
}
638650

639651
/// Selects all anchors connected to the selected subpath, and deselects all handles, for the given layer.
640-
pub fn select_connected_anchors(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, mouse: DVec2) {
652+
pub fn select_connected(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, mouse: DVec2, points: bool, segments: bool) {
641653
let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else {
642654
return;
643655
};
@@ -655,18 +667,39 @@ impl ShapeState {
655667
}
656668
}
657669
state.clear_points();
670+
658671
if selected_stack.is_empty() {
659-
// Fall back on just selecting all points in the layer
660-
for &point in vector_data.point_domain.ids() {
661-
state.select_point(ManipulatorPointId::Anchor(point))
672+
// Fall back on just selecting all points/segments in the layer
673+
if points {
674+
for &point in vector_data.point_domain.ids() {
675+
state.select_point(ManipulatorPointId::Anchor(point));
676+
}
662677
}
663-
} else {
664-
// Select all connected points
665-
while let Some(point) = selected_stack.pop() {
666-
let anchor_point = ManipulatorPointId::Anchor(point);
667-
if !state.is_point_selected(anchor_point) {
668-
state.select_point(anchor_point);
669-
selected_stack.extend(vector_data.connected_points(point));
678+
if segments {
679+
for &segment in vector_data.segment_domain.ids() {
680+
state.select_segment(segment);
681+
}
682+
}
683+
return;
684+
}
685+
686+
let mut connected_points = HashSet::new();
687+
688+
while let Some(point) = selected_stack.pop() {
689+
if !connected_points.contains(&point) {
690+
connected_points.insert(point);
691+
selected_stack.extend(vector_data.connected_points(point));
692+
}
693+
}
694+
695+
if points {
696+
connected_points.iter().for_each(|point| state.select_point(ManipulatorPointId::Anchor(*point)));
697+
}
698+
699+
if segments {
700+
for (id, _, start, end) in vector_data.segment_bezier_iter() {
701+
if connected_points.contains(&start) || connected_points.contains(&end) {
702+
state.select_segment(id);
670703
}
671704
}
672705
}

editor/src/messages/tool/tool_messages/path_tool.rs

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use super::select_tool::extend_lasso;
22
use super::tool_prelude::*;
33
use crate::consts::{
4-
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE, SEGMENT_INSERTION_DISTANCE,
5-
SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
4+
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, DOUBLE_CLICK_MILLISECONDS, DRAG_DIRECTION_MODE_DETERMINATION_THRESHOLD, DRAG_THRESHOLD, HANDLE_ROTATE_SNAP_ANGLE,
5+
SEGMENT_INSERTION_DISTANCE, SEGMENT_OVERLAY_SIZE, SELECTION_THRESHOLD, SELECTION_TOLERANCE,
66
};
77
use crate::messages::portfolio::document::overlays::utility_functions::{path_overlays, selected_segments};
88
use crate::messages::portfolio::document::overlays::utility_types::{DrawHandles, OverlayContext};
@@ -12,7 +12,7 @@ use crate::messages::portfolio::document::utility_types::transformation::Axis;
1212
use crate::messages::preferences::SelectionMode;
1313
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
1414
use crate::messages::tool::common_functionality::shape_editor::{
15-
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
15+
ClosestSegment, ManipulatorAngle, OpposingHandleLengths, SelectedLayerState, SelectedPointsInfo, SelectionChange, SelectionShape, SelectionShapeType, ShapeState,
1616
};
1717
use crate::messages::tool::common_functionality::snapping::{SnapCache, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager};
1818
use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate};
@@ -58,7 +58,10 @@ pub enum PathToolMessage {
5858
},
5959
Escape,
6060
ClosePath,
61-
FlipSmoothSharp,
61+
DoubleClick {
62+
extend_selection: Key,
63+
shrink_selection: Key,
64+
},
6265
GRS {
6366
// Should be `Key::KeyG` (Grab), `Key::KeyR` (Rotate), or `Key::KeyS` (Scale)
6467
key: Key,
@@ -319,7 +322,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
319322
fn actions(&self) -> ActionList {
320323
match self.fsm_state {
321324
PathToolFsmState::Ready => actions!(PathToolMessageDiscriminant;
322-
FlipSmoothSharp,
325+
DoubleClick,
323326
MouseDown,
324327
Delete,
325328
NudgeSelectedPoints,
@@ -334,7 +337,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
334337
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
335338
Escape,
336339
RightClick,
337-
FlipSmoothSharp,
340+
DoubleClick,
338341
DragStop,
339342
PointerMove,
340343
Delete,
@@ -343,7 +346,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
343346
SwapSelectedHandles,
344347
),
345348
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
346-
FlipSmoothSharp,
349+
DoubleClick,
347350
DragStop,
348351
PointerMove,
349352
Delete,
@@ -462,6 +465,8 @@ struct PathToolData {
462465
adjacent_anchor_offset: Option<DVec2>,
463466
sliding_point_info: Option<SlidingPointInfo>,
464467
started_drawing_from_inside: bool,
468+
first_selected_with_single_click: bool,
469+
stored_selection: Option<HashMap<LayerNodeIdentifier, SelectedLayerState>>,
465470
}
466471

467472
impl PathToolData {
@@ -544,8 +549,9 @@ impl PathToolData {
544549

545550
self.drag_start_pos = input.mouse.position;
546551

547-
if !self.saved_points_before_anchor_convert_smooth_sharp.is_empty() && (input.time - self.last_click_time > 500) {
552+
if input.time - self.last_click_time > DOUBLE_CLICK_MILLISECONDS {
548553
self.saved_points_before_anchor_convert_smooth_sharp.clear();
554+
self.stored_selection = None;
549555
}
550556

551557
self.last_click_time = input.time;
@@ -685,20 +691,18 @@ impl PathToolData {
685691
PathToolFsmState::MoldingSegment
686692
}
687693
}
688-
// We didn't find a segment, so consider selecting the nearest shape instead and start drawing
694+
// If no other layers are selected and this is a single-click, then also select the layer (exception)
689695
else if let Some(layer) = document.click(input) {
690-
shape_editor.deselect_all_points();
691-
shape_editor.deselect_all_segments();
692-
if extend_selection {
693-
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
694-
} else {
696+
if shape_editor.selected_shape_state.is_empty() {
697+
self.first_selected_with_single_click = true;
695698
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
696699
}
697-
self.drag_start_pos = input.mouse.position;
698-
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
699700

700701
self.started_drawing_from_inside = true;
701702

703+
self.drag_start_pos = input.mouse.position;
704+
self.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
705+
702706
let selection_shape = if lasso_select { SelectionShapeType::Lasso } else { SelectionShapeType::Box };
703707
PathToolFsmState::Drawing { selection_shape }
704708
}
@@ -1557,7 +1561,9 @@ impl Fsm for PathToolFsmState {
15571561
},
15581562
) => {
15591563
tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position);
1564+
15601565
tool_data.started_drawing_from_inside = false;
1566+
tool_data.stored_selection = None;
15611567

15621568
if selection_shape == SelectionShapeType::Lasso {
15631569
extend_lasso(&mut tool_data.lasso_polygon, input.mouse.position);
@@ -1604,6 +1610,7 @@ impl Fsm for PathToolFsmState {
16041610
break_colinear_molding,
16051611
},
16061612
) => {
1613+
tool_data.stored_selection = None;
16071614
let mut selected_only_handles = true;
16081615

16091616
let selected_points = shape_editor.selected_points();
@@ -1727,6 +1734,7 @@ impl Fsm for PathToolFsmState {
17271734
if tool_data.adjacent_anchor_offset.is_some() {
17281735
tool_data.adjacent_anchor_offset = None;
17291736
}
1737+
tool_data.stored_selection = None;
17301738

17311739
responses.add(OverlaysMessage::Draw);
17321740

@@ -1895,12 +1903,16 @@ impl Fsm for PathToolFsmState {
18951903
SelectionMode::Directional => tool_data.calculate_selection_mode_from_direction(document.metadata()),
18961904
selection_mode => selection_mode,
18971905
};
1906+
tool_data.started_drawing_from_inside = false;
18981907

18991908
if tool_data.drag_start_pos.distance(previous_mouse) < 1e-8 {
1900-
// If click happens inside of a shape then don't set selected nodes to empty
1901-
if document.click(input).is_none() {
1902-
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
1909+
// Clicked inside or outside the shape then deselect all of the points/segments
1910+
if document.click(input).is_some() && tool_data.stored_selection.is_none() {
1911+
tool_data.stored_selection = Some(shape_editor.selected_shape_state.clone());
19031912
}
1913+
1914+
shape_editor.deselect_all_points();
1915+
shape_editor.deselect_all_segments();
19041916
} else {
19051917
match selection_shape {
19061918
SelectionShapeType::Box => {
@@ -2072,8 +2084,8 @@ impl Fsm for PathToolFsmState {
20722084
shape_editor.delete_point_and_break_path(document, responses);
20732085
PathToolFsmState::Ready
20742086
}
2075-
(_, PathToolMessage::FlipSmoothSharp) => {
2076-
// Double-clicked on a point
2087+
(_, PathToolMessage::DoubleClick { extend_selection, shrink_selection }) => {
2088+
// Double-clicked on a point (flip smooth/sharp behavior)
20772089
let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD);
20782090
if nearest_point.is_some() {
20792091
// Flip the selected point between smooth and sharp
@@ -2090,13 +2102,70 @@ impl Fsm for PathToolFsmState {
20902102

20912103
return PathToolFsmState::Ready;
20922104
}
2093-
20942105
// Double-clicked on a filled region
2095-
if let Some(layer) = document.click(input) {
2096-
// Select all points in the layer
2097-
shape_editor.select_connected_anchors(document, layer, input.mouse.position);
2106+
else if let Some(layer) = document.click(input) {
2107+
let extend_selection = input.keyboard.get(extend_selection as usize);
2108+
let shrink_selection = input.keyboard.get(shrink_selection as usize);
2109+
2110+
if shape_editor.is_selected_layer(layer) {
2111+
if extend_selection && !tool_data.first_selected_with_single_click {
2112+
responses.add(NodeGraphMessage::SelectedNodesRemove { nodes: vec![layer.to_node()] });
2113+
2114+
if let Some(selection) = &tool_data.stored_selection {
2115+
let mut selection = selection.clone();
2116+
selection.remove(&layer);
2117+
shape_editor.selected_shape_state = selection;
2118+
tool_data.stored_selection = None;
2119+
}
2120+
} else if shrink_selection && !tool_data.first_selected_with_single_click {
2121+
// Only deselect all the points of the double clicked layer
2122+
if let Some(selection) = &tool_data.stored_selection {
2123+
let selection = selection.clone();
2124+
shape_editor.selected_shape_state = selection;
2125+
tool_data.stored_selection = None;
2126+
}
2127+
2128+
let state = shape_editor.selected_shape_state.get_mut(&layer).expect("No state for selected layer");
2129+
state.deselect_all_points_in_layer();
2130+
state.deselect_all_segments_in_layer();
2131+
} else if !tool_data.first_selected_with_single_click {
2132+
// Select according to the selected editing mode
2133+
let point_editing_mode = tool_options.path_editing_mode.point_editing_mode;
2134+
let segment_editing_mode = tool_options.path_editing_mode.segment_editing_mode;
2135+
shape_editor.select_connected(document, layer, input.mouse.position, point_editing_mode, segment_editing_mode);
2136+
2137+
// Select all the other layers back again
2138+
if let Some(selection) = &tool_data.stored_selection {
2139+
let mut selection = selection.clone();
2140+
selection.remove(&layer);
2141+
2142+
for (layer, state) in selection {
2143+
shape_editor.selected_shape_state.insert(layer, state);
2144+
}
2145+
tool_data.stored_selection = None;
2146+
}
2147+
}
2148+
2149+
// If it was the very first click without there being an existing selection,
2150+
// then the single-click behavior and double-click behavior should not collide
2151+
tool_data.first_selected_with_single_click = false;
2152+
} else if extend_selection {
2153+
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
2154+
2155+
if let Some(selection) = &tool_data.stored_selection {
2156+
shape_editor.selected_shape_state = selection.clone();
2157+
tool_data.stored_selection = None;
2158+
}
2159+
} else {
2160+
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
2161+
}
2162+
20982163
responses.add(OverlaysMessage::Draw);
20992164
}
2165+
// Double clicked on the background
2166+
else {
2167+
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
2168+
}
21002169

21012170
PathToolFsmState::Ready
21022171
}

0 commit comments

Comments
 (0)