diff --git a/editor/liveblocks.config.ts b/editor/liveblocks.config.ts index a1150d640da9..9e5331c943eb 100644 --- a/editor/liveblocks.config.ts +++ b/editor/liveblocks.config.ts @@ -1,8 +1,8 @@ -import { LiveObject } from '@liveblocks/client' -import { createClient } from '@liveblocks/client' +import { LiveObject, createClient } from '@liveblocks/client' import { createRoomContext } from '@liveblocks/react' -import type { CanvasVector, WindowPoint } from './src/core/shared/math-utils' import { getProjectID } from './src/common/env-vars' +import type { ActiveFrameAction } from './src/components/canvas/commands/set-active-frames-command' +import type { CanvasRectangle, CanvasVector, WindowPoint } from './src/core/shared/math-utils' import { projectIdToRoomId } from './src/core/shared/multiplayer' export const liveblocksThrottle = 100 // ms @@ -19,9 +19,16 @@ export type Presence = { cursor: WindowPoint | null canvasScale: number | null canvasOffset: CanvasVector | null + activeFrames?: PresenceActiveFrame[] following: string | null } +export type PresenceActiveFrame = { + action: ActiveFrameAction + frame: CanvasRectangle + source: CanvasRectangle +} + export function initialPresence(): Presence { return { cursor: null, diff --git a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx index 06c2a7c7f843..e5340b153d4e 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/absolute-resize-bounding-box-strategy.tsx @@ -27,6 +27,7 @@ import { EdgePositionLeft, EdgePositionTop, EdgePositionTopLeft } from '../../ca import { isEdgePositionEqualTo } from '../../canvas-utils' import { pushIntendedBoundsAndUpdateGroups } from '../../commands/push-intended-bounds-and-update-groups-command' import { queueTrueUpElement } from '../../commands/queue-true-up-command' +import { activeFrameTargetRect, setActiveFrames } from '../../commands/set-active-frames-command' import { setCursorCommand } from '../../commands/set-cursor-command' import { setElementsToRerenderCommand } from '../../commands/set-elements-to-rerender-command' import { setSnappingGuidelines } from '../../commands/set-snapping-guidelines-command' @@ -263,6 +264,13 @@ export function absoluteResizeBoundingBoxStrategy( 'starting-metadata', ), queueTrueUpElement(childGroups.map(trueUpGroupElementChanged)), + setActiveFrames([ + { + action: 'resize', + target: activeFrameTargetRect(newFrame), + source: originalBoundingBox, + }, + ]), ] }) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts b/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts index 2b0bc7bdf709..595a42c8f804 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts @@ -4,10 +4,10 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import type { CanvasVector } from '../../../../core/shared/math-utils' import { canvasRectangle, - CanvasRectangle, isInfinityRectangle, offsetPoint, rectContainsPoint, + zeroRectIfNullOrInfinity, } from '../../../../core/shared/math-utils' import { absolute } from '../../../../utils/utils' import { CSSCursor } from '../../canvas-types' @@ -20,13 +20,10 @@ import type { InteractionCanvasState, StrategyApplicationResult, } from '../canvas-strategy-types' -import { - emptyStrategyApplicationResult, - getTargetPathsFromInteractionTarget, - strategyApplicationResult, -} from '../canvas-strategy-types' +import { emptyStrategyApplicationResult, strategyApplicationResult } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' import type { ElementInstanceMetadataMap } from '../../../../core/shared/element-template' +import { activeFrameTargetRect, setActiveFrames } from '../../commands/set-active-frames-command' export function isReorderAllowed(siblings: Array): boolean { return siblings.every((sibling) => !isRootOfGeneratedElement(sibling)) @@ -81,12 +78,26 @@ export function applyReorderCommon( isValidTarget, ) + const sourceFrame = zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(target, canvasState.startingMetadata) ?? null, + ) const newIndexFound = newIndex > -1 const newResultOrLastIndex = newIndexFound ? newIndex : lastReorderIdx + const targetFrame = zeroRectIfNullOrInfinity( + newResultOrLastIndex > -1 + ? MetadataUtils.getFrameInCanvasCoords( + siblings[newResultOrLastIndex], + canvasState.startingMetadata, + ) + : sourceFrame, + ) if (newResultOrLastIndex === unpatchedIndex) { return strategyApplicationResult( [ + setActiveFrames([ + { action: 'reorder', target: activeFrameTargetRect(targetFrame), source: sourceFrame }, + ]), updateHighlightedViews('mid-interaction', []), setElementsToRerenderCommand(siblings), setCursorCommand(CSSCursor.Move), @@ -98,6 +109,9 @@ export function applyReorderCommon( } else { return strategyApplicationResult( [ + setActiveFrames([ + { action: 'reorder', target: activeFrameTargetRect(targetFrame), source: sourceFrame }, + ]), reorderElement('always', target, absolute(newResultOrLastIndex)), setElementsToRerenderCommand(siblings), updateHighlightedViews('mid-interaction', []), diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx index 633b6cafe950..2b577744d8d4 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-border-radius-strategy.tsx @@ -75,6 +75,7 @@ import { withUnderlyingTarget } from '../../../editor/store/editor-state' import type { ProjectContentTreeRoot } from '../../../assets' import { getModifiableJSXAttributeAtPath } from '../../../../core/shared/jsx-attributes' import { showToastCommand } from '../../commands/show-toast-command' +import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command' export const SetBorderRadiusStrategyId = 'SET_BORDER_RADIUS_STRATEGY' @@ -157,6 +158,15 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = ( ...commands(selectedElement), ...getAddOverflowHiddenCommands(selectedElement, canvasState.projectContents), setElementsToRerenderCommand(selectedElements), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-radius', + target: activeFrameTargetPath(path), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), + ), + })), + ), ]), } } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx index dc556042d85e..6d3e46bd820c 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-flex-gap-strategy.tsx @@ -1,7 +1,11 @@ import { styleStringInArray } from '../../../../utils/common-constants' import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import type { CanvasVector } from '../../../../core/shared/math-utils' -import { canvasPoint, canvasVector } from '../../../../core/shared/math-utils' +import { + canvasPoint, + zeroRectIfNullOrInfinity, + canvasVector, +} from '../../../../core/shared/math-utils' import { optionalMap } from '../../../../core/shared/optional-utils' import { assertNever } from '../../../../core/shared/utils' import type { Modifiers } from '../../../../utils/modifiers' @@ -38,6 +42,7 @@ import { import type { InteractionSession } from '../interaction-state' import { areAllSiblingsInOneDimensionFlexOrFlow } from './flow-reorder-helpers' import { colorTheme } from '../../../../uuiui' +import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command' export const SetFlexGapStrategyId = 'SET_FLEX_GAP_STRATEGY' @@ -170,6 +175,15 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( ), setCursorCommand(cursorFromFlexDirection(flexGap.direction)), setElementsToRerenderCommand([...selectedElements, ...children]), + setActiveFrames([ + { + action: 'set-gap', + target: activeFrameTargetPath(selectedElement), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(selectedElement, canvasState.startingMetadata), + ), + }, + ]), ]) }, } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx index 82e6dfa34bfc..4c63ec013d7c 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/set-padding-strategy.tsx @@ -76,6 +76,7 @@ import { adjustCssLengthProperties, } from '../../commands/adjust-css-length-command' import type { ElementPathTrees } from '../../../../core/shared/element-path-tree' +import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command' const StylePaddingProp = stylePropPathMappingFn('padding', styleStringInArray) const IndividualPaddingProps: Array = [ @@ -257,6 +258,15 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti value, ), ), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-padding', + target: activeFrameTargetPath(path), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), + ), + })), + ), ]) } @@ -273,6 +283,15 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti ]), ), setProperty('always', selectedElement, StylePaddingProp, paddingString), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-padding', + target: activeFrameTargetPath(path), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), + ), + })), + ), ]) } @@ -291,6 +310,15 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti value, ), ), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-padding', + target: activeFrameTargetPath(path), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), + ), + })), + ), ]) }, } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts index ba372ccda8fa..e6acc439b6e5 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/shared-move-strategies-helpers.ts @@ -11,14 +11,10 @@ import type { ElementInstanceMetadataMap, JSXElement, } from '../../../../core/shared/element-template' -import type { - CanvasPoint, - CanvasRectangle, - CanvasVector, - LocalRectangle, -} from '../../../../core/shared/math-utils' +import type { CanvasPoint, CanvasRectangle, CanvasVector } from '../../../../core/shared/math-utils' import { boundingRectangleArray, + zeroRectIfNullOrInfinity, canvasRectangleToLocalRectangle, canvasVector, nullIfInfinity, @@ -70,6 +66,7 @@ import { setCssLengthProperty, setValueKeepingOriginalUnit, } from '../../commands/set-css-length-command' +import { activeFrameTargetRect, setActiveFrames } from '../../commands/set-active-frames-command' export interface MoveCommandsOptions { ignoreLocalFrame?: boolean @@ -134,6 +131,15 @@ export function applyMoveCommon( updateHighlightedViews('mid-interaction', []), setElementsToRerenderCommand(targets), setCursorCommand(CSSCursor.Select), + setActiveFrames( + commandsForSelectedElements.intendedBounds.map((b) => ({ + action: 'move', // TODO this could also show "duplicate" when applicable + target: activeFrameTargetRect(b.frame), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(b.target, canvasState.startingMetadata), + ), + })), + ), ]) } else { const constrainedDragAxis = @@ -171,6 +177,15 @@ export function applyMoveCommon( ), setElementsToRerenderCommand([...targets, ...targetsForSnapping]), setCursorCommand(CSSCursor.Select), + setActiveFrames( + commandsForSelectedElements.intendedBounds.map((b) => ({ + action: 'move', // TODO this could also show "duplicate" when applicable + target: activeFrameTargetRect(b.frame), + source: zeroRectIfNullOrInfinity( + MetadataUtils.getFrameInCanvasCoords(b.target, canvasState.startingMetadata), + ), + })), + ), ]) } } else { diff --git a/editor/src/components/canvas/commands/commands.ts b/editor/src/components/canvas/commands/commands.ts index b4f3b7de365c..0bee5bf8aeb1 100644 --- a/editor/src/components/canvas/commands/commands.ts +++ b/editor/src/components/canvas/commands/commands.ts @@ -85,6 +85,7 @@ import type { PushIntendedBoundsAndUpdateGroups } from './push-intended-bounds-a import { runPushIntendedBoundsAndUpdateGroups } from './push-intended-bounds-and-update-groups-command' import type { PushIntendedBoundsAndUpdateHuggingElements } from './push-intended-bounds-and-update-hugging-elements-command' import { runPushIntendedBoundsAndUpdateHuggingElements } from './push-intended-bounds-and-update-hugging-elements-command' +import { runSetActiveFrames, type SetActiveFrames } from './set-active-frames-command' export interface CommandFunctionResult { editorStatePatches: Array @@ -137,6 +138,7 @@ export type CanvasCommand = | DeleteElement | WrapInContainerCommand | QueueTrueUpElement + | SetActiveFrames export function runCanvasCommand( editorState: EditorState, @@ -219,6 +221,8 @@ export function runCanvasCommand( return runWrapInContainerCommand(editorState, command) case 'QUEUE_TRUE_UP_ELEMENT': return runQueueTrueUpElement(editorState, command) + case 'SET_ACTIVE_FRAMES': + return runSetActiveFrames(editorState, command) default: const _exhaustiveCheck: never = command throw new Error(`Unhandled canvas command ${JSON.stringify(command)}`) diff --git a/editor/src/components/canvas/commands/set-active-frames-command.ts b/editor/src/components/canvas/commands/set-active-frames-command.ts new file mode 100644 index 000000000000..148a0e75e2b5 --- /dev/null +++ b/editor/src/components/canvas/commands/set-active-frames-command.ts @@ -0,0 +1,99 @@ +import type { CanvasRectangle } from '../../../core/shared/math-utils' +import type { ElementPath } from '../../../core/shared/project-file-types' +import { assertNever } from '../../../core/shared/utils' +import type { EditorState } from '../../editor/store/editor-state' +import type { BaseCommand, CommandFunction } from './commands' + +export type ActiveFrameAction = + | 'move' + | 'resize' + | 'set-gap' + | 'set-padding' + | 'set-radius' + | 'reorder' + +export function activeFrameActionToString(action: ActiveFrameAction): string { + switch (action) { + case 'move': + return 'Move' + case 'reorder': + return 'Reorder' + case 'resize': + return 'Resize' + case 'set-gap': + return 'Gap' + case 'set-padding': + return 'Padding' + case 'set-radius': + return 'Radius' + default: + assertNever(action) + } +} + +export type ActiveFrame = { + action: ActiveFrameAction + target: ActiveFrameTarget + source: CanvasRectangle +} + +export type ActiveFrameTargetRect = { + type: 'ACTIVE_FRAME_TARGET_RECT' + rect: CanvasRectangle +} + +export function isActiveFrameTargetRect( + target: ActiveFrameTarget, +): target is ActiveFrameTargetRect { + return target.type === 'ACTIVE_FRAME_TARGET_RECT' +} + +export function activeFrameTargetRect(rect: CanvasRectangle): ActiveFrameTargetRect { + return { + type: 'ACTIVE_FRAME_TARGET_RECT', + rect: rect, + } +} + +export type ActiveFrameTargetPath = { + type: 'ACTIVE_FRAME_TARGET_PATH' + path: ElementPath +} + +export function isActiveFrameTargetPath( + target: ActiveFrameTarget, +): target is ActiveFrameTargetPath { + return target.type === 'ACTIVE_FRAME_TARGET_PATH' +} + +export function activeFrameTargetPath(path: ElementPath): ActiveFrameTargetPath { + return { + type: 'ACTIVE_FRAME_TARGET_PATH', + path: path, + } +} + +export type ActiveFrameTarget = ActiveFrameTargetRect | ActiveFrameTargetPath + +export interface SetActiveFrames extends BaseCommand { + type: 'SET_ACTIVE_FRAMES' + activeFrames: Array +} + +export function setActiveFrames(activeFrames: Array): SetActiveFrames { + return { + type: 'SET_ACTIVE_FRAMES', + whenToRun: 'mid-interaction', + activeFrames: activeFrames, + } +} + +export const runSetActiveFrames: CommandFunction = ( + _: EditorState, + command: SetActiveFrames, +) => { + return { + commandDescription: `Set active frames`, + editorStatePatches: [{ activeFrames: { $push: command.activeFrames } }], + } +} diff --git a/editor/src/components/canvas/multiplayer-cursors.tsx b/editor/src/components/canvas/multiplayer-cursors.tsx index b4c882867f8a..d246d1b433b0 100644 --- a/editor/src/components/canvas/multiplayer-cursors.tsx +++ b/editor/src/components/canvas/multiplayer-cursors.tsx @@ -1,6 +1,7 @@ import type { User } from '@liveblocks/client' import { motion } from 'framer-motion' import React from 'react' +import type { Presence, PresenceActiveFrame, UserMeta } from '../../../liveblocks.config' import { useOthers, useOthersListener, @@ -9,9 +10,11 @@ import { useStorage, useUpdateMyPresence, } from '../../../liveblocks.config' -import type { Presence, UserMeta } from '../../../liveblocks.config' +import { getCollaborator, useAddMyselfToCollaborators } from '../../core/commenting/comment-hooks' +import { MetadataUtils } from '../../core/model/element-metadata-utils' +import { mapDropNulls } from '../../core/shared/array-utils' import type { CanvasPoint } from '../../core/shared/math-utils' -import { pointsEqual, windowPoint } from '../../core/shared/math-utils' +import { pointsEqual, windowPoint, zeroRectIfNullOrInfinity } from '../../core/shared/math-utils' import { multiplayerColorFromIndex, normalizeOthersList } from '../../core/shared/multiplayer' import { assertNever } from '../../core/shared/utils' import { UtopiaTheme, useColorTheme } from '../../uuiui' @@ -20,10 +23,15 @@ import { isLoggedIn } from '../editor/action-types' import { switchEditorMode } from '../editor/actions/action-creators' import { EditorModes, isFollowMode } from '../editor/editor-modes' import { useDispatch } from '../editor/store/dispatch-context' -import { Substores, useEditorState } from '../editor/store/store-hook' +import { + Substores, + useEditorState, + useRefEditorState, + useSelectorWithCallback, +} from '../editor/store/store-hook' import CanvasActions from './canvas-actions' +import { activeFrameActionToString } from './commands/set-active-frames-command' import { canvasPointToWindowPoint, windowToCanvasCoordinates } from './dom-lookup' -import { getCollaborator, useAddMyselfToCollaborators } from '../../core/commenting/comment-hooks' export const MultiplayerPresence = React.memo(() => { const dispatch = useDispatch() @@ -89,8 +97,9 @@ export const MultiplayerPresence = React.memo(() => { return ( <> - + + ) }) @@ -331,3 +340,135 @@ const FollowingOverlay = React.memo(() => { ) }) FollowingOverlay.displayName = 'FollowingOverlay' + +const MultiplayerShadows = React.memo(() => { + const me = useSelf() + const updateMyPresence = useUpdateMyPresence() + + const collabs = useStorage((store) => store.collaborators) + const others = useOthers((list) => { + const presences = normalizeOthersList(me.id, list) + return presences.map((p) => ({ + presenceInfo: p, + userInfo: collabs[p.id], + })) + }) + + const shadows = React.useMemo(() => { + return others.flatMap( + (other) => + other.presenceInfo.presence.activeFrames?.map((activeFrame) => ({ + activeFrame: activeFrame, + colorIndex: other.userInfo.colorIndex, + })) ?? [], + ) + }, [others]) + + const myActiveFrames = useEditorState( + Substores.restOfEditor, + (store) => store.editor.activeFrames, + 'MultiplayerShadows activeFrames', + ) + + const canvasScale = useEditorState( + Substores.canvasOffset, + (store) => store.editor.canvas.scale, + 'MultiplayerShadows canvasScale', + ) + const canvasOffset = useEditorState( + Substores.canvasOffset, + (store) => store.editor.canvas.roundedCanvasOffset, + 'MultiplayerShadows canvasOffset', + ) + + const editorRef = useRefEditorState((store) => ({ + jsxMetadata: store.editor.jsxMetadata, + })) + + useSelectorWithCallback( + Substores.canvas, + (store) => store.editor.canvas.interactionSession?.interactionData, + (interactionData) => { + if (interactionData?.type === 'DRAG') { + updateMyPresence({ + activeFrames: mapDropNulls(({ target, action, source }): PresenceActiveFrame | null => { + const { jsxMetadata } = editorRef.current + switch (target.type) { + case 'ACTIVE_FRAME_TARGET_RECT': + return { frame: target.rect, action, source } + case 'ACTIVE_FRAME_TARGET_PATH': + const frame = MetadataUtils.getFrameInCanvasCoords(target.path, jsxMetadata) + return { frame: zeroRectIfNullOrInfinity(frame), action, source } + default: + assertNever(target) + } + }, myActiveFrames), + }) + } else { + updateMyPresence({ activeFrames: [] }) + } + }, + 'MultiplayerShadows update presence shadows', + ) + + return ( + <> + {shadows.map((shadow, index) => { + const { frame, action, source } = shadow.activeFrame + const color = multiplayerColorFromIndex(shadow.colorIndex) + const framePosition = canvasPointToWindowPoint(frame, canvasScale, canvasOffset) + const sourcePosition = canvasPointToWindowPoint(source, canvasScale, canvasOffset) + return ( + +
+ + {activeFrameActionToString(action)} + + + ) + })} + + ) +}) +MultiplayerShadows.displayName = 'MultiplayerShadows' diff --git a/editor/src/components/editor/actions/actions.tsx b/editor/src/components/editor/actions/actions.tsx index 7049e1f2829e..491508909bc6 100644 --- a/editor/src/components/editor/actions/actions.tsx +++ b/editor/src/components/editor/actions/actions.tsx @@ -911,6 +911,7 @@ export function restoreEditorState( colorSwatches: currentEditor.colorSwatches, internalClipboard: currentEditor.internalClipboard, filesModifiedByAnotherUser: currentEditor.filesModifiedByAnotherUser, + activeFrames: currentEditor.activeFrames, } } diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 9fb59e08a0bd..0e13163027be 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -179,6 +179,7 @@ import { GridMenuWidth } from '../../canvas/stored-layout' import type { VariablesInScope } from '../../canvas/ui-jsx-canvas' import * as Y from 'yjs' import { isFeatureEnabled } from '../../../utils/feature-switches' +import type { ActiveFrame } from '../../canvas/commands/set-active-frames-command' const ObjectPathImmutable: any = OPI @@ -1467,6 +1468,7 @@ export interface EditorState { colorSwatches: Array internalClipboard: InternalClipboard filesModifiedByAnotherUser: Array + activeFrames: ActiveFrame[] } export function editorState( @@ -1548,6 +1550,7 @@ export function editorState( colorSwatches: Array, internalClipboardData: InternalClipboard, filesModifiedByAnotherUser: Array, + activeFrames: ActiveFrame[], ): EditorState { return { id: id, @@ -1628,6 +1631,7 @@ export function editorState( colorSwatches: colorSwatches, internalClipboard: internalClipboardData, filesModifiedByAnotherUser: filesModifiedByAnotherUser, + activeFrames: activeFrames, } } @@ -2503,6 +2507,7 @@ export function createEditorState(dispatch: EditorDispatch): EditorState { elements: [], }, filesModifiedByAnotherUser: [], + activeFrames: [], } } @@ -2878,6 +2883,7 @@ export function editorModelFromPersistentModel( elements: [], }, filesModifiedByAnotherUser: [], + activeFrames: [], } return editor } diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 15c86c08b5df..ff4d9a557a96 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -538,6 +538,12 @@ import { elementPaste } from '../actions/action-creators' import type { ProjectMetadataFromServer, ProjectServerState } from './project-server-state' import { projectServerState, projectMetadataFromServer } from './project-server-state' import type { VariablesInScope } from '../../canvas/ui-jsx-canvas' +import type { + ActiveFrame, + ActiveFrameTarget, + ActiveFrameTargetPath, + ActiveFrameTargetRect, +} from '../../canvas/commands/set-active-frames-command' export const ProjectMetadataFromServerKeepDeepEquality: KeepDeepEqualityCall = combine3EqualityCalls( @@ -4155,6 +4161,51 @@ export const TrueUpTargetKeepDeepEquality: KeepDeepEqualityCall = return keepDeepEqualityResult(newValue, false) } +export const ActiveFrameTargetPathKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall( + (data) => data.path, + ElementPathKeepDeepEquality, + (path) => ({ type: 'ACTIVE_FRAME_TARGET_PATH', path }), + ) + +export const ActiveFrameTargetRectKeepDeepEquality: KeepDeepEqualityCall = + combine1EqualityCall( + (data) => data.rect, + CanvasRectangleKeepDeepEquality, + (rect) => ({ type: 'ACTIVE_FRAME_TARGET_RECT', rect }), + ) + +export const ActiveFrameTargetKeepDeepEquality: KeepDeepEqualityCall = ( + oldValue, + newValue, +) => { + switch (oldValue.type) { + case 'ACTIVE_FRAME_TARGET_PATH': + if (oldValue.type === newValue.type) { + return ActiveFrameTargetPathKeepDeepEquality(oldValue, newValue) + } + break + case 'ACTIVE_FRAME_TARGET_RECT': + if (oldValue.type === newValue.type) { + return ActiveFrameTargetRectKeepDeepEquality(oldValue, newValue) + } + break + default: + assertNever(oldValue) + } + return keepDeepEqualityResult(newValue, false) +} + +export const FrameOrPathKeepDeepEquality: KeepDeepEqualityCall = combine3EqualityCalls( + (data) => data.target, + ActiveFrameTargetKeepDeepEquality, + (data) => data.action, + createCallWithTripleEquals(), + (data) => data.source, + CanvasRectangleKeepDeepEquality, + (target, action, source) => ({ target, action, source }), +) + export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( oldValue, newValue, @@ -4442,6 +4493,11 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( newValue.filesModifiedByAnotherUser, ) + const activeFramesResults = arrayDeepEquality(FrameOrPathKeepDeepEquality)( + oldValue.activeFrames, + newValue.activeFrames, + ) + const areEqual = idResult.areEqual && vscodeBridgeIdResult.areEqual && @@ -4519,7 +4575,8 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( refreshingDependenciesResults.areEqual && colorSwatchesResults.areEqual && internalClipboardResults.areEqual && - filesModifiedByAnotherUserResults.areEqual + filesModifiedByAnotherUserResults.areEqual && + activeFramesResults.areEqual if (areEqual) { return keepDeepEqualityResult(oldValue, true) @@ -4603,6 +4660,7 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( colorSwatchesResults.value, internalClipboardResults.value, filesModifiedByAnotherUserResults.value, + activeFramesResults.value, ) return keepDeepEqualityResult(newEditorState, false) diff --git a/editor/src/components/editor/store/store-hook-substore-helpers.ts b/editor/src/components/editor/store/store-hook-substore-helpers.ts index d85dabb2a6c0..fc620726cd1b 100644 --- a/editor/src/components/editor/store/store-hook-substore-helpers.ts +++ b/editor/src/components/editor/store/store-hook-substore-helpers.ts @@ -172,4 +172,5 @@ export const EmptyEditorStateForKeysOnly: EditorState = { elements: [], }, filesModifiedByAnotherUser: [], + activeFrames: [], }