diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index 6531203e..2ba584bc 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -10,7 +10,7 @@ import { getNormalizedZoom } from "../scene"; import { centerScrollOn } from "../scene/scroll"; import { getStateForZoom } from "../scene/zoom"; import { AppState, NormalizedZoomValue } from "../types"; -import { getShortcutKey, setCursor, updateActiveTool } from "../utils"; +import { getShortcutKey, updateActiveTool } from "../utils"; import { register } from "./register"; import { Tooltip } from "../components/Tooltip"; import { newElementWith } from "../element/mutateElement"; @@ -21,6 +21,7 @@ import { } from "../appState"; import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; import { Bounds } from "../element/bounds"; +import { setCursor } from "../cursor"; export const actionChangeViewBackgroundColor = register({ name: "changeViewBackgroundColor", diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 4d422994..a7c34c5a 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -1,6 +1,6 @@ import { KEYS } from "../keys"; import { isInvisiblySmallElement } from "../element"; -import { updateActiveTool, resetCursor } from "../utils"; +import { updateActiveTool } from "../utils"; import { ToolButton } from "../components/ToolButton"; import { done } from "../components/icons"; import { t } from "../i18n"; @@ -15,6 +15,7 @@ import { } from "../element/binding"; import { isBindingElement, isLinearElement } from "../element/typeChecks"; import { AppState } from "../types"; +import { resetCursor } from "../cursor"; export const actionFinalize = register({ name: "finalize", diff --git a/src/actions/actionFrame.ts b/src/actions/actionFrame.ts index 339545f8..1266920e 100644 --- a/src/actions/actionFrame.ts +++ b/src/actions/actionFrame.ts @@ -4,7 +4,8 @@ import { removeAllElementsFromFrame } from "../frame"; import { getFrameElements } from "../frame"; import { KEYS } from "../keys"; import { AppClassProperties, AppState } from "../types"; -import { setCursorForShape, updateActiveTool } from "../utils"; +import { updateActiveTool } from "../utils"; +import { setCursorForShape } from "../cursor"; import { register } from "./register"; const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => { diff --git a/src/components/App.tsx b/src/components/App.tsx index f2c58165..4ad7b889 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -241,18 +241,14 @@ import { isInputLike, isToolIcon, isWritableElement, - resetCursor, resolvablePromise, sceneCoordsToViewportCoords, - setCursor, - setCursorForShape, tupleToCoors, viewportCoordsToSceneCoords, withBatchedUpdates, wrapEvent, withBatchedUpdatesThrottled, updateObject, - setEraserCursor, updateActiveTool, getShortcutKey, isTransparent, @@ -371,6 +367,12 @@ import { Renderer } from "../scene/Renderer"; import { ShapeCache } from "../scene/ShapeCache"; import { LaserToolOverlay } from "./LaserTool/LaserTool"; import { LaserPathManager } from "./LaserTool/LaserPathManager"; +import { + setEraserCursor, + setCursor, + resetCursor, + setCursorForShape, +} from "../cursor"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); diff --git a/src/cursor.ts b/src/cursor.ts new file mode 100644 index 00000000..364ce155 --- /dev/null +++ b/src/cursor.ts @@ -0,0 +1,103 @@ +import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants"; +import OpenColor from "open-color"; +import { AppState, DataURL } from "./types"; +import { isHandToolActive, isEraserActive } from "./appState"; + +const laserPointerCursorSVG_tag = ``; +const laserPointerCursorBackgroundSVG = ``; +const laserPointerCursorIconSVG = ``; + +const laserPointerCursorDataURL_lightMode = `data:${ + MIME_TYPES.svg +},${encodeURIComponent( + `${laserPointerCursorSVG_tag}${laserPointerCursorIconSVG}`, +)}`; +const laserPointerCursorDataURL_darkMode = `data:${ + MIME_TYPES.svg +},${encodeURIComponent( + `${laserPointerCursorSVG_tag}${laserPointerCursorBackgroundSVG}${laserPointerCursorIconSVG}`, +)}`; + +export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = ""; + } +}; + +export const setCursor = ( + interactiveCanvas: HTMLCanvasElement | null, + cursor: string, +) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = cursor; + } +}; + +let eraserCanvasCache: any; +let previewDataURL: string; +export const setEraserCursor = ( + interactiveCanvas: HTMLCanvasElement | null, + theme: AppState["theme"], +) => { + const cursorImageSizePx = 20; + + const drawCanvas = () => { + const isDarkTheme = theme === THEME.DARK; + eraserCanvasCache = document.createElement("canvas"); + eraserCanvasCache.theme = theme; + eraserCanvasCache.height = cursorImageSizePx; + eraserCanvasCache.width = cursorImageSizePx; + const context = eraserCanvasCache.getContext("2d")!; + context.lineWidth = 1; + context.beginPath(); + context.arc( + eraserCanvasCache.width / 2, + eraserCanvasCache.height / 2, + 5, + 0, + 2 * Math.PI, + ); + context.fillStyle = isDarkTheme ? OpenColor.black : OpenColor.white; + context.fill(); + context.strokeStyle = isDarkTheme ? OpenColor.white : OpenColor.black; + context.stroke(); + previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL; + }; + if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) { + drawCanvas(); + } + + setCursor( + interactiveCanvas, + `url(${previewDataURL}) ${cursorImageSizePx / 2} ${ + cursorImageSizePx / 2 + }, auto`, + ); +}; + +export const setCursorForShape = ( + interactiveCanvas: HTMLCanvasElement | null, + appState: Pick, +) => { + if (!interactiveCanvas) { + return; + } + if (appState.activeTool.type === "selection") { + resetCursor(interactiveCanvas); + } else if (isHandToolActive(appState)) { + interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB; + } else if (isEraserActive(appState)) { + setEraserCursor(interactiveCanvas, appState.theme); + // do nothing if image tool is selected which suggests there's + // a image-preview set as the cursor + // Ignore custom type as well and let host decide + } else if (appState.activeTool.type === "laser") { + const url = + appState.theme === THEME.LIGHT + ? laserPointerCursorDataURL_lightMode + : laserPointerCursorDataURL_darkMode; + interactiveCanvas.style.cursor = `url(${url}), auto`; + } else if (!["image", "custom"].includes(appState.activeTool.type)) { + interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; + } +}; diff --git a/src/element/embeddable.ts b/src/element/embeddable.ts index 80585bc7..4aa6f0fd 100644 --- a/src/element/embeddable.ts +++ b/src/element/embeddable.ts @@ -2,7 +2,8 @@ import { register } from "../actions/register"; import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants"; import { t } from "../i18n"; import { ExcalidrawProps } from "../types"; -import { getFontString, setCursorForShape, updateActiveTool } from "../utils"; +import { getFontString, updateActiveTool } from "../utils"; +import { setCursorForShape } from "../cursor"; import { newTextElement } from "./newElement"; import { getContainerElement, wrapText } from "./textElement"; import { isEmbeddableElement } from "./typeChecks"; diff --git a/src/utils.ts b/src/utils.ts index 65dfe140..f9513920 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,13 +1,9 @@ -import oc from "open-color"; import { COLOR_PALETTE } from "./colors"; import { - CURSOR_TYPE, DEFAULT_VERSION, EVENT, FONT_FAMILY, isDarwin, - MIME_TYPES, - THEME, WINDOWS_EMOJI_FALLBACK_FONT, } from "./constants"; import { @@ -15,20 +11,11 @@ import { FontString, NonDeletedExcalidrawElement, } from "./element/types"; -import { ActiveTool, AppState, DataURL, ToolType, Zoom } from "./types"; +import { ActiveTool, AppState, ToolType, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; -import { isEraserActive, isHandToolActive } from "./appState"; import { ResolutionType } from "./utility-types"; import React from "react"; -const laserPointerCursorSVG = ` - -`; - -const laserPointerCursorDataURL = `data:${MIME_TYPES.svg},${encodeURIComponent( - `${laserPointerCursorSVG}`, -)}`; - let mockDateTime: string | null = null; export const setDateTimeForTests = (dateTime: string) => { @@ -402,87 +389,6 @@ export const updateActiveTool = ( }; }; -export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => { - if (interactiveCanvas) { - interactiveCanvas.style.cursor = ""; - } -}; - -export const setCursor = ( - interactiveCanvas: HTMLCanvasElement | null, - cursor: string, -) => { - if (interactiveCanvas) { - interactiveCanvas.style.cursor = cursor; - } -}; - -let eraserCanvasCache: any; -let previewDataURL: string; -export const setEraserCursor = ( - interactiveCanvas: HTMLCanvasElement | null, - theme: AppState["theme"], -) => { - const cursorImageSizePx = 20; - - const drawCanvas = () => { - const isDarkTheme = theme === THEME.DARK; - eraserCanvasCache = document.createElement("canvas"); - eraserCanvasCache.theme = theme; - eraserCanvasCache.height = cursorImageSizePx; - eraserCanvasCache.width = cursorImageSizePx; - const context = eraserCanvasCache.getContext("2d")!; - context.lineWidth = 1; - context.beginPath(); - context.arc( - eraserCanvasCache.width / 2, - eraserCanvasCache.height / 2, - 5, - 0, - 2 * Math.PI, - ); - context.fillStyle = isDarkTheme ? oc.black : oc.white; - context.fill(); - context.strokeStyle = isDarkTheme ? oc.white : oc.black; - context.stroke(); - previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL; - }; - if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) { - drawCanvas(); - } - - setCursor( - interactiveCanvas, - `url(${previewDataURL}) ${cursorImageSizePx / 2} ${ - cursorImageSizePx / 2 - }, auto`, - ); -}; - -export const setCursorForShape = ( - interactiveCanvas: HTMLCanvasElement | null, - appState: Pick, -) => { - if (!interactiveCanvas) { - return; - } - if (appState.activeTool.type === "selection") { - resetCursor(interactiveCanvas); - } else if (isHandToolActive(appState)) { - interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB; - } else if (isEraserActive(appState)) { - setEraserCursor(interactiveCanvas, appState.theme); - // do nothing if image tool is selected which suggests there's - // a image-preview set as the cursor - // Ignore custom type as well and let host decide - } else if (appState.activeTool.type === "laser") { - const url = laserPointerCursorDataURL; - interactiveCanvas.style.cursor = `url(${url}), auto`; - } else if (!["image", "custom"].includes(appState.activeTool.type)) { - interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; - } -}; - export const isFullScreen = () => document.fullscreenElement?.nodeName === "HTML";