diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index a3405fe5b3..429852e9e6 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -859,9 +859,8 @@ impl MessageHandler for DocumentMessageHand ); responses.push_back(ArtboardMessage::RenderArtboards.into()); - let document_transform = &self.movement_handler; - - let scale = 0.5 + ASYMPTOTIC_EFFECT + document_transform.scale * SCALE_EFFECT; + let document_transform_scale = self.movement_handler.snapped_scale(); + let scale = 0.5 + ASYMPTOTIC_EFFECT + document_transform_scale * SCALE_EFFECT; let viewport_size = ipp.viewport_bounds.size(); let viewport_mid = ipp.viewport_bounds.center(); let [bounds1, bounds2] = self.graphene_document.visible_layers_bounding_box().unwrap_or([viewport_mid; 2]); @@ -872,9 +871,9 @@ impl MessageHandler for DocumentMessageHand let scrollbar_multiplier = bounds_length - viewport_size; let scrollbar_size = viewport_size / bounds_length; - let log = document_transform.scale.log2(); + let log = document_transform_scale.log2(); let ruler_interval = if log < 0. { 100. * 2_f64.powf(-log.ceil()) } else { 100. / 2_f64.powf(log.ceil()) }; - let ruler_spacing = ruler_interval * document_transform.scale; + let ruler_spacing = ruler_interval * document_transform_scale; let ruler_origin = self.graphene_document.root.transform.transform_point2(DVec2::ZERO); diff --git a/editor/src/document/movement_handler.rs b/editor/src/document/movement_handler.rs index 0023f2b6c3..071e0cdda4 100644 --- a/editor/src/document/movement_handler.rs +++ b/editor/src/document/movement_handler.rs @@ -17,16 +17,27 @@ use std::collections::VecDeque; #[impl_message(Message, DocumentMessage, Movement)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize)] pub enum MovementMessage { - MouseMove { snap_angle: Key }, + MouseMove { + snap_angle: Key, + wait_for_snap_angle_release: bool, + snap_zoom: Key, + zoom_from_viewport: Option, + }, TranslateCanvasBegin, - WheelCanvasTranslate { use_y_as_x: bool }, + WheelCanvasTranslate { + use_y_as_x: bool, + }, RotateCanvasBegin, ZoomCanvasBegin, TransformCanvasEnd, SetCanvasRotation(f64), SetCanvasZoom(f64), - IncreaseCanvasZoom, - DecreaseCanvasZoom, + IncreaseCanvasZoom { + center_on_mouse: bool, + }, + DecreaseCanvasZoom { + center_on_mouse: bool, + }, WheelCanvasZoom, ZoomCanvasToFitAll, TranslateCanvas(DVec2), @@ -35,27 +46,37 @@ pub enum MovementMessage { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct MovementMessageHandler { - translating: bool, - pub translation: DVec2, - rotating: bool, - pub rotation: f64, + pub pan: DVec2, + panning: bool, + snap_tilt: bool, + snap_tilt_released: bool, + + pub tilt: f64, + tilting: bool, + + pub zoom: f64, zooming: bool, - pub scale: f64, - snap_rotate: bool, - mouse_pos: ViewportPosition, + snap_zoom: bool, + + mouse_position: ViewportPosition, } impl Default for MovementMessageHandler { fn default() -> Self { Self { - scale: 1., - translating: false, - translation: DVec2::ZERO, - rotating: false, - rotation: 0., + pan: DVec2::ZERO, + panning: false, + snap_tilt: false, + snap_tilt_released: false, + + tilt: 0., + tilting: false, + + zoom: 1., zooming: false, - snap_rotate: false, - mouse_pos: ViewportPosition::default(), + snap_zoom: false, + + mouse_position: ViewportPosition::default(), } } } @@ -63,25 +84,36 @@ impl Default for MovementMessageHandler { impl MovementMessageHandler { pub fn snapped_angle(&self) -> f64 { let increment_radians: f64 = VIEWPORT_ROTATE_SNAP_INTERVAL.to_radians(); - if self.snap_rotate { - (self.rotation / increment_radians).round() * increment_radians + if self.snap_tilt { + (self.tilt / increment_radians).round() * increment_radians } else { - self.rotation + self.tilt } } + + pub fn snapped_scale(&self) -> f64 { + if self.snap_zoom { + *VIEWPORT_ZOOM_LEVELS + .iter() + .min_by(|a, b| (**a - self.zoom).abs().partial_cmp(&(**b - self.zoom).abs()).unwrap()) + .unwrap_or(&self.zoom) + } else { + self.zoom + } + } + pub fn calculate_offset_transform(&self, offset: DVec2) -> DAffine2 { // TODO: replace with DAffine2::from_scale_angle_translation and fix the errors let offset_transform = DAffine2::from_translation(offset); - let scale_transform = DAffine2::from_scale(DVec2::new(self.scale, self.scale)); + let scale_transform = DAffine2::from_scale(DVec2::splat(self.snapped_scale())); let angle_transform = DAffine2::from_angle(self.snapped_angle()); - let translation_transform = DAffine2::from_translation(self.translation); + let translation_transform = DAffine2::from_translation(self.pan); scale_transform * offset_transform * angle_transform * translation_transform } fn create_document_transform(&self, viewport_bounds: &ViewportBounds, responses: &mut VecDeque) { let half_viewport = viewport_bounds.size() / 2.; - let scaled_half_viewport = half_viewport / self.scale; - + let scaled_half_viewport = half_viewport / self.snapped_scale(); responses.push_back( DocumentOperation::SetLayerTransform { path: vec![], @@ -101,6 +133,14 @@ impl MovementMessageHandler { .into(), ); } + pub fn center_zoom(&self, viewport_bounds: DVec2, zoom_factor: f64, mouse: DVec2) -> Message { + let new_viewport_bounds = viewport_bounds / zoom_factor; + let delta_size = viewport_bounds - new_viewport_bounds; + let mouse_fraction = mouse / viewport_bounds; + let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction); + + MovementMessage::TranslateCanvas(delta).into() + } } impl MessageHandler for MovementMessageHandler { @@ -109,128 +149,129 @@ impl MessageHandler for Moveme use MovementMessage::*; match message { TranslateCanvasBegin => { - self.translating = true; - self.mouse_pos = ipp.mouse.position; + self.panning = true; + self.mouse_position = ipp.mouse.position; } RotateCanvasBegin => { - self.rotating = true; - self.mouse_pos = ipp.mouse.position; + self.tilting = true; + self.mouse_position = ipp.mouse.position; } ZoomCanvasBegin => { self.zooming = true; - self.mouse_pos = ipp.mouse.position; + self.mouse_position = ipp.mouse.position; } TransformCanvasEnd => { - self.rotation = self.snapped_angle(); + self.tilt = self.snapped_angle(); + self.zoom = self.snapped_scale(); responses.push_back(ToolMessage::DocumentIsDirty.into()); - self.snap_rotate = false; - self.translating = false; - self.rotating = false; + self.snap_tilt = false; + self.snap_tilt_released = false; + self.snap_zoom = false; + self.panning = false; + self.tilting = false; self.zooming = false; } - MouseMove { snap_angle } => { - if self.translating { - let delta = ipp.mouse.position - self.mouse_pos; - let transformed_delta = document.root.transform.inverse().transform_vector2(delta); + MouseMove { + snap_angle, + wait_for_snap_angle_release, + snap_zoom, + zoom_from_viewport, + } => { + if self.panning { + let delta = ipp.mouse.position - self.mouse_position; - self.translation += transformed_delta; - responses.push_back(ToolMessage::DocumentIsDirty.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + responses.push_back(TranslateCanvas(delta).into()); } - if self.rotating { + if self.tilting { let new_snap = ipp.keyboard.get(snap_angle as usize); - // When disabling snap, keep the viewed rotation as it was previously. - if !new_snap && self.snap_rotate { - self.rotation = self.snapped_angle(); + if !(wait_for_snap_angle_release && new_snap && !self.snap_tilt_released) { + // When disabling snap, keep the viewed rotation as it was previously. + if !new_snap && self.snap_tilt { + self.tilt = self.snapped_angle(); + } + self.snap_tilt = new_snap; + self.snap_tilt_released = true; } - self.snap_rotate = new_snap; let half_viewport = ipp.viewport_bounds.size() / 2.; let rotation = { - let start_vec = self.mouse_pos - half_viewport; + let start_vec = self.mouse_position - half_viewport; let end_vec = ipp.mouse.position - half_viewport; start_vec.angle_between(end_vec) }; - self.rotation += rotation; - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: self.snapped_angle() }.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + responses.push_back(SetCanvasRotation(self.tilt + rotation).into()); } + if self.zooming { - let difference = self.mouse_pos.y as f64 - ipp.mouse.position.y as f64; + let zoom_start = self.snapped_scale(); + + let new_snap = ipp.keyboard.get(snap_zoom as usize); + // When disabling snap, keep the viewed zoom as it was previously + if !new_snap && self.snap_zoom { + self.zoom = self.snapped_scale(); + } + self.snap_zoom = new_snap; + + let difference = self.mouse_position.y as f64 - ipp.mouse.position.y as f64; let amount = 1. + difference * VIEWPORT_ZOOM_MOUSE_RATE; - let new = (self.scale * amount).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); - self.scale = new; - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.scale }.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + self.zoom *= amount; + if let Some(mouse) = zoom_from_viewport { + let zoom_factor = self.snapped_scale() / zoom_start; + + responses.push_back(SetCanvasZoom(self.zoom).into()); + responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, mouse)); + } else { + responses.push_back(SetCanvasZoom(self.zoom).into()); + } } - self.mouse_pos = ipp.mouse.position; + self.mouse_position = ipp.mouse.position; } SetCanvasZoom(new) => { - self.scale = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); - responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.scale }.into()); + self.zoom = new.clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); + responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.snapped_scale() }.into()); responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); self.create_document_transform(&ipp.viewport_bounds, responses); } - IncreaseCanvasZoom => { - // TODO: Eliminate redundant code by making this call SetCanvasZoom - self.scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.scale).unwrap_or(&self.scale); - responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.scale }.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + IncreaseCanvasZoom { center_on_mouse } => { + let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().find(|scale| **scale > self.zoom).unwrap_or(&self.zoom); + if center_on_mouse { + responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); + } + responses.push_back(SetCanvasZoom(new_scale).into()); } - DecreaseCanvasZoom => { - // TODO: Eliminate redundant code by making this call SetCanvasZoom - self.scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.scale).unwrap_or(&self.scale); - responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.scale }.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + DecreaseCanvasZoom { center_on_mouse } => { + let new_scale = *VIEWPORT_ZOOM_LEVELS.iter().rev().find(|scale| **scale < self.zoom).unwrap_or(&self.zoom); + if center_on_mouse { + responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), new_scale / self.zoom, ipp.mouse.position)); + } + responses.push_back(SetCanvasZoom(new_scale).into()); } WheelCanvasZoom => { - // TODO: Eliminate redundant code by making this call SetCanvasZoom let scroll = ipp.mouse.scroll_delta.scroll_delta(); - let mouse = ipp.mouse.position; - let viewport_bounds = ipp.viewport_bounds.size(); let mut zoom_factor = 1. + scroll.abs() * VIEWPORT_ZOOM_WHEEL_RATE; if ipp.mouse.scroll_delta.y > 0 { zoom_factor = 1. / zoom_factor }; - let new_viewport_bounds = viewport_bounds / zoom_factor; - let delta_size = viewport_bounds - new_viewport_bounds; - let mouse_fraction = mouse / viewport_bounds; - let delta = delta_size * (DVec2::splat(0.5) - mouse_fraction); - let transformed_delta = document.root.transform.inverse().transform_vector2(delta); - let new = (self.scale * zoom_factor).clamp(VIEWPORT_ZOOM_SCALE_MIN, VIEWPORT_ZOOM_SCALE_MAX); - self.scale = new; - self.translation += transformed_delta; - responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.scale }.into()); - responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + responses.push_back(self.center_zoom(ipp.viewport_bounds.size(), zoom_factor, ipp.mouse.position)); + responses.push_back(SetCanvasZoom(self.zoom * zoom_factor).into()); } WheelCanvasTranslate { use_y_as_x } => { let delta = match use_y_as_x { false => -ipp.mouse.scroll_delta.as_dvec2(), true => (-ipp.mouse.scroll_delta.y as f64, 0.).into(), } * VIEWPORT_SCROLL_RATE; - let transformed_delta = document.root.transform.inverse().transform_vector2(delta); - self.translation += transformed_delta; - responses.push_back(ToolMessage::DocumentIsDirty.into()); - self.create_document_transform(&ipp.viewport_bounds, responses); + responses.push_back(TranslateCanvas(delta).into()); } SetCanvasRotation(new_radians) => { - self.rotation = new_radians; + self.tilt = new_radians; self.create_document_transform(&ipp.viewport_bounds, responses); responses.push_back(ToolMessage::DocumentIsDirty.into()); - responses.push_back(FrontendMessage::SetCanvasRotation { new_radians }.into()); + responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: self.snapped_angle() }.into()); } ZoomCanvasToFitAll => { if let Some([pos1, pos2]) = document.visible_layers_bounding_box() { @@ -244,9 +285,9 @@ impl MessageHandler for Moveme let size = 1. / size; let new_scale = size.min_element(); - self.translation += center; - self.scale *= new_scale; - responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.scale }.into()); + self.pan += center; + self.zoom *= new_scale; + responses.push_back(FrontendMessage::SetCanvasZoom { new_zoom: self.zoom }.into()); responses.push_back(ToolMessage::DocumentIsDirty.into()); responses.push_back(DocumentMessage::DirtyRenderDocumentInOutlineView.into()); self.create_document_transform(&ipp.viewport_bounds, responses); @@ -255,14 +296,14 @@ impl MessageHandler for Moveme TranslateCanvas(delta) => { let transformed_delta = document.root.transform.inverse().transform_vector2(delta); - self.translation += transformed_delta; + self.pan += transformed_delta; responses.push_back(ToolMessage::DocumentIsDirty.into()); self.create_document_transform(&ipp.viewport_bounds, responses); } TranslateCanvasByViewportFraction(delta) => { let transformed_delta = document.root.transform.inverse().transform_vector2(delta * ipp.viewport_bounds.size()); - self.translation += transformed_delta; + self.pan += transformed_delta; responses.push_back(ToolMessage::DocumentIsDirty.into()); self.create_document_transform(&ipp.viewport_bounds, responses); } @@ -285,7 +326,7 @@ impl MessageHandler for Moveme TranslateCanvasByViewportFraction, ); - if self.translating || self.rotating || self.zooming { + if self.panning || self.tilting || self.zooming { let transforming = actions!(MovementMessageDiscriminant; TransformCanvasEnd, ); diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index dc2dff3434..5e1b7d6649 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -146,19 +146,23 @@ impl Default for Mapping { entry! {action=TransformLayerMessage::TypeNegate, key_down=KeyMinus}, entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyComma}, entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod}, - entry! {action=TransformLayerMessage::MouseMove{slow_key: KeyShift, snap_key: KeyControl}, triggers=[KeyShift, KeyControl]}, + entry! {action=TransformLayerMessage::MouseMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]}, // Select - entry! {action=SelectMessage::MouseMove{snap_angle: KeyShift}, message=InputMapperMessage::PointerMove}, - entry! {action=SelectMessage::DragStart{add_to_selection: KeyShift}, key_down=Lmb}, + entry! {action=SelectMessage::MouseMove { snap_angle: KeyShift }, message=InputMapperMessage::PointerMove}, + entry! {action=SelectMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb}, entry! {action=SelectMessage::DragStop, key_up=Lmb}, entry! {action=SelectMessage::Abort, key_down=Rmb}, entry! {action=SelectMessage::Abort, key_down=KeyEscape}, // Navigate - entry! {action=NavigateMessage::MouseMove{snap_angle: KeyControl}, message=InputMapperMessage::PointerMove}, + entry! {action=NavigateMessage::ClickZoom { zoom_in: false }, key_up=Lmb, modifiers=[KeyShift]}, + entry! {action=NavigateMessage::ClickZoom { zoom_in: true }, key_up=Lmb}, + entry! {action=NavigateMessage::MouseMove { snap_angle: KeyControl, snap_zoom: KeyControl }, message=InputMapperMessage::PointerMove}, + entry! {action=NavigateMessage::TranslateCanvasBegin, key_down=Mmb}, entry! {action=NavigateMessage::RotateCanvasBegin, key_down=Rmb}, entry! {action=NavigateMessage::ZoomCanvasBegin, key_down=Lmb}, entry! {action=NavigateMessage::TransformCanvasEnd, key_up=Rmb}, entry! {action=NavigateMessage::TransformCanvasEnd, key_up=Lmb}, + entry! {action=NavigateMessage::TransformCanvasEnd, key_up=Mmb}, // Eyedropper entry! {action=EyedropperMessage::LeftMouseDown, key_down=Lmb}, entry! {action=EyedropperMessage::RightMouseDown, key_down=Rmb}, @@ -167,25 +171,25 @@ impl Default for Mapping { entry! {action=RectangleMessage::DragStop, key_up=Lmb}, entry! {action=RectangleMessage::Abort, key_down=Rmb}, entry! {action=RectangleMessage::Abort, key_down=KeyEscape}, - entry! {action=RectangleMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]}, + entry! {action=RectangleMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, // Ellipse entry! {action=EllipseMessage::DragStart, key_down=Lmb}, entry! {action=EllipseMessage::DragStop, key_up=Lmb}, entry! {action=EllipseMessage::Abort, key_down=Rmb}, entry! {action=EllipseMessage::Abort, key_down=KeyEscape}, - entry! {action=EllipseMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]}, + entry! {action=EllipseMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, // Shape entry! {action=ShapeMessage::DragStart, key_down=Lmb}, entry! {action=ShapeMessage::DragStop, key_up=Lmb}, entry! {action=ShapeMessage::Abort, key_down=Rmb}, entry! {action=ShapeMessage::Abort, key_down=KeyEscape}, - entry! {action=ShapeMessage::Resize{center: KeyAlt, lock_ratio: KeyShift}, triggers=[KeyAlt, KeyShift]}, + entry! {action=ShapeMessage::Resize { center: KeyAlt, lock_ratio: KeyShift }, triggers=[KeyAlt, KeyShift]}, // Line entry! {action=LineMessage::DragStart, key_down=Lmb}, entry! {action=LineMessage::DragStop, key_up=Lmb}, entry! {action=LineMessage::Abort, key_down=Rmb}, entry! {action=LineMessage::Abort, key_down=KeyEscape}, - entry! {action=LineMessage::Redraw{center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift}, triggers=[KeyAlt, KeyShift, KeyControl]}, + entry! {action=LineMessage::Redraw { center: KeyAlt, lock_angle: KeyControl, snap_angle: KeyShift }, triggers=[KeyAlt, KeyShift, KeyControl]}, // Path entry! {action=PathMessage::DragStart, key_down=Lmb}, entry! {action=PathMessage::PointerMove, message=InputMapperMessage::PointerMove}, @@ -235,22 +239,22 @@ impl Default for Mapping { entry! {action=TransformLayerMessage::BeginRotate, key_down=KeyR}, entry! {action=TransformLayerMessage::BeginScale, key_down=KeyS}, // Document movement - entry! {action=MovementMessage::MouseMove{snap_angle: KeyShift}, message=InputMapperMessage::PointerMove}, + entry! {action=MovementMessage::MouseMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, message=InputMapperMessage::PointerMove}, entry! {action=MovementMessage::RotateCanvasBegin, key_down=Mmb, modifiers=[KeyControl]}, entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]}, entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb}, entry! {action=MovementMessage::TransformCanvasEnd, key_up=Mmb}, entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Lmb, modifiers=[KeySpace]}, entry! {action=MovementMessage::TransformCanvasEnd, key_up=Lmb, modifiers=[KeySpace]}, - entry! {action=MovementMessage::IncreaseCanvasZoom, key_down=KeyPlus, modifiers=[KeyControl]}, - entry! {action=MovementMessage::IncreaseCanvasZoom, key_down=KeyEquals, modifiers=[KeyControl]}, - entry! {action=MovementMessage::DecreaseCanvasZoom, key_down=KeyMinus, modifiers=[KeyControl]}, + entry! {action=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }, key_down=KeyPlus, modifiers=[KeyControl]}, + entry! {action=MovementMessage::IncreaseCanvasZoom { center_on_mouse: false }, key_down=KeyEquals, modifiers=[KeyControl]}, + entry! {action=MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }, key_down=KeyMinus, modifiers=[KeyControl]}, entry! {action=MovementMessage::SetCanvasZoom(1.), key_down=Key1, modifiers=[KeyControl]}, entry! {action=MovementMessage::SetCanvasZoom(2.), key_down=Key2, modifiers=[KeyControl]}, entry! {action=MovementMessage::ZoomCanvasToFitAll, key_down=Key0, modifiers=[KeyControl]}, entry! {action=MovementMessage::WheelCanvasZoom, message=InputMapperMessage::MouseScroll, modifiers=[KeyControl]}, - entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: true}, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]}, - entry! {action=MovementMessage::WheelCanvasTranslate{use_y_as_x: false}, message=InputMapperMessage::MouseScroll}, + entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: true }, message=InputMapperMessage::MouseScroll, modifiers=[KeyShift]}, + entry! {action=MovementMessage::WheelCanvasTranslate { use_y_as_x: false }, message=InputMapperMessage::MouseScroll}, entry! {action=MovementMessage::TranslateCanvasByViewportFraction(DVec2::new(1., 0.)), key_down=KeyPageUp, modifiers=[KeyShift]}, entry! {action=MovementMessage::TranslateCanvasByViewportFraction(DVec2::new(-1., 0.)), key_down=KeyPageDown, modifiers=[KeyShift]}, entry! {action=MovementMessage::TranslateCanvasByViewportFraction(DVec2::new(0., 1.)), key_down=KeyPageUp}, diff --git a/editor/src/tool/tools/navigate.rs b/editor/src/tool/tools/navigate.rs index 6e584f3ad9..85a650971b 100644 --- a/editor/src/tool/tools/navigate.rs +++ b/editor/src/tool/tools/navigate.rs @@ -2,17 +2,20 @@ use crate::input::keyboard::MouseMotion; use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::tool::{Fsm, ToolActionHandlerData}; use crate::{input::keyboard::Key, message_prelude::*}; +use glam::DVec2; use serde::{Deserialize, Serialize}; #[derive(Default)] pub struct Navigate { fsm_state: NavigateToolFsmState, + data: NavigateToolData, } #[impl_message(Message, ToolMessage, Navigate)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum NavigateMessage { - MouseMove { snap_angle: Key }, + ClickZoom { zoom_in: bool }, + MouseMove { snap_angle: Key, snap_zoom: Key }, TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin, @@ -27,7 +30,7 @@ impl<'a> MessageHandler> for Navigate { return; } - let new_state = self.fsm_state.transition(action, data.0, data.1, &mut (), 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; @@ -39,7 +42,7 @@ impl<'a> MessageHandler> for Navigate { use NavigateToolFsmState::*; match self.fsm_state { Ready => actions!(NavigateMessageDiscriminant; TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin), - _ => actions!(NavigateMessageDiscriminant; MouseMove, TransformCanvasEnd), + _ => actions!(NavigateMessageDiscriminant; ClickZoom, MouseMove, TransformCanvasEnd), } } } @@ -47,8 +50,8 @@ impl<'a> MessageHandler> for Navigate { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum NavigateToolFsmState { Ready, - Translating, - Rotating, + Panning, + Tilting, Zooming, } @@ -58,34 +61,64 @@ impl Default for NavigateToolFsmState { } } +#[derive(Clone, Debug, Default)] +struct NavigateToolData { + drag_start: DVec2, +} + impl Fsm for NavigateToolFsmState { - type ToolData = (); + type ToolData = NavigateToolData; fn transition( self, message: ToolMessage, _document: &crate::document::DocumentMessageHandler, _tool_data: &crate::tool::DocumentToolData, - _data: &mut Self::ToolData, - _input: &crate::input::InputPreprocessor, + data: &mut Self::ToolData, + input: &crate::input::InputPreprocessor, messages: &mut VecDeque, ) -> Self { if let ToolMessage::Navigate(navigate) = message { use NavigateMessage::*; match navigate { - MouseMove { snap_angle } => { - messages.push_front(MovementMessage::MouseMove { snap_angle }.into()); + ClickZoom { zoom_in } => { + messages.push_front(MovementMessage::TransformCanvasEnd.into()); + + // Mouse has not moved from mousedown to mouseup + if data.drag_start == input.mouse.position { + messages.push_front(if zoom_in { + MovementMessage::IncreaseCanvasZoom { center_on_mouse: true }.into() + } else { + MovementMessage::DecreaseCanvasZoom { center_on_mouse: true }.into() + }); + } + + NavigateToolFsmState::Ready + } + MouseMove { snap_angle, snap_zoom } => { + messages.push_front( + MovementMessage::MouseMove { + snap_angle, + wait_for_snap_angle_release: false, + snap_zoom, + zoom_from_viewport: Some(data.drag_start), + } + .into(), + ); self } TranslateCanvasBegin => { + data.drag_start = input.mouse.position; messages.push_front(MovementMessage::TranslateCanvasBegin.into()); - NavigateToolFsmState::Translating + NavigateToolFsmState::Panning } RotateCanvasBegin => { + data.drag_start = input.mouse.position; messages.push_front(MovementMessage::RotateCanvasBegin.into()); - NavigateToolFsmState::Rotating + NavigateToolFsmState::Tilting } ZoomCanvasBegin => { + data.drag_start = input.mouse.position; messages.push_front(MovementMessage::ZoomCanvasBegin.into()); NavigateToolFsmState::Zooming } @@ -106,37 +139,65 @@ impl Fsm for NavigateToolFsmState { fn update_hints(&self, responses: &mut VecDeque) { let hint_data = match self { NavigateToolFsmState::Ready => HintData(vec![ + HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::Lmb), + label: String::from("Zoom In"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyShift])], + mouse: None, + label: String::from("Zoom Out"), + plus: true, + }, + ]), + HintGroup(vec![ + HintInfo { + key_groups: vec![], + mouse: Some(MouseMotion::LmbDrag), + label: String::from("Zoom"), + plus: false, + }, + HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl])], + mouse: None, + label: String::from("Snap Increments"), + plus: true, + }, + ]), HintGroup(vec![HintInfo { key_groups: vec![], mouse: Some(MouseMotion::MmbDrag), - label: String::from("Translate"), + label: String::from("Pan"), plus: false, }]), HintGroup(vec![ HintInfo { key_groups: vec![], mouse: Some(MouseMotion::RmbDrag), - label: String::from("Rotate (drag around centre)"), + label: String::from("Tilt"), plus: false, }, HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], mouse: None, - label: String::from("Snap rotation to 15° increments"), + label: String::from("Snap 15°"), plus: true, }, ]), - HintGroup(vec![HintInfo { - key_groups: vec![], - mouse: Some(MouseMotion::LmbDrag), - label: String::from("Zoom in and out (drag up and down)"), - plus: false, - }]), ]), - NavigateToolFsmState::Rotating => HintData(vec![HintGroup(vec![HintInfo { + NavigateToolFsmState::Tilting => HintData(vec![HintGroup(vec![HintInfo { + key_groups: vec![KeysGroup(vec![Key::KeyControl])], + mouse: None, + label: String::from("Snap 15°"), + plus: false, + }])]), + NavigateToolFsmState::Zooming => HintData(vec![HintGroup(vec![HintInfo { key_groups: vec![KeysGroup(vec![Key::KeyControl])], mouse: None, - label: String::from("Snap to 15° increments"), + label: String::from("Snap Increments"), plus: false, }])]), _ => HintData(Vec::new()), diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index 408b4bfba6..bdfbbed484 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -438,13 +438,13 @@ impl JsEditorHandle { /// Zoom in to the next step pub fn increase_canvas_zoom(&self) { - let message = MovementMessage::IncreaseCanvasZoom; + 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; + let message = MovementMessage::DecreaseCanvasZoom { center_on_mouse: false }; self.dispatch(message); }