Skip to content

Commit

Permalink
Resize grid cell with keyboard (#6313)
Browse files Browse the repository at this point in the history
**Problem:**

It should be possible to resize grid cells with the keyboard in addition
to the mouse based counterpart.

**Fix:**

Expand the existing keyboard strategy with a variation that allows to
resize the current selected grid cell when using the arrow keys and the
`shift` modifier. In order to avoid any headaches the resize is relative
to the bottom-right corner of the cell, so it's intuitive to perform
resizes and subsequent moves if needed without having to worry about
"which side am I resizing from?".

It _is_ possible to perform both moves and resizes as part of the same
interaction session.

![Kapture 2024-09-04 at 15 50
20](https://github.com/user-attachments/assets/f67a1ea3-d5a1-4397-a151-d6a9cbc8ad96)

Fixes #6312
ruggi authored Sep 5, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 093751d commit 2f7220a
Showing 3 changed files with 104 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ import { wrapInContainerCommand } from '../commands/wrap-in-container-command'
import type { ElementPath } from 'utopia-shared/src/types'
import { reparentSubjectsForInteractionTarget } from './strategies/reparent-helpers/reparent-strategy-helpers'
import { getReparentTargetUnified } from './strategies/reparent-helpers/reparent-strategy-parent-lookup'
import { gridRearrangeKeyboardStrategy } from './strategies/grid-rearrange-keyboard-strategy'
import { gridRearrangeResizeKeyboardStrategy } from './strategies/grid-rearrange-keyboard-strategy'

export type CanvasStrategyFactory = (
canvasState: InteractionCanvasState,
@@ -111,7 +111,7 @@ const moveOrReorderStrategies: MetaCanvasStrategy = (
gridRearrangeMoveStrategy,
rearrangeGridSwapStrategy,
gridRearrangeMoveDuplicateStrategy,
gridRearrangeKeyboardStrategy,
gridRearrangeResizeKeyboardStrategy,
],
)
}
Original file line number Diff line number Diff line change
@@ -545,8 +545,8 @@ export function getGridCellBoundsFromCanvas(
const cellHeight = cellEndCoords.row - cellOriginCoords.row + 1

return {
originCell: cellOriginCoords,
endCell: cellEndCoords,
column: cellOriginCoords.column,
row: cellOriginCoords.row,
width: cellWidth,
height: cellHeight,
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import { MetadataUtils } from '../../../../core/model/element-metadata-utils'
import * as EP from '../../../../core/shared/element-path'
import {
gridPositionValue,
type GridElementProperties,
} from '../../../../core/shared/element-template'
import { assertNever } from '../../../../core/shared/utils'
import { emptyModifiers, Modifier } from '../../../../utils/modifiers'
import type { GridPositionValue } from '../../../../core/shared/element-template'
import { gridPositionValue } from '../../../../core/shared/element-template'
import { GridControls, GridControlsKey } from '../../controls/grid-controls'
import type { CanvasStrategy, InteractionCanvasState } from '../canvas-strategy-types'
import type {
CanvasStrategy,
CustomStrategyState,
InteractionCanvasState,
} from '../canvas-strategy-types'
import {
emptyStrategyApplicationResult,
getTargetPathsFromInteractionTarget,
strategyApplicationResult,
} from '../canvas-strategy-types'
import type { InteractionSession, KeyState } from '../interaction-state'
import type { InteractionSession } from '../interaction-state'
import { getGridCellBoundsFromCanvas, setGridPropsCommands } from './grid-helpers'
import { accumulatePresses } from './shared-keyboard-strategy-helpers'

export function gridRearrangeKeyboardStrategy(
export function gridRearrangeResizeKeyboardStrategy(
canvasState: InteractionCanvasState,
interactionSession: InteractionSession | null,
customState: CustomStrategyState,
): CanvasStrategy | null {
if (interactionSession?.activeControl.type !== 'KEYBOARD_CATCHER_CONTROL') {
if (
interactionSession?.activeControl.type !== 'KEYBOARD_CATCHER_CONTROL' ||
interactionSession.interactionData.type !== 'KEYBOARD'
) {
return null
}

@@ -49,15 +53,26 @@ export function gridRearrangeKeyboardStrategy(
}
const gridTemplate = grid.specialSizeMeasurements.containerGridProperties

const cellBounds = getGridCellBoundsFromCanvas(cell, canvasState.scale, canvasState.canvasOffset)
if (cellBounds == null) {
const initialCellBounds = getGridCellBoundsFromCanvas(
cell,
canvasState.scale,
canvasState.canvasOffset,
)
if (initialCellBounds == null) {
return null
}

const resizing =
Array.from(interactionSession.interactionData.keyStates).at(
interactionSession.interactionData.keyStates.length - 1,
)?.modifiers.shift ?? false

const label = resizing ? 'Grid resize' : 'Grid rearrange'

return {
id: 'GRID_KEYBOARD_REARRANGE',
name: 'Grid rearrange',
descriptiveLabel: 'Grid rearrange',
id: 'GRID_KEYBOARD_REARRANGE_RESIZE',
name: label,
descriptiveLabel: label,
icon: {
category: 'modalities',
type: 'reorder-large',
@@ -85,34 +100,51 @@ export function gridRearrangeKeyboardStrategy(

const interactionData = interactionSession.interactionData

const horizontalDelta = getKeysDelta(interactionData.keyStates, 'horizontal')
const verticalDelta = getKeysDelta(interactionData.keyStates, 'vertical')

let gridProps: Partial<GridElementProperties> = {
...cell.specialSizeMeasurements.elementGridProperties,
}

if (horizontalDelta !== 0) {
const { from, to } = getNewBounds(
cellBounds.originCell.column + horizontalDelta,
gridTemplate.gridTemplateColumns.dimensions.length,
cellBounds.width,
)
gridProps.gridColumnStart = gridPositionValue(from)
gridProps.gridColumnEnd = gridPositionValue(to)
let gridColumnStart: GridPositionValue = gridPositionValue(initialCellBounds.column)
let gridColumnEnd: GridPositionValue = gridPositionValue(
initialCellBounds.column + initialCellBounds.width,
)
let gridRowStart: GridPositionValue = gridPositionValue(initialCellBounds.row)
let gridRowEnd: GridPositionValue = gridPositionValue(
initialCellBounds.row + initialCellBounds.height,
)

const cols = gridTemplate.gridTemplateColumns.dimensions.length
const rows = gridTemplate.gridTemplateRows.dimensions.length

for (const keyState of interactionData.keyStates) {
const resize = keyState.modifiers.shift
for (const key of keyState.keysPressed) {
// column changes
const horizDelta = key === 'left' ? -1 : key === 'right' ? 1 : null
if (horizDelta != null) {
const bounds = { start: gridColumnStart, end: gridColumnEnd }
const { start, end } = processPress(horizDelta, resize, cols, bounds)

gridColumnStart = start
gridColumnEnd = end
}

// row changes
const vertDelta = key === 'up' ? -1 : key === 'down' ? 1 : null
if (vertDelta != null) {
const bounds = { start: gridRowStart, end: gridRowEnd }
const { start, end } = processPress(vertDelta, resize, rows, bounds)

gridRowStart = start
gridRowEnd = end
}
}
}

if (verticalDelta !== 0) {
const { from, to } = getNewBounds(
cellBounds.originCell.row + verticalDelta,
gridTemplate.gridTemplateRows.dimensions.length,
cellBounds.height,
)
gridProps.gridRowStart = gridPositionValue(from)
gridProps.gridRowEnd = gridPositionValue(to)
}

return strategyApplicationResult(setGridPropsCommands(target, gridTemplate, gridProps))
return strategyApplicationResult(
setGridPropsCommands(target, gridTemplate, {
gridColumnStart,
gridColumnEnd,
gridRowStart,
gridRowEnd,
}),
)
},
}
}
@@ -123,57 +155,41 @@ function fitness(interactionSession: InteractionSession | null): number {
}

const accumulatedPresses = accumulatePresses(interactionSession.interactionData.keyStates)
const matches = accumulatedPresses.some(
(accumulatedPress) =>
Array.from(accumulatedPress.keysPressed).some(
(key) => key === 'left' || key === 'right' || key === 'up' || key === 'down',
) && Modifier.equal(accumulatedPress.modifiers, emptyModifiers),
const matches = accumulatedPresses.some((accumulatedPress) =>
Array.from(accumulatedPress.keysPressed).some(
(key) => key === 'left' || key === 'right' || key === 'up' || key === 'down',
),
)

return matches ? 1 : 0
}

function getKeysDelta(keyStates: KeyState[], direction: 'vertical' | 'horizontal'): number {
return keyStates.reduce((total, cur) => {
let presses = 0
cur.keysPressed.forEach((key) => {
switch (direction) {
case 'horizontal':
presses += key === 'left' ? -1 : key === 'right' ? 1 : 0
break
case 'vertical':
presses += key === 'up' ? -1 : key === 'down' ? 1 : 0
break
default:
assertNever(direction)
}
})
return total + presses
}, 0)
}

function getNewBounds(
start: number,
// process a keypress event and return the updated start/end grid cell bounds
function processPress(
amount: 1 | -1,
resize: boolean,
cellsCount: number,
size: number,
initialBounds: {
start: GridPositionValue
end: GridPositionValue
},
): {
from: number
to: number
start: GridPositionValue
end: GridPositionValue
} {
const lowerLimit = 1
const upperLimit = cellsCount + 1

let from = start
let to = start + size

if (to > upperLimit) {
to = upperLimit
from = to - size
}
if (from < lowerLimit) {
from = lowerLimit
to = from + size
let newBounds = { ...initialBounds }

const start = newBounds.start.numericalPosition ?? 1
const end = newBounds.end.numericalPosition ?? 1

if (resize) {
newBounds.end = gridPositionValue(Math.max(start + 1, Math.min(cellsCount + 1, end + amount)))
} else {
const size = end - start
const newStart = Math.min(cellsCount - size + 1, Math.max(1, start + amount))
newBounds.start = gridPositionValue(newStart)
newBounds.end = gridPositionValue(newStart + size)
}

return { from: from, to: to }
return newBounds
}

0 comments on commit 2f7220a

Please sign in to comment.