Start scrolling if we are dragging close to the window edges. (#2299)
Start scrolling when we get close to the edges of the window. This works for brush selecting, translating, and resizing. https://github.com/tldraw/tldraw/assets/2523721/4a5effc8-5445-411b-b317-36097233d36c ### Change Type - [ ] `patch` — Bug fix - [x] `minor` — New feature - [ ] `major` — Breaking change - [ ] `dependencies` — Changes to package dependencies[^1] - [ ] `documentation` — Changes to the documentation only[^2] - [ ] `tests` — Changes to any test code only[^2] - [ ] `internal` — Any other changes that don't affect the published package[^2] - [ ] I don't know [^1]: publishes a `patch` release, for devDependencies use `internal` [^2]: will not publish a new version ### Test Plan 1. Select a shape. 2. Move it towards the edge of the window. The camera position should change. 3. Also try resizing, brush selecting. - [x] Unit Tests - [ ] End to end tests ### Release Notes - Adds the logic to change the camera position when you get close to the edges of the window. This allows you to drag, resize, brush select past the edges of the current viewport. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
0171d1498d
commit
4e50c9c162
26 changed files with 428 additions and 15 deletions
|
@ -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",
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"packages": ["packages/*"],
|
||||
"version": "2.0.0-alpha.19"
|
||||
}
|
||||
|
|
|
@ -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<TLEventHandlers> {
|
|||
// (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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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<TLUserPreferences> = T.object<TLUserPrefere
|
|||
color: T.string.nullable().optional(),
|
||||
isDarkMode: T.boolean.nullable().optional(),
|
||||
animationSpeed: T.number.nullable().optional(),
|
||||
edgeScrollSpeed: T.number.nullable().optional(),
|
||||
isSnapMode: T.boolean.nullable().optional(),
|
||||
})
|
||||
|
||||
|
@ -46,10 +48,11 @@ const Versions = {
|
|||
AddAnimationSpeed: 1,
|
||||
AddIsSnapMode: 2,
|
||||
MakeFieldsNullable: 3,
|
||||
AddEdgeScrollSpeed: 4,
|
||||
} as const
|
||||
|
||||
const userMigrations = defineMigrations({
|
||||
currentVersion: Versions.MakeFieldsNullable,
|
||||
currentVersion: Versions.AddEdgeScrollSpeed,
|
||||
migrators: {
|
||||
[Versions.AddAnimationSpeed]: {
|
||||
up: (user) => {
|
||||
|
@ -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<Omit<TLUserPreferences, 'id'>>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<TLEventHandlers> {
|
|||
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<TLEventHandlers> {
|
|||
// 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<TLEventHandlers> {
|
|||
|
||||
onEnter?: TLEnterEventHandler
|
||||
onExit?: TLExitEventHandler
|
||||
onTick?: TLTickEventHandler
|
||||
}
|
||||
|
|
|
@ -136,6 +136,8 @@ export type UiEvent =
|
|||
| TLCancelEvent
|
||||
| TLCompleteEvent
|
||||
|
||||
/** @public */
|
||||
export type TLTickEventHandler = () => void
|
||||
/** @public */
|
||||
export type TLEnterEventHandler = (info: any, from: string) => void
|
||||
/** @public */
|
||||
|
|
66
packages/editor/src/lib/utils/edgeScrolling.ts
Normal file
66
packages/editor/src/lib/utils/edgeScrolling.ts
Normal file
|
@ -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,
|
||||
})
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in a new issue