From ab0df9118e0c69a479baebaa5e6375ae5131617c Mon Sep 17 00:00:00 2001 From: Lu Wilson Date: Tue, 23 May 2023 07:12:11 -0700 Subject: [PATCH] Add SVG cursors for all cursor types (#1416) Fixes #1410 This PR adds custom SVGs for all cursor types. This will unblock some upcoming collaboration features! It also adds some basic debugging for custom cursors. ![2023-05-19 at 11 02 57 - Coffee Shrimp](https://github.com/tldraw/tldraw/assets/15892272/dbc84d04-604f-43e5-acd2-69df956e5784) It uses custom cursors for any shape-related UI, like links. ![2023-05-19 at 11 07 04 - Amaranth Aphid](https://github.com/tldraw/tldraw/assets/15892272/7eb25f6a-0552-47bd-b2b9-f6c3dc2fca70) But it sticks with the default browser cursors for the non-canvas UI. ![2023-05-23 at 15 06 29 - Apricot Bovid](https://github.com/tldraw/tldraw/assets/15892272/2fe35afb-095a-4454-a6c3-aa8337b71506) ### Change Type - [x] `minor` ### Test Plan 1. Enable debug mode. 2. From the debug menu, enable "Debug cursors". 3. Hover the cursor over the shapes that appear. 4. Check that the cursor appears correctly over each one. 5. (Don't forget to turn off "Debug cursors" after use). ### Release Notes - Added consistent custom cursors. --------- Co-authored-by: Steve Ruiz --- packages/editor/api-report.md | 1 + packages/editor/editor.css | 43 ++++++++++++--- .../statechart/TLSelectTool/children/Idle.ts | 17 +++++- packages/editor/src/lib/hooks/useCursor.ts | 50 ++++++++++++----- packages/editor/src/lib/utils/debug-flags.ts | 1 + packages/ui/src/lib/components/DebugPanel.tsx | 54 +++++++++++++++++++ 6 files changed, 145 insertions(+), 21 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 9e912bd5c..b79ef3806 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -636,6 +636,7 @@ export const debugFlags: { peopleMenu: Atom; logMessages: Atom; resetConnectionEveryPing: Atom; + debugCursors: Atom; }; // @internal (undocumented) diff --git a/packages/editor/editor.css b/packages/editor/editor.css index c063d33b1..9ec9b0f24 100644 --- a/packages/editor/editor.css +++ b/packages/editor/editor.css @@ -24,7 +24,32 @@ --layer-canvas: 200; /* Misc */ --tl-zoom: 1; - --tl-cursor: default; + + /* These cursor values get programmatically overridden */ + /* They're just here to help your editor autocomplete */ + --tl-cursor: var(--tl-cursor-default); + --tl-cursor-none: none; + --tl-cursor-default: default; + --tl-cursor-pointer: pointer; + --tl-cursor-cross: crosshair; + --tl-cursor-move: move; + --tl-cursor-grab: grab; + --tl-cursor-grabbing: grabbing; + --tl-cursor-text: text; + --tl-cursor-resize-edge: ew-resize; + --tl-cursor-resize-corner: nesw-resize; + --tl-cursor-ew-resize: ew-resize; + --tl-cursor-ns-resize: ns-resize; + --tl-cursor-nesw-resize: nesw-resize; + --tl-cursor-nwse-resize: nwse-resize; + --tl-cursor-rotate: pointer; + --tl-cursor-nwse-rotate: pointer; + --tl-cursor-nesw-rotate: pointer; + --tl-cursor-senw-rotate: pointer; + --tl-cursor-swne-rotate: pointer; + --tl-cursor-zoom-in: zoom-in; + --tl-cursor-zoom-out: zoom-out; + --tl-scale: calc(1 / var(--tl-zoom)); --tl-font-draw: 'tldraw_draw', sans-serif; --tl-font-sans: 'tldraw_sans', sans-serif; @@ -523,7 +548,7 @@ input, .tl-mobile-rotate__bg { pointer-events: all; r: calc(max(calc(14px * var(--tl-scale)), 20px / max(1, var(--tl-zoom)))); - cursor: grab; + cursor: var(--tl-cursor-grab); } .tl-mobile-rotate__fg { @@ -548,7 +573,7 @@ input, fill: transparent; stroke: transparent; pointer-events: all; - cursor: grab; + cursor: var(--tl-cursor-grabbing); r: calc(12px / var(--tl-zoom)); } @@ -565,7 +590,7 @@ input, } .tl-handle__bg:hover { - cursor: grab; + cursor: var(--tl-cursor-grab); fill: var(--color-selection-fill); } @@ -821,6 +846,7 @@ input, user-select: all; -webkit-user-select: text; overflow: hidden; + cursor: var(--tl-cursor-text); } .tl-text-input::selection { @@ -926,12 +952,12 @@ input, pointer-events: all; z-index: 999; overflow: hidden; - cursor: pointer; display: block; color: var(--color-text); text-overflow: ellipsis; text-decoration: none; color: var(--color-text-2); + cursor: var(--tl-cursor-pointer); } .tl-bookmark__link:hover { @@ -956,7 +982,7 @@ input, font-weight: 400; color: var(--color-text-1); padding: 13px; - cursor: pointer; + cursor: var(--tl-cursor-pointer); border: none; outline: none; pointer-events: all; @@ -1351,6 +1377,7 @@ input, user-select: all; -webkit-user-select: text; white-space: pre; + cursor: var(--tl-cursor-text); } /* If mobile use 16px as font size */ @@ -1378,7 +1405,7 @@ input, border-radius: var(--radius-2); box-shadow: var(--shadow-1); pointer-events: all; - cursor: pointer; + cursor: var(--tl-cursor-pointer); outline: none; display: flex; } @@ -1526,7 +1553,7 @@ it from receiving any pointer events or affecting the cursor. */ font-weight: 500; padding: var(--space-4); border-radius: var(--radius-3); - cursor: pointer; + cursor: var(--tl-cursor-pointer); color: inherit; background-color: transparent; } diff --git a/packages/editor/src/lib/app/statechart/TLSelectTool/children/Idle.ts b/packages/editor/src/lib/app/statechart/TLSelectTool/children/Idle.ts index 46da243a7..18aa74657 100644 --- a/packages/editor/src/lib/app/statechart/TLSelectTool/children/Idle.ts +++ b/packages/editor/src/lib/app/statechart/TLSelectTool/children/Idle.ts @@ -1,5 +1,6 @@ import { Vec2d } from '@tldraw/primitives' -import { createShapeId, TLShape } from '@tldraw/tlschema' +import { createShapeId, TLGeoShapeProps, TLShape } from '@tldraw/tlschema' +import { debugFlags } from '../../../../utils/debug-flags' import { TLClickEventInfo, TLEventHandlers, @@ -26,6 +27,20 @@ export class Idle extends StateNode { if (hoveringShape.id !== focusLayerId) { this.app.setHoveredId(hoveringShape.id) } + + // Custom cursor debugging! + // Change the cursor to the type specified by the shape's text label + if (debugFlags.debugCursors.value) { + if (hoveringShape.type !== 'geo') break + const cursorType = (hoveringShape.props as TLGeoShapeProps).text + try { + this.app.setCursor({ type: cursorType }) + } catch (e) { + console.error(`Cursor type not recognized: '${cursorType}'`) + this.app.setCursor({ type: 'default' }) + } + } + break } } diff --git a/packages/editor/src/lib/hooks/useCursor.ts b/packages/editor/src/lib/hooks/useCursor.ts index 0e85464e8..da83a13f5 100644 --- a/packages/editor/src/lib/hooks/useCursor.ts +++ b/packages/editor/src/lib/hooks/useCursor.ts @@ -4,14 +4,30 @@ import { useApp } from './useApp' import { useContainer } from './useContainer' import { useQuickReactor } from './useQuickReactor' +const DEFAULT_SVG = `` +const POINTER_SVG = `` +const CROSS_SVG = `` +const MOVE_SVG = `` + const CORNER_SVG = `` const EDGE_SVG = `` const ROTATE_CORNER_SVG = `` -const TEXT_SVG = `` -const GRABBING_SVG = `` -const GRAB_SVG = `` +const TEXT_SVG = `` +const GRABBING_SVG = `` +const GRAB_SVG = `` -function getCursorCss(svg: string, r: number, tr: number, f: boolean, color: string) { +const ZOOM_IN_SVG = `` +const ZOOM_OUT_SVG = `` + +function getCursorCss( + svg: string, + r: number, + tr: number, + f: boolean, + color: string, + hotspotX = 16, + hotspotY = 16 +) { const a = (-tr - r) * (PI / 180) const s = Math.sin(a) const c = Math.cos(a) @@ -23,19 +39,19 @@ function getCursorCss(svg: string, r: number, tr: number, f: boolean, color: str r + tr } 16 16)${f ? ` scale(-1,-1) translate(0, -32)` : ''}' filter='url(%23shadow)'>` + svg.replaceAll(`"`, `'`) + - '") 16 16, pointer' + `") ${hotspotX} ${hotspotY}, pointer` ) } const CURSORS: Record string> = { none: () => 'none', - default: () => 'default', - pointer: () => 'pointer', - cross: () => 'crosshair', - move: () => 'move', + default: (r, f, c) => getCursorCss(DEFAULT_SVG, r, 0, f, c, 12, 10), + pointer: (r, f, c) => getCursorCss(POINTER_SVG, r, 0, f, c, 14, 10), + cross: (r, f, c) => getCursorCss(CROSS_SVG, r, 0, f, c), + move: (r, f, c) => getCursorCss(MOVE_SVG, r, 0, f, c), grab: (r, f, c) => getCursorCss(GRAB_SVG, r, 0, f, c), grabbing: (r, f, c) => getCursorCss(GRABBING_SVG, r, 0, f, c), - text: (r, f, c) => getCursorCss(TEXT_SVG, r, 0, f, c), + text: (r, f, c) => getCursorCss(TEXT_SVG, r, 0, f, c, 4, 10), 'resize-edge': (r, f, c) => getCursorCss(EDGE_SVG, r, 0, f, c), 'resize-corner': (r, f, c) => getCursorCss(CORNER_SVG, r, 0, f, c), 'ew-resize': (r, f, c) => getCursorCss(EDGE_SVG, r, 0, f, c), @@ -47,8 +63,8 @@ const CURSORS: Record st 'nesw-rotate': (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 90, f, c), 'senw-rotate': (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 180, f, c), 'swne-rotate': (r, f, c) => getCursorCss(ROTATE_CORNER_SVG, r, 270, f, c), - 'zoom-in': () => 'zoom-in', - 'zoom-out': () => 'zoom-out', + 'zoom-in': (r, f, c) => getCursorCss(ZOOM_IN_SVG, r, 0, f, c), + 'zoom-out': (r, f, c) => getCursorCss(ZOOM_OUT_SVG, r, 0, f, c), } export function getCursor(cursor: TLCursorType, rotation = 0, color = 'black') { @@ -67,4 +83,14 @@ export function useCursor() { }, [app, container] ) + + useQuickReactor( + 'useStaticCursor', + () => { + for (const key in CURSORS) { + container.style.setProperty(`--tl-cursor-${key}`, getCursor(key)) + } + }, + [app, container] + ) } diff --git a/packages/editor/src/lib/utils/debug-flags.ts b/packages/editor/src/lib/utils/debug-flags.ts index 7332729e1..8b81d10ac 100644 --- a/packages/editor/src/lib/utils/debug-flags.ts +++ b/packages/editor/src/lib/utils/debug-flags.ts @@ -23,6 +23,7 @@ export const debugFlags = { peopleMenu: createDebugValue('tldrawPeopleMenu', false), logMessages: createDebugValue('tldrawUiLog', []), resetConnectionEveryPing: createDebugValue('tldrawResetConnectionEveryPing', false), + debugCursors: createDebugValue('tldrawDebugCursors', false), } declare global { diff --git a/packages/ui/src/lib/components/DebugPanel.tsx b/packages/ui/src/lib/components/DebugPanel.tsx index 974accdc6..849fb34ef 100644 --- a/packages/ui/src/lib/components/DebugPanel.tsx +++ b/packages/ui/src/lib/components/DebugPanel.tsx @@ -151,6 +151,36 @@ function DebugMenuContent({ Count shapes and nodes + { + if (!debugFlags.debugCursors.value) { + debugFlags.debugCursors.set(true) + + const MAX_COLUMNS = 5 + const partials = CURSOR_NAMES.map((name, i) => { + return { + id: app.createShapeId(), + type: 'geo', + x: (i % MAX_COLUMNS) * 175, + y: Math.floor(i / MAX_COLUMNS) * 175, + props: { + text: name, + w: 150, + h: 150, + fill: 'semi', + }, + } + }) + + app.createShapes(partials) + } else { + debugFlags.debugCursors.set(false) + } + }} + > + {debugFlags.debugCursors.value ? 'Debug cursors ✓' : 'Debug cursors'} + + {(() => { if (error) throw Error('oh no!') })()} @@ -192,6 +222,30 @@ function DebugMenuContent({ ) } +const CURSOR_NAMES = [ + 'none', + 'default', + 'pointer', + 'cross', + 'move', + 'grab', + 'grabbing', + 'text', + 'resize-edge', + 'resize-corner', + 'ew-resize', + 'ns-resize', + 'nesw-resize', + 'nwse-resize', + 'rotate', + 'nwse-rotate', + 'nesw-rotate', + 'senw-rotate', + 'swne-rotate', + 'zoom-in', + 'zoom-out', +] + function ExampleDialog({ title = 'title', body = 'hello hello hello',