Merge pull request #657 from itorr/codex/fix-canvas-zoom-shortcuts
fix(keyboard): route zoom shortcuts to canvas
This commit is contained in:
commit
cb08d351e8
|
|
@ -42,6 +42,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Route browser Command/Ctrl plus and minus shortcuts to canvas zoom instead of page zoom.
|
||||
- Resolve `$name` references in imported `.pen` fills, stroke fills, font families, dimensions, and spacing without requiring a `--` prefix. (#563)
|
||||
- Prevent the stock photo tool from replacing text, lines, structural layers, or containers with content while supporting closed shape geometry.
|
||||
- Preserve explicit text alignment metadata on imported Figma vectors across save and reload.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import type { Editor } from '@open-pencil/core/editor'
|
||||
|
||||
export function createProfilerActions(editor: Editor) {
|
||||
function activeCanvas() {
|
||||
return (
|
||||
document.querySelector<HTMLCanvasElement>(
|
||||
'[data-active-pane="true"] [data-test-id="canvas-element"]'
|
||||
) ?? document.querySelector<HTMLCanvasElement>('[data-test-id="canvas-element"]')
|
||||
)
|
||||
}
|
||||
|
||||
function viewportScreenCenter() {
|
||||
const canvas = document.querySelector<HTMLCanvasElement>('[data-test-id="canvas-element"]')
|
||||
const canvas = activeCanvas()
|
||||
if (canvas) {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
|
||||
|
|
@ -11,7 +19,7 @@ export function createProfilerActions(editor: Editor) {
|
|||
}
|
||||
|
||||
function viewportCanvasCenter() {
|
||||
const canvas = document.querySelector<HTMLCanvasElement>('[data-test-id="canvas-element"]')
|
||||
const canvas = activeCanvas()
|
||||
if (canvas) {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
return { x: rect.width / 2, y: rect.height / 2 }
|
||||
|
|
|
|||
|
|
@ -40,6 +40,13 @@ function commandShortcuts(...commands: EditorCommandId[]): ShortcutDefinition[]
|
|||
})
|
||||
}
|
||||
|
||||
function zoomAtViewportCenter(delta: number): ShortcutAction {
|
||||
return ({ store }) => {
|
||||
const center = store.viewportCanvasCenter()
|
||||
store.applyZoom(delta, center.x, center.y)
|
||||
}
|
||||
}
|
||||
|
||||
function opacityBindings(): ShortcutDefinition[] {
|
||||
return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].map((digit) => ({
|
||||
id: `selection-opacity-${digit}`,
|
||||
|
|
@ -138,6 +145,28 @@ export function registerKeyboardShortcuts(options: KeyboardShortcutOptions) {
|
|||
run: () => openSettingsDialog(),
|
||||
global: true
|
||||
},
|
||||
{
|
||||
id: 'zoom-in',
|
||||
keys: [appMenuTinykeysShortcut('zoom-in') ?? '$mod+Equal', '$mod+Shift+Equal'],
|
||||
run: zoomAtViewportCenter(-100),
|
||||
global: true
|
||||
},
|
||||
{
|
||||
id: 'zoom-out',
|
||||
keys: appMenuTinykeysShortcut('zoom-out') ?? '$mod+Minus',
|
||||
run: zoomAtViewportCenter(100),
|
||||
global: true
|
||||
},
|
||||
{
|
||||
id: 'zoom-in-unmodified',
|
||||
keys: 'Shift+Equal',
|
||||
run: zoomAtViewportCenter(-100)
|
||||
},
|
||||
{
|
||||
id: 'zoom-out-unmodified',
|
||||
keys: 'Minus',
|
||||
run: zoomAtViewportCenter(100)
|
||||
},
|
||||
{
|
||||
id: 'close-tab',
|
||||
keys: appMenuTinykeysShortcut('close') ?? '$mod+KeyW',
|
||||
|
|
|
|||
|
|
@ -321,6 +321,116 @@ test.describe('opacity shortcuts', () => {
|
|||
})
|
||||
|
||||
test.describe('zoom shortcuts', () => {
|
||||
test('⌘=, ⌘+ and ⌘- zoom the canvas and prevent browser defaults', async () => {
|
||||
await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.zoomTo100()
|
||||
})
|
||||
|
||||
const pressShortcut = async (shortcut: string, code: 'Equal' | 'Minus') => {
|
||||
await editor.page.evaluate((expectedCode) => {
|
||||
delete document.documentElement.dataset.observedShortcut
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.code !== expectedCode) return
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
document.documentElement.dataset.observedShortcut = JSON.stringify({
|
||||
defaultPrevented: event.defaultPrevented,
|
||||
shiftKey: event.shiftKey
|
||||
})
|
||||
}
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
}, code)
|
||||
|
||||
await editor.page.keyboard.press(shortcut)
|
||||
await editor.canvas.waitForRender()
|
||||
const event = await editor.page.evaluate(() => {
|
||||
const observed = document.documentElement.dataset.observedShortcut
|
||||
if (!observed) throw new Error('Expected shortcut keydown event')
|
||||
return JSON.parse(observed) as { defaultPrevented: boolean; shiftKey: boolean }
|
||||
})
|
||||
return { event, zoom: await getZoom() }
|
||||
}
|
||||
|
||||
const zoomedIn = await pressShortcut('ControlOrMeta+Equal', 'Equal')
|
||||
const zoomedInWithShift = await pressShortcut('ControlOrMeta+Shift+Equal', 'Equal')
|
||||
const zoomedOut = await pressShortcut('ControlOrMeta+Minus', 'Minus')
|
||||
|
||||
expect(zoomedIn.event).toEqual({ defaultPrevented: true, shiftKey: false })
|
||||
expect(zoomedInWithShift.event).toEqual({ defaultPrevented: true, shiftKey: true })
|
||||
expect(zoomedOut.event).toEqual({ defaultPrevented: true, shiftKey: false })
|
||||
expect(zoomedIn.zoom).toBeGreaterThan(1)
|
||||
expect(zoomedInWithShift.zoom).toBeGreaterThan(zoomedIn.zoom)
|
||||
expect(zoomedOut.zoom).toBeLessThan(zoomedInWithShift.zoom)
|
||||
})
|
||||
|
||||
test('Figma plus and minus shortcuts zoom around the active canvas center', async () => {
|
||||
await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.zoomTo100()
|
||||
store.splitPane(store.activePaneId.value, 'horizontal')
|
||||
})
|
||||
await expect(editor.page.locator('[data-active-pane]')).toHaveCount(2)
|
||||
const splitHandle = editor.page.locator('[data-split-id]').first()
|
||||
const splitHandleBounds = await splitHandle.boundingBox()
|
||||
if (!splitHandleBounds) throw new Error('Expected canvas split handle')
|
||||
await editor.page.mouse.move(
|
||||
splitHandleBounds.x,
|
||||
splitHandleBounds.y + splitHandleBounds.height / 2
|
||||
)
|
||||
await editor.page.mouse.down()
|
||||
await editor.page.mouse.move(
|
||||
splitHandleBounds.x + 100,
|
||||
splitHandleBounds.y + splitHandleBounds.height / 2
|
||||
)
|
||||
await editor.page.mouse.up()
|
||||
const activeCanvas = editor.page.locator('[data-active-pane="true"]')
|
||||
const inactiveCanvas = editor.page.locator('[data-active-pane="false"]')
|
||||
const activeBounds = await activeCanvas.boundingBox()
|
||||
const inactiveBounds = await inactiveCanvas.boundingBox()
|
||||
if (!activeBounds || !inactiveBounds) throw new Error('Expected canvas pane bounds')
|
||||
expect(activeBounds.width).toBeLessThan(inactiveBounds.width)
|
||||
|
||||
const centerBefore = await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const center = store.viewportCanvasCenter()
|
||||
return { center, canvasPoint: store.screenToCanvas(center.x, center.y) }
|
||||
})
|
||||
|
||||
expect(centerBefore.center.x).toBeCloseTo(activeBounds.width / 2)
|
||||
expect(centerBefore.center.y).toBeCloseTo(activeBounds.height / 2)
|
||||
|
||||
await editor.page.keyboard.press('Shift+Equal')
|
||||
await editor.canvas.waitForRender()
|
||||
const zoomedIn = await getZoom()
|
||||
const centerAfter = await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const center = store.viewportCanvasCenter()
|
||||
return { center, canvasPoint: store.screenToCanvas(center.x, center.y) }
|
||||
})
|
||||
|
||||
await editor.page.keyboard.press('Minus')
|
||||
await editor.canvas.waitForRender()
|
||||
|
||||
const inactivePaneId = await inactiveCanvas.getAttribute('data-pane-id')
|
||||
if (!inactivePaneId) throw new Error('Expected inactive canvas pane')
|
||||
await editor.page.evaluate((paneId) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
store.closePane(paneId)
|
||||
}, inactivePaneId)
|
||||
await expect(editor.page.locator('[data-active-pane]')).toHaveCount(1)
|
||||
|
||||
expect(centerAfter.center).toEqual(centerBefore.center)
|
||||
expect(centerAfter.canvasPoint.x).toBeCloseTo(centerBefore.canvasPoint.x)
|
||||
expect(centerAfter.canvasPoint.y).toBeCloseTo(centerBefore.canvasPoint.y)
|
||||
expect(zoomedIn).toBeGreaterThan(1)
|
||||
expect(await getZoom()).toBeLessThan(zoomedIn)
|
||||
})
|
||||
|
||||
test('⌘0 zooms to 100%', async () => {
|
||||
await editor.canvas.clearCanvas()
|
||||
await editor.canvas.drawRect(100, 100, 60, 60)
|
||||
|
|
|
|||
Loading…
Reference in a new issue