From a27a880537a291a4d3b0ff411302cf0ebce48bd7 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:07:31 +0100 Subject: [PATCH 1/7] fix user bar --- editor/src/components/user-bar.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/editor/src/components/user-bar.tsx b/editor/src/components/user-bar.tsx index 9b7b98c6e453..3930108c6aa8 100644 --- a/editor/src/components/user-bar.tsx +++ b/editor/src/components/user-bar.tsx @@ -65,14 +65,13 @@ const MultiplayerUserBar = React.memo(() => { const { user: myUser } = useMyUserAndPresence() const myName = normalizeMultiplayerName(myUser.name) - const others = useOthers((list) => - normalizeOthersList(myUser.id, list).map((other) => ({ - id: other.id, - name: myUser.name, - colorIndex: myUser.colorIndex, - picture: myUser.avatar, - })), - ) + const collabs = useStorage((store) => store.collaborators) + + const others = useOthers((list) => { + return normalizeOthersList(myUser.id, list).map((other) => { + return collabs[other.id] + }) + }) const visibleOthers = React.useMemo(() => { return others.slice(0, MAX_VISIBLE_OTHER_PLAYERS) @@ -136,7 +135,7 @@ const MultiplayerUserBar = React.memo(() => { name={multiplayerInitialsFromName(name)} tooltip={name} color={multiplayerColorFromIndex(other.colorIndex)} - picture={other.picture} + picture={other.avatar} border={true} coloredTooltip={true} onClick={toggleFollowing(other.id)} From 792209a6991671c97d67741ef6fb8512227bff70 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Mon, 20 Nov 2023 19:08:29 +0100 Subject: [PATCH 2/7] shadows (wip) --- editor/liveblocks.config.ts | 4 +- .../absolute-resize-bounding-box-strategy.tsx | 2 + .../strategies/reorder-utils.ts | 19 ++++ .../strategies/set-border-radius-strategy.tsx | 2 + .../strategies/set-flex-gap-strategy.tsx | 2 + .../strategies/set-padding-strategy.tsx | 4 + .../shared-move-strategies-helpers.ts | 13 +++ .../components/canvas/commands/commands.ts | 4 + .../commands/set-active-frames-command.ts | 61 +++++++++++ .../components/canvas/multiplayer-cursors.tsx | 102 +++++++++++++++++- .../src/components/editor/actions/actions.tsx | 1 + .../components/editor/store/editor-state.ts | 6 ++ .../store/store-deep-equality-instances.ts | 20 +++- .../store/store-hook-substore-helpers.ts | 1 + 14 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 editor/src/components/canvas/commands/set-active-frames-command.ts diff --git a/editor/liveblocks.config.ts b/editor/liveblocks.config.ts index 4fa4820d29a4..169ea31575f0 100644 --- a/editor/liveblocks.config.ts +++ b/editor/liveblocks.config.ts @@ -1,9 +1,10 @@ import { LiveObject } from '@liveblocks/client' import { 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 { projectIdToRoomId } from './src/core/shared/multiplayer' +import type { CanvasRectangle, CanvasVector, WindowPoint } from './src/core/shared/math-utils' +import type { ActiveFrameAction } from './src/components/canvas/commands/set-active-frames-command' export const liveblocksThrottle = 100 // ms @@ -19,6 +20,7 @@ export type Presence = { cursor: WindowPoint | null canvasScale: number | null canvasOffset: CanvasVector | null + shadows?: { action: ActiveFrameAction; frame: CanvasRectangle }[] } export function initialPresence(): Presence { 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..8f0df1a07f36 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 { 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,7 @@ export function absoluteResizeBoundingBoxStrategy( 'starting-metadata', ), queueTrueUpElement(childGroups.map(trueUpGroupElementChanged)), + setActiveFrames([{ frame: newFrame, action: 'resize' }]), ] }) 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..a685d929e55b 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts @@ -5,6 +5,7 @@ import type { CanvasVector } from '../../../../core/shared/math-utils' import { canvasRectangle, CanvasRectangle, + isFiniteRectangle, isInfinityRectangle, offsetPoint, rectContainsPoint, @@ -27,6 +28,7 @@ import { } from '../canvas-strategy-types' import type { InteractionSession } from '../interaction-state' import type { ElementInstanceMetadataMap } from '../../../../core/shared/element-template' +import { setActiveFrames } from '../../commands/set-active-frames-command' export function isReorderAllowed(siblings: Array): boolean { return siblings.every((sibling) => !isRootOfGeneratedElement(sibling)) @@ -83,10 +85,22 @@ export function applyReorderCommon( const newIndexFound = newIndex > -1 const newResultOrLastIndex = newIndexFound ? newIndex : lastReorderIdx + const targetFrame = + newResultOrLastIndex > -1 + ? MetadataUtils.getFrameInCanvasCoords( + siblings[newResultOrLastIndex], + canvasState.startingMetadata, + ) + : MetadataUtils.getFrameInCanvasCoords(target, canvasState.startingMetadata) if (newResultOrLastIndex === unpatchedIndex) { return strategyApplicationResult( [ + setActiveFrames( + targetFrame != null && isFiniteRectangle(targetFrame) + ? [{ frame: targetFrame, action: 'reorder' }] + : [], + ), updateHighlightedViews('mid-interaction', []), setElementsToRerenderCommand(siblings), setCursorCommand(CSSCursor.Move), @@ -98,6 +112,11 @@ export function applyReorderCommon( } else { return strategyApplicationResult( [ + setActiveFrames( + targetFrame != null && isFiniteRectangle(targetFrame) + ? [{ frame: targetFrame, action: 'reorder' }] + : [], + ), 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..81cbdee9ea44 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 { setActiveFrames } from '../../commands/set-active-frames-command' export const SetBorderRadiusStrategyId = 'SET_BORDER_RADIUS_STRATEGY' @@ -157,6 +158,7 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = ( ...commands(selectedElement), ...getAddOverflowHiddenCommands(selectedElement, canvasState.projectContents), setElementsToRerenderCommand(selectedElements), + setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-radius' }))), ]), } } 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..8cef973f47bc 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 @@ -38,6 +38,7 @@ import { import type { InteractionSession } from '../interaction-state' import { areAllSiblingsInOneDimensionFlexOrFlow } from './flow-reorder-helpers' import { colorTheme } from '../../../../uuiui' +import { setActiveFrames } from '../../commands/set-active-frames-command' export const SetFlexGapStrategyId = 'SET_FLEX_GAP_STRATEGY' @@ -170,6 +171,7 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( ), setCursorCommand(cursorFromFlexDirection(flexGap.direction)), setElementsToRerenderCommand([...selectedElements, ...children]), + setActiveFrames([{ path: selectedElement, action: 'set-gap' }]), ]) }, } 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..7385e989a158 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 { setActiveFrames } from '../../commands/set-active-frames-command' const StylePaddingProp = stylePropPathMappingFn('padding', styleStringInArray) const IndividualPaddingProps: Array = [ @@ -257,6 +258,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti value, ), ), + setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-padding' }))), ]) } @@ -273,6 +275,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti ]), ), setProperty('always', selectedElement, StylePaddingProp, paddingString), + setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-padding' }))), ]) } @@ -291,6 +294,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti value, ), ), + setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-padding' }))), ]) }, } 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..e35d6973a5f7 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 @@ -70,6 +70,7 @@ import { setCssLengthProperty, setValueKeepingOriginalUnit, } from '../../commands/set-css-length-command' +import { setActiveFrames } from '../../commands/set-active-frames-command' export interface MoveCommandsOptions { ignoreLocalFrame?: boolean @@ -134,6 +135,12 @@ export function applyMoveCommon( updateHighlightedViews('mid-interaction', []), setElementsToRerenderCommand(targets), setCursorCommand(CSSCursor.Select), + setActiveFrames( + commandsForSelectedElements.intendedBounds.map((b) => ({ + frame: b.frame, + action: 'move', // TODO this could also show "duplicate" when applicable + })), + ), ]) } else { const constrainedDragAxis = @@ -171,6 +178,12 @@ export function applyMoveCommon( ), setElementsToRerenderCommand([...targets, ...targetsForSnapping]), setCursorCommand(CSSCursor.Select), + setActiveFrames( + commandsForSelectedElements.intendedBounds.map((b) => ({ + frame: b.frame, + action: 'move', // TODO this could also show "duplicate" when applicable + })), + ), ]) } } 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..4c6167aa2169 --- /dev/null +++ b/editor/src/components/canvas/commands/set-active-frames-command.ts @@ -0,0 +1,61 @@ +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 'Set flex gap' + case 'set-padding': + return 'Set padding' + case 'set-radius': + return 'Set radius' + default: + assertNever(action) + } +} + +export type ActiveFrame = { + action: ActiveFrameAction + frame?: CanvasRectangle + path?: ElementPath +} + +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 f3b0b000ceb7..d05c2b57f893 100644 --- a/editor/src/components/canvas/multiplayer-cursors.tsx +++ b/editor/src/components/canvas/multiplayer-cursors.tsx @@ -13,6 +13,7 @@ import type { Presence, UserMeta } from '../../../liveblocks.config' import type { CanvasPoint } from '../../core/shared/math-utils' import { pointsEqual, windowPoint } from '../../core/shared/math-utils' import { multiplayerColorFromIndex, normalizeOthersList } from '../../core/shared/multiplayer' +import { isFiniteRectangle } from '../../core/shared/math-utils' import { assertNever } from '../../core/shared/utils' import { UtopiaTheme, useColorTheme } from '../../uuiui' import type { EditorAction } from '../editor/action-types' @@ -24,6 +25,9 @@ import { Substores, useEditorState } from '../editor/store/store-hook' import CanvasActions from './canvas-actions' import { canvasPointToWindowPoint, windowToCanvasCoordinates } from './dom-lookup' import { useAddMyselfToCollaborators } from '../../core/commenting/comment-hooks' +import { mapDropNulls } from '../../core/shared/array-utils' +import { MetadataUtils } from '../../core/model/element-metadata-utils' +import { activeFrameActionToString } from './commands/set-active-frames-command' export const MultiplayerPresence = React.memo(() => { const dispatch = useDispatch() @@ -81,8 +85,9 @@ export const MultiplayerPresence = React.memo(() => { return ( <> - + + ) }) @@ -323,3 +328,98 @@ const FollowingOverlay = React.memo(() => { ) }) FollowingOverlay.displayName = 'FollowingOverlay' + +const MultiplayerShadows = React.memo(() => { + const me = useSelf() + 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.shadows ?? []).map((shadow) => ({ + shadow: shadow, + colorIndex: other.userInfo.colorIndex, + })), + ) + }, [others]) + + const updateMyPresence = useUpdateMyPresence() + + const jsxMetadata = useEditorState(Substores.metadata, (store) => store.editor.jsxMetadata, '') + + const activeFrames = 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', + ) + + React.useEffect(() => { + updateMyPresence({ + shadows: mapDropNulls((activeFrame) => { + if (activeFrame.frame != null) { + return { frame: activeFrame.frame, action: activeFrame.action } + } else if (activeFrame.path != null) { + const rect = MetadataUtils.getFrameInCanvasCoords(activeFrame.path, jsxMetadata) + return rect != null && isFiniteRectangle(rect) + ? { frame: rect, action: activeFrame.action } + : null + } else { + return null + } + }, activeFrames), + }) + }, [activeFrames, updateMyPresence, jsxMetadata]) + + return ( + <> + {shadows.map((shadow, index) => { + const { frame, action } = shadow.shadow + const color = multiplayerColorFromIndex(shadow.colorIndex) + const position = canvasPointToWindowPoint(frame, 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 0c7e6717c5a0..2d86af1a66f1 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..c484ce17bf4e 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,7 @@ 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 } from '../../canvas/commands/set-active-frames-command' export const ProjectMetadataFromServerKeepDeepEquality: KeepDeepEqualityCall = combine3EqualityCalls( @@ -4155,6 +4156,16 @@ export const TrueUpTargetKeepDeepEquality: KeepDeepEqualityCall = return keepDeepEqualityResult(newValue, false) } +export const FrameOrPathKeepDeepEquality: KeepDeepEqualityCall = combine3EqualityCalls( + (data) => data.frame, + undefinableDeepEquality(CanvasRectangleKeepDeepEquality), + (data) => data.path, + undefinableDeepEquality(ElementPathKeepDeepEquality), + (data) => data.action, + createCallWithTripleEquals(), + (frame, path, action) => ({ frame, path, action }), +) + export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( oldValue, newValue, @@ -4442,6 +4453,11 @@ export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( newValue.filesModifiedByAnotherUser, ) + const activeFramesResults = arrayDeepEquality(FrameOrPathKeepDeepEquality)( + oldValue.activeFrames, + newValue.activeFrames, + ) + const areEqual = idResult.areEqual && vscodeBridgeIdResult.areEqual && @@ -4519,7 +4535,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 +4620,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: [], } From b8252fc98e6fe8e8f95a3bccf33d5a444b48582a Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:31:00 +0100 Subject: [PATCH 3/7] readability --- editor/liveblocks.config.ts | 7 ++- .../commands/set-active-frames-command.ts | 6 +-- .../components/canvas/multiplayer-cursors.tsx | 46 +++++++++++-------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/editor/liveblocks.config.ts b/editor/liveblocks.config.ts index 169ea31575f0..79692979e70a 100644 --- a/editor/liveblocks.config.ts +++ b/editor/liveblocks.config.ts @@ -20,7 +20,12 @@ export type Presence = { cursor: WindowPoint | null canvasScale: number | null canvasOffset: CanvasVector | null - shadows?: { action: ActiveFrameAction; frame: CanvasRectangle }[] + activeFrames?: PresenceActiveFrame[] +} + +export type PresenceActiveFrame = { + action: ActiveFrameAction + frame: CanvasRectangle } export function initialPresence(): Presence { diff --git a/editor/src/components/canvas/commands/set-active-frames-command.ts b/editor/src/components/canvas/commands/set-active-frames-command.ts index 4c6167aa2169..da7129671624 100644 --- a/editor/src/components/canvas/commands/set-active-frames-command.ts +++ b/editor/src/components/canvas/commands/set-active-frames-command.ts @@ -21,11 +21,11 @@ export function activeFrameActionToString(action: ActiveFrameAction): string { case 'resize': return 'Resize' case 'set-gap': - return 'Set flex gap' + return 'Gap' case 'set-padding': - return 'Set padding' + return 'Padding' case 'set-radius': - return 'Set radius' + return 'Radius' default: assertNever(action) } diff --git a/editor/src/components/canvas/multiplayer-cursors.tsx b/editor/src/components/canvas/multiplayer-cursors.tsx index d05c2b57f893..1cba578233ea 100644 --- a/editor/src/components/canvas/multiplayer-cursors.tsx +++ b/editor/src/components/canvas/multiplayer-cursors.tsx @@ -9,7 +9,7 @@ import { useStorage, useUpdateMyPresence, } from '../../../liveblocks.config' -import type { Presence, UserMeta } from '../../../liveblocks.config' +import type { Presence, PresenceActiveFrame, UserMeta } from '../../../liveblocks.config' import type { CanvasPoint } from '../../core/shared/math-utils' import { pointsEqual, windowPoint } from '../../core/shared/math-utils' import { multiplayerColorFromIndex, normalizeOthersList } from '../../core/shared/multiplayer' @@ -331,6 +331,8 @@ 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) @@ -339,20 +341,24 @@ const MultiplayerShadows = React.memo(() => { userInfo: collabs[p.id], })) }) + const shadows = React.useMemo(() => { - return others.flatMap((other) => - (other.presenceInfo.presence.shadows ?? []).map((shadow) => ({ - shadow: shadow, - colorIndex: other.userInfo.colorIndex, - })), + return others.flatMap( + (other) => + other.presenceInfo.presence.activeFrames?.map((activeFrame) => ({ + activeFrame: activeFrame, + colorIndex: other.userInfo.colorIndex, + })) ?? [], ) }, [others]) - const updateMyPresence = useUpdateMyPresence() - - const jsxMetadata = useEditorState(Substores.metadata, (store) => store.editor.jsxMetadata, '') + const jsxMetadata = useEditorState( + Substores.metadata, + (store) => store.editor.jsxMetadata, + 'MultiplayerShadows jsxMetadata', + ) - const activeFrames = useEditorState( + const myActiveFrames = useEditorState( Substores.restOfEditor, (store) => store.editor.activeFrames, 'MultiplayerShadows activeFrames', @@ -371,25 +377,25 @@ const MultiplayerShadows = React.memo(() => { React.useEffect(() => { updateMyPresence({ - shadows: mapDropNulls((activeFrame) => { - if (activeFrame.frame != null) { - return { frame: activeFrame.frame, action: activeFrame.action } - } else if (activeFrame.path != null) { - const rect = MetadataUtils.getFrameInCanvasCoords(activeFrame.path, jsxMetadata) - return rect != null && isFiniteRectangle(rect) - ? { frame: rect, action: activeFrame.action } + activeFrames: mapDropNulls(({ frame, path, action }): PresenceActiveFrame | null => { + if (frame != null) { + return { frame, action } + } else if (path != null) { + const canvasFrame = MetadataUtils.getFrameInCanvasCoords(path, jsxMetadata) + return canvasFrame != null && isFiniteRectangle(canvasFrame) + ? { frame: canvasFrame, action } : null } else { return null } - }, activeFrames), + }, myActiveFrames), }) - }, [activeFrames, updateMyPresence, jsxMetadata]) + }, [myActiveFrames, updateMyPresence, jsxMetadata]) return ( <> {shadows.map((shadow, index) => { - const { frame, action } = shadow.shadow + const { frame, action } = shadow.activeFrame const color = multiplayerColorFromIndex(shadow.colorIndex) const position = canvasPointToWindowPoint(frame, canvasScale, canvasOffset) return ( From 43ab3d6cc543c2fe61b287311c5d1142f151806b Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:19:10 +0100 Subject: [PATCH 4/7] better active frame, source shadow --- editor/liveblocks.config.ts | 8 +- .../absolute-resize-bounding-box-strategy.tsx | 10 +- .../strategies/reorder-utils.ts | 35 ++--- .../strategies/set-border-radius-strategy.tsx | 13 +- .../strategies/set-flex-gap-strategy.tsx | 18 ++- .../strategies/set-padding-strategy.tsx | 33 ++++- .../shared-move-strategies-helpers.ts | 13 +- .../commands/set-active-frames-command.ts | 42 +++++- .../components/canvas/multiplayer-cursors.tsx | 128 +++++++++++------- .../store/store-deep-equality-instances.ts | 52 ++++++- editor/src/core/shared/math-utils.ts | 6 + 11 files changed, 263 insertions(+), 95 deletions(-) diff --git a/editor/liveblocks.config.ts b/editor/liveblocks.config.ts index 3480db81308a..9e5331c943eb 100644 --- a/editor/liveblocks.config.ts +++ b/editor/liveblocks.config.ts @@ -1,10 +1,9 @@ -import { LiveObject } from '@liveblocks/client' -import { createClient } from '@liveblocks/client' +import { LiveObject, createClient } from '@liveblocks/client' import { createRoomContext } from '@liveblocks/react' import { getProjectID } from './src/common/env-vars' -import { projectIdToRoomId } from './src/core/shared/multiplayer' -import type { CanvasRectangle, CanvasVector, WindowPoint } from './src/core/shared/math-utils' 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 @@ -27,6 +26,7 @@ export type Presence = { export type PresenceActiveFrame = { action: ActiveFrameAction frame: CanvasRectangle + source: CanvasRectangle } export function initialPresence(): Presence { 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 8f0df1a07f36..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,7 +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 { setActiveFrames } from '../../commands/set-active-frames-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' @@ -264,7 +264,13 @@ export function absoluteResizeBoundingBoxStrategy( 'starting-metadata', ), queueTrueUpElement(childGroups.map(trueUpGroupElementChanged)), - setActiveFrames([{ frame: newFrame, action: 'resize' }]), + 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 a685d929e55b..127c30ee9edc 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts @@ -4,8 +4,7 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import type { CanvasVector } from '../../../../core/shared/math-utils' import { canvasRectangle, - CanvasRectangle, - isFiniteRectangle, + canvasRectangleOrZeroRect, isInfinityRectangle, offsetPoint, rectContainsPoint, @@ -21,14 +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 { setActiveFrames } from '../../commands/set-active-frames-command' +import { activeFrameTargetRect, setActiveFrames } from '../../commands/set-active-frames-command' export function isReorderAllowed(siblings: Array): boolean { return siblings.every((sibling) => !isRootOfGeneratedElement(sibling)) @@ -83,24 +78,26 @@ export function applyReorderCommon( isValidTarget, ) + const sourceFrame = canvasRectangleOrZeroRect( + MetadataUtils.getFrameInCanvasCoords(target, canvasState.startingMetadata) ?? null, + ) const newIndexFound = newIndex > -1 const newResultOrLastIndex = newIndexFound ? newIndex : lastReorderIdx - const targetFrame = + const targetFrame = canvasRectangleOrZeroRect( newResultOrLastIndex > -1 ? MetadataUtils.getFrameInCanvasCoords( siblings[newResultOrLastIndex], canvasState.startingMetadata, ) - : MetadataUtils.getFrameInCanvasCoords(target, canvasState.startingMetadata) + : MetadataUtils.getFrameInCanvasCoords(target, canvasState.startingMetadata), + ) if (newResultOrLastIndex === unpatchedIndex) { return strategyApplicationResult( [ - setActiveFrames( - targetFrame != null && isFiniteRectangle(targetFrame) - ? [{ frame: targetFrame, action: 'reorder' }] - : [], - ), + setActiveFrames([ + { action: 'reorder', target: activeFrameTargetRect(targetFrame), source: sourceFrame }, + ]), updateHighlightedViews('mid-interaction', []), setElementsToRerenderCommand(siblings), setCursorCommand(CSSCursor.Move), @@ -112,11 +109,9 @@ export function applyReorderCommon( } else { return strategyApplicationResult( [ - setActiveFrames( - targetFrame != null && isFiniteRectangle(targetFrame) - ? [{ frame: targetFrame, action: 'reorder' }] - : [], - ), + 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 81cbdee9ea44..db02e5cd3c9a 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 @@ -16,6 +16,7 @@ import { } from '../../../../core/shared/element-template' import type { CanvasPoint, CanvasVector, Size } from '../../../../core/shared/math-utils' import { + canvasRectangleOrZeroRect, canvasVector, clamp, product, @@ -75,7 +76,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 { setActiveFrames } from '../../commands/set-active-frames-command' +import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command' export const SetBorderRadiusStrategyId = 'SET_BORDER_RADIUS_STRATEGY' @@ -158,7 +159,15 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = ( ...commands(selectedElement), ...getAddOverflowHiddenCommands(selectedElement, canvasState.projectContents), setElementsToRerenderCommand(selectedElements), - setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-radius' }))), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-radius', + target: activeFrameTargetPath(path), + source: canvasRectangleOrZeroRect( + 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 8cef973f47bc..c16ad1a9aafe 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, + canvasRectangleOrZeroRect, + 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,7 +42,7 @@ import { import type { InteractionSession } from '../interaction-state' import { areAllSiblingsInOneDimensionFlexOrFlow } from './flow-reorder-helpers' import { colorTheme } from '../../../../uuiui' -import { setActiveFrames } from '../../commands/set-active-frames-command' +import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command' export const SetFlexGapStrategyId = 'SET_FLEX_GAP_STRATEGY' @@ -171,7 +175,15 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( ), setCursorCommand(cursorFromFlexDirection(flexGap.direction)), setElementsToRerenderCommand([...selectedElements, ...children]), - setActiveFrames([{ path: selectedElement, action: 'set-gap' }]), + setActiveFrames([ + { + action: 'set-gap', + target: activeFrameTargetPath(selectedElement), + source: canvasRectangleOrZeroRect( + 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 7385e989a158..b9c7bb2d8f64 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 @@ -48,6 +48,7 @@ import { flattenSelection, getMultiselectBounds } from './shared-move-strategies import type { CanvasPoint, CanvasVector } from '../../../../core/shared/math-utils' import { canvasPoint, + canvasRectangleOrZeroRect, canvasVector, isInfinityRectangle, roundTo, @@ -76,7 +77,7 @@ import { adjustCssLengthProperties, } from '../../commands/adjust-css-length-command' import type { ElementPathTrees } from '../../../../core/shared/element-path-tree' -import { setActiveFrames } from '../../commands/set-active-frames-command' +import { activeFrameTargetPath, setActiveFrames } from '../../commands/set-active-frames-command' const StylePaddingProp = stylePropPathMappingFn('padding', styleStringInArray) const IndividualPaddingProps: Array = [ @@ -258,7 +259,15 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti value, ), ), - setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-padding' }))), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-padding', + target: activeFrameTargetPath(path), + source: canvasRectangleOrZeroRect( + MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), + ), + })), + ), ]) } @@ -275,7 +284,15 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti ]), ), setProperty('always', selectedElement, StylePaddingProp, paddingString), - setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-padding' }))), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-padding', + target: activeFrameTargetPath(path), + source: canvasRectangleOrZeroRect( + MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), + ), + })), + ), ]) } @@ -294,7 +311,15 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti value, ), ), - setActiveFrames(selectedElements.map((path) => ({ path, action: 'set-padding' }))), + setActiveFrames( + selectedElements.map((path) => ({ + action: 'set-padding', + target: activeFrameTargetPath(path), + source: canvasRectangleOrZeroRect( + 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 e35d6973a5f7..d7b4e86a4491 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 @@ -19,6 +19,7 @@ import type { } from '../../../../core/shared/math-utils' import { boundingRectangleArray, + canvasRectangleOrZeroRect, canvasRectangleToLocalRectangle, canvasVector, nullIfInfinity, @@ -70,7 +71,7 @@ import { setCssLengthProperty, setValueKeepingOriginalUnit, } from '../../commands/set-css-length-command' -import { setActiveFrames } from '../../commands/set-active-frames-command' +import { activeFrameTargetRect, setActiveFrames } from '../../commands/set-active-frames-command' export interface MoveCommandsOptions { ignoreLocalFrame?: boolean @@ -137,8 +138,11 @@ export function applyMoveCommon( setCursorCommand(CSSCursor.Select), setActiveFrames( commandsForSelectedElements.intendedBounds.map((b) => ({ - frame: b.frame, action: 'move', // TODO this could also show "duplicate" when applicable + target: activeFrameTargetRect(b.frame), + source: canvasRectangleOrZeroRect( + MetadataUtils.getFrameInCanvasCoords(b.target, canvasState.startingMetadata), + ), })), ), ]) @@ -180,8 +184,11 @@ export function applyMoveCommon( setCursorCommand(CSSCursor.Select), setActiveFrames( commandsForSelectedElements.intendedBounds.map((b) => ({ - frame: b.frame, action: 'move', // TODO this could also show "duplicate" when applicable + target: activeFrameTargetRect(b.frame), + source: canvasRectangleOrZeroRect( + MetadataUtils.getFrameInCanvasCoords(b.target, canvasState.startingMetadata), + ), })), ), ]) diff --git a/editor/src/components/canvas/commands/set-active-frames-command.ts b/editor/src/components/canvas/commands/set-active-frames-command.ts index da7129671624..148a0e75e2b5 100644 --- a/editor/src/components/canvas/commands/set-active-frames-command.ts +++ b/editor/src/components/canvas/commands/set-active-frames-command.ts @@ -33,10 +33,48 @@ export function activeFrameActionToString(action: ActiveFrameAction): string { export type ActiveFrame = { action: ActiveFrameAction - frame?: CanvasRectangle - path?: ElementPath + 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 diff --git a/editor/src/components/canvas/multiplayer-cursors.tsx b/editor/src/components/canvas/multiplayer-cursors.tsx index 9724b6a774a0..d7534b79262d 100644 --- a/editor/src/components/canvas/multiplayer-cursors.tsx +++ b/editor/src/components/canvas/multiplayer-cursors.tsx @@ -14,7 +14,7 @@ import { getCollaborator, useAddMyselfToCollaborators } from '../../core/comment import { MetadataUtils } from '../../core/model/element-metadata-utils' import { mapDropNulls } from '../../core/shared/array-utils' import type { CanvasPoint } from '../../core/shared/math-utils' -import { isFiniteRectangle, pointsEqual, windowPoint } from '../../core/shared/math-utils' +import { canvasRectangleOrZeroRect, pointsEqual, windowPoint } from '../../core/shared/math-utils' import { multiplayerColorFromIndex, normalizeOthersList } from '../../core/shared/multiplayer' import { assertNever } from '../../core/shared/utils' import { UtopiaTheme, useColorTheme } from '../../uuiui' @@ -23,7 +23,7 @@ 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 } from '../editor/store/store-hook' import CanvasActions from './canvas-actions' import { activeFrameActionToString } from './commands/set-active-frames-command' import { canvasPointToWindowPoint, windowToCanvasCoordinates } from './dom-lookup' @@ -359,12 +359,6 @@ const MultiplayerShadows = React.memo(() => { ) }, [others]) - const jsxMetadata = useEditorState( - Substores.metadata, - (store) => store.editor.jsxMetadata, - 'MultiplayerShadows jsxMetadata', - ) - const myActiveFrames = useEditorState( Substores.restOfEditor, (store) => store.editor.activeFrames, @@ -382,54 +376,90 @@ const MultiplayerShadows = React.memo(() => { 'MultiplayerShadows canvasOffset', ) + const interactionData = useEditorState( + Substores.canvas, + (s) => s.editor.canvas.interactionSession?.interactionData, + 'MultiplayerShadows interactionData', + ) + + const editorRef = useRefEditorState((store) => ({ + jsxMetadata: store.editor.jsxMetadata, + })) + React.useEffect(() => { - updateMyPresence({ - activeFrames: mapDropNulls(({ frame, path, action }): PresenceActiveFrame | null => { - if (frame != null) { - return { frame, action } - } else if (path != null) { - const canvasFrame = MetadataUtils.getFrameInCanvasCoords(path, jsxMetadata) - return canvasFrame != null && isFiniteRectangle(canvasFrame) - ? { frame: canvasFrame, action } - : null - } else { - return null - } - }, myActiveFrames), - }) - }, [myActiveFrames, updateMyPresence, jsxMetadata]) + if (interactionData?.type === 'DRAG' || interactionData == null) { + 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: canvasRectangleOrZeroRect(frame), action, source } + default: + assertNever(target) + } + }, myActiveFrames), + }) + } + }, [myActiveFrames, updateMyPresence, editorRef, interactionData]) return ( <> {shadows.map((shadow, index) => { - const { frame, action } = shadow.activeFrame + const { frame, action, source } = shadow.activeFrame const color = multiplayerColorFromIndex(shadow.colorIndex) - const position = canvasPointToWindowPoint(frame, canvasScale, canvasOffset) + const framePosition = canvasPointToWindowPoint(frame, canvasScale, canvasOffset) + const sourcePosition = canvasPointToWindowPoint(source, canvasScale, canvasOffset) return ( - - {activeFrameActionToString(action)} - + +
+ + {activeFrameActionToString(action)} + + ) })} 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 c484ce17bf4e..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,7 +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 } from '../../canvas/commands/set-active-frames-command' +import type { + ActiveFrame, + ActiveFrameTarget, + ActiveFrameTargetPath, + ActiveFrameTargetRect, +} from '../../canvas/commands/set-active-frames-command' export const ProjectMetadataFromServerKeepDeepEquality: KeepDeepEqualityCall = combine3EqualityCalls( @@ -4156,14 +4161,49 @@ 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.frame, - undefinableDeepEquality(CanvasRectangleKeepDeepEquality), - (data) => data.path, - undefinableDeepEquality(ElementPathKeepDeepEquality), + (data) => data.target, + ActiveFrameTargetKeepDeepEquality, (data) => data.action, createCallWithTripleEquals(), - (frame, path, action) => ({ frame, path, action }), + (data) => data.source, + CanvasRectangleKeepDeepEquality, + (target, action, source) => ({ target, action, source }), ) export const EditorStateKeepDeepEquality: KeepDeepEqualityCall = ( diff --git a/editor/src/core/shared/math-utils.ts b/editor/src/core/shared/math-utils.ts index 87f9696d2d7f..9613622d5db7 100644 --- a/editor/src/core/shared/math-utils.ts +++ b/editor/src/core/shared/math-utils.ts @@ -136,6 +136,12 @@ export function forceFiniteRectangle( throw new Error('invariant: we expected a finite Rectangle') } +export function canvasRectangleOrZeroRect( + rectangle: MaybeInfinityCanvasRectangle | null, +): CanvasRectangle { + return rectangle != null && isFiniteRectangle(rectangle) ? rectangle : zeroCanvasRect +} + export function canvasRectangle(rectangle: null | undefined): null export function canvasRectangle(rectangle: SimpleRectangle): CanvasRectangle export function canvasRectangle( From ee3b786bd494a922700cbe127c66d41428deda94 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:21:12 +0100 Subject: [PATCH 5/7] reuse --- .../canvas/canvas-strategies/strategies/reorder-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 127c30ee9edc..8926ac476efa 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/reorder-utils.ts @@ -89,7 +89,7 @@ export function applyReorderCommon( siblings[newResultOrLastIndex], canvasState.startingMetadata, ) - : MetadataUtils.getFrameInCanvasCoords(target, canvasState.startingMetadata), + : sourceFrame, ) if (newResultOrLastIndex === unpatchedIndex) { From eb3b899eb2c238cf0e7b057e2fb3b7aaf2bf1d18 Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:54:37 +0100 Subject: [PATCH 6/7] zeroRectIfNullOrInfinity --- .../canvas-strategies/strategies/reorder-utils.ts | 6 +++--- .../strategies/set-border-radius-strategy.tsx | 3 +-- .../strategies/set-flex-gap-strategy.tsx | 4 ++-- .../strategies/set-padding-strategy.tsx | 7 +++---- .../strategies/shared-move-strategies-helpers.ts | 13 ++++--------- .../src/components/canvas/multiplayer-cursors.tsx | 4 ++-- editor/src/core/shared/math-utils.ts | 6 ------ 7 files changed, 15 insertions(+), 28 deletions(-) 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 8926ac476efa..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, - canvasRectangleOrZeroRect, isInfinityRectangle, offsetPoint, rectContainsPoint, + zeroRectIfNullOrInfinity, } from '../../../../core/shared/math-utils' import { absolute } from '../../../../utils/utils' import { CSSCursor } from '../../canvas-types' @@ -78,12 +78,12 @@ export function applyReorderCommon( isValidTarget, ) - const sourceFrame = canvasRectangleOrZeroRect( + const sourceFrame = zeroRectIfNullOrInfinity( MetadataUtils.getFrameInCanvasCoords(target, canvasState.startingMetadata) ?? null, ) const newIndexFound = newIndex > -1 const newResultOrLastIndex = newIndexFound ? newIndex : lastReorderIdx - const targetFrame = canvasRectangleOrZeroRect( + const targetFrame = zeroRectIfNullOrInfinity( newResultOrLastIndex > -1 ? MetadataUtils.getFrameInCanvasCoords( siblings[newResultOrLastIndex], 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 db02e5cd3c9a..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 @@ -16,7 +16,6 @@ import { } from '../../../../core/shared/element-template' import type { CanvasPoint, CanvasVector, Size } from '../../../../core/shared/math-utils' import { - canvasRectangleOrZeroRect, canvasVector, clamp, product, @@ -163,7 +162,7 @@ export const setBorderRadiusStrategy: CanvasStrategyFactory = ( selectedElements.map((path) => ({ action: 'set-radius', target: activeFrameTargetPath(path), - source: canvasRectangleOrZeroRect( + 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 c16ad1a9aafe..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 @@ -3,7 +3,7 @@ import { MetadataUtils } from '../../../../core/model/element-metadata-utils' import type { CanvasVector } from '../../../../core/shared/math-utils' import { canvasPoint, - canvasRectangleOrZeroRect, + zeroRectIfNullOrInfinity, canvasVector, } from '../../../../core/shared/math-utils' import { optionalMap } from '../../../../core/shared/optional-utils' @@ -179,7 +179,7 @@ export const setFlexGapStrategy: CanvasStrategyFactory = ( { action: 'set-gap', target: activeFrameTargetPath(selectedElement), - source: canvasRectangleOrZeroRect( + 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 b9c7bb2d8f64..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 @@ -48,7 +48,6 @@ import { flattenSelection, getMultiselectBounds } from './shared-move-strategies import type { CanvasPoint, CanvasVector } from '../../../../core/shared/math-utils' import { canvasPoint, - canvasRectangleOrZeroRect, canvasVector, isInfinityRectangle, roundTo, @@ -263,7 +262,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti selectedElements.map((path) => ({ action: 'set-padding', target: activeFrameTargetPath(path), - source: canvasRectangleOrZeroRect( + source: zeroRectIfNullOrInfinity( MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), ), })), @@ -288,7 +287,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti selectedElements.map((path) => ({ action: 'set-padding', target: activeFrameTargetPath(path), - source: canvasRectangleOrZeroRect( + source: zeroRectIfNullOrInfinity( MetadataUtils.getFrameInCanvasCoords(path, canvasState.startingMetadata), ), })), @@ -315,7 +314,7 @@ export const setPaddingStrategy: CanvasStrategyFactory = (canvasState, interacti selectedElements.map((path) => ({ action: 'set-padding', target: activeFrameTargetPath(path), - source: canvasRectangleOrZeroRect( + 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 d7b4e86a4491..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,15 +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, - canvasRectangleOrZeroRect, + zeroRectIfNullOrInfinity, canvasRectangleToLocalRectangle, canvasVector, nullIfInfinity, @@ -140,7 +135,7 @@ export function applyMoveCommon( commandsForSelectedElements.intendedBounds.map((b) => ({ action: 'move', // TODO this could also show "duplicate" when applicable target: activeFrameTargetRect(b.frame), - source: canvasRectangleOrZeroRect( + source: zeroRectIfNullOrInfinity( MetadataUtils.getFrameInCanvasCoords(b.target, canvasState.startingMetadata), ), })), @@ -186,7 +181,7 @@ export function applyMoveCommon( commandsForSelectedElements.intendedBounds.map((b) => ({ action: 'move', // TODO this could also show "duplicate" when applicable target: activeFrameTargetRect(b.frame), - source: canvasRectangleOrZeroRect( + source: zeroRectIfNullOrInfinity( MetadataUtils.getFrameInCanvasCoords(b.target, canvasState.startingMetadata), ), })), diff --git a/editor/src/components/canvas/multiplayer-cursors.tsx b/editor/src/components/canvas/multiplayer-cursors.tsx index d7534b79262d..8fa2b6cd417e 100644 --- a/editor/src/components/canvas/multiplayer-cursors.tsx +++ b/editor/src/components/canvas/multiplayer-cursors.tsx @@ -14,7 +14,7 @@ import { getCollaborator, useAddMyselfToCollaborators } from '../../core/comment import { MetadataUtils } from '../../core/model/element-metadata-utils' import { mapDropNulls } from '../../core/shared/array-utils' import type { CanvasPoint } from '../../core/shared/math-utils' -import { canvasRectangleOrZeroRect, 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' @@ -396,7 +396,7 @@ const MultiplayerShadows = React.memo(() => { return { frame: target.rect, action, source } case 'ACTIVE_FRAME_TARGET_PATH': const frame = MetadataUtils.getFrameInCanvasCoords(target.path, jsxMetadata) - return { frame: canvasRectangleOrZeroRect(frame), action, source } + return { frame: zeroRectIfNullOrInfinity(frame), action, source } default: assertNever(target) } diff --git a/editor/src/core/shared/math-utils.ts b/editor/src/core/shared/math-utils.ts index 9613622d5db7..87f9696d2d7f 100644 --- a/editor/src/core/shared/math-utils.ts +++ b/editor/src/core/shared/math-utils.ts @@ -136,12 +136,6 @@ export function forceFiniteRectangle( throw new Error('invariant: we expected a finite Rectangle') } -export function canvasRectangleOrZeroRect( - rectangle: MaybeInfinityCanvasRectangle | null, -): CanvasRectangle { - return rectangle != null && isFiniteRectangle(rectangle) ? rectangle : zeroCanvasRect -} - export function canvasRectangle(rectangle: null | undefined): null export function canvasRectangle(rectangle: SimpleRectangle): CanvasRectangle export function canvasRectangle( From 163cca700929156e1e0ce55a8753cdee9011284f Mon Sep 17 00:00:00 2001 From: Federico Ruggi <1081051+ruggi@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:05:54 +0100 Subject: [PATCH 7/7] useSelectorWithCallback --- .../components/canvas/multiplayer-cursors.tsx | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/editor/src/components/canvas/multiplayer-cursors.tsx b/editor/src/components/canvas/multiplayer-cursors.tsx index 8fa2b6cd417e..d246d1b433b0 100644 --- a/editor/src/components/canvas/multiplayer-cursors.tsx +++ b/editor/src/components/canvas/multiplayer-cursors.tsx @@ -23,7 +23,12 @@ 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, useRefEditorState } 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' @@ -376,34 +381,35 @@ const MultiplayerShadows = React.memo(() => { 'MultiplayerShadows canvasOffset', ) - const interactionData = useEditorState( - Substores.canvas, - (s) => s.editor.canvas.interactionSession?.interactionData, - 'MultiplayerShadows interactionData', - ) - const editorRef = useRefEditorState((store) => ({ jsxMetadata: store.editor.jsxMetadata, })) - React.useEffect(() => { - if (interactionData?.type === 'DRAG' || interactionData == null) { - 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), - }) - } - }, [myActiveFrames, updateMyPresence, editorRef, interactionData]) + 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 ( <>