Remove helpers / extraneous API methods. (#1745)

This PR removes several extraneous computed values from the editor. It
adds some silly instance state onto the instance state record and
unifies a few methods which were inconsistent. This is fit and finish
work 🧽

## Computed Values

In general, where once we had a getter and setter for `isBlahMode`,
which really masked either an `_isBlahMode` atom on the editor or
`instanceState.isBlahMode`, these are merged into `instanceState`; they
can be accessed / updated via `editor.instanceState` /
`editor.updateInstanceState`.

## tldraw select tool specific things

This PR also removes some tldraw specific state checks and creates new
component overrides to allow us to include them in tldraw/tldraw.

### Change Type

- [x] `major` — Breaking change

### Test Plan

- [x] Unit Tests
- [x] End to end tests

### Release Notes

- [tldraw] rename `useReadonly` to `useReadOnly`
- [editor] remove `Editor.isDarkMode`
- [editor] remove `Editor.isChangingStyle`
- [editor] remove `Editor.isCoarsePointer`
- [editor] remove `Editor.isDarkMode`
- [editor] remove `Editor.isFocused`
- [editor] remove `Editor.isGridMode`
- [editor] remove `Editor.isPenMode`
- [editor] remove `Editor.isReadOnly`
- [editor] remove `Editor.isSnapMode`
- [editor] remove `Editor.isToolLocked`
- [editor] remove `Editor.locale`
- [editor] rename `Editor.pageState` to `Editor.currentPageState`
- [editor] add `Editor.pageStates`
- [editor] add `Editor.setErasingIds`
- [editor] add `Editor.setEditingId`
- [editor] add several new component overrides
This commit is contained in:
Steve Ruiz 2023-07-18 22:50:23 +01:00 committed by GitHub
parent a7d3a77cb0
commit 3e31ef2a7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
154 changed files with 1728 additions and 1706 deletions

View file

@ -36,7 +36,7 @@ export async function setupPage(page: PlaywrightTestArgs['page']) {
await page.goto('http://localhost:5420/end-to-end')
await page.waitForSelector('.tl-canvas')
await page.evaluate(() => {
editor.animationSpeed = 0
editor.user.updateUserPreferences({ animationSpeed: 0 })
})
}

View file

@ -172,26 +172,6 @@ test.describe('Keyboard Shortcuts', () => {
data: { source: 'kbd' },
})
// distribute horizontal
await page.keyboard.press('Control+a')
await page.mouse.click(200, 200, { button: 'right' })
await page.getByTestId('menu-item.arrange').click()
await page.getByTestId('menu-item.distribute-horizontal').click()
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'distribute-shapes',
data: { operation: 'horizontal', source: 'context-menu' },
})
// distribute vertical — Shift+Alt+V
await page.keyboard.press('Control+a')
await page.mouse.click(200, 200, { button: 'right' })
await page.getByTestId('menu-item.arrange').click()
await page.getByTestId('menu-item.distribute-vertical').click()
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'distribute-shapes',
data: { operation: 'vertical', source: 'context-menu' },
})
// flip-h — Shift+H
await page.keyboard.press('Shift+h')
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
@ -328,6 +308,37 @@ test.describe('Keyboard Shortcuts', () => {
})
})
test.describe('Context menu', async () => {
test.beforeEach(async ({ browser }) => {
page = await browser.newPage()
await setupPage(page)
})
test('distribute horizontal', async () => {
// distribute horizontal
await page.keyboard.press('Control+a')
await page.mouse.click(200, 200, { button: 'right' })
await page.getByTestId('menu-item.arrange').click()
await page.getByTestId('menu-item.distribute-horizontal').click()
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'distribute-shapes',
data: { operation: 'horizontal', source: 'context-menu' },
})
})
test('distribute vertical', async () => {
// distribute vertical — Shift+Alt+V
await page.keyboard.press('Control+a')
await page.mouse.click(200, 200, { button: 'right' })
await page.getByTestId('menu-item.arrange').click()
await page.getByTestId('menu-item.distribute-vertical').click()
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
name: 'distribute-shapes',
data: { operation: 'vertical', source: 'context-menu' },
})
})
})
test.describe('Delete bug', () => {
test.beforeAll(async ({ browser }) => {
page = await browser.newPage()

View file

@ -56,7 +56,7 @@ export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
component(shape: CardShape) {
const bounds = this.editor.getBounds(shape)
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
return (
<HTMLContainer

View file

@ -43,7 +43,7 @@ export class CardShapeUtil extends ShapeUtil<ICardShape> {
// Render method — the React component that will be rendered for the shape
component(shape: ICardShape) {
const bounds = this.editor.getBounds(shape)
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
// Unfortunately eslint will think this is a class components
// eslint-disable-next-line react-hooks/rules-of-hooks

View file

@ -1,20 +1,21 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
;(window as any).__tldraw_ui_event = { id: 'NOTHING_YET' }
;(window as any).__tldraw_editor_events = []
export default function EndToEnd() {
;(window as any).__tldraw_editor_events = []
return (
<div className="tldraw__editor">
<Tldraw
autoFocus
onUiEvent={(name, data) => {
;(window as any).__tldraw_ui_event = { name, data }
}}
onMount={(editor) => {
editor.on('event', (info) => {
;(window as any).__tldraw_editor_events.push(info)
})
}}
onUiEvent={(name, data) => {
;(window as any).__tldraw_ui_event = { name, data }
}}
/>
</div>
)

View file

@ -32,7 +32,7 @@ class IdleState extends StateNode {
if (editor.inputs.shiftKey) {
editor.select(...editor.selectedIds, info.shape.id)
} else {
if (!editor.isSelected(info.shape.id)) {
if (!editor.selectedIds.includes(info.shape.id)) {
editor.select(info.shape.id)
}
this.parent.transition('pointing', info)

View file

@ -39,7 +39,6 @@ import { TLAssetPartial } from '@tldraw/tlschema';
import { TLBaseShape } from '@tldraw/tlschema';
import { TLBookmarkAsset } from '@tldraw/tlschema';
import { TLCamera } from '@tldraw/tlschema';
import { TLCursor } from '@tldraw/tlschema';
import { TLCursorType } from '@tldraw/tlschema';
import { TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema';
import { TLDocument } from '@tldraw/tlschema';
@ -354,8 +353,6 @@ export class Editor extends EventEmitter<TLEventMap> {
}): this;
animateToShape(shapeId: TLShapeId, opts?: TLAnimationOptions): this;
animateToUser(userId: string): void;
get animationSpeed(): number;
set animationSpeed(animationSpeed: number);
// @internal (undocumented)
annotateError(error: unknown, { origin, willCrashApp, tags, extras, }: {
origin: string;
@ -368,17 +365,13 @@ export class Editor extends EventEmitter<TLEventMap> {
bailToMark(id: string): this;
batch(fn: () => void): this;
// (undocumented)
blur: () => boolean;
blur: () => void;
bringForward(ids?: TLShapeId[]): this;
bringToFront(ids?: TLShapeId[]): this;
get brush(): Box2dModel | null;
set brush(brush: Box2dModel | null);
get camera(): TLCamera;
get cameraState(): "idle" | "moving";
cancel(): this;
cancelDoubleClick(): void;
get canMoveCamera(): boolean;
set canMoveCamera(canMove: boolean);
get canRedo(): boolean;
get canUndo(): boolean;
// @internal (undocumented)
@ -412,17 +405,14 @@ export class Editor extends EventEmitter<TLEventMap> {
get currentPage(): TLPage;
get currentPageId(): TLPageId;
get currentPageShapeIds(): Set<TLShapeId>;
get currentPageState(): TLInstancePageState;
get currentTool(): StateNode | undefined;
get currentToolId(): string;
get cursor(): TLCursor;
set cursor(cursor: TLCursor);
deleteAssets(ids: TLAssetId[]): this;
deleteOpenMenu(id: string): this;
deletePage(id: TLPageId): void;
deletePage(id: TLPageId): this;
deleteShapes(ids?: TLShapeId[]): this;
deselect(...ids: TLShapeId[]): this;
get devicePixelRatio(): number;
set devicePixelRatio(dpr: number);
dispatch(info: TLEventInfo): this;
readonly disposables: Set<() => void>;
dispose(): void;
@ -431,9 +421,7 @@ export class Editor extends EventEmitter<TLEventMap> {
duplicatePage(id?: TLPageId, createId?: TLPageId): this;
duplicateShapes(ids?: TLShapeId[], offset?: VecLike): this;
get editingId(): null | TLShapeId;
set editingId(id: null | TLShapeId);
get erasingIds(): TLShapeId[];
set erasingIds(ids: TLShapeId[]);
get erasingIdsSet(): Set<TLShapeId>;
// @internal (undocumented)
externalAssetContentHandlers: {
@ -455,7 +443,7 @@ export class Editor extends EventEmitter<TLEventMap> {
findCommonAncestor(shapes: TLShape[], predicate?: (shape: TLShape) => boolean): TLShapeId | undefined;
flipShapes(operation: 'horizontal' | 'vertical', ids?: TLShapeId[]): this;
// (undocumented)
focus: () => boolean;
focus: () => void;
get focusLayerId(): TLPageId | TLShapeId;
set focusLayerId(next: TLPageId | TLShapeId);
getAncestorPageId(shape?: TLShape): TLPageId | undefined;
@ -468,7 +456,6 @@ export class Editor extends EventEmitter<TLEventMap> {
handleId: "end" | "start";
}[];
getAssetById(id: TLAssetId): TLAsset | undefined;
getAssetBySrc(src: string): TLBookmarkAsset | TLImageAsset | TLVideoAsset | undefined;
getAssetForExternalContent(info: TLExternalAssetContent_2): Promise<TLAsset | undefined>;
getBounds<T extends TLShape>(shape: T): Box2d;
getBoundsById<T extends TLShape>(id: T['id']): Box2d | undefined;
@ -495,12 +482,10 @@ export class Editor extends EventEmitter<TLEventMap> {
getPageCenter(shape: TLShape): null | Vec2d;
getPageCenterById(id: TLShapeId): null | Vec2d;
getPageCorners(shape: TLShape): Vec2d[];
getPageInfoById(id: TLPage['id']): TLPage | undefined;
getPageMaskById(id: TLShapeId): undefined | VecLike[];
getPagePointById(id: TLShapeId): undefined | Vec2d;
getPageRotation(shape: TLShape): number;
getPageRotationById(id: TLShapeId): number;
getPageStateByPageId(id: TLPageId): TLInstancePageState | undefined;
getPageTransform(shape: TLShape): Matrix2d | undefined;
getPageTransformById(id: TLShapeId): Matrix2d | undefined;
getParentIdForNewShapeAtPoint(point: VecLike, shapeType: TLShape['type']): TLPageId | TLShapeId;
@ -562,40 +547,17 @@ export class Editor extends EventEmitter<TLEventMap> {
interrupt(): this;
isAncestorSelected(id: TLShapeId): boolean;
readonly isAndroid: boolean;
get isChangingStyle(): boolean;
set isChangingStyle(v: boolean);
readonly isChromeForIos: boolean;
get isCoarsePointer(): boolean;
set isCoarsePointer(v: boolean);
get isDarkMode(): boolean;
set isDarkMode(isDarkMode: boolean);
readonly isFirefox: boolean;
get isFocused(): boolean;
set isFocused(isFocused: boolean);
get isFocusMode(): boolean;
set isFocusMode(isFocusMode: boolean);
get isGridMode(): boolean;
set isGridMode(isGridMode: boolean);
isIn(path: string): boolean;
isInAny(...paths: string[]): boolean;
readonly isIos: boolean;
get isMenuOpen(): boolean;
get isPenMode(): boolean;
set isPenMode(isPenMode: boolean);
isPointInShape(point: VecLike, shape: TLShape): boolean;
get isReadOnly(): boolean;
set isReadOnly(isReadOnly: boolean);
readonly isSafari: boolean;
isSelected(id: TLShapeId): boolean;
isShapeInPage(shape: TLShape, pageId?: TLPageId): boolean;
isShapeOfType<T extends TLUnknownShape>(shape: TLUnknownShape, type: T['type']): shape is T;
isShapeOrAncestorLocked(shape?: TLShape): boolean;
get isSnapMode(): boolean;
set isSnapMode(isSnapMode: boolean);
get isToolLocked(): boolean;
set isToolLocked(isToolLocked: boolean);
get locale(): string;
set locale(locale: string);
mark(markId?: string, onUndo?: boolean, onRedo?: boolean): string;
moveShapesToPage(ids: TLShapeId[], pageId: TLPageId): this;
nudgeShapes(ids: TLShapeId[], direction: Vec2dModel, major?: boolean, ephemeral?: boolean): this;
@ -603,7 +565,7 @@ export class Editor extends EventEmitter<TLEventMap> {
get openMenus(): string[];
packShapes(ids?: TLShapeId[], padding?: number): this;
get pages(): TLPage[];
get pageState(): TLInstancePageState;
get pageStates(): TLInstancePageState[];
pageToScreen(x: number, y: number, z?: number, camera?: Vec2dModel): {
x: number;
y: number;
@ -612,9 +574,6 @@ export class Editor extends EventEmitter<TLEventMap> {
pan(dx: number, dy: number, opts?: TLAnimationOptions): this;
panZoomIntoView(ids: TLShapeId[], opts?: TLAnimationOptions): this;
popFocusLayer(): this;
// @internal (undocumented)
get projectName(): string;
set projectName(name: string);
putContent(content: TLContent, options?: {
point?: VecLike;
select?: boolean;
@ -659,12 +618,9 @@ export class Editor extends EventEmitter<TLEventMap> {
y: number;
z: number;
};
get scribble(): null | TLScribble;
set scribble(scribble: null | TLScribble);
select(...ids: TLShapeId[]): this;
selectAll(): this;
get selectedIds(): TLShapeId[];
get selectedIdsSet(): ReadonlySet<TLShapeId>;
get selectedPageBounds(): Box2d | null;
get selectedShapes(): TLShape[];
get selectionBounds(): Box2d | undefined;
@ -676,8 +632,11 @@ export class Editor extends EventEmitter<TLEventMap> {
setCamera(x: number, y: number, z?: number, { stopFollowing }?: TLViewportOptions): this;
setCurrentPageId(pageId: TLPageId, { stopFollowing }?: TLViewportOptions): this;
setCurrentTool(id: string, info?: {}): this;
// (undocumented)
setEditingId(id: null | TLShapeId): void;
// (undocumented)
setErasingIds(ids: TLShapeId[]): void;
setOpacity(opacity: number, ephemeral?: boolean, squashing?: boolean): this;
setPageState(partial: Partial<TLInstancePageState>, ephemeral?: boolean): void;
setSelectedIds(ids: TLShapeId[], squashing?: boolean): this;
setStyle<T>(style: StyleProp<T>, value: T, ephemeral?: boolean, squashing?: boolean): this;
get shapesArray(): TLShape[];
@ -709,6 +668,7 @@ export class Editor extends EventEmitter<TLEventMap> {
undo(): HistoryManager<this>;
ungroupShapes(ids?: TLShapeId[]): this;
updateAssets(assets: TLAssetPartial[]): this;
updateCurrentPageState(partial: Partial<Omit<TLInstancePageState, 'editingId' | 'focusLayerId' | 'pageId' | 'selectedIds'>>, ephemeral?: boolean): this;
updateDocumentSettings(settings: Partial<TLDocument>): this;
updateInstanceState(partial: Partial<Omit<TLInstance, 'currentPageId'>>, ephemeral?: boolean, squashing?: boolean): this;
updatePage(partial: RequiredKeys<TLPage, 'id'>, squashing?: boolean): this;
@ -722,8 +682,6 @@ export class Editor extends EventEmitter<TLEventMap> {
get viewportScreenBounds(): Box2d;
get viewportScreenCenter(): Vec2d;
visitDescendants(parentId: TLParentId, visitor: (id: TLShapeId) => false | void): void;
get zoomBrush(): Box2dModel | null;
set zoomBrush(zoomBrush: Box2dModel | null);
zoomIn(point?: Vec2d, opts?: TLAnimationOptions): this;
get zoomLevel(): number;
zoomOut(point?: Vec2d, opts?: TLAnimationOptions): this;
@ -1276,6 +1234,14 @@ export function setRuntimeOverrides(input: Partial<typeof runtime>): void;
// @public (undocumented)
export function setUserPreferences(user: TLUserPreferences): void;
// @public (undocumented)
export const ShapeIndicator: React_3.NamedExoticComponent<{
id: TLShapeId;
color?: string | undefined;
opacity?: number | undefined;
className?: string | undefined;
}>;
// @public (undocumented)
export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
constructor(editor: Editor);
@ -1567,6 +1533,9 @@ export type TLAnimationOptions = Partial<{
// @public (undocumented)
export type TLAnyShapeUtilConstructor = TLShapeUtilConstructor<any>;
// @public (undocumented)
export type TLBackgroundComponent = ComponentType;
// @public (undocumented)
export type TLBaseBoxShape = TLBaseShape<string, {
w: number;
@ -1585,6 +1554,14 @@ export interface TLBaseEventInfo {
type: UiEventType;
}
// @public (undocumented)
export type TLBrushComponent = ComponentType<{
brush: Box2dModel;
color?: string;
opacity?: number;
className?: string;
}>;
// @public (undocumented)
export type TLCancelEvent = (info: TLCancelEventInfo) => void;
@ -1610,6 +1587,16 @@ export type TLClickEventInfo = TLBaseEventInfo & {
// @public (undocumented)
export type TLCLickEventName = 'double_click' | 'quadruple_click' | 'triple_click';
// @public (undocumented)
export type TLCollaboratorHintComponent = ComponentType<{
className?: string;
point: Vec2dModel;
viewport: Box2d;
zoom: number;
opacity?: number;
color: string;
}>;
// @public (undocumented)
export type TLCommand<Name extends string = any, Data = any> = {
type: 'command';
@ -1648,6 +1635,16 @@ export interface TLContent {
shapes: TLShape[];
}
// @public (undocumented)
export type TLCursorComponent = ComponentType<{
className?: string;
point: null | Vec2dModel;
zoom: number;
color?: string;
name: null | string;
chatMessage: string;
}>;
// @public (undocumented)
export const TldrawEditor: React_2.NamedExoticComponent<TldrawEditorProps>;
@ -1831,6 +1828,27 @@ export type TLExternalContent = {
point?: VecLike;
};
// @public (undocumented)
export type TLGridComponent = ComponentType<{
x: number;
y: number;
z: number;
size: number;
}>;
// @public (undocumented)
export type TLHandleComponent = ComponentType<{
shapeId: TLShapeId;
handle: TLHandle;
className?: string;
}>;
// @public (undocumented)
export type TLHandlesComponent = ComponentType<{
className?: string;
children: any;
}>;
// @public (undocumented)
export type TLHistoryEntry = TLCommand | TLHistoryMark;
@ -1842,6 +1860,11 @@ export type TLHistoryMark = {
onRedo: boolean;
};
// @public (undocumented)
export type TLHoveredShapeIndicatorComponent = ComponentType<{
shapeId: TLShapeId;
}>;
// @public (undocumented)
export type TLInterruptEvent = (info: TLInterruptEventInfo) => void;
@ -2003,6 +2026,21 @@ export type TLRotationSnapshot = {
}[];
};
// @public (undocumented)
export type TLScribbleComponent = ComponentType<{
scribble: TLScribble;
zoom: number;
color?: string;
opacity?: number;
className?: string;
}>;
// @public (undocumented)
export type TLSelectionBackgroundComponent = React_3.ComponentType<object>;
// @public (undocumented)
export type TLSelectionForegroundComponent = ComponentType<object>;
// @public (undocumented)
export type TLSelectionHandle = RotateCorner | SelectionCorner | SelectionEdge;
@ -2035,6 +2073,14 @@ export interface TLSessionStateSnapshot {
version: number;
}
// @public (undocumented)
export type TLShapeIndicatorComponent = React_3.ComponentType<{
id: TLShapeId;
color?: string | undefined;
opacity?: number;
className?: string;
}>;
// @public (undocumented)
export interface TLShapeUtilCanvasSvgDef {
// (undocumented)
@ -2058,6 +2104,16 @@ export interface TLShapeUtilConstructor<T extends TLUnknownShape, U extends Shap
// @public (undocumented)
export type TLShapeUtilFlag<T> = (shape: T) => boolean;
// @public (undocumented)
export type TLSnapLineComponent = React_3.ComponentType<{
className?: string;
line: SnapLine;
zoom: number;
}>;
// @public (undocumented)
export type TLSpinnerComponent = ComponentType<object>;
// @public (undocumented)
export interface TLStateNodeConstructor {
// (undocumented)
@ -2107,6 +2163,9 @@ export type TLStoreWithStatus = {
readonly error?: undefined;
};
// @public (undocumented)
export type TLSvgDefsComponent = React.ComponentType;
// @public (undocumented)
export type TLTickEvent = (elapsed: number) => void;

View file

@ -42,7 +42,22 @@ export {
export { HTMLContainer, type HTMLContainerProps } from './lib/components/HTMLContainer'
export { PositionedOnCanvas } from './lib/components/PositionedOnCanvas'
export { SVGContainer, type SVGContainerProps } from './lib/components/SVGContainer'
export { ShapeIndicator, type TLShapeIndicatorComponent } from './lib/components/ShapeIndicator'
export { type TLBackgroundComponent } from './lib/components/default-components/DefaultBackground'
export { type TLBrushComponent } from './lib/components/default-components/DefaultBrush'
export { type TLCollaboratorHintComponent } from './lib/components/default-components/DefaultCollaboratorHint'
export { type TLCursorComponent } from './lib/components/default-components/DefaultCursor'
export { DefaultErrorFallback } from './lib/components/default-components/DefaultErrorFallback'
export { type TLGridComponent } from './lib/components/default-components/DefaultGrid'
export { type TLHandleComponent } from './lib/components/default-components/DefaultHandle'
export { type TLHandlesComponent } from './lib/components/default-components/DefaultHandles'
export { type TLHoveredShapeIndicatorComponent } from './lib/components/default-components/DefaultHoveredShapeIndicator'
export { type TLScribbleComponent } from './lib/components/default-components/DefaultScribble'
export { type TLSelectionBackgroundComponent } from './lib/components/default-components/DefaultSelectionBackground'
export { type TLSelectionForegroundComponent } from './lib/components/default-components/DefaultSelectionForeground'
export { type TLSnapLineComponent } from './lib/components/default-components/DefaultSnapLine'
export { type TLSpinnerComponent } from './lib/components/default-components/DefaultSpinner'
export { type TLSvgDefsComponent } from './lib/components/default-components/DefaultSvgDefs'
export {
TAB_ID,
createSessionStateSnapshotSignal,

View file

@ -261,7 +261,9 @@ function TldrawEditorWithReadyStore({
}, [container, shapeUtils, tools, store, user, initialState])
React.useLayoutEffect(() => {
if (editor && autoFocus) editor.isFocused = true
if (editor && autoFocus) {
editor.focus()
}
}, [editor, autoFocus])
const onMountEvent = useEvent((editor: Editor) => {

View file

@ -117,59 +117,53 @@ export const Canvas = track(function Canvas() {
)
})
const GridWrapper = track(function GridWrapper() {
function GridWrapper() {
const editor = useEditor()
const gridSize = useValue('gridSize', () => editor.documentSettings.gridSize, [editor])
const { x, y, z } = useValue('camera', () => editor.camera, [editor])
const isGridMode = useValue('isGridMode', () => editor.instanceState.isGridMode, [editor])
const { Grid } = useEditorComponents()
// get grid from context
const { gridSize } = editor.documentSettings
const { x, y, z } = editor.camera
const isGridMode = editor.isGridMode
if (!(Grid && isGridMode)) return null
return <Grid x={x} y={y} z={z} size={gridSize} />
})
}
const ScribbleWrapper = track(function ScribbleWrapper() {
function ScribbleWrapper() {
const editor = useEditor()
const scribble = editor.scribble
const zoom = editor.zoomLevel
const scribble = useValue('scribble', () => editor.instanceState.scribble, [editor])
const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor])
const { Scribble } = useEditorComponents()
if (!(Scribble && scribble)) return null
return <Scribble className="tl-user-scribble" scribble={scribble} zoom={zoom} />
})
return <Scribble className="tl-user-scribble" scribble={scribble} zoom={zoomLevel} />
}
const BrushWrapper = track(function BrushWrapper() {
function BrushWrapper() {
const editor = useEditor()
const { brush } = editor
const brush = useValue('brush', () => editor.instanceState.brush, [editor])
const { Brush } = useEditorComponents()
if (!(Brush && brush)) return null
return <Brush className="tl-user-brush" brush={brush} />
})
}
export const ZoomBrushWrapper = track(function Zoom() {
function ZoomBrushWrapper() {
const editor = useEditor()
const { zoomBrush } = editor
const zoomBrush = useValue('zoomBrush', () => editor.instanceState.zoomBrush, [editor])
const { ZoomBrush } = useEditorComponents()
if (!(ZoomBrush && zoomBrush)) return null
return <ZoomBrush className="tl-user-brush" brush={zoomBrush} />
})
}
export const SnapLinesWrapper = track(function SnapLines() {
function SnapLinesWrapper() {
const editor = useEditor()
const {
snaps: { lines },
zoomLevel,
} = editor
const lines = useValue('snapLines', () => editor.snaps.lines, [editor])
const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor])
const { SnapLine } = useEditorComponents()
if (!(SnapLine && lines.length > 0)) return null
@ -181,24 +175,22 @@ export const SnapLinesWrapper = track(function SnapLines() {
))}
</>
)
})
}
const MIN_HANDLE_DISTANCE = 48
const HandlesWrapper = track(function HandlesWrapper() {
function HandlesWrapper() {
const editor = useEditor()
const { Handles } = useEditorComponents()
const zoom = editor.zoomLevel
const isChangingStyle = editor.isChangingStyle
const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor])
const onlySelectedShape = useValue('onlySelectedShape', () => editor.onlySelectedShape, [editor])
const isChangingStyle = useValue('isChangingStyle', () => editor.instanceState.isChangingStyle, [
editor,
])
const isReadOnly = useValue('isChangingStyle', () => editor.instanceState.isReadOnly, [editor])
const onlySelectedShape = editor.onlySelectedShape
const shouldDisplayHandles =
editor.isInAny('select.idle', 'select.pointing_handle') &&
!isChangingStyle &&
!editor.isReadOnly
if (!(onlySelectedShape && shouldDisplayHandles)) return null
if (!Handles || !onlySelectedShape || isChangingStyle || isReadOnly) return null
const handles = editor.getHandles(onlySelectedShape)
@ -216,7 +208,7 @@ const HandlesWrapper = track(function HandlesWrapper() {
const prev = handles[i - 1]
const next = handles[i + 1]
if (prev && next) {
if (Math.hypot(prev.y - next.y, prev.x - next.x) < MIN_HANDLE_DISTANCE / zoom) {
if (Math.hypot(prev.y - next.y, prev.x - next.x) < MIN_HANDLE_DISTANCE / zoomLevel) {
continue
}
}
@ -228,17 +220,17 @@ const HandlesWrapper = track(function HandlesWrapper() {
handlesToDisplay.sort((a) => (a.type === 'vertex' ? 1 : -1))
return (
<svg className="tl-user-handles tl-overlays__item">
<Handles>
<g transform={Matrix2d.toCssString(transform)}>
{handlesToDisplay.map((handle) => {
return <HandleWrapper key={handle.id} shapeId={onlySelectedShape.id} handle={handle} />
})}
</g>
</svg>
</Handles>
)
})
}
const HandleWrapper = ({ shapeId, handle }: { shapeId: TLShapeId; handle: TLHandle }) => {
function HandleWrapper({ shapeId, handle }: { shapeId: TLShapeId; handle: TLHandle }) {
const events = useHandleEvents(shapeId, handle.id)
const { Handle } = useEditorComponents()
@ -279,43 +271,45 @@ const ShapesToDisplay = track(function ShapesToDisplay() {
)
})
const SelectedIdIndicators = track(function SelectedIdIndicators() {
function SelectedIdIndicators() {
const editor = useEditor()
const shouldDisplay =
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.pointing_shape',
'select.pointing_selection',
'select.pointing_handle'
) && !editor.isChangingStyle
const selectedIds = useValue('selectedIds', () => editor.currentPageState.selectedIds, [editor])
const shouldDisplay = useValue(
'should display selected ids',
() => {
// todo: move to tldraw selected ids wrapper
return (
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.pointing_shape',
'select.pointing_selection',
'select.pointing_handle'
) && !editor.instanceState.isChangingStyle
)
},
[editor]
)
if (!shouldDisplay) return null
return (
<>
{editor.selectedIds.map((id) => (
{selectedIds.map((id) => (
<ShapeIndicator key={id + '_indicator'} className="tl-user-indicator__selected" id={id} />
))}
</>
)
})
}
const HoveredShapeIndicator = function HoveredShapeIndicator() {
const editor = useEditor()
const { HoveredShapeIndicator } = useEditorComponents()
const hoveredId = useValue('hovered id', () => editor.currentPageState.hoveredId, [editor])
if (!hoveredId || !HoveredShapeIndicator) return null
const displayingHoveredId = useValue(
'hovered id and should display',
() =>
editor.isInAny('select.idle', 'select.editing_shape') ? editor.pageState.hoveredId : null,
[editor]
)
if (!displayingHoveredId) return null
return <ShapeIndicator className="tl-user-indicator__hovered" id={displayingHoveredId} />
return <HoveredShapeIndicator shapeId={hoveredId} />
}
const HintedShapeIndicator = track(function HintedShapeIndicator() {

View file

@ -45,12 +45,13 @@ export const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }
)
}
export type TLShapeIndicatorComponent = (props: {
/** @public */
export type TLShapeIndicatorComponent = React.ComponentType<{
id: TLShapeId
color?: string | undefined
opacity?: number
className?: string
}) => JSX.Element | null
}>
const _ShapeIndicator: TLShapeIndicatorComponent = ({ id, className, color, opacity }) => {
const editor = useEditor()
@ -79,4 +80,5 @@ const _ShapeIndicator: TLShapeIndicatorComponent = ({ id, className, color, opac
)
}
/** @public */
export const ShapeIndicator = React.memo(_ShapeIndicator)

View file

@ -1,7 +1,7 @@
import { ComponentType } from 'react'
/** @public */
export type TLBackgroundComponent = ComponentType<object> | null
export type TLBackgroundComponent = ComponentType
export function DefaultBackground() {
return <div className="tl-background" />

View file

@ -6,6 +6,7 @@ import { Box2d } from '../../primitives/Box2d'
import { Vec2d } from '../../primitives/Vec2d'
import { clamp } from '../../primitives/utils'
/** @public */
export type TLCollaboratorHintComponent = ComponentType<{
className?: string
point: Vec2dModel

View file

@ -31,7 +31,7 @@ export const DefaultErrorFallback: TLErrorFallbackComponent = ({ error, editor }
() => {
try {
if (editor) {
return editor.isDarkMode
return editor.user.isDarkMode
}
} catch {
// we're in a funky error state so this might not work for spooky

View file

@ -2,6 +2,7 @@ import { TLHandle, TLShapeId } from '@tldraw/tlschema'
import classNames from 'classnames'
import { ComponentType } from 'react'
/** @public */
export type TLHandleComponent = ComponentType<{
shapeId: TLShapeId
handle: TLHandle

View file

@ -0,0 +1,11 @@
import { ComponentType } from 'react'
/** @public */
export type TLHandlesComponent = ComponentType<{
className?: string
children: any
}>
export const DefaultHandles: TLHandlesComponent = ({ children }) => {
return <svg className="tl-user-handles tl-overlays__item">{children}</svg>
}

View file

@ -0,0 +1,12 @@
import { TLShapeId } from '@tldraw/tlschema'
import { ComponentType } from 'react'
import { ShapeIndicator } from '../ShapeIndicator'
/** @public */
export type TLHoveredShapeIndicatorComponent = ComponentType<{
shapeId: TLShapeId
}>
export const DefaultHoveredShapeIndicator: TLHoveredShapeIndicatorComponent = ({ shapeId }) => {
return <ShapeIndicator className="tl-user-indicator__hovered" id={shapeId} />
}

View file

@ -153,6 +153,7 @@ function GapsSnapLine({ gaps, direction, zoom }: { zoom: number } & GapsSnapLine
)
}
/** @public */
export type TLSnapLineComponent = React.ComponentType<{
className?: string
line: SnapLine

View file

@ -1,5 +1,6 @@
import { ComponentType } from 'react'
/** @public */
export type TLSpinnerComponent = ComponentType<object>
export const DefaultSpinner: TLSpinnerComponent = () => {

View file

@ -1,5 +1,5 @@
/** @public */
export type TLSvgDefsComponent = () => any
export type TLSvgDefsComponent = React.ComponentType
export const DefaultSvgDefs = () => {
return null

File diff suppressed because it is too large Load diff

View file

@ -227,7 +227,7 @@ export class ClickManager {
this._clickState !== 'idle' &&
this._clickScreenPoint &&
this._clickScreenPoint.dist(this.editor.inputs.currentScreenPoint) >
(this.editor.isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
(this.editor.instanceState.isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE)
) {
this.cancelDoubleClickTimeout()
}

View file

@ -1,3 +1,4 @@
import { computed } from '@tldraw/state'
import { TLUserPreferences } from '../../config/TLUserPreferences'
import { TLUser } from '../../config/createTLUser'
@ -6,40 +7,40 @@ export class UserPreferencesManager {
updateUserPreferences = (userPreferences: Partial<TLUserPreferences>) => {
this.user.setUserPreferences({
...this.user.userPreferences.value,
...this.userPreferences,
...userPreferences,
})
}
get userPreferences() {
return this.user.userPreferences
@computed get userPreferences() {
return this.user.userPreferences.value
}
get isDarkMode() {
return this.user.userPreferences.value.isDarkMode
@computed get isDarkMode() {
return this.userPreferences.isDarkMode
}
get animationSpeed() {
return this.user.userPreferences.value.animationSpeed
@computed get animationSpeed() {
return this.userPreferences.animationSpeed
}
get id() {
return this.user.userPreferences.value.id
@computed get id() {
return this.userPreferences.id
}
get name() {
return this.user.userPreferences.value.name
@computed get name() {
return this.userPreferences.name
}
get locale() {
return this.user.userPreferences.value.locale
@computed get locale() {
return this.userPreferences.locale
}
get color() {
return this.user.userPreferences.value.color
@computed get color() {
return this.userPreferences.color
}
get isSnapMode() {
return this.user.userPreferences.value.isSnapMode
@computed get isSnapMode() {
return this.userPreferences.isSnapMode
}
}

View file

@ -0,0 +1,25 @@
import { atom } from '@tldraw/state'
import { getAtomManager } from './getRecordManager'
describe('atom manager', () => {
it('manages an atom object', () => {
const cb = jest.fn()
const A = atom('abc', { a: 1, b: 2, c: 3 })
const manager = getAtomManager(A, cb)
expect(A.lastChangedEpoch).toBe(0)
manager.a = 2
expect(manager.a).toBe(2)
expect(A.lastChangedEpoch).toBe(1)
manager.b = 4
expect(manager.b).toBe(4)
expect(A.lastChangedEpoch).toBe(2)
manager.b
expect(A.value).toMatchObject({ a: 2, b: 4, c: 3 })
expect(cb).toHaveBeenCalledTimes(2)
})
})

View file

@ -0,0 +1,28 @@
import { Atom, computed } from '@tldraw/state'
export function getAtomManager<T extends { [key: string]: any }>(
atom: Atom<T>,
transform?: (prev: T, next: T) => T
): T {
const update = (value: Partial<T>) => {
const curr = atom.value
const next = { ...curr, ...value }
const final = transform?.(atom.value, atom.value) ?? next
atom.set(final)
}
return Object.defineProperties(
{} as T,
Object.keys(atom.value).reduce((acc, key) => {
acc[key as keyof T] = computed(atom, key, {
get() {
return atom.value[key as keyof T]
},
set(value: T[keyof T]) {
update({ [key]: value } as any)
},
})
return acc
}, {} as { [key in keyof T]: PropertyDescriptor })
)
}

View file

@ -40,7 +40,7 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
// Not a class component, but eslint can't tell that :(
const {
erasingIdsSet,
pageState: { hintingIds, focusLayerId },
currentPageState: { hintingIds, focusLayerId },
zoomLevel,
} = this.editor
@ -87,13 +87,13 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
override onChildrenChange: TLOnChildrenChangeHandler<TLGroupShape> = (group) => {
const children = this.editor.getSortedChildIds(group.id)
if (children.length === 0) {
if (this.editor.pageState.focusLayerId === group.id) {
if (this.editor.currentPageState.focusLayerId === group.id) {
this.editor.popFocusLayer()
}
this.editor.deleteShapes([group.id])
return
} else if (children.length === 1) {
if (this.editor.pageState.focusLayerId === group.id) {
if (this.editor.currentPageState.focusLayerId === group.id) {
this.editor.popFocusLayer()
}
this.editor.reparentShapesById(children, group.parentId)

View file

@ -9,7 +9,7 @@ export class Idle extends StateNode {
}
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
override onCancel = () => {

View file

@ -0,0 +1,21 @@
import { T } from '@tldraw/validate'
export type TLinstanceState = {
canMoveCamera: boolean
isFocused: boolean
devicePixelRatio: number
isCoarsePointer: boolean
openMenus: string[]
isChangingStyle: boolean
isReadOnly: boolean
}
export const instanceStateValidator = T.object<TLinstanceState>({
canMoveCamera: T.boolean,
isFocused: T.boolean,
devicePixelRatio: T.number,
isCoarsePointer: T.boolean,
openMenus: T.arrayOf(T.string),
isChangingStyle: T.boolean,
isReadOnly: T.boolean,
})

View file

@ -8,13 +8,13 @@ export function useCoarsePointer() {
// detect coarse VS fine pointer. For now, let's assume that you have a fine
// pointer if you're on Firefox on desktop.
if (editor.isFirefox && !editor.isAndroid && !editor.isIos) {
editor.isCoarsePointer = false
editor.updateInstanceState({ isCoarsePointer: false })
return
}
if (window.matchMedia) {
const mql = window.matchMedia('(pointer: coarse)')
const handler = () => {
editor.isCoarsePointer = mql.matches
editor.updateInstanceState({ isCoarsePointer: !!mql.matches })
}
handler()
if (mql) {

View file

@ -77,7 +77,7 @@ export function useCursor() {
useQuickReactor(
'useCursor',
() => {
const { type, rotation } = editor.cursor
const { type, rotation } = editor.instanceState.cursor
if (STATIC_CURSORS.includes(type)) {
container.style.setProperty('--tl-cursor', `var(--tl-cursor-${type})`)

View file

@ -7,7 +7,7 @@ import { useEditor } from './useEditor'
export function useDarkMode() {
const editor = useEditor()
const container = useContainer()
const isDarkMode = useValue('isDarkMode', () => editor.isDarkMode, [editor])
const isDarkMode = useValue('isDarkMode', () => editor.user.isDarkMode, [editor])
const forceSrgb = useValue(debugFlags.forceSrgb)
React.useEffect(() => {

View file

@ -9,13 +9,13 @@ export function useDocumentEvents() {
const editor = useEditor()
const container = useContainer()
const isAppFocused = useValue('isFocused', () => editor.isFocused, [editor])
const isAppFocused = useValue('isFocused', () => editor.instanceState.isFocused, [editor])
useEffect(() => {
if (typeof matchMedia !== undefined) return
function updateDevicePixelRatio() {
editor.devicePixelRatio = window.devicePixelRatio
editor.updateInstanceState({ devicePixelRatio: window.devicePixelRatio })
}
const MM = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`)
@ -78,7 +78,7 @@ export function useDocumentEvents() {
if (!editor.inputs.keys.has('Comma')) {
const { x, y, z } = editor.inputs.currentScreenPoint
const {
pageState: { hoveredId },
currentPageState: { hoveredId },
} = editor
editor.inputs.keys.add('Comma')
@ -91,7 +91,7 @@ export function useDocumentEvents() {
ctrlKey: e.metaKey || e.ctrlKey,
pointerId: 0,
button: 0,
isPen: editor.isPenMode,
isPen: editor.instanceState.isPenMode,
...(hoveredId
? {
target: 'shape',
@ -156,7 +156,7 @@ export function useDocumentEvents() {
if (editor.inputs.keys.has(e.code)) {
const { x, y, z } = editor.inputs.currentScreenPoint
const {
pageState: { hoveredId },
currentPageState: { hoveredId },
} = editor
editor.inputs.keys.delete(e.code)
@ -170,7 +170,7 @@ export function useDocumentEvents() {
ctrlKey: e.metaKey || e.ctrlKey,
pointerId: 0,
button: 0,
isPen: editor.isPenMode,
isPen: editor.instanceState.isPenMode,
...(hoveredId
? {
target: 'shape',

View file

@ -16,6 +16,11 @@ import {
} from '../components/default-components/DefaultErrorFallback'
import { DefaultGrid, TLGridComponent } from '../components/default-components/DefaultGrid'
import { DefaultHandle, TLHandleComponent } from '../components/default-components/DefaultHandle'
import { DefaultHandles, TLHandlesComponent } from '../components/default-components/DefaultHandles'
import {
DefaultHoveredShapeIndicator,
TLHoveredShapeIndicatorComponent,
} from '../components/default-components/DefaultHoveredShapeIndicator'
import {
DefaultScribble,
TLScribbleComponent,
@ -57,10 +62,12 @@ interface BaseEditorComponents {
Scribble: TLScribbleComponent
CollaboratorScribble: TLScribbleComponent
SnapLine: TLSnapLineComponent
Handles: TLHandlesComponent
Handle: TLHandleComponent
Spinner: TLSpinnerComponent
SelectionForeground: TLSelectionForegroundComponent
SelectionBackground: TLSelectionBackgroundComponent
HoveredShapeIndicator: TLHoveredShapeIndicatorComponent
}
/** @public */
@ -96,6 +103,7 @@ export function EditorComponentsProvider({ overrides, children }: ComponentsCont
Grid: DefaultGrid,
Scribble: DefaultScribble,
SnapLine: DefaultSnapLine,
Handles: DefaultHandles,
Handle: DefaultHandle,
CollaboratorScribble: DefaultScribble,
ErrorFallback: DefaultErrorFallback,
@ -104,6 +112,7 @@ export function EditorComponentsProvider({ overrides, children }: ComponentsCont
Spinner: DefaultSpinner,
SelectionBackground: DefaultSelectionBackground,
SelectionForeground: DefaultSelectionForeground,
HoveredShapeIndicator: DefaultHoveredShapeIndicator,
...overrides,
}),
[overrides]

View file

@ -48,7 +48,7 @@ export function useGestureEvents(ref: React.RefObject<HTMLDivElement>) {
let pinchState = null as null | 'zooming' | 'panning'
const onWheel: Handler<'wheel', WheelEvent> = ({ event }) => {
if (!editor.isFocused) {
if (!editor.instanceState.isFocused) {
return
}

View file

@ -8,7 +8,7 @@ import { useEditor } from './useEditor'
const pointerEventHandler = (editor: Editor, shapeId: TLShapeId, name: TLPointerEventName) => {
return (e: React.PointerEvent) => {
if (name !== 'pointer_move' && editor.pageState.editingId === shapeId)
if (name !== 'pointer_move' && editor.currentPageState.editingId === shapeId)
(e as any).isKilled = true
if ((e as any).isKilled) return

View file

@ -0,0 +1,24 @@
import { createTLStore } from '../config/createTLStore'
import { Editor } from '../editor/Editor'
let editor: Editor
beforeEach(() => {
editor = new Editor({
shapeUtils: [],
tools: [],
store: createTLStore({ shapeUtils: [] }),
getContainer: () => document.body,
})
})
describe('user', () => {
it('gets a user with the correct color', () => {
expect(editor.user.isDarkMode).toBe(false)
})
it('gets a user with the correct', () => {
editor.user.updateUserPreferences({ isDarkMode: true })
expect(editor.user.isDarkMode).toBe(true)
})
})

View file

@ -808,7 +808,7 @@ export function useMenuSchema(): TLUiMenuSchema;
export function useNativeClipboardEvents(): void;
// @public (undocumented)
export function useReadonly(): boolean;
export function useReadOnly(): boolean;
// @public (undocumented)
export function useToasts(): TLUiToastsContextType;

View file

@ -83,7 +83,7 @@ export {
type TLUiMenuSchemaContextType,
type TLUiMenuSchemaProviderProps,
} from './lib/ui/hooks/useMenuSchema'
export { useReadonly } from './lib/ui/hooks/useReadonly'
export { useReadOnly } from './lib/ui/hooks/useReadOnly'
export {
useToasts,
type TLUiToast,

View file

@ -7,6 +7,8 @@ import {
TldrawEditorProps,
} from '@tldraw/editor'
import { useMemo } from 'react'
import { TldrawHandles } from './canvas/TldrawHandles'
import { TldrawHoveredShapeIndicator } from './canvas/TldrawHoveredShapeIndicator'
import { TldrawScribble } from './canvas/TldrawScribble'
import { TldrawSelectionForeground } from './canvas/TldrawSelectionForeground'
import { defaultShapeTools } from './defaultShapeTools'
@ -37,6 +39,8 @@ export function Tldraw(
() => ({
Scribble: TldrawScribble,
SelectionForeground: TldrawSelectionForeground,
Handles: TldrawHandles,
HoveredShapeIndicator: TldrawHoveredShapeIndicator,
...rest.components,
}),
[rest.components]

View file

@ -0,0 +1,14 @@
import { TLHandlesComponent, useEditor, useValue } from '@tldraw/editor'
export const TldrawHandles: TLHandlesComponent = ({ children }) => {
const editor = useEditor()
const shouldDisplayHandles = useValue(
'shouldDisplayHandles',
() => editor.isInAny('select.idle', 'select.pointing_handle'),
[editor]
)
if (!shouldDisplayHandles) return null
return <svg className="tl-user-handles tl-overlays__item">{children}</svg>
}

View file

@ -0,0 +1,19 @@
import {
ShapeIndicator,
TLHoveredShapeIndicatorComponent,
useEditor,
useValue,
} from '@tldraw/editor'
export const TldrawHoveredShapeIndicator: TLHoveredShapeIndicatorComponent = ({ shapeId }) => {
const editor = useEditor()
const hideHoveredShapeIndicator = useValue(
'hide hovered',
() => editor.isInAny('select.idle', 'select.editing_shape'),
[editor]
)
if (hideHoveredShapeIndicator) return null
return <ShapeIndicator className="tl-user-indicator__hovered" id={shapeId} />
}
//

View file

@ -1,16 +1,7 @@
import { EASINGS, TLScribble, getSvgPathFromPoints } from '@tldraw/editor'
import { EASINGS, TLScribbleComponent, getSvgPathFromPoints } from '@tldraw/editor'
import classNames from 'classnames'
import { getStroke } from '../shapes/shared/freehand/getStroke'
/** @public */
export type TLScribbleComponent = (props: {
scribble: TLScribble
zoom: number
color?: string
opacity?: number
className?: string
}) => any
export const TldrawScribble: TLScribbleComponent = ({
scribble,
zoom,
@ -18,7 +9,7 @@ export const TldrawScribble: TLScribbleComponent = ({
opacity,
className,
}) => {
if (!scribble.points.length) return
if (!scribble.points.length) return null
const d = getSvgPathFromPoints(
getStroke(scribble.points, {

View file

@ -1,6 +1,7 @@
import {
RotateCorner,
TLEmbedShape,
TLSelectionForegroundComponent,
TLTextShape,
getCursor,
toDomPrecision,
@ -11,6 +12,7 @@ import {
} from '@tldraw/editor'
import classNames from 'classnames'
import { useRef } from 'react'
import { useReadOnly } from '../ui/hooks/useReadOnly'
import { CropHandles } from './CropHandles'
const IS_FIREFOX =
@ -18,437 +20,440 @@ const IS_FIREFOX =
navigator.userAgent &&
navigator.userAgent.toLowerCase().indexOf('firefox') > -1
export const TldrawSelectionForeground = track(function SelectionFg() {
const editor = useEditor()
const rSvg = useRef<SVGSVGElement>(null)
export const TldrawSelectionForeground: TLSelectionForegroundComponent = track(
function SelectionFg() {
const editor = useEditor()
const rSvg = useRef<SVGSVGElement>(null)
const isReadonlyMode = editor.isReadOnly
const topEvents = useSelectionEvents('top')
const rightEvents = useSelectionEvents('right')
const bottomEvents = useSelectionEvents('bottom')
const leftEvents = useSelectionEvents('left')
const topLeftEvents = useSelectionEvents('top_left')
const topRightEvents = useSelectionEvents('top_right')
const bottomRightEvents = useSelectionEvents('bottom_right')
const bottomLeftEvents = useSelectionEvents('bottom_left')
const isReadonlyMode = useReadOnly()
const topEvents = useSelectionEvents('top')
const rightEvents = useSelectionEvents('right')
const bottomEvents = useSelectionEvents('bottom')
const leftEvents = useSelectionEvents('left')
const topLeftEvents = useSelectionEvents('top_left')
const topRightEvents = useSelectionEvents('top_right')
const bottomRightEvents = useSelectionEvents('bottom_right')
const bottomLeftEvents = useSelectionEvents('bottom_left')
const isDefaultCursor = !editor.isMenuOpen && editor.cursor.type === 'default'
const isCoarsePointer = editor.isCoarsePointer
const isDefaultCursor = !editor.isMenuOpen && editor.instanceState.cursor.type === 'default'
const isCoarsePointer = editor.instanceState.isCoarsePointer
let bounds = editor.selectionBounds
const shapes = editor.selectedShapes
const onlyShape = editor.onlySelectedShape
const isLockedShape = onlyShape && editor.isShapeOrAncestorLocked(onlyShape)
let bounds = editor.selectionBounds
const shapes = editor.selectedShapes
const onlyShape = editor.onlySelectedShape
const isLockedShape = onlyShape && editor.isShapeOrAncestorLocked(onlyShape)
// if all shapes have an expandBy for the selection outline, we can expand by the l
const expandOutlineBy = onlyShape
? editor.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape)
: 0
// if all shapes have an expandBy for the selection outline, we can expand by the l
const expandOutlineBy = onlyShape
? editor.getShapeUtil(onlyShape).expandSelectionOutlinePx(onlyShape)
: 0
useTransform(rSvg, bounds?.x, bounds?.y, 1, editor.selectionRotation, {
x: -expandOutlineBy,
y: -expandOutlineBy,
})
useTransform(rSvg, bounds?.x, bounds?.y, 1, editor.selectionRotation, {
x: -expandOutlineBy,
y: -expandOutlineBy,
})
if (!bounds) return null
bounds = bounds.clone().expandBy(expandOutlineBy)
if (!bounds) return null
bounds = bounds.clone().expandBy(expandOutlineBy)
const zoom = editor.zoomLevel
const rotation = editor.selectionRotation
const isChangingStyles = editor.isChangingStyle
const zoom = editor.zoomLevel
const rotation = editor.selectionRotation
const isChangingStyle = editor.instanceState.isChangingStyle
const width = Math.max(1, bounds.width)
const height = Math.max(1, bounds.height)
const width = Math.max(1, bounds.width)
const height = Math.max(1, bounds.height)
const size = 8 / zoom
const isTinyX = width < size * 2
const isTinyY = height < size * 2
const size = 8 / zoom
const isTinyX = width < size * 2
const isTinyY = height < size * 2
const isSmallX = width < size * 4
const isSmallY = height < size * 4
const isSmallCropX = width < size * 5
const isSmallCropY = height < size * 5
const isSmallX = width < size * 4
const isSmallY = height < size * 4
const isSmallCropX = width < size * 5
const isSmallCropY = height < size * 5
const mobileHandleMultiplier = isCoarsePointer ? 1.75 : 1
const targetSize = (6 / zoom) * mobileHandleMultiplier
const mobileHandleMultiplier = isCoarsePointer ? 1.75 : 1
const targetSize = (6 / zoom) * mobileHandleMultiplier
const targetSizeX = (isSmallX ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const targetSizeY = (isSmallY ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const targetSizeX = (isSmallX ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const targetSizeY = (isSmallY ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const showSelectionBounds =
(onlyShape ? !editor.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) &&
!isChangingStyles
const showSelectionBounds =
(onlyShape ? !editor.getShapeUtil(onlyShape).hideSelectionBoundsFg(onlyShape) : true) &&
!isChangingStyle
let shouldDisplayBox =
(showSelectionBounds &&
let shouldDisplayBox =
(showSelectionBounds &&
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.pointing_canvas',
'select.pointing_selection',
'select.pointing_shape',
'select.crop.idle',
'select.crop.pointing_crop',
'select.pointing_resize_handle',
'select.pointing_crop_handle',
'select.editing_shape'
)) ||
(showSelectionBounds &&
editor.isIn('select.resizing') &&
onlyShape &&
editor.isShapeOfType<TLTextShape>(onlyShape, 'text'))
if (
onlyShape &&
editor.isShapeOfType<TLEmbedShape>(onlyShape, 'embed') &&
shouldDisplayBox &&
IS_FIREFOX
) {
shouldDisplayBox = false
}
const showCropHandles =
editor.isInAny(
'select.pointing_crop_handle',
'select.crop.idle',
'select.crop.pointing_crop'
) &&
!isChangingStyle &&
!isReadonlyMode
const shouldDisplayControls =
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.pointing_canvas',
'select.pointing_selection',
'select.pointing_shape',
'select.crop.idle',
'select.crop.pointing_crop',
'select.pointing_resize_handle',
'select.pointing_crop_handle',
'select.editing_shape'
)) ||
(showSelectionBounds &&
editor.isIn('select.resizing') &&
'select.crop.idle'
) &&
!isChangingStyle &&
!isReadonlyMode
const showCornerRotateHandles =
!isCoarsePointer &&
!(isTinyX || isTinyY) &&
(shouldDisplayControls || showCropHandles) &&
(onlyShape ? !editor.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) &&
!isLockedShape
const showMobileRotateHandle =
isCoarsePointer &&
(!isSmallX || !isSmallY) &&
(shouldDisplayControls || showCropHandles) &&
(onlyShape ? !editor.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) &&
!isLockedShape
const showResizeHandles =
shouldDisplayControls &&
(onlyShape
? editor.getShapeUtil(onlyShape).canResize(onlyShape) &&
!editor.getShapeUtil(onlyShape).hideResizeHandles(onlyShape)
: true) &&
!showCropHandles &&
!isLockedShape
const hideAlternateCornerHandles = isTinyX || isTinyY
const showOnlyOneHandle = isTinyX && isTinyY
const hideAlternateCropHandles = isSmallCropX || isSmallCropY
const showHandles = showResizeHandles || showCropHandles
const hideRotateCornerHandles = !showCornerRotateHandles
const hideMobileRotateHandle = !shouldDisplayControls || !showMobileRotateHandle
const hideTopLeftCorner = !shouldDisplayControls || !showHandles
const hideTopRightCorner = !shouldDisplayControls || !showHandles || hideAlternateCornerHandles
const hideBottomLeftCorner =
!shouldDisplayControls || !showHandles || hideAlternateCornerHandles
const hideBottomRightCorner =
!shouldDisplayControls || !showHandles || (showOnlyOneHandle && !showCropHandles)
let hideEdgeTargetsDueToCoarsePointer = isCoarsePointer
if (
hideEdgeTargetsDueToCoarsePointer &&
shapes.every((shape) => editor.getShapeUtil(shape).isAspectRatioLocked(shape))
) {
hideEdgeTargetsDueToCoarsePointer = false
}
// If we're showing crop handles, then show the edges too.
// If we're showing resize handles, then show the edges only
// if we're not hiding them for some other reason
let hideEdgeTargets = true
if (showCropHandles) {
hideEdgeTargets = hideAlternateCropHandles
} else if (showResizeHandles) {
hideEdgeTargets =
hideAlternateCornerHandles || showOnlyOneHandle || hideEdgeTargetsDueToCoarsePointer
}
const textHandleHeight = Math.min(24 / zoom, height - targetSizeY * 3)
const showTextResizeHandles =
shouldDisplayControls &&
isCoarsePointer &&
onlyShape &&
editor.isShapeOfType<TLTextShape>(onlyShape, 'text'))
editor.isShapeOfType<TLTextShape>(onlyShape, 'text') &&
textHandleHeight * zoom >= 4
if (
onlyShape &&
editor.isShapeOfType<TLEmbedShape>(onlyShape, 'embed') &&
shouldDisplayBox &&
IS_FIREFOX
) {
shouldDisplayBox = false
}
const showCropHandles =
editor.isInAny(
'select.pointing_crop_handle',
'select.crop.idle',
'select.crop.pointing_crop'
) &&
!isChangingStyles &&
!isReadonlyMode
const shouldDisplayControls =
editor.isInAny(
'select.idle',
'select.pointing_selection',
'select.pointing_shape',
'select.crop.idle'
) &&
!isChangingStyles &&
!isReadonlyMode
const showCornerRotateHandles =
!isCoarsePointer &&
!(isTinyX || isTinyY) &&
(shouldDisplayControls || showCropHandles) &&
(onlyShape ? !editor.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) &&
!isLockedShape
const showMobileRotateHandle =
isCoarsePointer &&
(!isSmallX || !isSmallY) &&
(shouldDisplayControls || showCropHandles) &&
(onlyShape ? !editor.getShapeUtil(onlyShape).hideRotateHandle(onlyShape) : true) &&
!isLockedShape
const showResizeHandles =
shouldDisplayControls &&
(onlyShape
? editor.getShapeUtil(onlyShape).canResize(onlyShape) &&
!editor.getShapeUtil(onlyShape).hideResizeHandles(onlyShape)
: true) &&
!showCropHandles &&
!isLockedShape
const hideAlternateCornerHandles = isTinyX || isTinyY
const showOnlyOneHandle = isTinyX && isTinyY
const hideAlternateCropHandles = isSmallCropX || isSmallCropY
const showHandles = showResizeHandles || showCropHandles
const hideRotateCornerHandles = !showCornerRotateHandles
const hideMobileRotateHandle = !shouldDisplayControls || !showMobileRotateHandle
const hideTopLeftCorner = !shouldDisplayControls || !showHandles
const hideTopRightCorner = !shouldDisplayControls || !showHandles || hideAlternateCornerHandles
const hideBottomLeftCorner = !shouldDisplayControls || !showHandles || hideAlternateCornerHandles
const hideBottomRightCorner =
!shouldDisplayControls || !showHandles || (showOnlyOneHandle && !showCropHandles)
let hideEdgeTargetsDueToCoarsePointer = isCoarsePointer
if (
hideEdgeTargetsDueToCoarsePointer &&
shapes.every((shape) => editor.getShapeUtil(shape).isAspectRatioLocked(shape))
) {
hideEdgeTargetsDueToCoarsePointer = false
}
// If we're showing crop handles, then show the edges too.
// If we're showing resize handles, then show the edges only
// if we're not hiding them for some other reason
let hideEdgeTargets = true
if (showCropHandles) {
hideEdgeTargets = hideAlternateCropHandles
} else if (showResizeHandles) {
hideEdgeTargets =
hideAlternateCornerHandles || showOnlyOneHandle || hideEdgeTargetsDueToCoarsePointer
}
const textHandleHeight = Math.min(24 / zoom, height - targetSizeY * 3)
const showTextResizeHandles =
shouldDisplayControls &&
isCoarsePointer &&
onlyShape &&
editor.isShapeOfType<TLTextShape>(onlyShape, 'text') &&
textHandleHeight * zoom >= 4
return (
<svg
ref={rSvg}
className="tl-overlays__item tl-selection__fg"
data-testid="selection-foreground"
>
{shouldDisplayBox && (
return (
<svg
ref={rSvg}
className="tl-overlays__item tl-selection__fg"
data-testid="selection-foreground"
>
{shouldDisplayBox && (
<rect
className={classNames('tl-selection__fg__outline')}
width={toDomPrecision(width)}
height={toDomPrecision(height)}
/>
)}
<RotateCornerHandle
data-testid="selection.rotate.top-left"
cx={0}
cy={0}
targetSize={targetSize}
corner="top_left_rotate"
cursor={isDefaultCursor ? getCursor('nwse-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-testid="selection.rotate.top-right"
cx={width + targetSize * 3}
cy={0}
targetSize={targetSize}
corner="top_right_rotate"
cursor={isDefaultCursor ? getCursor('nesw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-testid="selection.rotate.bottom-left"
cx={0}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_left_rotate"
cursor={isDefaultCursor ? getCursor('swne-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-testid="selection.rotate.bottom-right"
cx={width + targetSize * 3}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_right_rotate"
cursor={isDefaultCursor ? getCursor('senw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>{' '}
<MobileRotateHandle
data-testid="selection.rotate.mobile"
cx={isSmallX ? -targetSize * 1.5 : width / 2}
cy={isSmallX ? height / 2 : -targetSize * 1.5}
size={size}
isHidden={hideMobileRotateHandle}
/>
{/* Targets */}
<rect
className={classNames('tl-selection__fg__outline')}
width={toDomPrecision(width)}
height={toDomPrecision(height)}
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.top"
aria-label="top target"
pointerEvents="all"
x={0}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...topEvents}
/>
)}
<RotateCornerHandle
data-testid="selection.rotate.top-left"
cx={0}
cy={0}
targetSize={targetSize}
corner="top_left_rotate"
cursor={isDefaultCursor ? getCursor('nwse-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-testid="selection.rotate.top-right"
cx={width + targetSize * 3}
cy={0}
targetSize={targetSize}
corner="top_right_rotate"
cursor={isDefaultCursor ? getCursor('nesw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-testid="selection.rotate.bottom-left"
cx={0}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_left_rotate"
cursor={isDefaultCursor ? getCursor('swne-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-testid="selection.rotate.bottom-right"
cx={width + targetSize * 3}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_right_rotate"
cursor={isDefaultCursor ? getCursor('senw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>{' '}
<MobileRotateHandle
data-testid="selection.rotate.mobile"
cx={isSmallX ? -targetSize * 1.5 : width / 2}
cy={isSmallX ? height / 2 : -targetSize * 1.5}
size={size}
isHidden={hideMobileRotateHandle}
/>
{/* Targets */}
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.top"
aria-label="top target"
pointerEvents="all"
x={0}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...topEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.right"
aria-label="right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...rightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.bottom"
aria-label="bottom target"
pointerEvents="all"
x={0}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...bottomEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.left"
aria-label="left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...leftEvents}
/>
{/* Corner Targets */}
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideTopLeftCorner,
})}
data-testid="selection.target.top-left"
aria-label="top-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...topLeftEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideTopRightCorner,
})}
data-testid="selection.target.top-right"
aria-label="top-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...topRightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideBottomRightCorner,
})}
data-testid="selection.target.bottom-right"
aria-label="bottom-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? targetSizeY : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...bottomRightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideBottomLeftCorner,
})}
data-testid="selection.target.bottom-left"
aria-label="bottom-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...bottomLeftEvents}
/>
{/* Resize Handles */}
{showResizeHandles && (
<>
<rect
data-testid="selection.resize.top-left"
className={classNames('tl-corner-handle', {
'tl-hidden': hideTopLeftCorner,
})}
aria-label="top_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-testid="selection.resize.top-right"
className={classNames('tl-corner-handle', {
'tl-hidden': hideTopRightCorner,
})}
aria-label="top_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-testid="selection.resize.bottom-right"
className={classNames('tl-corner-handle', {
'tl-hidden': hideBottomRightCorner,
})}
aria-label="bottom_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-testid="selection.resize.bottom-left"
className={classNames('tl-corner-handle', {
'tl-hidden': hideBottomLeftCorner,
})}
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
</>
)}
{showTextResizeHandles && (
<>
<rect
data-testid="selection.text-resize.left.handle"
className="tl-text-handle"
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
rx={size / 4}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
<rect
data-testid="selection.text-resize.right.handle"
className="tl-text-handle"
aria-label="bottom_left handle"
rx={size / 4}
x={toDomPrecision(width - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
</>
)}
{/* Crop Handles */}
{showCropHandles && (
<CropHandles
{...{
size,
width,
height,
hideAlternateHandles: hideAlternateCropHandles,
}}
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.right"
aria-label="right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...rightEvents}
/>
)}
</svg>
)
})
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.bottom"
aria-label="bottom target"
pointerEvents="all"
x={0}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...bottomEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-testid="selection.resize.left"
aria-label="left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...leftEvents}
/>
{/* Corner Targets */}
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideTopLeftCorner,
})}
data-testid="selection.target.top-left"
aria-label="top-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...topLeftEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideTopRightCorner,
})}
data-testid="selection.target.top-right"
aria-label="top-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...topRightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideBottomRightCorner,
})}
data-testid="selection.target.bottom-right"
aria-label="bottom-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? targetSizeY : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...bottomRightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideBottomLeftCorner,
})}
data-testid="selection.target.bottom-left"
aria-label="bottom-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...bottomLeftEvents}
/>
{/* Resize Handles */}
{showResizeHandles && (
<>
<rect
data-testid="selection.resize.top-left"
className={classNames('tl-corner-handle', {
'tl-hidden': hideTopLeftCorner,
})}
aria-label="top_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-testid="selection.resize.top-right"
className={classNames('tl-corner-handle', {
'tl-hidden': hideTopRightCorner,
})}
aria-label="top_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-testid="selection.resize.bottom-right"
className={classNames('tl-corner-handle', {
'tl-hidden': hideBottomRightCorner,
})}
aria-label="bottom_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-testid="selection.resize.bottom-left"
className={classNames('tl-corner-handle', {
'tl-hidden': hideBottomLeftCorner,
})}
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
</>
)}
{showTextResizeHandles && (
<>
<rect
data-testid="selection.text-resize.left.handle"
className="tl-text-handle"
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
rx={size / 4}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
<rect
data-testid="selection.text-resize.right.handle"
className="tl-text-handle"
aria-label="bottom_left handle"
rx={size / 4}
x={toDomPrecision(width - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
</>
)}
{/* Crop Handles */}
{showCropHandles && (
<CropHandles
{...{
size,
width,
height,
hideAlternateHandles: hideAlternateCropHandles,
}}
/>
)}
</svg>
)
}
)
export const RotateCornerHandle = function RotateCornerHandle({
cx,

View file

@ -110,7 +110,7 @@ describe('When dragging the arrow', () => {
})
it('returns to arrow.idle, keeping shape, on pointer up when tool lock is active', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
const shapesBefore = editor.shapesArray.length
editor
.setCurrentTool('arrow')

View file

@ -17,7 +17,6 @@ import {
TLOnHandleChangeHandler,
TLOnResizeHandler,
TLOnTranslateStartHandler,
TLShapeId,
TLShapePartial,
TLShapeUtilCanvasSvgDef,
TLShapeUtilFlag,
@ -370,26 +369,24 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
}
override onTranslateStart: TLOnTranslateStartHandler<TLArrowShape> = (shape) => {
let startBinding: TLShapeId | null =
const startBindingId =
shape.props.start.type === 'binding' ? shape.props.start.boundShapeId : null
let endBinding: TLShapeId | null =
shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : null
const endBindingId = shape.props.end.type === 'binding' ? shape.props.end.boundShapeId : null
// If at least one bound shape is in the selection, do nothing;
// If no bound shapes are in the selection, unbind any bound shapes
const { selectedIds } = this.editor
if (
(startBinding &&
(this.editor.isSelected(startBinding) || this.editor.isAncestorSelected(startBinding))) ||
(endBinding &&
(this.editor.isSelected(endBinding) || this.editor.isAncestorSelected(endBinding)))
(startBindingId &&
(selectedIds.includes(startBindingId) || this.editor.isAncestorSelected(startBindingId))) ||
(endBindingId &&
(selectedIds.includes(endBindingId) || this.editor.isAncestorSelected(endBindingId)))
) {
return
}
startBinding = null
endBinding = null
const { start, end } = getArrowTerminalsInArrowSpace(this.editor, shape)
return {
@ -560,7 +557,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
'select.pointing_handle',
'select.dragging_handle',
'arrow.dragging'
) && !this.editor.isReadOnly
) && !this.editor.instanceState.isReadOnly
const info = this.editor.getArrowInfo(shape)
const bounds = this.editor.getBounds(shape)
@ -914,7 +911,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
}
override toSvg(shape: TLArrowShape, ctx: SvgExportContext) {
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
const color = theme[shape.props.color].solid

View file

@ -8,7 +8,7 @@ export class Idle extends StateNode {
}
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
override onCancel = () => {

View file

@ -221,7 +221,7 @@ export class DrawShapeUtil extends ShapeUtil<TLDrawShape> {
}
override toSvg(shape: TLDrawShape, ctx: SvgExportContext) {
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
const { color } = shape.props

View file

@ -459,7 +459,7 @@ export class Drawing extends StateNode {
let didSnap = false
let snapSegment: TLDrawShapeSegment | undefined = undefined
const shouldSnap = this.editor.isSnapMode ? !ctrlKey : ctrlKey
const shouldSnap = this.editor.user.isSnapMode ? !ctrlKey : ctrlKey
if (shouldSnap) {
if (newSegments.length > 2) {

View file

@ -8,7 +8,7 @@ export class Idle extends StateNode {
}
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
override onCancel = () => {

View file

@ -84,7 +84,7 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
const isHoveringWhileEditingSameShape = useValue(
'is hovering',
() => {
const { editingId, hoveredId } = this.editor.pageState
const { editingId, hoveredId } = this.editor.currentPageState
if (editingId && hoveredId !== editingId) {
const editingShape = this.editor.getShapeById(editingId)

View file

@ -123,7 +123,7 @@ describe('When in the pointing state', () => {
})
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
expect(editor.shapesArray.length).toBe(0)
editor.setCurrentTool('frame')
editor.pointerDown(50, 50)
@ -152,7 +152,7 @@ describe('When in the resizing state', () => {
})
it('Returns to frame.idle on complete if tool lock is enabled', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor.setCurrentTool('frame')
editor.pointerDown(50, 50)
editor.pointerMove(100, 100)

View file

@ -67,7 +67,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
}
override toSvg(shape: TLFrameShape): SVGElement | Promise<SVGElement> {
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')

View file

@ -15,7 +15,7 @@ export const FrameLabelInput = forwardRef<
// and sending us back into edit mode
e.stopPropagation()
e.currentTarget.blur()
editor.editingId = null
editor.setEditingId(null)
}
},
[editor]

View file

@ -152,7 +152,7 @@ describe('When in the pointing state', () => {
})
it('Creates a geo and returns to geo.idle on pointer up if tool lock is enabled', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
expect(editor.shapesArray.length).toBe(0)
editor.setCurrentTool('geo')
editor.pointerDown(50, 50)
@ -181,7 +181,7 @@ describe('When in the resizing state while creating a geo shape', () => {
})
it('Returns to geo.idle on complete if tool lock is enabled', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor.setCurrentTool('geo')
editor.pointerDown(50, 50)
editor.pointerMove(100, 100)

View file

@ -572,7 +572,7 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
override toSvg(shape: TLGeoShape, ctx: SvgExportContext) {
const { id, props } = shape
const strokeWidth = STROKE_SIZES[props.size]
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))
let svgElm: SVGElement

View file

@ -8,7 +8,7 @@ export class Idle extends StateNode {
}
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
override onKeyUp: TLEventHandlers['onKeyUp'] = (info) => {
@ -17,7 +17,7 @@ export class Idle extends StateNode {
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
// todo: ensure that this only works with the most recently created shape, not just any geo shape that happens to be selected at the time
this.editor.mark('editing shape')
this.editor.editingId = shape.id
this.editor.setEditingId(shape.id)
this.editor.setCurrentTool('select.editing_shape', {
...info,
target: 'shape',

View file

@ -161,12 +161,12 @@ export class HighlightShapeUtil extends ShapeUtil<TLHighlightShape> {
}
override toSvg(shape: TLHighlightShape) {
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
return highlighterToSvg(getStrokeWidth(shape), shape, OVERLAY_OPACITY, theme)
}
override toBackgroundSvg(shape: TLHighlightShape) {
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
return highlighterToSvg(getStrokeWidth(shape), shape, UNDERLAY_OPACITY, theme)
}

View file

@ -88,7 +88,7 @@ describe('When dragging the line', () => {
})
it('returns to line.idle, keeping shape, on pointer up if tool lock is enabled', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
const shapesBefore = editor.shapesArray.length
editor
.setCurrentTool('line')
@ -116,7 +116,7 @@ describe('When dragging the line', () => {
describe('When extending the line with the shift-key in tool-lock mode', () => {
it('extends a line by joining-the-dots', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor
.setCurrentTool('line')
.pointerDown(0, 0, { target: 'canvas' })
@ -133,7 +133,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
})
it('extends a line after a click by shift-click dragging', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor
.setCurrentTool('line')
.pointerDown(0, 0, { target: 'canvas' })
@ -150,7 +150,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
})
it('extends a line by shift-click dragging', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor
.setCurrentTool('line')
.pointerDown(0, 0, { target: 'canvas' })
@ -168,7 +168,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
})
it('extends a line by shift-clicking even after canceling a pointerdown', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor
.setCurrentTool('line')
.pointerDown(0, 0, { target: 'canvas' })
@ -188,7 +188,7 @@ describe('When extending the line with the shift-key in tool-lock mode', () => {
})
it('extends a line by shift-clicking even after canceling a pointermove', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor
.setCurrentTool('line')
.pointerDown(0, 0, { target: 'canvas' })

View file

@ -348,7 +348,7 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
}
override toSvg(shape: TLLineShape) {
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
const color = theme[shape.props.color].solid
const spline = getSplineForLineShape(shape)
return getLineSvg(shape, spline, color, STROKE_SIZES[shape.props.size])

View file

@ -7,7 +7,7 @@ export class Idle extends StateNode {
override onEnter = (info: { shapeId: TLShapeId }) => {
this.shapeId = info.shapeId
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
override onPointerDown: TLEventHandlers['onPointerDown'] = () => {

View file

@ -110,7 +110,7 @@ describe('When in the pointing state', () => {
})
it('Returns to the note tool on complete from translating when tool lock is enabled', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
editor.setCurrentTool('note')
editor.pointerDown(50, 50)
editor.pointerMove(55, 55)
@ -135,7 +135,7 @@ describe('When in the pointing state', () => {
})
it('Creates a frame and returns to frame.idle on pointer up if tool lock is enabled', () => {
editor.isToolLocked = true
editor.updateInstanceState({ isToolLocked: true })
expect(editor.shapesArray.length).toBe(0)
editor.setCurrentTool('note')
editor.pointerDown(50, 50)

View file

@ -122,7 +122,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
override toSvg(shape: TLNoteShape, ctx: SvgExportContext) {
ctx.addExportDef(getFontDefForExport(shape.props.font))
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
const bounds = this.getBounds(shape)
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')

View file

@ -8,7 +8,7 @@ export class Idle extends StateNode {
}
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
override onCancel = () => {

View file

@ -68,7 +68,7 @@ export class Pointing extends StateNode {
} else {
if (!shape) return
this.editor.editingId = shape.id
this.editor.setEditingId(shape.id)
this.editor.setCurrentTool('select.editing_shape', {
...this.info,
target: 'shape',

View file

@ -17,7 +17,7 @@ export interface ShapeFillProps {
export function useDefaultColorTheme() {
const editor = useEditor()
return getDefaultColorTheme(editor)
return getDefaultColorTheme({ isDarkMode: editor.user.isDarkMode })
}
export const ShapeFill = React.memo(function ShapeFill({ d, color, fill }: ShapeFillProps) {
@ -42,7 +42,7 @@ const PatternFill = function PatternFill({ d, color }: ShapeFillProps) {
const editor = useEditor()
const theme = useDefaultColorTheme()
const zoomLevel = useValue('zoomLevel', () => editor.zoomLevel, [editor])
const isDarkMode = useValue('isDarkMode', () => editor.isDarkMode, [editor])
const isDarkMode = useValue('isDarkMode', () => editor.user.isDarkMode, [editor])
const intZoom = Math.ceil(zoomLevel)
const teenyTiny = editor.zoomLevel <= 0.18

View file

@ -184,7 +184,7 @@ const getDefaultPatterns = () => {
function usePattern() {
const editor = useEditor()
const dpr = editor.devicePixelRatio
const dpr = editor.instanceState.devicePixelRatio
const [isReady, setIsReady] = useState(false)
const defaultPatterns = useMemo(() => getDefaultPatterns(), [])
const [backgroundUrls, setBackgroundUrls] = useState<PatternDef[]>(defaultPatterns)

View file

@ -20,7 +20,10 @@ export function useEditableText<T extends Extract<TLShape, { props: { text: stri
const rInput = useRef<HTMLTextAreaElement>(null)
const isEditing = useValue('isEditing', () => editor.pageState.editingId === id, [editor, id])
const isEditing = useValue('isEditing', () => editor.currentPageState.editingId === id, [
editor,
id,
])
const rSkipSelectOnFocus = useRef(false)
const rSelectionRanges = useRef<Range[] | null>()

View file

@ -77,7 +77,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
props: { text, color },
} = shape
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
const { width, height } = this.getMinDimensions(shape)
const {
@ -151,7 +151,7 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
override toSvg(shape: TLTextShape, ctx: SvgExportContext) {
ctx.addExportDef(getFontDefForExport(shape.props.font))
const theme = getDefaultColorTheme(this.editor)
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.isDarkMode })
const bounds = this.getBounds(shape)
const text = shape.props.text

View file

@ -41,7 +41,7 @@ export class Idle extends StateNode {
if (this.editor.isShapeOfType<TLTextShape>(shape, 'text')) {
requestAnimationFrame(() => {
this.editor.setSelectedIds([shape.id])
this.editor.editingId = shape.id
this.editor.setEditingId(shape.id)
this.editor.setCurrentTool('select.editing_shape', {
...info,
target: 'shape',
@ -56,7 +56,7 @@ export class Idle extends StateNode {
}
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
override onKeyDown: TLEventHandlers['onKeyDown'] = (info) => {
@ -64,7 +64,7 @@ export class Idle extends StateNode {
const shape = this.editor.selectedShapes[0]
if (shape && this.editor.isShapeOfType<TLGeoShape>(shape, 'geo')) {
this.editor.setCurrentTool('select')
this.editor.editingId = shape.id
this.editor.setEditingId(shape.id)
this.editor.root.current.value!.transition('editing_shape', {
...info,
target: 'shape',

View file

@ -86,7 +86,7 @@ export class Pointing extends StateNode {
true
)
this.editor.editingId = id
this.editor.setEditingId(id)
this.editor.setCurrentTool('select')
this.editor.root.current.value?.transition('editing_shape', {})
}

View file

@ -9,6 +9,6 @@ export class EraserTool extends StateNode {
static override children = () => [Idle, Pointing, Erasing]
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
}

View file

@ -60,12 +60,12 @@ export class Erasing extends StateNode {
}
private onScribbleUpdate = (scribble: TLScribble) => {
this.editor.scribble = scribble
this.editor.updateInstanceState({ scribble })
}
private onScribbleComplete = () => {
this.editor.off('tick', this.scribble.tick)
this.editor.scribble = null
this.editor.updateInstanceState({ scribble: null })
}
override onExit = () => {
@ -124,17 +124,17 @@ export class Erasing extends StateNode {
// Remove the hit shapes, except if they're in the list of excluded shapes
// (these excluded shapes will be any frames or groups the pointer was inside of
// when the user started erasing)
this.editor.erasingIds = [...erasing].filter((id) => !excludedShapeIds.has(id))
this.editor.setErasingIds([...erasing].filter((id) => !excludedShapeIds.has(id)))
}
complete() {
this.editor.deleteShapes(this.editor.pageState.erasingIds)
this.editor.erasingIds = []
this.editor.deleteShapes(this.editor.currentPageState.erasingIds)
this.editor.setErasingIds([])
this.parent.transition('idle', {})
}
cancel() {
this.editor.erasingIds = []
this.editor.setErasingIds([])
this.editor.bailToMark(this.markId)
this.parent.transition('idle', this.info)
}

View file

@ -28,7 +28,7 @@ export class Pointing extends StateNode {
}
}
this.editor.erasingIds = [...erasing]
this.editor.setErasingIds([...erasing])
}
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
@ -61,12 +61,12 @@ export class Pointing extends StateNode {
this.editor.deleteShapes(erasingIds)
}
this.editor.erasingIds = []
this.editor.setErasingIds([])
this.parent.transition('idle', {})
}
cancel() {
this.editor.erasingIds = []
this.editor.setErasingIds([])
this.parent.transition('idle', {})
}
}

View file

@ -4,7 +4,7 @@ export class Idle extends StateNode {
static override id = 'idle'
override onEnter = () => {
this.editor.cursor = { type: 'grab', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'grab', rotation: 0 } }, true)
}
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {

View file

@ -5,7 +5,7 @@ export class Pointing extends StateNode {
override onEnter = () => {
this.editor.stopCameraAnimation()
this.editor.cursor = { type: 'grabbing', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'grabbing', rotation: 0 } }, true)
}
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {

View file

@ -8,6 +8,6 @@ export class LaserTool extends StateNode {
static override children = () => [Idle, Lasering]
override onEnter = () => {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
}

View file

@ -12,7 +12,7 @@ export class Lasering extends StateNode {
}
override onExit = () => {
this.editor.erasingIds = []
this.editor.setErasingIds([])
this.scribble.stop()
}
@ -47,12 +47,12 @@ export class Lasering extends StateNode {
}
private onScribbleUpdate = (scribble: TLScribble) => {
this.editor.scribble = scribble
this.editor.updateInstanceState({ scribble })
}
private onScribbleComplete = () => {
this.editor.off('tick', this.scribble.tick)
this.editor.scribble = null
this.editor.updateInstanceState({ scribble: null })
}
override onCancel: TLEventHandlers['onCancel'] = () => {

View file

@ -41,8 +41,8 @@ export class SelectTool extends StateNode {
]
override onExit = () => {
if (this.editor.pageState.editingId) {
this.editor.editingId = null
if (this.editor.currentPageState.editingId) {
this.editor.setEditingId(null)
}
}
}

View file

@ -57,7 +57,7 @@ export class Brushing extends StateNode {
override onExit = () => {
this.initialSelectedIds = []
this.editor.brush = null
this.editor.updateInstanceState({ brush: null })
}
override onPointerMove = () => {
@ -168,12 +168,12 @@ export class Brushing extends StateNode {
}
}
this.editor.brush = { ...this.brush.toJson() }
this.editor.updateInstanceState({ brush: { ...this.brush.toJson() } })
this.editor.setSelectedIds(Array.from(results), true)
}
override onInterrupt: TLInterruptEvent = () => {
this.editor.brush = null
this.editor.updateInstanceState({ brush: null })
}
private handleHit(

View file

@ -5,7 +5,7 @@ export class Idle extends StateNode {
static override id = 'idle'
override onEnter = () => {
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
const { onlySelectedShape } = this.editor
@ -22,7 +22,7 @@ export class Idle extends StateNode {
}
override onExit: TLExitEventHandler = () => {
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
this.editor.off('change-history', this.cleanupCroppingState)
}

View file

@ -27,12 +27,12 @@ export class TranslatingCrop extends StateNode {
this.snapshot = this.createSnapshot()
this.editor.mark(this.markId)
this.editor.cursor = { type: 'move', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'move', rotation: 0 } }, true)
this.updateShapes()
}
override onExit = () => {
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
}
override onPointerMove = () => {

View file

@ -63,10 +63,12 @@ export class Cropping extends StateNode {
if (!selectedShape) return
const cursorType = CursorTypeMap[this.info.handle!]
this.editor.cursor = {
type: cursorType,
rotation: selectedShape.rotation,
}
this.editor.updateInstanceState({
cursor: {
type: cursorType,
rotation: selectedShape.rotation,
},
})
}
private getDefaultCrop = (): TLImageShapeCrop => ({

View file

@ -56,7 +56,10 @@ export class DraggingHandle extends StateNode {
this.initialPageTransform = this.editor.getPageTransform(shape)!
this.initialPageRotation = this.editor.getPageRotation(shape)!
this.editor.cursor = { type: isCreating ? 'cross' : 'grabbing', rotation: 0 }
this.editor.updateInstanceState(
{ cursor: { type: isCreating ? 'cross' : 'grabbing', rotation: 0 } },
true
)
// <!-- Only relevant to arrows
const handles = this.editor.getHandles(shape)!.sort(sortByIndex)
@ -159,7 +162,7 @@ export class DraggingHandle extends StateNode {
this.parent.currentToolIdMask = undefined
this.editor.hintingIds = []
this.editor.snaps.clear()
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
}
private complete() {
@ -195,7 +198,7 @@ export class DraggingHandle extends StateNode {
const { editor, shapeId } = this
const { initialHandle, initialPageRotation, initialAdjacentHandle } = this
const {
isSnapMode,
user: { isSnapMode },
hintingIds,
snaps,
inputs: { currentPagePoint, originPagePoint, shiftKey, ctrlKey, altKey, pointerVelocity },

View file

@ -29,12 +29,12 @@ export class EditingShape extends StateNode {
}
override onExit = () => {
if (!this.editor.pageState.editingId) return
const { editingId } = this.editor.pageState
if (!this.editor.currentPageState.editingId) return
const { editingId } = this.editor.currentPageState
if (!editingId) return
// Clear the editing shape
this.editor.editingId = null
this.editor.setEditingId(null)
const shape = this.editor.getShapeById(editingId)!
const util = this.editor.getShapeUtil(shape)
@ -48,7 +48,7 @@ export class EditingShape extends StateNode {
case 'shape': {
const { shape } = info
const { editingId } = this.editor.pageState
const { editingId } = this.editor.currentPageState
if (editingId) {
if (shape.id === editingId) {
@ -70,7 +70,7 @@ export class EditingShape extends StateNode {
util.canEdit?.(shape) &&
!this.editor.isShapeOrAncestorLocked(shape)
) {
this.editor.editingId = shape.id
this.editor.setEditingId(shape.id)
this.editor.hoveredId = shape.id
this.editor.setSelectedIds([shape.id])
return

View file

@ -38,10 +38,10 @@ export class Idle extends StateNode {
if (hoveringShape.type !== 'geo') break
const cursorType = (hoveringShape as TLGeoShape).props.text
try {
this.editor.cursor = { type: cursorType, rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: cursorType, rotation: 0 } }, true)
} catch (e) {
console.error(`Cursor type not recognized: '${cursorType}'`)
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
}
}
@ -87,7 +87,7 @@ export class Idle extends StateNode {
break
}
case 'handle': {
if (this.editor.isReadOnly) break
if (this.editor.instanceState.isReadOnly) break
if (this.editor.inputs.altKey) {
this.parent.transition('pointing_shape', info)
} else {
@ -142,12 +142,12 @@ export class Idle extends StateNode {
switch (info.target) {
case 'canvas': {
// Create text shape and transition to editing_shape
if (this.editor.isReadOnly) break
if (this.editor.instanceState.isReadOnly) break
this.handleDoubleClickOnCanvas(info)
break
}
case 'selection': {
if (this.editor.isReadOnly) break
if (this.editor.instanceState.isReadOnly) break
const { onlySelectedShape } = this.editor
if (onlySelectedShape) {
@ -188,7 +188,12 @@ export class Idle extends StateNode {
const util = this.editor.getShapeUtil(shape)
// Allow playing videos and embeds
if (shape.type !== 'video' && shape.type !== 'embed' && this.editor.isReadOnly) break
if (
shape.type !== 'video' &&
shape.type !== 'embed' &&
this.editor.instanceState.isReadOnly
)
break
if (util.onDoubleClick) {
// Call the shape's double click handler
@ -216,7 +221,7 @@ export class Idle extends StateNode {
break
}
case 'handle': {
if (this.editor.isReadOnly) break
if (this.editor.instanceState.isReadOnly) break
const { shape, handle } = info
const util = this.editor.getShapeUtil(shape)
@ -242,12 +247,12 @@ export class Idle extends StateNode {
break
}
case 'shape': {
const { selectedIds } = this.editor.pageState
const { selectedIds } = this.editor.currentPageState
const { shape } = info
const targetShape = this.editor.getOutermostSelectableShape(
shape,
(parent) => !this.editor.isSelected(parent.id)
(parent) => !selectedIds.includes(parent.id)
)
if (!selectedIds.includes(targetShape.id)) {
@ -261,7 +266,7 @@ export class Idle extends StateNode {
override onEnter = () => {
this.editor.hoveredId = null
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
this.parent.currentToolIdMask = undefined
}
@ -302,7 +307,7 @@ export class Idle extends StateNode {
}
override onKeyUp = (info: TLKeyboardEventInfo) => {
if (this.editor.isReadOnly) {
if (this.editor.instanceState.isReadOnly) {
switch (info.code) {
case 'Enter': {
if (this.shouldStartEditingShape() && this.editor.onlySelectedShape) {
@ -376,7 +381,7 @@ export class Idle extends StateNode {
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
this.editor.mark('editing shape')
this.editor.editingId = shape.id
this.editor.setEditingId(shape.id)
this.parent.transition('editing_shape', info)
}
@ -416,7 +421,7 @@ export class Idle extends StateNode {
},
])
this.editor.editingId = id
this.editor.setEditingId(id)
this.editor.select(id)
this.parent.transition('editing_shape', info)
}

View file

@ -14,10 +14,12 @@ export class PointingCropHandle extends StateNode {
private updateCursor(shape: TLShape) {
const cursorType = CursorTypeMap[this.info.handle!]
this.editor.cursor = {
type: cursorType,
rotation: shape.rotation,
}
this.editor.updateInstanceState({
cursor: {
type: cursorType,
rotation: shape.rotation,
},
})
}
override onEnter = (info: TLPointingCropHandleInfo) => {
@ -31,7 +33,7 @@ export class PointingCropHandle extends StateNode {
}
override onExit = () => {
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
this.parent.currentToolIdMask = undefined
}

View file

@ -14,12 +14,12 @@ export class PointingHandle extends StateNode {
this.editor.hintingIds = [initialTerminal.boundShapeId]
}
this.editor.cursor = { type: 'grabbing', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'grabbing', rotation: 0 } }, true)
}
override onExit = () => {
this.editor.hintingIds = []
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
}
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {

View file

@ -34,10 +34,9 @@ export class PointingResizeHandle extends StateNode {
private updateCursor() {
const selected = this.editor.selectedShapes
const cursorType = CursorTypeMap[this.info.handle!]
this.editor.cursor = {
type: cursorType,
rotation: selected.length === 1 ? selected[0].rotation : 0,
}
this.editor.updateInstanceState({
cursor: { type: cursorType, rotation: selected.length === 1 ? selected[0].rotation : 0 },
})
}
override onEnter = (info: PointingResizeHandleInfo) => {

View file

@ -12,10 +12,12 @@ export class PointingRotateHandle extends StateNode {
private updateCursor() {
const { selectionRotation } = this.editor
this.editor.cursor = {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: selectionRotation,
}
this.editor.updateInstanceState({
cursor: {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: selectionRotation,
},
})
}
override onEnter = (info: PointingRotateHandleInfo) => {
@ -26,7 +28,7 @@ export class PointingRotateHandle extends StateNode {
override onExit = () => {
this.parent.currentToolIdMask = undefined
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
}
override onPointerMove = () => {

View file

@ -18,7 +18,7 @@ export class PointingSelection extends StateNode {
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
if (this.editor.inputs.isDragging) {
if (this.editor.isReadOnly) return
if (this.editor.instanceState.isReadOnly) return
this.parent.transition('translating', info)
}
}

View file

@ -26,7 +26,7 @@ export class PointingShape extends StateNode {
}
const isWithinSelection =
this.editor.isSelected(this.selectingShape.id) ||
this.editor.selectedIds.includes(this.selectingShape.id) ||
this.editor.isAncestorSelected(this.selectingShape.id)
const isBehindSelectionBounds =
@ -79,11 +79,12 @@ export class PointingShape extends StateNode {
// if the shape has an ancestor which is a focusable layer and it is not focused but it is selected
// then we should focus the layer and select the shape
const { selectedIds } = this.editor
const targetShape = this.editor.getOutermostSelectableShape(
this.eventTargetShape,
// if a group is selected, we want to stop before reaching that group
// so we can drill down into the group
(parent) => !this.editor.isSelected(parent.id)
(parent) => !selectedIds.includes(parent.id)
)
if (this.editor.selectedIds.includes(targetShape.id)) {
@ -120,7 +121,7 @@ export class PointingShape extends StateNode {
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
if (this.editor.inputs.isDragging) {
if (this.editor.isReadOnly) return
if (this.editor.instanceState.isReadOnly) return
this.parent.transition('translating', info)
}
}

View file

@ -56,7 +56,7 @@ export class Resizing extends StateNode {
this.creationCursorOffset = creationCursorOffset
if (info.isCreating) {
this.editor.cursor = { type: 'cross', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'cross', rotation: 0 } }, true)
}
this.snapshot = this._createSnapshot()
@ -103,7 +103,7 @@ export class Resizing extends StateNode {
this.handleResizeEnd()
if (this.editAfterComplete && this.editor.onlySelectedShape) {
this.editor.editingId = this.editor.onlySelectedShape.id
this.editor.setEditingId(this.editor.onlySelectedShape.id)
this.editor.setCurrentTool('select')
this.editor.root.current.value!.transition('editing_shape', {})
return
@ -208,7 +208,7 @@ export class Resizing extends StateNode {
.sub(this.creationCursorOffset)
const originPagePoint = this.editor.inputs.originPagePoint.clone().sub(cursorHandleOffset)
if (this.editor.isGridMode && !ctrlKey) {
if (this.editor.instanceState.isGridMode && !ctrlKey) {
const { gridSize } = this.editor.documentSettings
currentPagePoint.snapToGrid(gridSize)
}
@ -218,7 +218,7 @@ export class Resizing extends StateNode {
this.editor.snaps.clear()
const shouldSnap = this.editor.isSnapMode ? !ctrlKey : ctrlKey
const shouldSnap = this.editor.user.isSnapMode ? !ctrlKey : ctrlKey
if (shouldSnap && selectionRotation % TAU === 0) {
const { nudge } = this.editor.snaps.snapResize({
@ -320,7 +320,7 @@ export class Resizing extends StateNode {
isFlippedY: boolean
rotation: number
}) {
const nextCursor = { ...this.editor.cursor }
const nextCursor = { ...this.editor.instanceState.cursor }
switch (dragHandle) {
case 'top_left':
@ -343,12 +343,12 @@ export class Resizing extends StateNode {
nextCursor.rotation = rotation
this.editor.cursor = nextCursor
this.editor.updateInstanceState({ cursor: nextCursor })
}
override onExit = () => {
this.parent.currentToolIdMask = undefined
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
this.editor.snaps.clear()
}

View file

@ -40,7 +40,7 @@ export class Rotating extends StateNode {
}
override onExit = () => {
this.editor.cursor = { type: 'none', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'none', rotation: 0 } }, true)
this.parent.currentToolIdMask = undefined
this.snapshot = {} as TLRotationSnapshot
@ -85,10 +85,12 @@ export class Rotating extends StateNode {
})
// Update cursor
this.editor.cursor = {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
}
this.editor.updateInstanceState({
cursor: {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
},
})
}
private cancel = () => {
@ -127,10 +129,12 @@ export class Rotating extends StateNode {
})
// Update cursor
this.editor.cursor = {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
}
this.editor.updateInstanceState({
cursor: {
type: CursorTypeMap[this.info.handle as RotateCorner],
rotation: newSelectionRotation + this.snapshot.initialSelectionRotation,
},
})
}
_getRotationFromPointerPosition({ snapToNearestDegree }: { snapToNearestDegree: boolean }) {
@ -150,7 +154,7 @@ export class Rotating extends StateNode {
} else if (snapToNearestDegree) {
newSelectionRotation = Math.round(newSelectionRotation / EPSILON) * EPSILON
if (this.editor.isCoarsePointer) {
if (this.editor.instanceState.isCoarsePointer) {
const snappedToRightAngle = snapAngle(newSelectionRotation, 4)
const angleToRightAngle = angleDelta(newSelectionRotation, snappedToRightAngle)
if (Math.abs(angleToRightAngle) < degreesToRadians(5)) {

View file

@ -36,12 +36,12 @@ export class ScribbleBrushing extends StateNode {
this.updateBrushSelection()
requestAnimationFrame(() => {
this.editor.brush = null
this.editor.updateInstanceState({ brush: null })
})
}
override onExit = () => {
this.editor.erasingIds = []
this.editor.setErasingIds([])
this.scribble.stop()
}
@ -87,12 +87,12 @@ export class ScribbleBrushing extends StateNode {
}
private onScribbleUpdate = (scribble: TLScribble) => {
this.editor.scribble = scribble
this.editor.updateInstanceState({ scribble })
}
private onScribbleComplete = () => {
this.editor.off('tick', this.scribble.tick)
this.editor.scribble = null
this.editor.updateInstanceState({ scribble: null })
}
private updateBrushSelection() {

View file

@ -64,7 +64,7 @@ export class Translating extends StateNode {
this.selectionSnapshot = {} as any
this.snapshot = {} as any
this.editor.snaps.clear()
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'default', rotation: 0 } }, true)
this.dragAndDropManager.clear()
}
@ -146,7 +146,7 @@ export class Translating extends StateNode {
if (this.editAfterComplete) {
const onlySelected = this.editor.onlySelectedShape
if (onlySelected) {
this.editor.editingId = onlySelected.id
this.editor.setEditingId(onlySelected.id)
this.editor.setCurrentTool('select')
this.editor.root.current.value!.transition('editing_shape', {})
}
@ -169,7 +169,7 @@ export class Translating extends StateNode {
this.isCloning = false
this.info = info
this.editor.cursor = { type: 'move', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'move', rotation: 0 } }, true)
this.selectionSnapshot = getTranslatingSnapshot(this.editor)
// Don't clone on create; otherwise clone on altKey
@ -344,7 +344,7 @@ export function moveShapesToPoint({
}) {
const {
inputs,
isGridMode,
instanceState: { isGridMode },
documentSettings: { gridSize },
} = editor
@ -366,7 +366,7 @@ export function moveShapesToPoint({
editor.snaps.clear()
const shouldSnap =
(editor.isSnapMode ? !inputs.ctrlKey : inputs.ctrlKey) &&
(editor.user.isSnapMode ? !inputs.ctrlKey : inputs.ctrlKey) &&
editor.inputs.pointerVelocity.len() < 0.5 // ...and if the user is not dragging fast
if (shouldSnap) {

View file

@ -18,8 +18,10 @@ export class ZoomTool extends StateNode {
override onExit = () => {
this.currentToolIdMask = undefined
this.editor.zoomBrush = null
this.editor.cursor = { type: 'default', rotation: 0 }
this.editor.updateInstanceState(
{ zoomBrush: null, cursor: { type: 'default', rotation: 0 } },
true
)
this.currentToolIdMask = undefined
}
@ -50,9 +52,9 @@ export class ZoomTool extends StateNode {
private updateCursor() {
if (this.editor.inputs.altKey) {
this.editor.cursor = { type: 'zoom-out', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'zoom-out', rotation: 0 } }, true)
} else {
this.editor.cursor = { type: 'zoom-in', rotation: 0 }
this.editor.updateInstanceState({ cursor: { type: 'zoom-in', rotation: 0 } }, true)
}
}
}

View file

@ -13,7 +13,7 @@ export class ZoomBrushing extends StateNode {
}
override onExit = () => {
this.editor.zoomBrush = null
this.editor.updateInstanceState({ zoomBrush: null })
}
override onPointerMove = () => {
@ -34,7 +34,7 @@ export class ZoomBrushing extends StateNode {
} = this.editor
this.zoomBrush.setTo(Box2d.FromPoints([originPagePoint, currentPagePoint]))
this.editor.zoomBrush = this.zoomBrush.toJson()
this.editor.updateInstanceState({ zoomBrush: this.zoomBrush.toJson() })
}
private cancel() {

View file

@ -119,7 +119,7 @@ const TldrawUiContent = React.memo(function TldrawUI({
const editor = useEditor()
const msg = useTranslation()
const breakpoint = useBreakpoint()
const isReadonlyMode = useValue('isReadOnlyMode', () => editor.isReadOnly, [editor])
const isReadonlyMode = useValue('isReadOnlyMode', () => editor.instanceState.isReadOnly, [editor])
const isFocusMode = useValue('focus', () => editor.instanceState.isFocusMode, [editor])
const isDebugMode = useValue('debug', () => editor.instanceState.isDebugMode, [editor])

View file

@ -3,7 +3,7 @@ import { useContainer } from '@tldraw/editor'
import { memo } from 'react'
import { TLUiMenuChild } from '../hooks/menuHelpers'
import { useActionsMenuSchema } from '../hooks/useActionsMenuSchema'
import { useReadonly } from '../hooks/useReadonly'
import { useReadOnly } from '../hooks/useReadOnly'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { Button } from './primitives/Button'
import { Popover, PopoverTrigger } from './primitives/Popover'
@ -13,7 +13,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
const msg = useTranslation()
const container = useContainer()
const menuSchema = useActionsMenuSchema()
const isReadonly = useReadonly()
const isReadonly = useReadOnly()
function getActionMenuItem(item: TLUiMenuChild) {
if (isReadonly && !item.readonlyOk) return null

View file

@ -6,7 +6,7 @@ import { TLUiMenuChild } from '../hooks/menuHelpers'
import { useBreakpoint } from '../hooks/useBreakpoint'
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
import { useReadonly } from '../hooks/useReadonly'
import { useReadOnly } from '../hooks/useReadOnly'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { MoveToPageMenu } from './MoveToPageMenu'
import { Button } from './primitives/Button'
@ -34,7 +34,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
}
} else {
// Weird route: selecting locked shapes on long press
if (editor.isCoarsePointer) {
if (editor.instanceState.isCoarsePointer) {
const {
selectedShapes,
inputs: { currentPagePoint },
@ -66,7 +66,7 @@ export const ContextMenu = function ContextMenu({ children }: { children: any })
const [_, handleOpenChange] = useMenuIsOpen('context menu', cb)
// If every item in the menu is readonly, then we don't want to show the menu
const isReadonly = useReadonly()
const isReadonly = useReadOnly()
const noItemsToShow =
contextTLUiMenuSchema.length === 0 ||
@ -98,7 +98,7 @@ function ContextMenuContent() {
const menuSchema = useContextMenuSchema()
const [_, handleSubOpenChange] = useMenuIsOpen('context menu sub')
const isReadonly = useReadonly()
const isReadonly = useReadOnly()
const breakpoint = useBreakpoint()
const container = useContainer()

Some files were not shown because too many files have changed in this diff Show more