Set the mouse cursor in the canvas based on the current tool and its state (#480)

* Add FrontendMouseCursor and DisplayMouseCursor

* Add update_cursor method to the Fsm trait and implement it for all tools

* Rename DisplayMouseCursor to UpdateMouseCursor

* Add 'To CSS Cursor Property' transform decorator and change the mouse cursor in the canvas based on the current tool and its state

* Implement update_cursor for Navigate tool properly

* Keep the cursor when dragging outside of the canvas

* Change the mouse cursor to 'zoom-in' when LMB dragging on canvas with Navigate tool

* Rename FrontendMouseCursor to MouseCursorIcon

* Rename 'event' to 'e' and replace v-on with @

* Change the definition of the MouseCursorIcon type in TS

* Replace switch with dictionary look-up

* Move the definition of MouseCursorIcon closer to where it's used
This commit is contained in:
asyncth 2022-01-16 12:57:03 +05:00 committed by Keavon Chambers
parent e877eea457
commit 0515cc4eca
17 changed files with 163 additions and 4 deletions

View File

@ -1,4 +1,4 @@
use super::utility_types::FrontendDocumentDetails;
use super::utility_types::{FrontendDocumentDetails, MouseCursorIcon};
use crate::document::layer_panel::{LayerPanelEntry, RawBuffer};
use crate::message_prelude::*;
use crate::misc::HintData;
@ -37,6 +37,7 @@ pub enum FrontendMessage {
UpdateDocumentRulers { origin: (f64, f64), spacing: f64, interval: f64 },
UpdateDocumentScrollbars { position: (f64, f64), size: (f64, f64), multiplier: (f64, f64) },
UpdateInputHints { hint_data: HintData },
UpdateMouseCursor { cursor: MouseCursorIcon },
UpdateOpenDocumentsList { open_documents: Vec<FrontendDocumentDetails> },
UpdateWorkingColors { primary: Color, secondary: Color },
}

View File

@ -6,3 +6,12 @@ pub struct FrontendDocumentDetails {
pub name: String,
pub id: u64,
}
#[derive(Clone, Copy, Debug, Eq, Deserialize, PartialEq, Serialize)]
pub enum MouseCursorIcon {
Default,
ZoomIn,
ZoomOut,
Grabbing,
Crosshair,
}

View File

@ -27,6 +27,7 @@ pub trait Fsm {
) -> Self;
fn update_hints(&self, responses: &mut VecDeque<Message>);
fn update_cursor(&self, responses: &mut VecDeque<Message>);
}
#[derive(Debug, Clone)]

View File

@ -49,5 +49,6 @@ pub enum ToolMessage {
#[child]
Shape(ShapeMessage),
SwapColors,
UpdateCursor,
UpdateHints,
}

View File

@ -31,12 +31,13 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, &InputPreprocessorMes
}
// Send the Abort state transition to the tool
let mut send_abort_to_tool = |tool_type, message: ToolMessage, update_hints: bool| {
let mut send_abort_to_tool = |tool_type, message: ToolMessage, update_hints_and_cursor: bool| {
if let Some(tool) = tool_data.tools.get_mut(&tool_type) {
tool.process_action(message, (document, document_data, input), responses);
if update_hints {
if update_hints_and_cursor {
tool.process_action(ToolMessage::UpdateHints, (document, document_data, input), responses);
tool.process_action(ToolMessage::UpdateCursor, (document, document_data, input), responses);
}
}
};

View File

@ -1,5 +1,6 @@
use super::shared::resize::Resize;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -35,11 +36,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -177,4 +184,8 @@ impl Fsm for EllipseToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}

View File

@ -1,5 +1,6 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::MouseMotion;
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -34,11 +35,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -127,4 +134,8 @@ impl Fsm for EyedropperToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}

View File

@ -1,5 +1,6 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::MouseMotion;
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -34,11 +35,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -121,4 +128,8 @@ impl Fsm for FillToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}

View File

@ -1,5 +1,6 @@
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::mouse::ViewportPosition;
use crate::input::InputPreprocessorMessageHandler;
@ -38,11 +39,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -207,6 +214,10 @@ impl Fsm for LineToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}
fn generate_transform(data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {

View File

@ -1,4 +1,5 @@
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -34,11 +35,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -211,4 +218,15 @@ impl Fsm for NavigateToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
let cursor = match *self {
NavigateToolFsmState::Ready => MouseCursorIcon::ZoomIn,
NavigateToolFsmState::Panning => MouseCursorIcon::Grabbing,
NavigateToolFsmState::Tilting => MouseCursorIcon::Default,
NavigateToolFsmState::Zooming => MouseCursorIcon::ZoomIn,
};
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
}
}

View File

@ -1,6 +1,7 @@
use crate::consts::{COLOR_ACCENT, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE};
use crate::document::utility_types::{VectorManipulatorSegment, VectorManipulatorShape};
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -41,11 +42,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -476,6 +483,10 @@ impl Fsm for PathToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
struct VectorManipulatorTypes {

View File

@ -1,4 +1,5 @@
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -44,11 +45,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -193,6 +200,10 @@ impl Fsm for PenToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
fn remove_preview(data: &PenToolData) -> Message {

View File

@ -1,5 +1,6 @@
use super::shared::resize::Resize;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -35,11 +36,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -177,4 +184,8 @@ impl Fsm for RectangleToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}

View File

@ -1,6 +1,7 @@
use crate::consts::{COLOR_ACCENT, SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE};
use crate::document::utility_types::{AlignAggregate, AlignAxis, FlipAxis};
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::mouse::ViewportPosition;
use crate::input::InputPreprocessorMessageHandler;
@ -46,11 +47,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -420,4 +427,8 @@ impl Fsm for SelectToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}

View File

@ -1,5 +1,6 @@
use super::shared::resize::Resize;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
@ -36,11 +37,17 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
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;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
@ -185,4 +192,8 @@ impl Fsm for ShapeToolFsmState {
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }.into());
}
}

View File

@ -123,7 +123,7 @@
<CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" />
</LayoutCol>
<LayoutCol :class="'canvas-area'">
<div class="canvas" ref="canvas">
<div class="canvas" ref="canvas" :style="{ cursor: canvasCursor }" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)">
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
<svg class="artwork" v-html="artworkSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasSvgWidth, height: canvasSvgHeight }"></svg>
@ -261,6 +261,7 @@ import {
UpdateCanvasRotation,
ToolName,
UpdateDocumentArtboards,
UpdateMouseCursor,
} from "@/dispatcher/js-messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
@ -338,6 +339,10 @@ export default defineComponent({
resetWorkingColors() {
this.editor.instance.reset_colors();
},
canvasPointerDown(e: PointerEvent) {
const canvas = this.$refs.canvas as HTMLElement;
canvas.setPointerCapture(e.pointerId);
},
},
mounted() {
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentArtwork, (UpdateDocumentArtwork) => {
@ -378,6 +383,10 @@ export default defineComponent({
this.documentRotation = (360 + (newRotation % 360)) % 360;
});
this.editor.dispatcher.subscribeJsMessage(UpdateMouseCursor, (updateMouseCursor) => {
this.canvasCursor = updateMouseCursor.cursor;
});
window.addEventListener("resize", this.viewportResize);
window.addEventListener("DOMContentLoaded", this.viewportResize);
},
@ -401,6 +410,7 @@ export default defineComponent({
overlaysSvg: "",
canvasSvgWidth: "100%",
canvasSvgHeight: "100%",
canvasCursor: "default",
activeTool: "Select" as ToolName,
activeToolOptions: {},
documentModeEntries,

View File

@ -201,6 +201,24 @@ export class UpdateDocumentRulers extends JsMessage {
readonly interval!: number;
}
export type MouseCursorIcon = "default" | "zoom-in" | "zoom-out" | "grabbing" | "crosshair";
const ToCssCursorProperty = Transform(({ value }) => {
const cssNames: Record<string, MouseCursorIcon> = {
ZoomIn: "zoom-in",
ZoomOut: "zoom-out",
Grabbing: "grabbing",
Crosshair: "crosshair",
};
return cssNames[value] || "default";
});
export class UpdateMouseCursor extends JsMessage {
@ToCssCursorProperty
readonly cursor!: MouseCursorIcon;
}
export class TriggerFileDownload extends JsMessage {
readonly document!: string;
@ -378,6 +396,7 @@ export const messageConstructors: Record<string, MessageMaker> = {
UpdateWorkingColors,
UpdateCanvasZoom,
UpdateCanvasRotation,
UpdateMouseCursor,
DisplayDialogError,
DisplayDialogPanic,
DisplayConfirmationToCloseDocument,