import { TLDrawShape, Vec, createShapeId } from '@tldraw/editor' import { TestEditor } from '../TestEditor' let editor: TestEditor beforeEach(() => { editor = new TestEditor() }) it('ignores touch events while in pen mode', async () => { editor.setCurrentTool('draw') editor.updateInstanceState({ isPenMode: true }) editor.dispatch({ type: 'pointer', name: 'pointer_down', isPen: false, pointerId: 1, point: new Vec(100, 100), shiftKey: false, altKey: false, ctrlKey: false, metaKey: false, accelKey: false, button: 1, target: 'canvas', }) expect(editor.getCurrentPageShapes().length).toBe(0) }) describe('auto-entering pen mode', () => { it('auto-enables pen mode for direct-display pen input', () => { expect(editor.getInstanceState().isPenMode).toBe(false) editor.pointerDown(100, 100, { isPen: true, isPenDirect: true }) expect(editor.getInstanceState().isPenMode).toBe(true) }) it('does not auto-enable pen mode for indirect pen input', () => { expect(editor.getInstanceState().isPenMode).toBe(false) // An indirect desktop tablet stylus (e.g. a Wacom Intuos) still reports as a pen, but should // not flip the editor into pen mode. editor.pointerDown(100, 100, { isPen: true, isPenDirect: false }) expect(editor.getInstanceState().isPenMode).toBe(false) }) it('still draws with indirect pen input without entering pen mode', () => { editor.setCurrentTool('draw') editor.pointerDown(100, 100, { isPen: true, isPenDirect: false }) editor.pointerMove(110, 110, { isPen: true, isPenDirect: false }) editor.pointerMove(120, 120, { isPen: true, isPenDirect: false }) editor.pointerUp(120, 120, { isPen: true, isPenDirect: false }) expect(editor.getInstanceState().isPenMode).toBe(false) expect(editor.getCurrentPageShapes().length).toBe(1) }) it('ignores non-pen input once pen mode has been enabled', () => { editor.setCurrentTool('draw') // A direct-display pen enables pen mode. editor.pointerDown(300, 300, { isPen: true, isPenDirect: true }) editor.pointerUp(300, 300, { isPen: true, isPenDirect: true }) expect(editor.getInstanceState().isPenMode).toBe(true) const shapeCount = editor.getCurrentPageShapes().length // A subsequent touch (non-pen) is ignored while in pen mode. editor.pointerDown(100, 100, { isPen: false }) editor.pointerMove(120, 120, { isPen: false }) editor.pointerUp(120, 120, { isPen: false }) expect(editor.getCurrentPageShapes().length).toBe(shapeCount) }) }) describe('forgiving palm touches when entering pen mode', () => { it('does not connect a palm touch to the stylus stroke', () => { editor.setCurrentTool('draw') expect(editor.getInstanceState().isPenMode).toBe(false) // A palm rests on the canvas first (not a pen, pen mode is still off) editor.pointerDown(100, 100, { isPen: false }) editor.expectToBeIn('draw.drawing') // Then the stylus lands, which switches the editor into pen mode editor.pointerDown(300, 300, { isPen: true }) expect(editor.getInstanceState().isPenMode).toBe(true) // The stylus draws its stroke editor.pointerMove(310, 310, { isPen: true }) editor.pointerMove(320, 320, { isPen: true }) editor.pointerUp(320, 320, { isPen: true }) expect(editor.getCurrentPageShapes().length).toBe(1) const shape = editor.getCurrentPageShapes()[0] as TLDrawShape // The stroke should begin where the stylus landed, not at the palm point. const bounds = editor.getShapePageBounds(shape.id)! expect(bounds.minX).toBeGreaterThan(200) expect(bounds.minY).toBeGreaterThan(200) }) it('forgives a palm touch that was already dragging before the stylus lands', () => { editor.setCurrentTool('draw') // A palm rests on the canvas and drags a little before the stylus arrives editor.pointerDown(100, 100, { isPen: false }) editor.pointerMove(120, 120, { isPen: false }) editor.pointerMove(140, 140, { isPen: false }) editor.expectToBeIn('draw.drawing') // The stylus lands, switching into pen mode and forgiving the palm stroke editor.pointerDown(300, 300, { isPen: true }) expect(editor.getInstanceState().isPenMode).toBe(true) editor.pointerMove(310, 310, { isPen: true }) editor.pointerMove(320, 320, { isPen: true }) editor.pointerUp(320, 320, { isPen: true }) expect(editor.getCurrentPageShapes().length).toBe(1) const shape = editor.getCurrentPageShapes()[0] as TLDrawShape const bounds = editor.getShapePageBounds(shape.id)! expect(bounds.minX).toBeGreaterThan(200) expect(bounds.minY).toBeGreaterThan(200) }) }) describe('palm touches while in pen mode', () => { it('ignores a palm touch without disturbing the pen drag in progress', () => { const id = createShapeId('box') editor.createShapes([ { id, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100, fill: 'solid' } }, ]) editor.updateInstanceState({ isPenMode: true }) const pen = { target: 'canvas', isPen: true } as const const palm = { target: 'canvas', isPen: false } as const editor.pointerDown(150, 150, pen) editor.pointerMove(170, 170, pen) editor.expectToBeIn('select.translating') // A palm lands and lifts while the pen is still down. It must be ignored before it // can reset the drag origin or clear the pen's pointing state. editor.pointerDown(600, 600, palm) editor.pointerUp(600, 600, palm) expect(editor.inputs.getIsPointing()).toBe(true) expect(editor.inputs.getOriginPagePoint()).toMatchObject({ x: 150, y: 150 }) editor.pointerMove(200, 200, pen) editor.expectShapeToMatch({ id, x: 150, y: 150 }) editor.pointerUp(200, 200, pen) editor.expectToBeIn('select.idle') }) })