diff --git a/apps/examples/src/examples/tool-with-child-states/README.md b/apps/examples/src/examples/tool-with-child-states/README.md new file mode 100644 index 000000000..0025ca810 --- /dev/null +++ b/apps/examples/src/examples/tool-with-child-states/README.md @@ -0,0 +1,12 @@ +--- +title: Tool with child states +component: ./ToolWithChildStatesExample.tsx +category: shapes/tools +priority: 2 +--- + +You can implement more complex behaviour in a custom tool by using child states + +--- + +Tools are nodes in tldraw's state machine. They are responsible for handling user input. You can create custom tools by extending the StateNode class and overriding its methods. In this example we expand on the sticker tool from the custom tool example to show how to create a tool that can handle more complex interactions by using child states. diff --git a/apps/examples/src/examples/tool-with-child-states/ToolWithChildStatesExample.tsx b/apps/examples/src/examples/tool-with-child-states/ToolWithChildStatesExample.tsx new file mode 100644 index 000000000..ab57f7e38 --- /dev/null +++ b/apps/examples/src/examples/tool-with-child-states/ToolWithChildStatesExample.tsx @@ -0,0 +1,258 @@ +import { + StateNode, + TLEventHandlers, + TLShapePartial, + TLTextShape, + Tldraw, + createShapeId, +} from 'tldraw' +import 'tldraw/tldraw.css' + +// There's a guide at the bottom of this file! + +const OFFSET = -12 + +// [1] +class StickerTool extends StateNode { + static override id = 'sticker' + static override initial = 'idle' + static override children = () => [Idle, Pointing, Dragging] +} + +// [2] +class Idle extends StateNode { + static override id = 'idle' + //[a] + override onEnter = () => { + this.editor.setCursor({ type: 'cross' }) + } + //[b] + override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => { + const { editor } = this + switch (info.target) { + case 'canvas': { + const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint) + if (hitShape) { + this.onPointerDown({ + ...info, + shape: hitShape, + target: 'shape', + }) + return + } + this.parent.transition('pointing', { shape: null }) + break + } + case 'shape': { + if (editor.inputs.shiftKey) { + editor.updateShape({ + id: info.shape.id, + type: 'text', + props: { text: '👻 boo!' }, + }) + } else { + this.parent.transition('pointing', { shape: info.shape }) + } + break + } + } + } + //[c] + override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => { + const { editor } = this + if (info.phase !== 'up') return + switch (info.target) { + case 'canvas': { + const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint) + + if (hitShape) { + this.onDoubleClick({ + ...info, + shape: hitShape, + target: 'shape', + }) + return + } + const { currentPagePoint } = editor.inputs + editor.createShape({ + type: 'text', + x: currentPagePoint.x + OFFSET, + y: currentPagePoint.y + OFFSET, + props: { text: '❤️' }, + }) + break + } + case 'shape': { + editor.deleteShapes([info.shape.id]) + break + } + } + } +} +// [3] +class Pointing extends StateNode { + static override id = 'pointing' + private shape: TLTextShape | null = null + + override onEnter = (info: { shape: TLTextShape | null }) => { + this.shape = info.shape + } + override onPointerUp: TLEventHandlers['onPointerUp'] = () => { + this.parent.transition('idle') + } + + override onPointerMove: TLEventHandlers['onPointerMove'] = () => { + if (this.editor.inputs.isDragging) { + this.parent.transition('dragging', { shape: this.shape }) + } + } +} + +// [4] +class Dragging extends StateNode { + static override id = 'dragging' + // [a] + private shape: TLShapePartial | null = null + private emojiArray = ['❤️', '🔥', '👍', '👎', '😭', '🤣'] + + // [b] + override onEnter = (info: { shape: TLShapePartial }) => { + const { currentPagePoint } = this.editor.inputs + const newShape = { + id: createShapeId(), + type: 'text', + x: currentPagePoint.x + OFFSET, + y: currentPagePoint.y + OFFSET, + props: { text: '❤️' }, + } + if (info.shape) { + this.shape = info.shape + } else { + this.editor.createShape(newShape) + this.shape = { ...newShape } + } + } + //[c] + override onPointerUp: TLEventHandlers['onPointerUp'] = () => { + this.parent.transition('idle') + } + //[d] + + override onPointerMove: TLEventHandlers['onPointerUp'] = () => { + const { shape } = this + const { originPagePoint, currentPagePoint } = this.editor.inputs + const distance = originPagePoint.dist(currentPagePoint) + if (shape) { + this.editor.updateShape({ + id: shape.id, + type: 'text', + props: { + text: this.emojiArray[Math.floor(distance / 20) % this.emojiArray.length], + }, + }) + } + } +} + +// [5] +const customTools = [StickerTool] +export default function ToolWithChildStatesExample() { + return ( +