diff --git a/libs/components/board-draw/src/TlDraw.tsx b/libs/components/board-draw/src/TlDraw.tsx index 21044ad51d..1e95506ab0 100644 --- a/libs/components/board-draw/src/TlDraw.tsx +++ b/libs/components/board-draw/src/TlDraw.tsx @@ -1,5 +1,13 @@ /* eslint-disable max-lines */ -import * as React from 'react'; +import { + memo, + useEffect, + useLayoutEffect, + useRef, + useMemo, + useState, + type RefObject, +} from 'react'; import { Renderer } from '@tldraw/core'; import { styled } from '@toeverything/components/ui'; import { @@ -132,13 +140,13 @@ export function Tldraw({ getSession, tools, }: TldrawProps) { - const [sId, set_sid] = React.useState(id); + const [sId, setSid] = useState(id); const { pageClientWidth } = usePageClientWidth(); // page padding left and right total 300px const editorShapeInitSize = pageClientWidth - 300; // Create a new app when the component mounts. - const [app, setApp] = React.useState(() => { + const [app, setApp] = useState(() => { const app = new TldrawApp({ id, callbacks, @@ -151,7 +159,7 @@ export function Tldraw({ }); // Create a new app if the `id` prop changes. - React.useLayoutEffect(() => { + useLayoutEffect(() => { if (id === sId) return; const newApp = new TldrawApp({ id, @@ -161,14 +169,14 @@ export function Tldraw({ tools, }); - set_sid(id); + setSid(id); setApp(newApp); }, [sId, id]); // Update the document if the `document` prop changes but the ids, // are the same, or else load a new document if the ids are different. - React.useEffect(() => { + useEffect(() => { if (!document) return; if (document.id === app.document.id) { @@ -179,34 +187,34 @@ export function Tldraw({ }, [document, app]); // Disable assets when the `disableAssets` prop changes. - React.useEffect(() => { + useEffect(() => { app.setDisableAssets(disableAssets); }, [app, disableAssets]); // Change the page when the `currentPageId` prop changes. - React.useEffect(() => { + useEffect(() => { if (!currentPageId) return; app.changePage(currentPageId); }, [currentPageId, app]); // Toggle the app's readOnly mode when the `readOnly` prop changes. - React.useEffect(() => { + useEffect(() => { app.readOnly = readOnly; }, [app, readOnly]); // Toggle the app's darkMode when the `darkMode` prop changes. - React.useEffect(() => { + useEffect(() => { if (darkMode !== app.settings.isDarkMode) { app.toggleDarkMode(); } }, [app, darkMode]); // Update the app's callbacks when any callback changes. - React.useEffect(() => { + useEffect(() => { app.callbacks = callbacks || {}; }, [app, callbacks]); - React.useLayoutEffect(() => { + useLayoutEffect(() => { if (typeof window === 'undefined') return; if (!window.document?.fonts) return; @@ -260,7 +268,7 @@ interface InnerTldrawProps { showSponsorLink?: boolean; } -const InnerTldraw = React.memo(function InnerTldraw({ +const InnerTldraw = memo(function InnerTldraw({ id, autofocus, showPages, @@ -276,7 +284,7 @@ const InnerTldraw = React.memo(function InnerTldraw({ }: InnerTldrawProps) { const app = useTldrawApp(); - const rWrapper = React.useRef(null); + const rWrapper = useRef(null); const state = app.useStore(); @@ -299,7 +307,7 @@ const InnerTldraw = React.memo(function InnerTldraw({ TLDR.get_shape_util(page.shapes[selectedIds[0]].type).hideResizeHandles; // Custom rendering meta, with dark mode for shapes - const meta: TDMeta = React.useMemo(() => { + const meta: TDMeta = useMemo(() => { return { isDarkMode: settings.isDarkMode, app }; }, [settings.isDarkMode, app]); @@ -308,7 +316,7 @@ const InnerTldraw = React.memo(function InnerTldraw({ : appState.selectByContain; // Custom theme, based on darkmode - const theme = React.useMemo(() => { + const theme = useMemo(() => { const { selectByContain } = appState; const { isDarkMode, isCadSelectMode } = settings; @@ -373,9 +381,11 @@ const InnerTldraw = React.memo(function InnerTldraw({ !isSelecting || !settings.showCloneHandles || pageState.camera.zoom < 0.2; + return ( @@ -477,17 +487,17 @@ const InnerTldraw = React.memo(function InnerTldraw({ ); }); -const OneOff = React.memo(function OneOff({ +const OneOff = memo(function OneOff({ focusableRef, autofocus, }: { autofocus?: boolean; - focusableRef: React.RefObject; + focusableRef: RefObject; }) { useKeyboardShortcuts(focusableRef); useStylesheet(); - React.useEffect(() => { + useEffect(() => { if (autofocus) { focusableRef.current?.focus(); } @@ -496,8 +506,8 @@ const OneOff = React.memo(function OneOff({ return null; }); -const StyledLayout = styled('div')<{ penColor: string }>( - ({ theme, penColor }) => { +const StyledLayout = styled('div')<{ penColor: string; panning: boolean }>( + ({ theme, panning, penColor }) => { return { position: 'relative', height: '100%', @@ -509,6 +519,7 @@ const StyledLayout = styled('div')<{ penColor: string }>( overflow: 'hidden', boxSizing: 'border-box', outline: 'none', + cursor: panning ? 'grab' : 'unset', '& .tl-container': { position: 'absolute', diff --git a/libs/components/board-state/src/tldraw-app.ts b/libs/components/board-state/src/tldraw-app.ts index 05a7e8007e..b062cc8b8c 100644 --- a/libs/components/board-state/src/tldraw-app.ts +++ b/libs/components/board-state/src/tldraw-app.ts @@ -219,8 +219,6 @@ export class TldrawApp extends StateManager { isPointing = false; - isForcePanning = false; - editingStartTime = -1; fileSystemHandle: FileSystemHandle | null = null; @@ -262,7 +260,7 @@ export class TldrawApp extends StateManager { constructor(props: TldrawAppCtorProps) { super( - TldrawApp.default_state, + TldrawApp.defaultState, props.id, TldrawApp.version, (prev, next, prevVersion) => { @@ -326,9 +324,9 @@ export class TldrawApp extends StateManager { ); this.patchState({ - ...TldrawApp.default_state, + ...TldrawApp.defaultState, appState: { - ...TldrawApp.default_state.appState, + ...TldrawApp.defaultState.appState, status: TDStatus.Idle, }, }); @@ -1473,13 +1471,13 @@ export class TldrawApp extends StateManager { this.replace_state( { - ...TldrawApp.default_state, + ...TldrawApp.defaultState, settings: { ...this.state.settings, }, document: migrate(document, TldrawApp.version), appState: { - ...TldrawApp.default_state.appState, + ...TldrawApp.defaultState.appState, ...this.state.appState, currentPageId: Object.keys(document.pages)[0], disableAssets: this.disableAssets, @@ -3913,7 +3911,11 @@ export class TldrawApp extends StateManager { break; } case ' ': { - this.isForcePanning = true; + this.patchState({ + settings: { + forcePanning: true, + }, + }); this.spaceKey = true; break; } @@ -3976,7 +3978,12 @@ export class TldrawApp extends StateManager { break; } case ' ': { - this.isForcePanning = false; + this.patchState({ + settings: { + forcePanning: + this.currentTool.type === TDShapeType.HandDraw, + }, + }); this.spaceKey = false; break; } @@ -4069,7 +4076,7 @@ export class TldrawApp extends StateManager { this.pan(delta); // When panning, we also want to call onPointerMove, except when "force panning" via spacebar / middle wheel button (it's called elsewhere in that case) - if (!this.isForcePanning) + if (!this.useStore.getState().settings.forcePanning) this.onPointerMove(info, e as unknown as React.PointerEvent); }; @@ -4098,7 +4105,7 @@ export class TldrawApp extends StateManager { onPointerMove: TLPointerEventHandler = (info, e) => { this.previousPoint = this.currentPoint; this.updateInputs(info, e); - if (this.isForcePanning && this.isPointing) { + if (this.useStore.getState().settings.forcePanning && this.isPointing) { this.onPan?.( { ...info, delta: Vec.neg(info.delta) }, e as unknown as WheelEvent @@ -4122,20 +4129,23 @@ export class TldrawApp extends StateManager { onPointerDown: TLPointerEventHandler = (info, e) => { if (e.buttons === 4) { - this.isForcePanning = true; + this.patchState({ + settings: { + forcePanning: true, + }, + }); } else if (this.isPointing) { return; } this.isPointing = true; this.originPoint = this.getPagePoint(info.point).concat(info.pressure); this.updateInputs(info, e); - if (this.isForcePanning) return; + if (this.useStore.getState().settings.forcePanning) return; this.currentTool.onPointerDown?.(info, e); }; onPointerUp: TLPointerEventHandler = (info, e) => { this.isPointing = false; - if (!this.shiftKey) this.isForcePanning = false; this.updateInputs(info, e); this.currentTool.onPointerUp?.(info, e); }; @@ -4522,7 +4532,7 @@ export class TldrawApp extends StateManager { assets: {}, }; - static default_state: TDSnapshot = { + static defaultState: TDSnapshot = { settings: { isCadSelectMode: false, isPenMode: false, @@ -4532,6 +4542,7 @@ export class TldrawApp extends StateManager { isSnapping: false, isDebugMode: false, isReadonlyMode: false, + forcePanning: false, keepStyleMenuOpen: false, nudgeDistanceLarge: 16, nudgeDistanceSmall: 1, diff --git a/libs/components/board-tools/src/hand-draw/hand-draw-tool.ts b/libs/components/board-tools/src/hand-draw/hand-draw-tool.ts index c579c8e7e5..180b936442 100644 --- a/libs/components/board-tools/src/hand-draw/hand-draw-tool.ts +++ b/libs/components/board-tools/src/hand-draw/hand-draw-tool.ts @@ -18,34 +18,19 @@ export class HandDrawTool extends BaseTool { /* ----------------- Event Handlers ----------------- */ - override onPointerDown: TLPointerEventHandler = () => { - if (this.app.readOnly) return; - if (this.status !== Status.Idle) return; - - this.set_status(Status.Pointing); + override onEnter = () => { + this.app.patchState({ + settings: { + forcePanning: true, + }, + }); }; - override onPointerMove: TLPointerEventHandler = (info, e) => { - if (this.app.readOnly) return; - const delta = Vec.div(info.delta, this.app.camera.zoom); - const prev = this.app.camera.point; - const next = Vec.sub(prev, delta); - if (Vec.isEqual(next, prev)) return; - - switch (this.status) { - case Status.Pointing: { - this.app.pan(Vec.neg(delta)); - - break; - } - } - }; - - override onPointerUp: TLPointerEventHandler = () => { - this.set_status(Status.Idle); - }; - - override onCancel = () => { - this.set_status(Status.Idle); + override onExit = () => { + this.app.patchState({ + settings: { + forcePanning: false, + }, + }); }; } diff --git a/libs/components/board-types/src/types.ts b/libs/components/board-types/src/types.ts index fbf3e5c308..6f80a965bc 100644 --- a/libs/components/board-types/src/types.ts +++ b/libs/components/board-types/src/types.ts @@ -84,6 +84,7 @@ export interface TDSnapshot { isPenMode: boolean; isReadonlyMode: boolean; isZoomSnap: boolean; + forcePanning: boolean; keepStyleMenuOpen: boolean; nudgeDistanceSmall: number; nudgeDistanceLarge: number; diff --git a/libs/components/editor-plugins/src/menu/left-menu/LeftMenuPlugin.tsx b/libs/components/editor-plugins/src/menu/left-menu/LeftMenuPlugin.tsx index 5281c21b04..4eda43b55c 100644 --- a/libs/components/editor-plugins/src/menu/left-menu/LeftMenuPlugin.tsx +++ b/libs/components/editor-plugins/src/menu/left-menu/LeftMenuPlugin.tsx @@ -10,7 +10,7 @@ import { import { PluginRenderRoot } from '../../utils'; import { Subject, throttleTime } from 'rxjs'; import { domToRect, last, Point } from '@toeverything/utils'; -const DRAG_THROTTLE_DELAY = 150; +const DRAG_THROTTLE_DELAY = 60; export class LeftMenuPlugin extends BasePlugin { private _mousedown?: boolean; private _root?: PluginRenderRoot;