diff --git a/assets/translations/main.json b/assets/translations/main.json index 58a58ee52..29bee296f 100644 --- a/assets/translations/main.json +++ b/assets/translations/main.json @@ -77,6 +77,8 @@ "action.toggle-dark-mode": "Toggle dark mode", "action.toggle-reduce-motion.menu": "Reduce motion", "action.toggle-reduce-motion": "Toggle reduce motion", + "action.toggle-edge-scrolling.menu": "Edge scrolling", + "action.toggle-edge-scrolling": "Toggle edge scrolling", "action.toggle-debug-mode.menu": "Debug mode", "action.toggle-debug-mode": "Toggle debug mode", "action.toggle-focus-mode.menu": "Focus mode", diff --git a/lerna.json b/lerna.json index c3e7f2dab..ea6ddccc7 100644 --- a/lerna.json +++ b/lerna.json @@ -1,7 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "packages": [ - "packages/*" - ], + "packages": ["packages/*"], "version": "2.0.0-alpha.19" } diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 7fa8ba97e..5c828eec6 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -488,6 +488,7 @@ export const defaultUserPreferences: Readonly<{ locale: "ar" | "ca" | "da" | "de" | "en" | "es" | "fa" | "fi" | "fr" | "gl" | "he" | "hi-in" | "hu" | "it" | "ja" | "ko-kr" | "ku" | "my" | "ne" | "no" | "pl" | "pt-br" | "pt-pt" | "ro" | "ru" | "sv" | "te" | "th" | "tr" | "uk" | "vi" | "zh-cn" | "zh-tw"; color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B"; isDarkMode: false; + edgeScrollSpeed: 1; animationSpeed: 0 | 1; isSnapMode: false; }>; @@ -1459,6 +1460,9 @@ export const MAX_ZOOM = 8; // @internal (undocumented) export const MIN_ZOOM = 0.1; +// @public +export function moveCameraWhenCloseToEdge(editor: Editor): void; + // @internal (undocumented) export const MULTI_CLICK_DURATION = 200; @@ -1963,6 +1967,8 @@ export abstract class StateNode implements Partial { // (undocumented) onRightClick?: TLEventHandlers['onRightClick']; // (undocumented) + onTick?: TLTickEventHandler; + // (undocumented) onTripleClick?: TLEventHandlers['onTripleClick']; // (undocumented) onWheel?: TLEventHandlers['onWheel']; @@ -2702,6 +2708,9 @@ export type TLSvgOptions = { // @public (undocumented) export type TLTickEvent = (elapsed: number) => void; +// @public (undocumented) +export type TLTickEventHandler = () => void; + // @public export interface TLUserPreferences { // (undocumented) @@ -2709,6 +2718,8 @@ export interface TLUserPreferences { // (undocumented) color?: null | string; // (undocumented) + edgeScrollSpeed?: null | number; + // (undocumented) id: string; // (undocumented) isDarkMode?: boolean | null; diff --git a/packages/editor/api/api.json b/packages/editor/api/api.json index 47cba068c..a61c7ce00 100644 --- a/packages/editor/api/api.json +++ b/packages/editor/api/api.json @@ -6126,7 +6126,7 @@ }, { "kind": "Content", - "text": "<{\n name: \"New User\";\n locale: \"ar\" | \"ca\" | \"da\" | \"de\" | \"en\" | \"es\" | \"fa\" | \"fi\" | \"fr\" | \"gl\" | \"he\" | \"hi-in\" | \"hu\" | \"it\" | \"ja\" | \"ko-kr\" | \"ku\" | \"my\" | \"ne\" | \"no\" | \"pl\" | \"pt-br\" | \"pt-pt\" | \"ro\" | \"ru\" | \"sv\" | \"te\" | \"th\" | \"tr\" | \"uk\" | \"vi\" | \"zh-cn\" | \"zh-tw\";\n color: \"#02B1CC\" | \"#11B3A3\" | \"#39B178\" | \"#55B467\" | \"#7B66DC\" | \"#9D5BD2\" | \"#BD54C6\" | \"#E34BA9\" | \"#EC5E41\" | \"#F04F88\" | \"#F2555A\" | \"#FF802B\";\n isDarkMode: false;\n animationSpeed: 0 | 1;\n isSnapMode: false;\n}>" + "text": "<{\n name: \"New User\";\n locale: \"ar\" | \"ca\" | \"da\" | \"de\" | \"en\" | \"es\" | \"fa\" | \"fi\" | \"fr\" | \"gl\" | \"he\" | \"hi-in\" | \"hu\" | \"it\" | \"ja\" | \"ko-kr\" | \"ku\" | \"my\" | \"ne\" | \"no\" | \"pl\" | \"pt-br\" | \"pt-pt\" | \"ro\" | \"ru\" | \"sv\" | \"te\" | \"th\" | \"tr\" | \"uk\" | \"vi\" | \"zh-cn\" | \"zh-tw\";\n color: \"#02B1CC\" | \"#11B3A3\" | \"#39B178\" | \"#55B467\" | \"#7B66DC\" | \"#9D5BD2\" | \"#BD54C6\" | \"#E34BA9\" | \"#EC5E41\" | \"#F04F88\" | \"#F2555A\" | \"#FF802B\";\n isDarkMode: false;\n edgeScrollSpeed: 1;\n animationSpeed: 0 | 1;\n isSnapMode: false;\n}>" } ], "fileUrlPath": "packages/editor/src/lib/config/TLUserPreferences.ts", @@ -28314,6 +28314,52 @@ ], "extendsTokenRanges": [] }, + { + "kind": "Function", + "canonicalReference": "@tldraw/editor!moveCameraWhenCloseToEdge:function(1)", + "docComment": "/**\n * Moves the camera when the mouse is close to the edge of the screen.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function moveCameraWhenCloseToEdge(editor: " + }, + { + "kind": "Reference", + "text": "Editor", + "canonicalReference": "@tldraw/editor!Editor:class" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/editor/src/lib/utils/edgeScrolling.ts", + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 4 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "editor", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "name": "moveCameraWhenCloseToEdge" + }, { "kind": "Function", "canonicalReference": "@tldraw/editor!openWindow:function(1)", @@ -36443,6 +36489,37 @@ "isProtected": false, "isAbstract": false }, + { + "kind": "Property", + "canonicalReference": "@tldraw/editor!StateNode#onTick:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "onTick?: " + }, + { + "kind": "Reference", + "text": "TLTickEventHandler", + "canonicalReference": "@tldraw/editor!TLTickEventHandler:type" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "onTick", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isStatic": false, + "isProtected": false, + "isAbstract": false + }, { "kind": "Property", "canonicalReference": "@tldraw/editor!StateNode#onTripleClick:member", @@ -43331,6 +43408,32 @@ "endIndex": 2 } }, + { + "kind": "TypeAlias", + "canonicalReference": "@tldraw/editor!TLTickEventHandler:type", + "docComment": "/**\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export type TLTickEventHandler = " + }, + { + "kind": "Content", + "text": "() => void" + }, + { + "kind": "Content", + "text": ";" + } + ], + "fileUrlPath": "packages/editor/src/lib/editor/types/event-types.ts", + "releaseTag": "Public", + "name": "TLTickEventHandler", + "typeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, { "kind": "Interface", "canonicalReference": "@tldraw/editor!TLUserPreferences:interface", @@ -43400,6 +43503,33 @@ "endIndex": 2 } }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/editor!TLUserPreferences#edgeScrollSpeed:member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "edgeScrollSpeed?: " + }, + { + "kind": "Content", + "text": "null | number" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": true, + "releaseTag": "Public", + "name": "edgeScrollSpeed", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/editor!TLUserPreferences#id:member", diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 3683b3b9b..73027b5f8 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -225,6 +225,7 @@ export { type TLPointerEventName, type TLPointerEventTarget, type TLTickEvent, + type TLTickEventHandler, type TLWheelEvent, type TLWheelEventInfo, type UiEvent, @@ -346,6 +347,7 @@ export { setPointerCapture, stopEventPropagation, } from './lib/utils/dom' +export { moveCameraWhenCloseToEdge } from './lib/utils/edgeScrolling' export { getIncrementedName } from './lib/utils/getIncrementedName' export { getPointerInfo } from './lib/utils/getPointerInfo' export { getSvgPathFromPoints } from './lib/utils/getSvgPathFromPoints' diff --git a/packages/editor/src/lib/config/TLUserPreferences.ts b/packages/editor/src/lib/config/TLUserPreferences.ts index 011c93b84..bec99889c 100644 --- a/packages/editor/src/lib/config/TLUserPreferences.ts +++ b/packages/editor/src/lib/config/TLUserPreferences.ts @@ -18,6 +18,7 @@ export interface TLUserPreferences { color?: string | null isDarkMode?: boolean | null animationSpeed?: number | null + edgeScrollSpeed?: number | null isSnapMode?: boolean | null } @@ -39,6 +40,7 @@ const userTypeValidator: T.Validator = T.object { @@ -86,6 +89,17 @@ const userMigrations = defineMigrations({ } }, }, + [Versions.AddEdgeScrollSpeed]: { + up: (user: TLUserPreferences) => { + return { + ...user, + edgeScrollSpeed: 1, + } + }, + down: ({ edgeScrollSpeed: _, ...user }: TLUserPreferences) => { + return user + }, + }, }, }) @@ -131,6 +145,7 @@ export const defaultUserPreferences = Object.freeze({ locale: getDefaultTranslationLocale(), color: getRandomColor(), isDarkMode: false, + edgeScrollSpeed: 1, animationSpeed: userPrefersReducedMotion() ? 0 : 1, isSnapMode: false, }) satisfies Readonly> diff --git a/packages/editor/src/lib/constants.ts b/packages/editor/src/lib/constants.ts index 256550726..e291420c9 100644 --- a/packages/editor/src/lib/constants.ts +++ b/packages/editor/src/lib/constants.ts @@ -92,3 +92,6 @@ export const CAMERA_MAX_RENDERING_INTERVAL = 620 /** @public */ export const HIT_TEST_MARGIN = 8 + +/** @internal */ +export const EDGE_SCROLL_SPEED = 20 diff --git a/packages/editor/src/lib/editor/managers/UserPreferencesManager.ts b/packages/editor/src/lib/editor/managers/UserPreferencesManager.ts index efca68b5a..1223c3110 100644 --- a/packages/editor/src/lib/editor/managers/UserPreferencesManager.ts +++ b/packages/editor/src/lib/editor/managers/UserPreferencesManager.ts @@ -52,6 +52,13 @@ export class UserPreferencesManager { return this.getIsDarkMode() } + /** + * The speed at which the user can scroll by dragging toward the edge of the screen. + */ + @computed getEdgeScrollSpeed() { + return this.user.userPreferences.get().edgeScrollSpeed ?? defaultUserPreferences.edgeScrollSpeed + } + @computed getAnimationSpeed() { return this.user.userPreferences.get().animationSpeed ?? defaultUserPreferences.animationSpeed } diff --git a/packages/editor/src/lib/editor/tools/StateNode.ts b/packages/editor/src/lib/editor/tools/StateNode.ts index 7cce7b8bd..cd4e8ca2d 100644 --- a/packages/editor/src/lib/editor/tools/StateNode.ts +++ b/packages/editor/src/lib/editor/tools/StateNode.ts @@ -8,6 +8,7 @@ import { TLEventInfo, TLExitEventHandler, TLPinchEventInfo, + TLTickEventHandler, } from '../types/event-types' type TLStateNodeType = 'branch' | 'leaf' | 'root' @@ -155,6 +156,7 @@ export abstract class StateNode implements Partial { enter = (info: any, from: string) => { this._isActive.set(true) this.onEnter?.(info, from) + if (this.onTick) this.editor.on('tick', this.onTick) if (this.children && this.initial && this.getIsActive()) { const initial = this.children[this.initial] this._current.set(initial) @@ -165,6 +167,7 @@ export abstract class StateNode implements Partial { // todo: move this logic into transition exit = (info: any, from: string) => { this._isActive.set(false) + if (this.onTick) this.editor.off('tick', this.onTick) this.onExit?.(info, from) if (!this.getIsActive()) { this.getCurrent()?.exit(info, from) @@ -223,4 +226,5 @@ export abstract class StateNode implements Partial { onEnter?: TLEnterEventHandler onExit?: TLExitEventHandler + onTick?: TLTickEventHandler } diff --git a/packages/editor/src/lib/editor/types/event-types.ts b/packages/editor/src/lib/editor/types/event-types.ts index 1b3476665..5ae0b20a1 100644 --- a/packages/editor/src/lib/editor/types/event-types.ts +++ b/packages/editor/src/lib/editor/types/event-types.ts @@ -136,6 +136,8 @@ export type UiEvent = | TLCancelEvent | TLCompleteEvent +/** @public */ +export type TLTickEventHandler = () => void /** @public */ export type TLEnterEventHandler = (info: any, from: string) => void /** @public */ diff --git a/packages/editor/src/lib/utils/edgeScrolling.ts b/packages/editor/src/lib/utils/edgeScrolling.ts new file mode 100644 index 000000000..f0670b2c2 --- /dev/null +++ b/packages/editor/src/lib/utils/edgeScrolling.ts @@ -0,0 +1,66 @@ +import { EDGE_SCROLL_SPEED } from '../constants' +import { Editor } from '../editor/Editor' + +/** + * Helper function to get the scroll offset for a given position. + * The closer the mouse is to the edge of the screen the faster we scroll. + * We also adjust the speed and the start offset based on the screen size and zoom level. + * + * @param editor - The mouse position on the screen in pixels + * @returns How much we should scroll in pixels + * @internal + */ +export function getEdgeProximityFactor(position: number, scrollOffset: number, extreme: number) { + if (position < 0) { + return 1 + } else if (position > extreme) { + return -1 + } else if (position < scrollOffset) { + return (scrollOffset - position) / scrollOffset + } else if (position > extreme - scrollOffset) { + return -(scrollOffset - extreme + position) / scrollOffset + } + return 0 +} + +/** + * Moves the camera when the mouse is close to the edge of the screen. + * @public + */ +export function moveCameraWhenCloseToEdge(editor: Editor) { + if (!editor.inputs.isDragging || editor.inputs.isPanning) return + + const { + inputs: { + currentScreenPoint: { x, y }, + }, + } = editor + const zoomLevel = editor.getZoomLevel() + const screenBounds = editor.getViewportScreenBounds() + + // Determines how far from the edges we start the scroll behaviour + const insetX = screenBounds.w < 1000 ? 40 : 32 + const insetY = screenBounds.h < 1000 ? 40 : 32 + + // Determines how much the speed is affected by the screen size + const screenSizeFactorX = screenBounds.w < 1000 ? 0.612 : 1 + const screenSizeFactorY = screenBounds.h < 1000 ? 0.612 : 1 + + // Determines the base speed of the scroll + const pxSpeed = editor.user.getEdgeScrollSpeed() * EDGE_SCROLL_SPEED + + const proximityFactorX = getEdgeProximityFactor(x, insetX, screenBounds.w) + const proximityFactorY = getEdgeProximityFactor(y, insetY, screenBounds.h) + + if (proximityFactorX === 0 && proximityFactorY === 0) return + + const scrollDeltaX = (pxSpeed * proximityFactorX * screenSizeFactorX) / zoomLevel + const scrollDeltaY = (pxSpeed * proximityFactorY * screenSizeFactorY) / zoomLevel + + const camera = editor.getCamera() + + editor.setCamera({ + x: camera.x + scrollDeltaX, + y: camera.y + scrollDeltaY, + }) +} diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 1c7a9210e..0abc4b3a3 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -101,6 +101,7 @@ import { TLStore } from '@tldraw/editor'; import { TLStoreWithStatus } from '@tldraw/editor'; import { TLSvgOptions } from '@tldraw/editor'; import { TLTextShape } from '@tldraw/editor'; +import { TLTickEventHandler } from '@tldraw/editor'; import { TLUnknownShape } from '@tldraw/editor'; import { TLVideoShape } from '@tldraw/editor'; import { UnionValidator } from '@tldraw/editor'; @@ -1536,6 +1537,8 @@ export interface TLUiEventMap { // (undocumented) 'toggle-debug-mode': null; // (undocumented) + 'toggle-edge-scrolling': null; + // (undocumented) 'toggle-focus-mode': null; // (undocumented) 'toggle-grid-mode': null; @@ -1810,7 +1813,7 @@ export type TLUiTranslation = { export type TLUiTranslationContextType = TLUiTranslation; // @public (undocumented) -export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; +export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-edge-scrolling.menu' | 'action.toggle-edge-scrolling' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; // @public (undocumented) export function toolbarItem(toolItem: TLUiToolItem): TLUiToolbarItem; diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 32c9f934d..a955e9d43 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -17181,6 +17181,33 @@ "endIndex": 2 } }, + { + "kind": "PropertySignature", + "canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"toggle-edge-scrolling\":member", + "docComment": "", + "excerptTokens": [ + { + "kind": "Content", + "text": "'toggle-edge-scrolling': " + }, + { + "kind": "Content", + "text": "null" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": false, + "isOptional": false, + "releaseTag": "Public", + "name": "\"toggle-edge-scrolling\"", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + } + }, { "kind": "PropertySignature", "canonicalReference": "@tldraw/tldraw!TLUiEventMap#\"toggle-focus-mode\":member", @@ -20234,7 +20261,7 @@ }, { "kind": "Content", - "text": "'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'" + "text": "'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-edge-scrolling.menu' | 'action.toggle-edge-scrolling' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'" }, { "kind": "Content", diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts index bca422156..63d92c803 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Brushing.ts @@ -13,7 +13,9 @@ import { TLPointerEventInfo, TLShape, TLShapeId, + TLTickEventHandler, Vec2d, + moveCameraWhenCloseToEdge, pointInPolygon, polygonsIntersect, } from '@tldraw/editor' @@ -60,6 +62,10 @@ export class Brushing extends StateNode { this.editor.updateInstanceState({ brush: null }) } + override onTick: TLTickEventHandler = () => { + moveCameraWhenCloseToEdge(this.editor) + } + override onPointerMove = () => { this.hitTestShapes() } diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts index 3f0bcb0b7..dcb6dd802 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Resizing.ts @@ -13,10 +13,12 @@ import { TLShape, TLShapeId, TLShapePartial, + TLTickEventHandler, Vec2d, VecLike, areAnglesCompatible, compact, + moveCameraWhenCloseToEdge, } from '@tldraw/editor' type ResizingInfo = TLPointerEventInfo & { @@ -72,6 +74,10 @@ export class Resizing extends StateNode { this.updateShapes() } + override onTick: TLTickEventHandler = () => { + moveCameraWhenCloseToEdge(this.editor) + } + override onPointerMove: TLEventHandlers['onPointerMove'] = () => { this.updateShapes() } diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts index 1465ca74a..1f22a86d6 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/Translating.ts @@ -10,9 +10,11 @@ import { TLPointerEventInfo, TLShape, TLShapePartial, + TLTickEventHandler, Vec2d, compact, isPageId, + moveCameraWhenCloseToEdge, } from '@tldraw/editor' import { DragAndDropManager } from '../DragAndDropManager' @@ -77,12 +79,10 @@ export class Translating extends StateNode { this.snapshot = this.selectionSnapshot this.handleStart() this.updateShapes() - this.editor.on('tick', this.updateParent) } override onExit = () => { this.parent.setCurrentToolIdMask(undefined) - this.editor.off('tick', this.updateParent) this.selectionSnapshot = {} as any this.snapshot = {} as any this.editor.snaps.clear() @@ -93,6 +93,14 @@ export class Translating extends StateNode { this.dragAndDropManager.clear() } + override onTick: TLTickEventHandler = () => { + this.dragAndDropManager.updateDroppingNode( + this.snapshot.movingShapes, + this.updateParentTransforms + ) + moveCameraWhenCloseToEdge(this.editor) + } + override onPointerMove = () => { this.updateShapes() } @@ -153,11 +161,6 @@ export class Translating extends StateNode { this.updateShapes() } - updateParent = () => { - const { snapshot } = this - this.dragAndDropManager.updateDroppingNode(snapshot.movingShapes, this.updateParentTransforms) - } - reset() { this.editor.bailToMark(this.markId) } diff --git a/packages/tldraw/src/lib/ui/hooks/useActions.tsx b/packages/tldraw/src/lib/ui/hooks/useActions.tsx index 6559a5c40..a1e03f1be 100644 --- a/packages/tldraw/src/lib/ui/hooks/useActions.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useActions.tsx @@ -994,6 +994,19 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { }, checkbox: true, }, + { + id: 'toggle-edge-scrolling', + label: 'action.toggle-edge-scrolling', + menuLabel: 'action.toggle-edge-scrolling.menu', + readonlyOk: true, + onSelect(source) { + trackEvent('toggle-edge-scrolling', { source }) + editor.user.updateUserPreferences({ + edgeScrollSpeed: editor.user.getEdgeScrollSpeed() === 0 ? 1 : 0, + }) + }, + checkbox: true, + }, { id: 'toggle-transparent', label: 'action.toggle-transparent', diff --git a/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx b/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx index 36cf60830..bc5c487de 100644 --- a/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useEventsProvider.tsx @@ -81,6 +81,7 @@ export interface TLUiEventMap { 'toggle-debug-mode': null 'toggle-lock': null 'toggle-reduce-motion': null + 'toggle-edge-scrolling': null 'exit-pen-mode': null 'stop-following': null 'open-cursor-chat': null diff --git a/packages/tldraw/src/lib/ui/hooks/useMenuSchema.tsx b/packages/tldraw/src/lib/ui/hooks/useMenuSchema.tsx index 530ad3182..85df243bf 100644 --- a/packages/tldraw/src/lib/ui/hooks/useMenuSchema.tsx +++ b/packages/tldraw/src/lib/ui/hooks/useMenuSchema.tsx @@ -51,6 +51,9 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr const isDarkMode = useValue('isDarkMode', () => editor.user.getIsDarkMode(), [editor]) const animationSpeed = useValue('animationSpeed', () => editor.user.getAnimationSpeed(), [editor]) + const edgeScrollSpeed = useValue('edgeScrollSpeed', () => editor.user.getEdgeScrollSpeed(), [ + editor, + ]) const isGridMode = useValue('isGridMode', () => editor.getInstanceState().isGridMode, [editor]) const isSnapMode = useValue('isSnapMode', () => editor.user.getIsSnapMode(), [editor]) const isToolLock = useValue('isToolLock', () => editor.getInstanceState().isToolLocked, [editor]) @@ -214,6 +217,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr menuItem(actions['toggle-grid'], { checked: isGridMode }), menuItem(actions['toggle-dark-mode'], { checked: isDarkMode }), menuItem(actions['toggle-focus-mode'], { checked: isFocusMode }), + menuItem(actions['toggle-edge-scrolling'], { checked: edgeScrollSpeed === 1 }), menuItem(actions['toggle-reduce-motion'], { checked: animationSpeed === 0 }), menuItem(actions['toggle-debug-mode'], { checked: isDebugMode }) ) @@ -258,6 +262,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr isFocusMode, exportBackground, isDebugMode, + edgeScrollSpeed, isZoomedTo100, oneEmbeddableBookmarkSelected, oneEmbedSelected, diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts index 50c1be511..2ff1bdbeb 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts @@ -81,6 +81,8 @@ export type TLUiTranslationKey = | 'action.toggle-dark-mode' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' + | 'action.toggle-edge-scrolling.menu' + | 'action.toggle-edge-scrolling' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-focus-mode.menu' diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts index 9363c67f7..6bf893d67 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts @@ -81,6 +81,8 @@ export const DEFAULT_TRANSLATION = { 'action.toggle-dark-mode': 'Toggle dark mode', 'action.toggle-reduce-motion.menu': 'Reduce motion', 'action.toggle-reduce-motion': 'Toggle reduce motion', + 'action.toggle-edge-scrolling.menu': 'Edge scrolling', + 'action.toggle-edge-scrolling': 'Toggle edge scrolling', 'action.toggle-debug-mode.menu': 'Debug mode', 'action.toggle-debug-mode': 'Toggle debug mode', 'action.toggle-focus-mode.menu': 'Focus mode', diff --git a/packages/tldraw/src/test/TLUserPreferences.test.ts b/packages/tldraw/src/test/TLUserPreferences.test.ts index b566ee198..b38cfd93a 100644 --- a/packages/tldraw/src/test/TLUserPreferences.test.ts +++ b/packages/tldraw/src/test/TLUserPreferences.test.ts @@ -109,15 +109,18 @@ describe('TLUserPreferences', () => { userPreferences, }), }) + // called once in the constructor of testeditor to set edge scroll speed to 0 + expect(setUserPreferences).toHaveBeenCalledTimes(1) expect(editor.user.getName()).toBe('blah') editor.user.updateUserPreferences({ name: null }) expect(editor.user.getName()).toBe('New User') - expect(setUserPreferences).toHaveBeenCalledTimes(1) + expect(setUserPreferences).toHaveBeenCalledTimes(2) expect(setUserPreferences).toHaveBeenLastCalledWith({ id: '123', name: null, + edgeScrollSpeed: 0, }) }) }) diff --git a/packages/tldraw/src/test/TestEditor.ts b/packages/tldraw/src/test/TestEditor.ts index 09409315f..e55e3c648 100644 --- a/packages/tldraw/src/test/TestEditor.ts +++ b/packages/tldraw/src/test/TestEditor.ts @@ -109,6 +109,9 @@ export class TestEditor extends Editor { }) return [{ box, text: textToMeasure }] } + + // Turn off edge scrolling for tests. Tests that require this can turn it back on. + this.user.updateUserPreferences({ edgeScrollSpeed: 0 }) } elm: HTMLDivElement diff --git a/packages/tldraw/src/test/resizing.test.ts b/packages/tldraw/src/test/resizing.test.ts index cc01af2a2..dc61ef9d6 100644 --- a/packages/tldraw/src/test/resizing.test.ts +++ b/packages/tldraw/src/test/resizing.test.ts @@ -3899,3 +3899,25 @@ describe('Resizing text from the right edge', () => { }) }) }) + +describe('When resizing near the edges of the screen', () => { + it('resizes past the edge of the screen', () => { + editor.user.updateUserPreferences({ edgeScrollSpeed: 1 }) + editor + .select(ids.boxA) + .pointerDown(10, 10, { + type: 'pointer', + target: 'selection', + handle: 'top_left', + }) + .expectShapeToMatch({ id: ids.boxA, x: 10, y: 10, props: { w: 100, h: 100 } }) + .pointerMove(10, 25) + jest.advanceTimersByTime(1000) + editor.expectShapeToMatch({ + id: ids.boxA, + x: -842.5, + y: -259.58, + props: { w: 952.5, h: 369.58 }, + }) + }) +}) diff --git a/packages/tldraw/src/test/selection-omnibus.test.ts b/packages/tldraw/src/test/selection-omnibus.test.ts index d22b4a093..7cf0c9feb 100644 --- a/packages/tldraw/src/test/selection-omnibus.test.ts +++ b/packages/tldraw/src/test/selection-omnibus.test.ts @@ -1708,3 +1708,45 @@ describe('right clicking', () => { expect(editor.getSelectedShapeIds()).toEqual([]) }) }) + +describe('When brushing close to the edges of the screen', () => { + it('selects shapes that are outside of the viewport', () => { + editor.user.updateUserPreferences({ edgeScrollSpeed: 1 }) + editor.createShapes([{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }]) + editor.createShapes([ + { id: ids.box2, type: 'geo', x: -150, y: -150, props: { w: 100, h: 100 } }, + ]) + + editor.pointerMove(300, 300) + editor.pointerDown() + editor.pointerMove(50, 50) + editor.expectToBeIn('select.brushing') + expect(editor.getSelectedShapeIds()).toEqual([ids.box1]) + editor.pointerMove(0, 0) + // still only box 1... + expect(editor.getSelectedShapeIds()).toEqual([ids.box1]) + jest.advanceTimersByTime(100) + // ...but now viewport will have moved to select box2 as well + expect(editor.getSelectedShapeIds()).toEqual([ids.box1, ids.box2]) + editor.pointerUp() + }) + + it('doesnt edge scroll to the other shape', () => { + editor.user.updateUserPreferences({ edgeScrollSpeed: 0 }) // <-- no edge scrolling + editor.createShapes([{ id: ids.box1, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }]) + editor.createShapes([ + { id: ids.box2, type: 'geo', x: -150, y: -150, props: { w: 100, h: 100 } }, + ]) + + editor.pointerMove(300, 300) + editor.pointerDown() + editor.pointerMove(50, 50) + editor.expectToBeIn('select.brushing') + expect(editor.getSelectedShapeIds()).toEqual([ids.box1]) + editor.pointerMove(0, 0) + expect(editor.getSelectedShapeIds()).toEqual([ids.box1]) + jest.advanceTimersByTime(100) + expect(editor.getSelectedShapeIds()).toEqual([ids.box1]) + editor.pointerUp() + }) +}) diff --git a/packages/tldraw/src/test/translating.test.ts b/packages/tldraw/src/test/translating.test.ts index b16945acf..08f8a7089 100644 --- a/packages/tldraw/src/test/translating.test.ts +++ b/packages/tldraw/src/test/translating.test.ts @@ -127,6 +127,40 @@ describe('When translating...', () => { .expectShapeToMatch({ id: ids.box1, x: 60, y: 60 }) }) + it('translates a single shape near the top left edge', () => { + editor.user.updateUserPreferences({ edgeScrollSpeed: 1 }) + editor.pointerDown(50, 50, ids.box1).pointerMove(0, 50) // [-50, 0] + + jest.advanceTimersByTime(100) + editor + // The change is bigger than expected because the camera moves + .expectShapeToMatch({ id: ids.box1, x: -160, y: 10 }) + // We'll continue moving in the x postion, but now we'll also move in the y position. + // The speed in the y position is smaller since we are further away from the edge. + .pointerMove(0, 25) + jest.advanceTimersByTime(100) + editor + .expectShapeToMatch({ id: ids.box1, x: -280, y: -42.54 }) + .pointerUp() + .expectShapeToMatch({ id: ids.box1, x: -280, y: -42.54 }) + }) + + it('translates a single shape near the bottom right edge', () => { + editor.user.updateUserPreferences({ edgeScrollSpeed: 1 }) + editor.pointerDown(50, 50, ids.box1).pointerMove(1080, 50) + + jest.advanceTimersByTime(100) + editor + // The change is bigger than expected because the camera moves + .expectShapeToMatch({ id: ids.box1, x: 1140, y: 10 }) + .pointerMove(1080, 800) + jest.advanceTimersByTime(100) + editor + .expectShapeToMatch({ id: ids.box1, x: 1280, y: 845.68 }) + .pointerUp() + .expectShapeToMatch({ id: ids.box1, x: 1280, y: 845.68 }) + }) + it('translates multiple shapes', () => { editor .select(ids.box1, ids.box2) @@ -173,6 +207,7 @@ describe('When cloning...', () => { }) it('clones a single shape and restores when stopping cloning', () => { + // Move the camera so that we are not at the edges, which causes the camera to move when we translate expect(editor.getCurrentPageShapeIds().size).toBe(3) expect(editor.getCurrentPageShapeIds().size).toBe(3) editor.select(ids.box1).pointerDown(50, 50, ids.box1).pointerMove(50, 40) // [0, -10]