Enhance the Navigate Tool zoom behavior (#461)

* Snap zoom

* Navigate zoom from centre

* Ctrl to snap zoom in navigate

* Use ctrl for global snap rotate

* Fix the rotation input on snap rotate

* Update hint to use ctrl

* Fix mouse centre on drag

* Click to zoom in

* Clean up centre zoom

* Update user input hints; tweak some variable names for clarity and standardization

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2022-01-08 16:30:03 +00:00 committed by Keavon Chambers
parent 26da229807
commit e39aa2a501
5 changed files with 250 additions and 145 deletions

View File

@ -859,9 +859,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> 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<DocumentMessage, &InputPreprocessor> 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);

View File

@ -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<DVec2>,
},
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<Message>) {
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<MovementMessage, (&Document, &InputPreprocessor)> for MovementMessageHandler {
@ -109,128 +149,129 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessor)> 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<MovementMessage, (&Document, &InputPreprocessor)> 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<MovementMessage, (&Document, &InputPreprocessor)> 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<MovementMessage, (&Document, &InputPreprocessor)> for Moveme
TranslateCanvasByViewportFraction,
);
if self.translating || self.rotating || self.zooming {
if self.panning || self.tilting || self.zooming {
let transforming = actions!(MovementMessageDiscriminant;
TransformCanvasEnd,
);

View File

@ -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},

View File

@ -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<ToolMessage, ToolActionHandlerData<'a>> 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<ToolMessage, ToolActionHandlerData<'a>> 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<ToolMessage, ToolActionHandlerData<'a>> 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<Message>,
) -> 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<Message>) {
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 to 15° increments"),
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 Increments"),
plus: false,
}])]),
_ => HintData(Vec::new()),

View File

@ -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);
}