Skip to content

Commit ffd409e

Browse files
elbertronnie0HyperCubeKeavon
committed
Add auto-panning when pointer goes beyond viewport edge with Select tool (#1625)
* Add code to shift viewport if mouse is beyond edge * Allow shifting viewport if mouse is stationary too * Group all modifier keys into SelectToolPointerKeys * Cleanup message ordering to remove shifting during resize * Slow down shifting by half * Clamp speed; code review cleanup --------- Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
1 parent feff26e commit ffd409e

5 files changed

Lines changed: 154 additions & 20 deletions

File tree

editor/src/consts.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.;
1818

1919
pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f64 = 0.95;
2020

21+
pub const DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS: f64 = 50.;
22+
pub const DRAG_BEYOND_VIEWPORT_SPEED_FACTOR: f64 = 0.5;
23+
2124
// Snapping point
2225
pub const SNAP_POINT_TOLERANCE: f64 = 5.;
2326

editor/src/messages/broadcast/broadcast_event.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
55
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)]
66
#[impl_message(Message, BroadcastMessage, TriggerEvent)]
77
pub enum BroadcastEvent {
8+
AnimationFrame,
89
CanvasTransformed,
910
ToolAbort,
1011
SelectionChanged,

editor/src/messages/input_mapper/default_mapping.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
99
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
1010
use crate::messages::prelude::*;
1111
use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate;
12+
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
1213

1314
use glam::DVec2;
1415

@@ -71,7 +72,7 @@ pub fn default_mapping() -> Mapping {
7172
entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, snap_key: Control }),
7273
//
7374
// SelectToolMessage
74-
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt }),
75+
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })),
7576
entry!(KeyDown(Lmb); action_dispatch=SelectToolMessage::DragStart { add_to_selection: Shift, select_deepest: Accel }),
7677
entry!(KeyUp(Lmb); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Shift }),
7778
entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter),

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

Lines changed: 142 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use super::tool_prelude::*;
44
use crate::application::generate_uuid;
5-
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
5+
use crate::consts::{DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS, DRAG_BEYOND_VIEWPORT_SPEED_FACTOR, ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
66
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
77
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
88
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@@ -52,6 +52,14 @@ impl fmt::Display for NestedSelectionBehavior {
5252
}
5353
}
5454

55+
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
56+
pub struct SelectToolPointerKeys {
57+
pub axis_align: Key,
58+
pub snap_angle: Key,
59+
pub center: Key,
60+
pub duplicate: Key,
61+
}
62+
5563
#[remain::sorted]
5664
#[impl_message(Message, ToolMessage, Select)]
5765
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
@@ -72,16 +80,13 @@ pub enum SelectToolMessage {
7280
},
7381
EditLayer,
7482
Enter,
75-
PointerMove {
76-
axis_align: Key,
77-
snap_angle: Key,
78-
center: Key,
79-
duplicate: Key,
80-
},
83+
PointerMove(SelectToolPointerKeys),
84+
PointerOutsideViewport(SelectToolPointerKeys),
8185
SelectOptions(SelectOptionsUpdate),
8286
SetPivot {
8387
position: PivotPosition,
8488
},
89+
ShiftViewport,
8590
}
8691

8792
impl ToolMetadata for SelectTool {
@@ -269,6 +274,7 @@ struct SelectToolData {
269274
selected_layers_count: usize,
270275
selected_layers_changed: bool,
271276
snap_candidates: Vec<SnapCandidatePoint>,
277+
subscribed_to_animation_frame: bool,
272278
}
273279

274280
impl SelectToolData {
@@ -590,16 +596,16 @@ impl Fsm for SelectToolFsmState {
590596

591597
state
592598
}
593-
(SelectToolFsmState::Dragging, SelectToolMessage::PointerMove { axis_align, duplicate, .. }) => {
599+
(SelectToolFsmState::Dragging, SelectToolMessage::PointerMove(modifier_keys)) => {
594600
tool_data.has_dragged = true;
595601

596-
if input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_none() {
602+
if input.keyboard.key(modifier_keys.duplicate) && tool_data.non_duplicated_layers.is_none() {
597603
tool_data.start_duplicates(document, responses);
598-
} else if !input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_some() {
604+
} else if !input.keyboard.key(modifier_keys.duplicate) && tool_data.non_duplicated_layers.is_some() {
599605
tool_data.stop_duplicates(document, responses);
600606
}
601607

602-
let axis_align = input.keyboard.key(axis_align);
608+
let axis_align = input.keyboard.key(modifier_keys.axis_align);
603609
let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start);
604610
let total_mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_start);
605611

@@ -644,12 +650,14 @@ impl Fsm for SelectToolFsmState {
644650
}
645651
tool_data.drag_current += mouse_delta;
646652

653+
setup_pointer_outside_edge_event(input.mouse.position, input.viewport_bounds.size(), tool_data, modifier_keys, responses);
654+
647655
SelectToolFsmState::Dragging
648656
}
649-
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { axis_align, center, .. }) => {
650-
if let Some(bounds) = &mut tool_data.bounding_box_manager {
657+
(SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => {
658+
if let Some(ref mut bounds) = &mut tool_data.bounding_box_manager {
651659
if let Some(movement) = &mut bounds.selected_edges {
652-
let (center, constrain) = (input.keyboard.key(center), input.keyboard.key(axis_align));
660+
let (center, constrain) = (input.keyboard.key(modifier_keys.center), input.keyboard.key(modifier_keys.axis_align));
653661

654662
let center = center.then_some(bounds.center_of_transformation);
655663
let snap = Some(SizeSnapData {
@@ -677,11 +685,13 @@ impl Fsm for SelectToolFsmState {
677685
);
678686

679687
selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse());
688+
689+
setup_pointer_outside_edge_event(input.mouse.position, input.viewport_bounds.size(), tool_data, modifier_keys, responses);
680690
}
681691
}
682692
SelectToolFsmState::ResizingBounds
683693
}
684-
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { snap_angle, .. }) => {
694+
(SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(modifier_keys)) => {
685695
if let Some(bounds) = &mut tool_data.bounding_box_manager {
686696
let angle = {
687697
let start_offset = tool_data.drag_start - bounds.center_of_transformation;
@@ -690,7 +700,7 @@ impl Fsm for SelectToolFsmState {
690700
start_offset.angle_between(end_offset)
691701
};
692702

693-
let snapped_angle = if input.keyboard.key(snap_angle) {
703+
let snapped_angle = if input.keyboard.key(modifier_keys.snap_angle) {
694704
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
695705
(angle / snap_resolution).round() * snap_resolution
696706
} else {
@@ -716,20 +726,24 @@ impl Fsm for SelectToolFsmState {
716726

717727
SelectToolFsmState::RotatingBounds
718728
}
719-
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { .. }) => {
729+
(SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove(modifier_keys)) => {
720730
let mouse_position = input.mouse.position;
721731
let snapped_mouse_position = mouse_position; //tool_data.snap_manager.snap_position(responses, document, mouse_position);
722732
tool_data.pivot.set_viewport_position(snapped_mouse_position, document, responses);
723733

734+
setup_pointer_outside_edge_event(mouse_position, input.viewport_bounds.size(), tool_data, modifier_keys, responses);
735+
724736
SelectToolFsmState::DraggingPivot
725737
}
726-
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove { .. }) => {
738+
(SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove(modifier_keys)) => {
727739
tool_data.drag_current = input.mouse.position;
728740
responses.add(OverlaysMessage::Draw);
729741

742+
setup_pointer_outside_edge_event(input.mouse.position, input.viewport_bounds.size(), tool_data, modifier_keys, responses);
743+
730744
SelectToolFsmState::DrawingBox
731745
}
732-
(SelectToolFsmState::Ready, SelectToolMessage::PointerMove { .. }) => {
746+
(SelectToolFsmState::Ready, SelectToolMessage::PointerMove(_)) => {
733747
let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
734748

735749
// Dragging the pivot overrules the other operations
@@ -747,6 +761,50 @@ impl Fsm for SelectToolFsmState {
747761

748762
SelectToolFsmState::Ready
749763
}
764+
(SelectToolFsmState::Dragging, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => {
765+
responses.add(SelectToolMessage::PointerMove(modifier_keys));
766+
767+
if let Some(shift) = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses) {
768+
tool_data.drag_current += shift;
769+
tool_data.drag_start += shift;
770+
}
771+
772+
SelectToolFsmState::Dragging
773+
}
774+
(SelectToolFsmState::ResizingBounds | SelectToolFsmState::DraggingPivot | SelectToolFsmState::DrawingBox, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => {
775+
responses.add(SelectToolMessage::PointerMove(modifier_keys));
776+
777+
responses.add(SelectToolMessage::ShiftViewport);
778+
779+
self
780+
}
781+
(SelectToolFsmState::ResizingBounds, SelectToolMessage::ShiftViewport) => {
782+
if let Some(shift) = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses) {
783+
if let Some(ref mut bounds) = &mut tool_data.bounding_box_manager {
784+
bounds.center_of_transformation += shift;
785+
bounds.original_bound_transform.translation += shift;
786+
}
787+
}
788+
789+
self
790+
}
791+
(SelectToolFsmState::DraggingPivot, SelectToolMessage::ShiftViewport) => {
792+
let _ = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses);
793+
794+
self
795+
}
796+
(SelectToolFsmState::DrawingBox, SelectToolMessage::ShiftViewport) => {
797+
if let Some(shift) = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses) {
798+
tool_data.drag_start += shift;
799+
}
800+
801+
self
802+
}
803+
(state, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => {
804+
unsubscribe_animation_frame(tool_data, modifier_keys, responses);
805+
806+
state
807+
}
750808
(SelectToolFsmState::Dragging, SelectToolMessage::Enter) => {
751809
let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON {
752810
true => DocumentMessage::Undo,
@@ -1011,3 +1069,68 @@ fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document_network:
10111069
responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path });
10121070
}
10131071
}
1072+
1073+
/// Shifts the viewport when the mouse reaches the edge of the viewport.
1074+
///
1075+
/// If the mouse was beyond any edge, it returns the amount shifted. Otherwise it returns None.
1076+
/// The shift is proportional to the distance between edge and mouse. It is also guaranteed to be integral.
1077+
fn shift_viewport_if_mouse_beyond_edge(mouse_position: DVec2, viewport_size: DVec2, responses: &mut VecDeque<Message>) -> Option<DVec2> {
1078+
let mouse_position = mouse_position.clamp(
1079+
DVec2::ZERO - DVec2::splat(DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS),
1080+
viewport_size + DVec2::splat(DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS),
1081+
);
1082+
let mouse_position_percent = mouse_position / viewport_size;
1083+
1084+
let mut shift_percent = DVec2::ZERO;
1085+
1086+
if mouse_position_percent.x < 0. {
1087+
shift_percent.x = -mouse_position_percent.x;
1088+
} else if mouse_position_percent.x > 1. {
1089+
shift_percent.x = 1. - mouse_position_percent.x;
1090+
}
1091+
1092+
if mouse_position_percent.y < 0. {
1093+
shift_percent.y = -mouse_position_percent.y;
1094+
} else if mouse_position_percent.y > 1. {
1095+
shift_percent.y = 1. - mouse_position_percent.y;
1096+
}
1097+
1098+
if shift_percent.x == 0. && shift_percent.y == 0. {
1099+
return None;
1100+
}
1101+
1102+
let delta = (shift_percent * DRAG_BEYOND_VIEWPORT_SPEED_FACTOR * viewport_size).round();
1103+
responses.add(NavigationMessage::TranslateCanvas { delta });
1104+
Some(delta)
1105+
}
1106+
1107+
fn setup_pointer_outside_edge_event(mouse_position: DVec2, viewport_size: DVec2, tool_data: &mut SelectToolData, modifier_keys: SelectToolPointerKeys, responses: &mut VecDeque<Message>) {
1108+
let is_pointer_outside_edge = mouse_position.x < 0. || mouse_position.x > viewport_size.x || mouse_position.y < 0. || mouse_position.y > viewport_size.y;
1109+
1110+
match is_pointer_outside_edge {
1111+
true => subscribe_animation_frame(tool_data, modifier_keys, responses),
1112+
false => unsubscribe_animation_frame(tool_data, modifier_keys, responses),
1113+
}
1114+
}
1115+
1116+
fn subscribe_animation_frame(tool_data: &mut SelectToolData, modifier_keys: SelectToolPointerKeys, responses: &mut VecDeque<Message>) {
1117+
if !tool_data.subscribed_to_animation_frame {
1118+
tool_data.subscribed_to_animation_frame = true;
1119+
1120+
responses.add(BroadcastMessage::SubscribeEvent {
1121+
on: BroadcastEvent::AnimationFrame,
1122+
send: Box::new(SelectToolMessage::PointerOutsideViewport(modifier_keys).into()),
1123+
});
1124+
}
1125+
}
1126+
1127+
fn unsubscribe_animation_frame(tool_data: &mut SelectToolData, modifier_keys: SelectToolPointerKeys, responses: &mut VecDeque<Message>) {
1128+
if tool_data.subscribed_to_animation_frame {
1129+
tool_data.subscribed_to_animation_frame = false;
1130+
1131+
responses.add(BroadcastMessage::UnsubscribeEvent {
1132+
on: BroadcastEvent::AnimationFrame,
1133+
message: Box::new(SelectToolMessage::PointerOutsideViewport(modifier_keys).into()),
1134+
});
1135+
}
1136+
}

frontend/wasm/src/editor_api.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ impl JsEditorHandle {
239239
*g.borrow_mut() = Some(Closure::new(move || {
240240
wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation());
241241

242+
call_closure_with_editor_and_handle(|editor, handle| {
243+
for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) {
244+
handle.send_frontend_message_to_js(message);
245+
}
246+
});
247+
242248
// Schedule ourself for another requestAnimationFrame callback
243249
request_animation_frame(f.borrow().as_ref().unwrap());
244250
}));

0 commit comments

Comments
 (0)