tool-with-ui
Steve Ruiz 2024-05-12 13:01:09 +01:00
rodzic 91903c9761
commit 119e812612
9 zmienionych plików z 512 dodań i 237 usunięć

Wyświetl plik

@ -0,0 +1,53 @@
import {
ContextMenu,
DefaultContextMenuContent,
ErrorScreen,
LoadingScreen,
TLToolUtilConstructor,
TldrawEditor,
TldrawUi,
defaultBindingUtils,
defaultEditorAssetUrls,
defaultShapeUtils,
usePreloadAssets,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { SimpleSelectToolUtil } from './SimpleTool'
const tools: TLToolUtilConstructor<any, any>[] = [SimpleSelectToolUtil]
//[2]
export default function NewToolExample() {
const assetLoading = usePreloadAssets(defaultEditorAssetUrls)
if (assetLoading.error) {
return <ErrorScreen>Could not load assets.</ErrorScreen>
}
if (!assetLoading.done) {
return <LoadingScreen>Loading assets...</LoadingScreen>
}
return (
<div className="tldraw__editor">
<TldrawEditor
initialState="@simple/select"
shapeUtils={defaultShapeUtils}
bindingUtils={defaultBindingUtils}
tools={tools}
onMount={(e) => {
e.createShapes([
{ type: 'geo', x: 200, y: 200 },
{ type: 'geo', x: 400, y: 400 },
])
}}
>
<TldrawUi>
<ContextMenu>
<DefaultContextMenuContent />
</ContextMenu>
</TldrawUi>
</TldrawEditor>
</div>
)
}

Wyświetl plik

@ -0,0 +1,11 @@
---
title: New tool
component: ./NewToolExample.tsx
category: editor-api
---
...
---
...

Wyświetl plik

@ -0,0 +1,271 @@
import { useRef } from 'react'
import {
Box,
BoxLike,
DefaultColorStyle,
DefaultDashStyle,
DefaultFillStyle,
DefaultSizeStyle,
SVGContainer,
SharedStyleMap,
TLEventInfo,
TLShapeId,
TLToolState,
ToolUtil,
dedupe,
useEditor,
useEditorComponents,
useValue,
} from 'tldraw'
interface SimpleSelectContext extends TLToolState {
readonly type: '@simple/select'
ids: TLShapeId[]
brush: BoxLike | null
state: 'idle' | 'pointing' | 'dragging'
}
const simpleSelectStyles = new SharedStyleMap()
simpleSelectStyles.applyValue(DefaultColorStyle, DefaultColorStyle.defaultValue)
simpleSelectStyles.applyValue(DefaultFillStyle, DefaultFillStyle.defaultValue)
simpleSelectStyles.applyValue(DefaultDashStyle, DefaultDashStyle.defaultValue)
simpleSelectStyles.applyValue(DefaultSizeStyle, DefaultSizeStyle.defaultValue)
export class SimpleSelectToolUtil extends ToolUtil<SimpleSelectContext> {
static override type = '@simple/select' as const
getDefaultContext(): SimpleSelectContext {
return {
type: '@simple/select',
ids: [],
brush: null,
state: 'idle',
}
}
underlay() {
return null
}
overlay() {
const { brush } = this.getContext()
return (
<>
<ShapeIndicators />
<HintedShapeIndicator />
<SelectionBrush brush={brush} />
</>
)
}
getStyles() {
return simpleSelectStyles
}
// This object is used for events, it's kept in memory and updated as the user interacts with the tool
private memo = {
initialSelectedIds: [] as TLShapeId[],
}
onEvent(event: TLEventInfo) {
const { editor, memo } = this
const context = this.getContext()
switch (context.state) {
case 'idle': {
if (event.name === 'pointer_down') {
this.setContext({
state: 'pointing',
})
}
break
}
case 'pointing': {
if (editor.inputs.isDragging) {
const { originPagePoint, currentPagePoint } = editor.inputs
const box = Box.FromPoints([originPagePoint, currentPagePoint])
this.setContext({ state: 'dragging', brush: box.toJson() })
// Stash the selected ids so we can restore them later
memo.initialSelectedIds = editor.getSelectedShapeIds()
}
break
}
case 'dragging': {
if (editor.inputs.isDragging) {
if (
event.name === 'pointer_move' ||
// for modifiers
event.name === 'key_down' ||
event.name === 'key_up'
) {
const { originPagePoint, currentPagePoint } = editor.inputs
const box = Box.FromPoints([originPagePoint, currentPagePoint])
// update the box in the context
this.setContext({ brush: box.toJson() })
const hitIds = new Set<TLShapeId>()
// If we're holding shift, add the initial selected ids to the hitIds set
if (editor.inputs.shiftKey) {
for (const id of memo.initialSelectedIds) {
hitIds.add(id)
}
}
// Test the rest of the shapes on the page (broad phase only for simplifity)
for (const shape of editor.getCurrentPageShapes()) {
if (hitIds.has(shape.id)) continue
const pageBounds = editor.getShapePageBounds(shape.id)
if (!pageBounds) continue
if (box.collides(pageBounds)) {
hitIds.add(shape.id)
}
}
// If the selected ids have changed, update the selection
const currentSelectedIds = editor.getSelectedShapeIds()
if (
currentSelectedIds.length !== hitIds.size ||
currentSelectedIds.some((id) => !hitIds.has(id))
) {
editor.setSelectedShapes(Array.from(hitIds))
}
}
} else {
this.setContext({ state: 'idle', brush: null })
}
break
}
}
}
}
/*
Times when states talk to each-other
- reading / writing to selection
- reading / writing to styles
- switching to other tools
Which pieces of state are "universal" vs which are "tool-specific"?
For example, the selection brush is only relevant to the select tool but the selected shape ids are relevant to all tools.
Therefore the selection brush should be stored in the select tool's context but the selected shape ids should be stored in the editor's context.
The erasing / cropping / hinted shapes are probably relevant only to their tools, and should be extracted from the default canvas into their tool's overlay / underlay.
*/
function SelectionBrush({ brush }: { brush: BoxLike | null }) {
if (!brush) return null
return (
<SVGContainer>
<rect
className="tl-brush tl-brush__default"
x={brush.x}
y={brush.y}
width={brush.w}
height={brush.h}
/>
</SVGContainer>
)
}
// function SelectedShapeIds({ shapeIds }: { shapeIds: TLShapeId[] }) {
// if (shapeIds.length === 0) return null
// const { ShapeIndicator } = useEditorComponents()
// if (!ShapeIndicator) return null
// return (
// <>
// {renderingShapes.map(({ id }) => (
// <ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
// ))}
// </>
// )
// return null
// }
function ShapeIndicators() {
const editor = useEditor()
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
const idsToDisplay = useValue(
'should display selected ids',
() => {
// todo: move to tldraw selected ids wrappe
const prev = rPreviousSelectedShapeIds.current
const next = new Set<TLShapeId>()
if (
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.editing_shape',
'select.pointing_shape',
'select.pointing_selection',
'select.pointing_handle'
) &&
!editor.getInstanceState().isChangingStyle
) {
const selected = editor.getSelectedShapeIds()
for (const id of selected) {
next.add(id)
}
if (editor.isInAny('select.idle', 'select.editing_shape')) {
const instanceState = editor.getInstanceState()
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
const hovered = editor.getHoveredShapeId()
if (hovered) next.add(hovered)
}
}
}
if (prev.size !== next.size) {
rPreviousSelectedShapeIds.current = next
return next
}
for (const id of next) {
if (!prev.has(id)) {
rPreviousSelectedShapeIds.current = next
return next
}
}
return prev
},
[editor]
)
const { ShapeIndicator } = useEditorComponents()
if (!ShapeIndicator) return null
return (
<>
{renderingShapes.map(({ id }) => (
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
))}
</>
)
}
function HintedShapeIndicator() {
const editor = useEditor()
const { ShapeIndicator } = useEditorComponents()
const ids = useValue('hinting shape ids', () => dedupe(editor.getHintingShapeIds()), [editor])
if (!ids.length) return null
if (!ShapeIndicator) return null
return (
<>
{ids.map((id) => (
<ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
))}
</>
)
}

Wyświetl plik

@ -186,6 +186,7 @@ export { GroupShapeUtil } from './lib/editor/shapes/group/GroupShapeUtil'
export { resizeBox, type ResizeBoxOptions } from './lib/editor/shapes/shared/resizeBox'
export { BaseBoxShapeTool } from './lib/editor/tools/BaseBoxShapeTool/BaseBoxShapeTool'
export { StateNode, type TLStateNodeConstructor } from './lib/editor/tools/StateNode'
export { ToolUtil, type TLToolState, type TLToolUtilConstructor } from './lib/editor/tools/ToolUtil'
export {
useSvgExportContext,
type SvgExportContext,

Wyświetl plik

@ -18,7 +18,7 @@ import { TLUser, createTLUser } from './config/createTLUser'
import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
import { Editor } from './editor/Editor'
import { TLStateNodeConstructor } from './editor/tools/StateNode'
import { TLToolUtilConstructor } from './editor/tools/ToolUtil'
import { TLCameraOptions } from './editor/types/misc-types'
import { ContainerProvider, useContainer } from './hooks/useContainer'
import { useCursor } from './hooks/useCursor'
@ -85,7 +85,7 @@ export interface TldrawEditorBaseProps {
/**
* An array of tools to add to the editor's state chart.
*/
tools?: readonly TLStateNodeConstructor[]
tools?: readonly TLToolUtilConstructor<any, any>[]
/**
* Whether to automatically focus the editor when it mounts.

Wyświetl plik

@ -1,6 +1,6 @@
import { react, useQuickReactor, useValue } from '@tldraw/state'
import { react, useQuickReactor, useStateTracking, useValue } from '@tldraw/state'
import { TLHandle, TLShapeId } from '@tldraw/tlschema'
import { dedupe, modulate, objectMapValues } from '@tldraw/utils'
import { modulate, objectMapValues } from '@tldraw/utils'
import classNames from 'classnames'
import { Fragment, JSX, useEffect, useRef, useState } from 'react'
import { COARSE_HANDLE_RADIUS, HANDLE_RADIUS, TEXT_SHADOW_LOD } from '../../constants'
@ -152,6 +152,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
)}
<GridWrapper />
<div ref={rHtmlLayer} className="tl-html-layer tl-shapes" draggable={false}>
<ActiveToolUnderlay />
<OnTheCanvasWrapper />
<SelectionBackgroundWrapper />
{hideShapes ? null : debugSvg ? <ShapesWithSVGs /> : <ShapesToDisplay />}
@ -159,12 +160,10 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
<div className="tl-overlays">
<div ref={rHtmlLayer2} className="tl-html-layer">
{debugGeometry ? <GeometryDebuggingView /> : null}
<ActiveToolOverlay />
<HandlesWrapper />
<BrushWrapper />
<ScribbleWrapper />
<ZoomBrushWrapper />
<ShapeIndicators />
<HintedShapeIndicator />
<SnapIndicatorWrapper />
<SelectionForegroundWrapper />
<LiveCollaborators />
@ -175,6 +174,18 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
)
}
function ActiveToolUnderlay() {
const editor = useEditor()
const activeTool = useValue('active tool', () => editor.getCurrentTool(), [editor])
return useStateTracking('Active tool underlay', () => activeTool.underlay?.())
}
function ActiveToolOverlay() {
const editor = useEditor()
const activeTool = useValue('active tool', () => editor.getCurrentTool(), [editor])
return useStateTracking('Active tool overlay', () => activeTool.overlay?.())
}
function GridWrapper() {
const editor = useEditor()
const gridSize = useValue('gridSize', () => editor.getDocumentSettings().gridSize, [editor])
@ -209,16 +220,6 @@ function ScribbleWrapper() {
)
}
function BrushWrapper() {
const editor = useEditor()
const brush = useValue('brush', () => editor.getInstanceState().brush, [editor])
const { Brush } = useEditorComponents()
if (!(Brush && brush)) return null
return <Brush className="tl-user-brush" brush={brush} />
}
function ZoomBrushWrapper() {
const editor = useEditor()
const zoomBrush = useValue('zoomBrush', () => editor.getInstanceState().zoomBrush, [editor])
@ -441,88 +442,6 @@ function ShapesToDisplay() {
)
}
function ShapeIndicators() {
const editor = useEditor()
const renderingShapes = useValue('rendering shapes', () => editor.getRenderingShapes(), [editor])
const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
const idsToDisplay = useValue(
'should display selected ids',
() => {
// todo: move to tldraw selected ids wrappe
const prev = rPreviousSelectedShapeIds.current
const next = new Set<TLShapeId>()
if (
editor.isInAny(
'select.idle',
'select.brushing',
'select.scribble_brushing',
'select.editing_shape',
'select.pointing_shape',
'select.pointing_selection',
'select.pointing_handle'
) &&
!editor.getInstanceState().isChangingStyle
) {
const selected = editor.getSelectedShapeIds()
for (const id of selected) {
next.add(id)
}
if (editor.isInAny('select.idle', 'select.editing_shape')) {
const instanceState = editor.getInstanceState()
if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
const hovered = editor.getHoveredShapeId()
if (hovered) next.add(hovered)
}
}
}
if (prev.size !== next.size) {
rPreviousSelectedShapeIds.current = next
return next
}
for (const id of next) {
if (!prev.has(id)) {
rPreviousSelectedShapeIds.current = next
return next
}
}
return prev
},
[editor]
)
const { ShapeIndicator } = useEditorComponents()
if (!ShapeIndicator) return null
return (
<>
{renderingShapes.map(({ id }) => (
<ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
))}
</>
)
}
function HintedShapeIndicator() {
const editor = useEditor()
const { ShapeIndicator } = useEditorComponents()
const ids = useValue('hinting shape ids', () => dedupe(editor.getHintingShapeIds()), [editor])
if (!ids.length) return null
if (!ShapeIndicator) return null
return (
<>
{ids.map((id) => (
<ShapeIndicator className="tl-user-indicator__hint" shapeId={id} key={id + '_hinting'} />
))}
</>
)
}
function CursorDef() {
return (
<g id="cursor">

Wyświetl plik

@ -66,7 +66,6 @@ import {
getIndicesAbove,
getIndicesBetween,
getOwnProperty,
hasOwnProperty,
last,
sortById,
sortByIndex,
@ -133,8 +132,7 @@ import { TextManager } from './managers/TextManager'
import { TickManager } from './managers/TickManager'
import { UserPreferencesManager } from './managers/UserPreferencesManager'
import { ShapeUtil, TLResizeMode, TLShapeUtilConstructor } from './shapes/ShapeUtil'
import { RootState } from './tools/RootState'
import { StateNode, TLStateNodeConstructor } from './tools/StateNode'
import { TLToolUtilConstructor, ToolUtil } from './tools/ToolUtil'
import { TLContent } from './types/clipboard-types'
import { TLEventMap } from './types/emit-types'
import {
@ -177,14 +175,14 @@ export interface TLEditorOptions {
* An array of shapes to use in the editor. These will be used to create and manage shapes in the editor.
*/
shapeUtils: readonly TLShapeUtilConstructor<TLUnknownShape>[]
/**
* An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.
*/
tools: readonly TLToolUtilConstructor<any>[]
/**
* An array of bindings to use in the editor. These will be used to create and manage bindings in the editor.
*/
bindingUtils: readonly TLBindingUtilConstructor<TLUnknownBinding>[]
/**
* An array of tools to use in the editor. These will be used to handle events and manage user interactions in the editor.
*/
tools: readonly TLStateNodeConstructor[]
/**
* Should return a containing html element which has all the styles applied to the editor. If not
* given, the body element will be used.
@ -213,12 +211,12 @@ export class Editor extends EventEmitter<TLEventMap> {
constructor({
store,
user,
shapeUtils,
bindingUtils,
tools,
shapeUtils = [],
bindingUtils = [],
tools = [],
getContainer,
cameraOptions,
initialState,
initialState: initialTool,
inferDarkMode,
}: TLEditorOptions) {
super()
@ -232,23 +230,17 @@ export class Editor extends EventEmitter<TLEventMap> {
},
})
this.snaps = new SnapManager(this)
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
this.getContainer = getContainer ?? (() => document.body)
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
this.snaps = new SnapManager(this)
this.textMeasure = new TextManager(this)
this._tickManager = new TickManager(this)
this.environment = new EnvironmentManager(this)
this.scribbles = new ScribbleManager(this)
this.performanceTracker = new PerformanceTracker()
class NewRoot extends RootState {
static override initial = initialState ?? ''
}
this.root = new NewRoot(this)
this.root.children = {}
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
const allShapeUtils = checkShapesAndAddCore(shapeUtils)
@ -285,18 +277,18 @@ export class Editor extends EventEmitter<TLEventMap> {
}
this.bindingUtils = _bindingUtils
// Tools.
// Accept tools from constructor parameters which may not conflict with the root note's default or
// "baked in" tools, select and zoom.
const toolMap: Record<string, ToolUtil<any>> = {}
for (const Tool of [...tools]) {
if (hasOwnProperty(this.root.children!, Tool.id)) {
throw Error(`Can't override tool with id "${Tool.id}"`)
}
this.root.children![Tool.id] = new Tool(this, this.root)
const tool = new Tool(this)
toolMap[Tool.type] = tool
tool.setContext(tool.getDefaultContext())
}
this.environment = new EnvironmentManager(this)
this.scribbles = new ScribbleManager(this)
if (!initialTool) throw Error('No initial tool provided')
this._tools.set(toolMap)
this._currentToolId.set(initialTool)
// Cleanup
@ -626,12 +618,6 @@ export class Editor extends EventEmitter<TLEventMap> {
})
})
if (initialState && this.root.children[initialState] === undefined) {
throw Error(`No state found for initialState "${initialState}".`)
}
this.root.enter(undefined, 'initial')
if (this.getInstanceState().followingUserId) {
this.stopFollowingUser()
}
@ -641,8 +627,6 @@ export class Editor extends EventEmitter<TLEventMap> {
requestAnimationFrame(() => {
this._tickManager.start()
})
this.performanceTracker = new PerformanceTracker()
}
/**
@ -652,13 +636,6 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
readonly store: TLStore
/**
* The root state of the statechart.
*
* @public
*/
readonly root: RootState
/**
* A set of functions to call when the app is disposed.
*
@ -971,7 +948,7 @@ export class Editor extends EventEmitter<TLEventMap> {
willCrashApp,
},
extras: {
activeStateNode: this.root.getPath(),
activeStateNode: this.getCurrentToolId(),
selectedShapes: this.getSelectedShapes(),
editingShape: editingShapeId ? this.getShape(editingShapeId) : undefined,
inputs: this.inputs,
@ -1025,7 +1002,7 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
@computed getPath() {
return this.root.getPath().split('root.')[1]
return 'root'
}
/**
@ -1042,18 +1019,6 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
isIn(path: string): boolean {
const ids = path.split('.').reverse()
let state = this.root as StateNode
while (ids.length > 0) {
const id = ids.pop()
if (!id) return true
const current = state.getCurrent()
if (current?.id === id) {
if (ids.length === 0) return true
state = current
continue
} else return false
}
return false
}
@ -1087,55 +1052,29 @@ export class Editor extends EventEmitter<TLEventMap> {
* @public
*/
setCurrentTool(id: string, info = {}): this {
this.root.transition(id, info)
this._currentToolId.set(id)
return this
}
/**
* The current selected tool.
*
* @public
*/
@computed getCurrentTool(): StateNode {
return this.root.getCurrent()!
}
// /**
// * The current selected tool.
// *
// * @public
// */
// @computed getCurrentTool(): StateNode {
// return this.root.getCurrent()!
// }
/**
* The id of the current selected tool.
*
* @public
*/
@computed getCurrentToolId(): string {
const currentTool = this.getCurrentTool()
if (!currentTool) return ''
return currentTool.getCurrentToolIdMask() ?? currentTool.id
}
/**
* Get a descendant by its path.
*
* @example
* ```ts
* state.getStateDescendant('select')
* state.getStateDescendant('select.brushing')
* ```
*
* @param path - The descendant's path of state ids, separated by periods.
*
* @public
*/
getStateDescendant<T extends StateNode>(path: string): T | undefined {
const ids = path.split('.').reverse()
let state = this.root as StateNode
while (ids.length > 0) {
const id = ids.pop()
if (!id) return state as T
const childState = state.children?.[id]
if (!childState) return undefined
state = childState
}
return state as T
}
// /**
// * The id of the current selected tool.
// *
// * @public
// */
// @computed getCurrentToolId(): string {
// const currentTool = this.getCurrentTool()
// if (!currentTool) return ''
// return currentTool.getCurrentToolIdMask() ?? currentTool.id
// }
/* ---------------- Document Settings --------------- */
@ -1962,6 +1901,24 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
/* ---------------------- Tool ---------------------- */
_tools = atom<Record<string, ToolUtil<any>>>('tools', {})
private _currentToolId = atom<string>('current tool id', '')
@computed getCurrentToolId() {
return this._currentToolId.get()
}
@computed getCurrentTool() {
return this._tools.get()[this.getCurrentToolId()]
}
getTool<T extends ToolUtil<any>>(id: string): T {
return this._tools.get()[id] as T
}
/* --------------------- Camera --------------------- */
/** @internal */
@ -7081,17 +7038,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// If the current tool is associated with a shape, return the styles for that shape.
// Otherwise, just return an empty map.
const currentTool = this.root.getCurrent()!
const styles = new SharedStyleMap()
if (!currentTool) return styles
if (currentTool.shapeType) {
for (const style of this.styleProps[currentTool.shapeType].keys()) {
styles.applyValue(style, this.getStyleForNextShape(style))
}
}
return styles
}
@ -8157,7 +8104,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
}
if (elapsed > 0) {
this.root.handleEvent({ type: 'misc', name: 'tick', elapsed })
this.getCurrentTool().onEvent({ type: 'misc', name: 'tick', elapsed })
}
this.scribbles.tick(elapsed)
})
@ -8182,7 +8129,7 @@ export class Editor extends EventEmitter<TLEventMap> {
}
}
this.root.handleEvent(info)
this.getCurrentTool().onEvent(info)
return
}
@ -8592,9 +8539,9 @@ export class Editor extends EventEmitter<TLEventMap> {
// changed then hand both events to the statechart
const clickInfo = this._clickManager.handlePointerEvent(info)
if (info.name !== clickInfo.name) {
this.root.handleEvent(info)
this.getCurrentTool().onEvent(info)
this.emit('event', info)
this.root.handleEvent(clickInfo)
this.getCurrentTool().onEvent(clickInfo)
this.emit('event', clickInfo)
return
}
@ -8603,7 +8550,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// Send the event to the statechart. It will be handled by all
// active states, starting at the root.
this.root.handleEvent(info)
this.getCurrentTool().onEvent(info)
this.emit('event', info)
return this

Wyświetl plik

@ -0,0 +1,62 @@
import { atom, computed } from '@tldraw/state'
import { ReactNode } from 'react'
import { ReadonlySharedStyleMap } from '../../utils/SharedStylesMap'
import { Editor } from '../Editor'
import { TLEventInfo } from '../types/event-types'
// When the tool is loaded, it adds its state to the editor's state
export interface TLToolState {
type: string
}
export abstract class ToolUtil<T extends TLToolState> {
constructor(public editor: Editor) {}
static type: TLToolState['type']
// does the default state need to differentiate between private state and sycned state, or is that a detail for the sync stuff?
abstract getDefaultContext(): T
private _context = atom<T>('tool context', {} as T)
@computed getContext() {
return this._context.get()
}
setContext(context: Partial<T>) {
this._context.set({ ...this._context.__unsafe__getWithoutCapture(), ...context })
}
/**
* Get the styles (if any) that are relevant to this tool, and which should be displayed when the tool is active.
*/
abstract getStyles(): ReadonlySharedStyleMap | null
/**
* A react component to be displayed when the tool is active behind the shapes.
*
* @param shape - The shape.
* @public
*/
abstract underlay(): ReactNode
/**
* A react component to be displayed when the tool is active in front of the shapes.
*
* @param shape - The shape.
* @public
*/
abstract overlay(): ReactNode
/**
* Handle an event.
*/
abstract onEvent(event: TLEventInfo): void
}
/** @public */
export interface TLToolUtilConstructor<T extends TLToolState, U extends ToolUtil<T> = ToolUtil<T>> {
new (editor: Editor): U
type: T['type']
}

Wyświetl plik

@ -18,31 +18,42 @@ const selectToolStyles: readonly StyleProp<any>[] = Object.freeze([
])
/** @public */
export function useRelevantStyles(stylesToCheck = selectToolStyles): ReadonlySharedStyleMap | null {
export function useRelevantStyles(
stylesToCheck: StyleProp<any>[] = []
): ReadonlySharedStyleMap | null {
const editor = useEditor()
return useValue(
'getRelevantStyles',
() => {
const styles = new SharedStyleMap(editor.getSharedStyles())
const isInShapeSpecificTool = !!editor.root.getCurrent()?.shapeType
const hasShapesSelected = editor.isIn('select') && editor.getSelectedShapeIds().length > 0
const currentTool = editor.getCurrentTool()
const currentToolStyles = currentTool.getStyles()
if (styles.size === 0 && editor.isIn('select') && editor.getSelectedShapeIds().length === 0) {
for (const style of stylesToCheck) {
styles.applyValue(style, editor.getStyleForNextShape(style))
}
console.log(stylesToCheck)
if (!currentToolStyles) {
return new SharedStyleMap(editor.getSharedStyles())
}
if (isInShapeSpecificTool || hasShapesSelected || styles.size > 0) {
// If size is 0 we may still want to return an empty styles map to allow
// the opacity slider to show up.
// This can happen in two situations:
// 1. When the user is in the select tool and has multiple shapes selected but they have no supported styles (beyond opacity).
// 2. When the user is in a shape-specific tool and the shape has no supported styles (beyond opacity obvs).
return styles
}
return currentToolStyles
return null
// const isInShapeSpecificTool = false
// const hasShapesSelected = editor.isIn('select') && editor.getSelectedShapeIds().length > 0
// if (styles.size === 0 && editor.isIn('select') && editor.getSelectedShapeIds().length === 0) {
// for (const style of stylesToCheck) {
// styles.applyValue(style, editor.getStyleForNextShape(style))
// }
// }
// if (isInShapeSpecificTool || hasShapesSelected || styles.size > 0) {
// // If size is 0 we may still want to return an empty styles map to allow
// // the opacity slider to show up.
// // This can happen in two situations:
// // 1. When the user is in the select tool and has multiple shapes selected but they have no supported styles (beyond opacity).
// // 2. When the user is in a shape-specific tool and the shape has no supported styles (beyond opacity obvs).
// return styles
// }
// return null
},
[editor]
)