From f6e2b6508437c5db75a9646f9127ef225f279bac Mon Sep 17 00:00:00 2001 From: Balazs Bajorics <2226774+balazsbajorics@users.noreply.github.com> Date: Fri, 22 Sep 2023 13:26:40 +0200 Subject: [PATCH] Feature/grid panes (#4200) --- editor/package.json | 2 + editor/pnpm-lock.yaml | 20 +- .../canvas/canvas-error-overlay.tsx | 62 ++ .../canvas/canvas-floating-toolbars.tsx | 46 ++ .../canvas/canvas-wrapper-component.tsx | 84 +-- .../components/canvas/design-panel-root.tsx | 195 ++---- .../canvas/floating-panels-state.spec.tsx | 195 ------ .../canvas/floating-panels-state.tsx | 639 ------------------ .../src/components/canvas/floating-panels.tsx | 444 ------------ editor/src/components/canvas/grid-panel.tsx | 145 ++++ .../canvas/grid-panels-container.tsx | 136 ++++ .../src/components/canvas/grid-panels-dnd.tsx | 70 ++ .../canvas/grid-panels-drag-targets.tsx | 106 +++ .../components/canvas/grid-panels-state.tsx | 225 ++++++ .../components/navigator/left-pane/index.tsx | 49 +- editor/src/components/titlebar/title-bar.tsx | 16 +- ...performance-regression-tests.spec.tsx.snap | 78 ++- .../performance-regression-tests.spec.tsx | 8 +- editor/src/core/shared/array-utils.ts | 5 + .../layout/resizable-flex-components.tsx | 1 + 20 files changed, 954 insertions(+), 1572 deletions(-) create mode 100644 editor/src/components/canvas/canvas-error-overlay.tsx create mode 100644 editor/src/components/canvas/canvas-floating-toolbars.tsx delete mode 100644 editor/src/components/canvas/floating-panels-state.spec.tsx delete mode 100644 editor/src/components/canvas/floating-panels-state.tsx delete mode 100644 editor/src/components/canvas/floating-panels.tsx create mode 100644 editor/src/components/canvas/grid-panel.tsx create mode 100644 editor/src/components/canvas/grid-panels-container.tsx create mode 100644 editor/src/components/canvas/grid-panels-dnd.tsx create mode 100644 editor/src/components/canvas/grid-panels-drag-targets.tsx create mode 100644 editor/src/components/canvas/grid-panels-state.tsx diff --git a/editor/package.json b/editor/package.json index 1571a218969a..68019ba7d6e9 100644 --- a/editor/package.json +++ b/editor/package.json @@ -118,6 +118,7 @@ "@svgr/plugin-jsx": "5.5.0", "@tippyjs/react": "4.1.0", "@types/fontfaceobserver": "0.0.6", + "@types/lodash.findlastindex": "4.6.7", "@types/react-syntax-highlighter": "11.0.4", "@types/w3c-css-typed-object-model-level-1": "^20180410.0.5", "@use-it/interval": "0.1.3", @@ -159,6 +160,7 @@ "keycode": "2.2.1", "localforage": "1.7.3", "lodash.clamp": "4.0.3", + "lodash.findlastindex": "4.6.0", "lru-cache": "7.10.1", "matter-js": "git://github.com/liabru/matter-js.git#e909b0466cea2051267bab92a38ccaa9bf7f154e", "mime-types": "2.1.24", diff --git a/editor/pnpm-lock.yaml b/editor/pnpm-lock.yaml index 823bea7b8ec0..0ae9d0cdff92 100644 --- a/editor/pnpm-lock.yaml +++ b/editor/pnpm-lock.yaml @@ -95,6 +95,7 @@ specifiers: '@types/jquery': 3.3.29 '@types/json-schema': 6.0.0 '@types/json5': 0.0.29 + '@types/lodash.findlastindex': 4.6.7 '@types/matter-js': 0.10.0 '@types/mime-types': 2.1.0 '@types/minimatch': 3.0.4 @@ -218,6 +219,7 @@ specifiers: livereloadify: 2.0.0 localforage: 1.7.3 lodash.clamp: 4.0.3 + lodash.findlastindex: 4.6.0 lru-cache: 7.10.1 matter-js: git://github.com/liabru/matter-js.git#e909b0466cea2051267bab92a38ccaa9bf7f154e mime-types: 2.1.24 @@ -344,6 +346,7 @@ dependencies: '@svgr/plugin-jsx': 5.5.0 '@tippyjs/react': 4.1.0_ef5jwxihqo6n7gxfmzogljlgcm '@types/fontfaceobserver': 0.0.6 + '@types/lodash.findlastindex': 4.6.7 '@types/react-syntax-highlighter': 11.0.4 '@types/w3c-css-typed-object-model-level-1': 20180410.0.5 '@use-it/interval': 0.1.3_react@18.1.0 @@ -385,6 +388,7 @@ dependencies: keycode: 2.2.1_ijco5rdhqzhtzo47bw5o33xhhu localforage: 1.7.3 lodash.clamp: 4.0.3 + lodash.findlastindex: 4.6.0 lru-cache: 7.10.1 matter-js: github.com/liabru/matter-js/e909b0466cea2051267bab92a38ccaa9bf7f154e mime-types: 2.1.24 @@ -4081,6 +4085,16 @@ packages: '@types/node': 16.10.2 dev: true + /@types/lodash.findlastindex/4.6.7: + resolution: {integrity: sha512-B1f1tlVFXAN4xTv9yytcY7G/eNHZjQ5xjHZdl+GhBLY+groZ/RRqHGBITTbaxxw3hZOpzVlbM/Ox5FI+LlHNmg==} + dependencies: + '@types/lodash': 4.14.198 + dev: false + + /@types/lodash/4.14.198: + resolution: {integrity: sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==} + dev: false + /@types/matter-js/0.10.0: resolution: {integrity: sha512-7KUBacbLrPF8bHf1rtI8/y2t3D2lmMKfQVfuessIl02x5OWsP/IRjo8Jn4r1/NuJlMbxyH72zPyNhS+TeRR77A==} dev: true @@ -6676,7 +6690,7 @@ packages: dev: true /component-indexof/0.0.3: - resolution: {integrity: sha512-puDQKvx/64HZXb4hBwIcvQLaLgux8o1CbWl39s41hrIIZDl1lJiD5jc22gj3RBeGK0ovxALDYpIbyjqDUUl0rw==} + resolution: {integrity: sha1-EdCRMSI5648yyPJa6csAL/6NPCQ=} dev: false /compressible/2.0.18: @@ -12053,6 +12067,10 @@ packages: resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=} dev: true + /lodash.findlastindex/4.6.0: + resolution: {integrity: sha512-wghZXum6rpsr8XsE0U3PZNaLtj1Q8m8Vn/hAgHtlDlzh49ZXNpcZmVbyTFRRjkKhzPAZ4CSFyxctNIk1BLmRBg==} + dev: false + /lodash.flattendeep/4.4.0: resolution: {integrity: sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=} dev: true diff --git a/editor/src/components/canvas/canvas-error-overlay.tsx b/editor/src/components/canvas/canvas-error-overlay.tsx new file mode 100644 index 000000000000..38b81e5d41be --- /dev/null +++ b/editor/src/components/canvas/canvas-error-overlay.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { useErrorOverlayRecords } from '../../core/shared/runtime-report-logs' +import { ReactErrorOverlay } from '../../third-party/react-error-overlay/react-error-overlay' +import { setFocus } from '../common/actions' +import { + clearHighlightedViews, + openCodeEditorFile, + switchEditorMode, +} from '../editor/actions/action-creators' +import { EditorModes } from '../editor/editor-modes' +import { useDispatch } from '../editor/store/dispatch-context' +import { useRefEditorState } from '../editor/store/store-hook' +import CanvasActions from './canvas-actions' +import { shouldShowErrorOverlay } from './canvas-utils' + +export const ErrorOverlayComponent = React.memo(() => { + const { errorRecords, overlayErrors } = useErrorOverlayRecords() + const overlayWillShow = shouldShowErrorOverlay(errorRecords, overlayErrors) + + const dispatch = useDispatch() + + const onOpenFile = React.useCallback( + (path: string) => { + dispatch([openCodeEditorFile(path, true), setFocus('codeEditor')]) + }, + [dispatch], + ) + + const postActionSessionInProgressRef = useRefEditorState( + (store) => store.postActionInteractionSession != null, + ) + + React.useEffect(() => { + if (overlayWillShow) { + if (postActionSessionInProgressRef.current) { + return + } + + // If this is showing, we need to clear any canvas drag state and apply the changes it would have resulted in, + // since that might have been the cause of the error being thrown, as well as switching back to select mode + setTimeout(() => { + // wrapping in a setTimeout so we don't dispatch from inside React lifecycle + + dispatch([ + CanvasActions.clearInteractionSession(true), + switchEditorMode(EditorModes.selectMode(null, false, 'none')), + clearHighlightedViews(), + ]) + }, 0) + } + }, [dispatch, overlayWillShow, postActionSessionInProgressRef]) + + return ( + + ) +}) +ErrorOverlayComponent.displayName = 'ErrorOverlayComponent' diff --git a/editor/src/components/canvas/canvas-floating-toolbars.tsx b/editor/src/components/canvas/canvas-floating-toolbars.tsx new file mode 100644 index 000000000000..252bb98d9c9c --- /dev/null +++ b/editor/src/components/canvas/canvas-floating-toolbars.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { FlexRow } from '../../uuiui' +import { CanvasToolbar } from '../editor/canvas-toolbar' +import { Substores, useEditorState } from '../editor/store/store-hook' +import { ErrorOverlayComponent } from './canvas-error-overlay' +import { SafeModeErrorOverlay } from './canvas-wrapper-component' +import { CanvasStrategyPicker } from './controls/select-mode/canvas-strategy-picker' + +export const CanvasFloatingToolbars = React.memo((props: { style: React.CSSProperties }) => { + const safeMode = useEditorState( + Substores.restOfEditor, + (store) => { + return store.editor.safeMode + }, + 'CanvasFloatingPanels safeMode', + ) + + return ( + + + + + + {/* The error overlays are deliberately the last here so they hide other canvas UI */} + {safeMode ? : } + + ) +}) +CanvasFloatingToolbars.displayName = 'CanvasFloatingToolbars' diff --git a/editor/src/components/canvas/canvas-wrapper-component.tsx b/editor/src/components/canvas/canvas-wrapper-component.tsx index e854ad1e1f43..fc9061eba36d 100644 --- a/editor/src/components/canvas/canvas-wrapper-component.tsx +++ b/editor/src/components/canvas/canvas-wrapper-component.tsx @@ -37,8 +37,10 @@ import { useDispatch } from '../editor/store/dispatch-context' import { shouldShowErrorOverlay } from './canvas-utils' import { useErrorOverlayRecords } from '../../core/shared/runtime-report-logs' import { FloatingPostActionMenu } from './controls/select-mode/post-action-menu' -import { FloatingPanelSizesAtom } from './floating-panels' import { isFeatureEnabled } from '../../utils/feature-switches' +import { unless } from '../../utils/react-conditionals' +import type { StandardLonghandProperties } from 'csstype' +import { CanvasFloatingToolbars } from './canvas-floating-toolbars' export function filterOldPasses(errorMessages: Array): Array { let passTimes: { [key: string]: number } = {} @@ -106,7 +108,6 @@ export const CanvasWrapperComponent = React.memo(() => { const scale = useEditorState(Substores.canvas, (store) => store.editor.canvas.scale, 'scale') const leftPanelWidthAtom = usePubSubAtomReadOnly(LeftPanelWidthAtom, AlwaysTrue) - const columnSize = usePubSubAtomReadOnly(FloatingPanelSizesAtom, AlwaysTrue) const leftPanelWidth = React.useMemo( () => (isNavigatorOverCanvas ? leftPanelWidthAtom + 10 : 0), [leftPanelWidthAtom, isNavigatorOverCanvas], @@ -162,33 +163,11 @@ export const CanvasWrapperComponent = React.memo(() => { dispatch={dispatch} /> ) : null} - - - - - - {/* The error overlays are deliberately the last here so they hide other canvas UI */} - {safeMode ? : } - + {unless( + isFeatureEnabled('Draggable Floating Panels'), + , + )} + { ) }) -const ErrorOverlayComponent = React.memo(() => { - const { errorRecords, overlayErrors } = useErrorOverlayRecords() - const overlayWillShow = shouldShowErrorOverlay(errorRecords, overlayErrors) - - const dispatch = useDispatch() - - const onOpenFile = React.useCallback( - (path: string) => { - dispatch([openCodeEditorFile(path, true), setFocus('codeEditor')]) - }, - [dispatch], - ) - - const postActionSessionInProgressRef = useRefEditorState( - (store) => store.postActionInteractionSession != null, - ) - - React.useEffect(() => { - if (overlayWillShow) { - if (postActionSessionInProgressRef.current) { - return - } - - // If this is showing, we need to clear any canvas drag state and apply the changes it would have resulted in, - // since that might have been the cause of the error being thrown, as well as switching back to select mode - setTimeout(() => { - // wrapping in a setTimeout so we don't dispatch from inside React lifecycle - - dispatch([ - CanvasActions.clearInteractionSession(true), - switchEditorMode(EditorModes.selectMode(null, false, 'none')), - clearHighlightedViews(), - ]) - }, 0) - } - }, [dispatch, overlayWillShow, postActionSessionInProgressRef]) - - return ( - - ) -}) - export const SafeModeErrorOverlay = React.memo(() => { const dispatch = useDispatch() const onTryAgain = React.useCallback(() => { diff --git a/editor/src/components/canvas/design-panel-root.tsx b/editor/src/components/canvas/design-panel-root.tsx index 78f668108ac3..a4f0fb79c59f 100644 --- a/editor/src/components/canvas/design-panel-root.tsx +++ b/editor/src/components/canvas/design-panel-root.tsx @@ -36,8 +36,9 @@ import { InsertMenuPane } from '../navigator/insert-menu-pane' import { CanvasToolbar } from '../editor/canvas-toolbar' import { useDispatch } from '../editor/store/dispatch-context' import { LeftPaneComponent } from '../navigator/left-pane' -import { FloatingPanelsContainer } from './floating-panels' -import type { Menu, Pane } from './floating-panels-state' +import { GridMenuWidth } from './grid-panels-state' +import { GridPanelsContainer } from './grid-panels-container' +import type { Menu, Pane, StoredPanel } from './grid-panels-state' import type { ResizableProps } from '../../uuiui-deps' import type { Direction } from 're-resizable/lib/resizer' import { isFeatureEnabled } from '../../utils/feature-switches' @@ -169,18 +170,7 @@ const DesignPanelRootInner = React.memo(() => { id='vscode-editor' style={{ height: 'calc(100% - 20px)', position: 'absolute', margin: 10, zIndex: 1 }} > - + , )} {unless( @@ -196,18 +186,7 @@ const DesignPanelRootInner = React.memo(() => { margin: 10, }} > - + , )} @@ -223,26 +202,10 @@ const DesignPanelRootInner = React.memo(() => { margin: 10, }} > - + , )} - {when(draggablePanelsEnabled, )} + {when(draggablePanelsEnabled, )} } @@ -270,56 +233,28 @@ export const DesignPanelRoot = React.memo(() => { DesignPanelRoot.displayName = 'DesignPanelRoot' interface ResizableRightPaneProps { - width: number - height: number - onResize: (menuName: 'inspector', direction: Direction, width: number, height: number) => void - setIsResizing: React.Dispatch> - resizableConfig: ResizableProps + panelData: StoredPanel } export const ResizableRightPane = React.memo((props) => { - const { onResize: onPanelResize, setIsResizing, width, height } = props + const defaultInspectorWidth = isFeatureEnabled('Draggable Floating Panels') + ? GridMenuWidth + : UtopiaTheme.layout.inspectorSmallWidth + const colorTheme = useColorTheme() const [, updateInspectorWidth] = useAtom(InspectorWidthAtom) - const [widthLocal, setWidthLocal] = React.useState(UtopiaTheme.layout.inspectorSmallWidth) + const [width, setWidth] = React.useState(defaultInspectorWidth) const resizableRef = React.useRef(null) - const onResizeStart = React.useCallback(() => { - setIsResizing('inspector') - }, [setIsResizing]) - const onResize = React.useCallback( - ( - event: MouseEvent | TouchEvent, - direction: ResizeDirection, - elementRef: HTMLElement, - delta: Size, - ) => { - const newWidth = resizableRef.current?.size.width - if (newWidth != null) { - // we have to use the instance ref to directly access the get size() getter, because re-resize's API only wants to tell us deltas, but we need the snapped width - if (isFeatureEnabled('Draggable Floating Panels')) { - onPanelResize('inspector', direction, newWidth, elementRef?.clientHeight) - } else { - setWidthLocal(newWidth) - } - updateInspectorWidth(newWidth > UtopiaTheme.layout.inspectorSmallWidth ? 'wide' : 'regular') - } - }, - [updateInspectorWidth, onPanelResize], - ) - const onResizeStop = React.useCallback( - ( - event: MouseEvent | TouchEvent, - direction: ResizeDirection, - elementRef: HTMLElement, - delta: Size, - ) => { - setIsResizing(null) - onResize(event, direction, elementRef, delta) - }, - [setIsResizing, onResize], - ) + const onResize = React.useCallback(() => { + const newWidth = resizableRef.current?.size.width + if (newWidth != null) { + // we have to use the instance ref to directly access the get size() getter, because re-resize's API only wants to tell us deltas, but we need the snapped width + setWidth(newWidth) + updateInspectorWidth(newWidth > defaultInspectorWidth ? 'wide' : 'regular') + } + }, [updateInspectorWidth, defaultInspectorWidth]) const selectedTab = useEditorState( Substores.restOfEditor, @@ -340,12 +275,12 @@ export const ResizableRightPane = React.memo((props) => ((props) => borderRadius: UtopiaTheme.panelStyles.panelBorderRadius, boxShadow: UtopiaTheme.panelStyles.shadows.medium, }} - onResizeStart={onResizeStart} + onResizeStart={onResize} onResize={onResize} - onResizeStop={onResizeStop} - {...props.resizableConfig} + onResizeStop={onResize} + snap={{ + x: [defaultInspectorWidth, UtopiaTheme.layout.inspectorLargeWidth], + }} + enable={{ + left: isFeatureEnabled('Draggable Floating Panels') ? false : true, + }} > - {when(isFeatureEnabled('Draggable Floating Panels'), )} + {when( + isFeatureEnabled('Draggable Floating Panels'), + , + )} ((props) => }) interface CodeEditorPaneProps { + panelData: StoredPanel small: boolean - width: number - height: number - onResize: (menuName: 'code-editor', direction: Direction, width: number, height: number) => void - setIsResizing: React.Dispatch> - resizableConfig: ResizableProps } export const CodeEditorPane = React.memo((props) => { - const { width, height, onResize: onPanelResize, setIsResizing, resizableConfig } = props const colorTheme = useColorTheme() const dispatch = useDispatch() const interfaceDesigner = useEditorState( @@ -401,11 +339,6 @@ export const CodeEditorPane = React.memo((props) => { ) const codeEditorEnabled = isCodeEditorEnabled() - const onResizeStart = React.useCallback(() => { - if (isFeatureEnabled('Draggable Floating Panels')) { - setIsResizing('code-editor') - } - }, [setIsResizing]) const onResizeStop = React.useCallback( ( event: MouseEvent | TouchEvent, @@ -414,62 +347,48 @@ export const CodeEditorPane = React.memo((props) => { delta: NumberSize, ) => { dispatch([EditorActions.resizeInterfaceDesignerCodePane(delta.width)]) - const newWidth = elementRef?.clientWidth - const newHeight = elementRef?.clientHeight - - if (isFeatureEnabled('Draggable Floating Panels')) { - onPanelResize('code-editor', direction, newWidth, newHeight) - setIsResizing(null) - } }, - [dispatch, onPanelResize, setIsResizing], - ) - const onResize = React.useCallback( - ( - event: MouseEvent | TouchEvent, - direction: ResizeDirection, - elementRef: HTMLElement, - delta: NumberSize, - ) => { - const newWidth = elementRef?.clientWidth - const newHeight = elementRef?.clientHeight - if (newWidth != null && newHeight != null) { - onPanelResize('code-editor', direction, newWidth, newHeight) - } - }, - [onPanelResize], + [dispatch], ) return ( - {when(isFeatureEnabled('Draggable Floating Panels'), )} + {when( + isFeatureEnabled('Draggable Floating Panels'), + , + )}
((props) => {
{when(codeEditorEnabled, )} diff --git a/editor/src/components/canvas/floating-panels-state.spec.tsx b/editor/src/components/canvas/floating-panels-state.spec.tsx deleted file mode 100644 index e98def9f96b3..000000000000 --- a/editor/src/components/canvas/floating-panels-state.spec.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { windowPoint } from '../../core/shared/math-utils' -import { - DefaultPanels, - dragPaneToNewPosition, - updatePanelPositionsBasedOnLocationAndSize, - updatePanelsToDefaultSizes, -} from './floating-panels-state' - -describe('dragPaneToNewPosition', () => { - const canvasSize = { width: 1000, height: 1000 } - it('drag navigator to the left of the code editor, then drag the code editor to the left of the navigator', () => { - const firstDragResult = dragPaneToNewPosition( - updatePanelPositionsBasedOnLocationAndSize( - updatePanelsToDefaultSizes(DefaultPanels, canvasSize), - canvasSize, - ), - canvasSize, - 'navigator', - 'leftMenu1', - windowPoint({ - x: 20, - y: 500, - }), - ) - const secondDragResult = dragPaneToNewPosition( - firstDragResult, - canvasSize, - 'code-editor', - 'leftMenu2', - windowPoint({ - x: 20, - y: 500, - }), - ) - expect(secondDragResult).toMatchInlineSnapshot(` - Object { - "panelContent": Object { - "leftMenu1": Array [ - Object { - "frame": Object { - "height": 1000, - "width": 500, - "x": 10, - "y": 0, - }, - "name": "code-editor", - "type": "pane", - }, - ], - "leftMenu2": Array [ - Object { - "frame": Object { - "height": 980, - "width": 260, - "x": 520, - "y": 0, - }, - "name": "navigator", - "type": "menu", - }, - ], - "rightMenu1": Array [ - Object { - "frame": Object { - "height": 980, - "width": 255, - "x": 735, - "y": 0, - }, - "name": "inspector", - "type": "menu", - }, - ], - "rightMenu2": Array [], - }, - } - `) - }) - it('drag navigator to the left of the code editor', () => { - const result = dragPaneToNewPosition( - updatePanelPositionsBasedOnLocationAndSize( - updatePanelsToDefaultSizes(DefaultPanels, canvasSize), - canvasSize, - ), - canvasSize, - 'navigator', - 'leftMenu1', - windowPoint({ - x: 20, - y: 500, - }), - ) - expect(result).toMatchInlineSnapshot(` - Object { - "panelContent": Object { - "leftMenu1": Array [ - Object { - "frame": Object { - "height": 980, - "width": 260, - "x": 10, - "y": 0, - }, - "name": "navigator", - "type": "menu", - }, - ], - "leftMenu2": Array [ - Object { - "frame": Object { - "height": 600, - "width": 500, - "x": 280, - "y": 0, - }, - "name": "code-editor", - "type": "pane", - }, - ], - "rightMenu1": Array [ - Object { - "frame": Object { - "height": 980, - "width": 255, - "x": 735, - "y": 0, - }, - "name": "inspector", - "type": "menu", - }, - ], - "rightMenu2": Array [], - }, - } - `) - }) - it('drag navigator to the left of the inspector', () => { - const result = dragPaneToNewPosition( - updatePanelPositionsBasedOnLocationAndSize( - updatePanelsToDefaultSizes(DefaultPanels, canvasSize), - canvasSize, - ), - canvasSize, - 'navigator', - 'leftMenu1', - windowPoint({ - x: 900, - y: 500, - }), - ) - - expect(result).toMatchInlineSnapshot(` - Object { - "panelContent": Object { - "leftMenu1": Array [ - Object { - "frame": Object { - "height": 1000, - "width": 500, - "x": 10, - "y": 0, - }, - "name": "code-editor", - "type": "pane", - }, - ], - "leftMenu2": Array [], - "rightMenu1": Array [ - Object { - "frame": Object { - "height": 646.6666666666666, - "width": 255, - "x": 735, - "y": 0, - }, - "name": "inspector", - "type": "menu", - }, - Object { - "frame": Object { - "height": 313.3333333333333, - "width": 255, - "x": 735, - "y": 656.6666666666666, - }, - "name": "navigator", - "type": "menu", - }, - ], - "rightMenu2": Array [], - }, - } - `) - }) -}) diff --git a/editor/src/components/canvas/floating-panels-state.tsx b/editor/src/components/canvas/floating-panels-state.tsx deleted file mode 100644 index 3fa283b14778..000000000000 --- a/editor/src/components/canvas/floating-panels-state.tsx +++ /dev/null @@ -1,639 +0,0 @@ -import type { Direction } from 're-resizable/lib/resizer' -import { eitherToMaybe, foldEither } from '../../core/shared/either' -import { modify, set, toFirst } from '../../core/shared/optics/optic-utilities' -import type { Size, WindowPoint, WindowRectangle } from '../../core/shared/math-utils' -import { rectContainsPoint } from '../../core/shared/math-utils' -import { - boundingRectangleArray, - rectContainsPointInclusive, - zeroWindowRect, -} from '../../core/shared/math-utils' -import { windowRectangle } from '../../core/shared/math-utils' -import { - filtered, - fromArrayIndex, - fromField, - fromObjectField, - objectValues, - traverseArray, -} from '../../core/shared/optics/optic-creators' -import type { Optic } from '../../core/shared/optics/optics' -import { UtopiaTheme } from '../../uuiui' -import { LeftPaneDefaultWidth } from '../editor/store/editor-state' - -export type Menu = 'inspector' | 'navigator' -export type Pane = 'code-editor' | 'preview' - -export const allMenusAndPanels: Array = [ - 'navigator', - 'code-editor', - 'inspector', - 'preview', -] - -export type PanelName = 'leftMenu1' | 'leftMenu2' | 'rightMenu1' | 'rightMenu2' - -export interface PanelData { - name: Menu | Pane - type: 'menu' | 'pane' - frame: WindowRectangle -} - -export interface PanelDataWithLocation extends PanelData { - location: PanelName - column: PanelColumn -} - -export type PanelColumn = Array - -export type PanelContent = { [location: string]: PanelColumn } - -export interface PanelState { - panelContent: PanelContent -} - -export const GapBetweenPanels = 10 - -export const panelStateToPanelContentOptic: Optic = - fromField('panelContent') - -export const panelStateToPanelColumns: Optic = - panelStateToPanelContentOptic.compose(objectValues()) - -export const panelStateToPanelDataOptic: Optic = - panelStateToPanelColumns.compose(traverseArray()) - -export function panelStateToColumnOptic(panelColumn: PanelName): Optic { - return panelStateToPanelContentOptic.compose(fromObjectField(panelColumn)) -} - -export function panelStateToNamedPanelOptic( - panelName: PanelData['name'], -): Optic { - return panelStateToPanelDataOptic.compose(filtered((panel) => panel.name === panelName)) -} - -export interface SizeConstraintsResizeValue { - minWidth: number - maxWidth: number - snap: { - x?: number[] - y?: number[] - } | null -} - -export interface SizeConstraintsValue { - resize: SizeConstraintsResizeValue | null - defaultSize: Size -} - -export const SizeConstraints: { - [key: string]: SizeConstraintsValue -} = { - inspector: { - resize: { - minWidth: UtopiaTheme.layout.inspectorSmallWidth, - maxWidth: UtopiaTheme.layout.inspectorLargeWidth, - snap: { x: [UtopiaTheme.layout.inspectorSmallWidth, UtopiaTheme.layout.inspectorLargeWidth] }, - }, - - defaultSize: { width: UtopiaTheme.layout.inspectorSmallWidth, height: 0 }, - }, - navigator: { - resize: { - minWidth: 240, - maxWidth: 350, - snap: null, - }, - defaultSize: { width: LeftPaneDefaultWidth, height: 0 }, - }, - 'code-editor': { - resize: null, - defaultSize: { - width: 500, - height: 600, - }, - }, -} - -export function emptyPanels(): PanelState { - return { - panelContent: { - leftMenu1: [], - leftMenu2: [], - rightMenu1: [], - rightMenu2: [], - }, - } -} - -export const DefaultPanels: PanelState = { - panelContent: { - leftMenu1: [ - { - name: 'navigator', - type: 'menu', - frame: windowRectangle({ x: 0, y: 0, width: LeftPaneDefaultWidth, height: 0 }), - }, - { - name: 'code-editor', - type: 'pane', - frame: windowRectangle({ x: 0, y: 0, width: 500, height: 600 }), - }, - ], - leftMenu2: [], - rightMenu1: [ - { - name: 'inspector', - type: 'menu', - frame: windowRectangle({ x: 0, y: 0, width: 255, height: 0 }), - }, - ], - rightMenu2: [], - }, -} - -export const DefaultSizes = { - left: 0, - right: 0, -} - -export function isMenuContainingPanel(panelColumn: PanelColumn): boolean { - return Object.values(panelColumn).some((value) => value.type === 'menu') -} - -export function updatePanelsToDefaultSizes( - currentPanelState: PanelState, - canvasSize: Size, -): PanelState { - let newPanelContent: PanelContent = emptyPanels().panelContent - for (const [columnName, columnContent] of Object.entries(currentPanelState.panelContent)) { - for (const panel of columnContent) { - let height = canvasSize.height - let width = panel.frame.width - - if (!isMenuContainingPanel(columnContent) && columnContent.length === 1) { - if (columnName === 'leftMenu1' || columnName === 'rightMenu2') { - height = canvasSize.height - } else { - height = Math.min(canvasSize.height, SizeConstraints['code-editor'].defaultSize.height) - } - width = SizeConstraints['code-editor'].defaultSize.width - } - if (isMenuContainingPanel(columnContent)) { - if (columnContent.length == 2) { - const panelNamesInColumn = columnContent.map((v) => v.name) - if ( - panelNamesInColumn.includes('navigator') && - panelNamesInColumn.includes('code-editor') - ) { - if (panel.name === 'code-editor') { - height = canvasSize.height / 3 - GapBetweenPanels * 2 - } else { - height = (canvasSize.height * 2) / 3 - GapBetweenPanels * 2 - } - } else if ( - panelNamesInColumn.includes('navigator') && - panelNamesInColumn.includes('inspector') - ) { - if (panel.name === 'navigator') { - height = canvasSize.height / 3 - GapBetweenPanels * 2 - } else { - height = (canvasSize.height * 2) / 3 - GapBetweenPanels * 2 - } - } else { - if (panel.name === 'code-editor') { - height = canvasSize.height / 3 - GapBetweenPanels * 2 - } else { - height = (canvasSize.height * 2) / 3 - GapBetweenPanels * 2 - } - } - } else { - height = - (canvasSize.height - GapBetweenPanels * (columnContent.length + 1)) / - columnContent.length - } - } - - if (columnContent.find((d) => d.name === 'inspector') != null) { - width = SizeConstraints['inspector'].defaultSize.width - } else if (columnContent.find((d) => d.name === 'navigator') != null) { - width = SizeConstraints['navigator'].defaultSize.width - } - - let columnToAddTo: PanelColumn - if (columnName in newPanelContent) { - columnToAddTo = newPanelContent[columnName] - } else { - columnToAddTo = [] - newPanelContent[columnName] = columnToAddTo - } - columnToAddTo.push({ - ...panel, - frame: windowRectangle({ - x: panel.frame.x, - y: panel.frame.y, - width: width, - height: height, - }), - }) - } - } - return { - panelContent: newPanelContent, - } -} - -export function updatePanelPositionsBasedOnLocationAndSize( - currentPanelsData: PanelState, - canvasSize: Size, -): PanelState { - let newPanelContent: PanelContent = emptyPanels().panelContent - for (const [columnName, columnContent] of Object.entries(currentPanelsData.panelContent)) { - columnContent.forEach((panel, panelIndex) => { - const y = columnContent.reduce((working, current, currentIndex) => { - if (currentIndex < panelIndex) { - return working + current.frame.height + GapBetweenPanels * (currentIndex + 1) - } - return working - }, 0) - - let x = 0 - switch (columnName) { - case 'leftMenu1': - x = GapBetweenPanels - break - case 'leftMenu2': - const leftMenu1FirstPanel = toFirst( - panelStateToColumnOptic('leftMenu1').compose(fromArrayIndex(0)), - currentPanelsData, - ) - x = foldEither( - () => { - return GapBetweenPanels - }, - (firstPanel) => { - return firstPanel.frame.width + GapBetweenPanels * 2 - }, - leftMenu1FirstPanel, - ) - break - case 'rightMenu1': - const rightMenu2FirstPanel = eitherToMaybe( - toFirst( - panelStateToColumnOptic('rightMenu2').compose(fromArrayIndex(0)), - currentPanelsData, - ), - ) - x = - canvasSize.width - - panel.frame.width - - (rightMenu2FirstPanel == null ? GapBetweenPanels : GapBetweenPanels * 2) - - (rightMenu2FirstPanel?.frame.width ?? 0) - break - case 'rightMenu2': - x = canvasSize.width - panel.frame.width - GapBetweenPanels - break - default: - break - } - - let columnToAddTo: PanelColumn - if (columnName in newPanelContent) { - columnToAddTo = newPanelContent[columnName] - } else { - columnToAddTo = [] - newPanelContent[columnName] = columnToAddTo - } - columnToAddTo.push({ - ...panel, - frame: windowRectangle({ - x: x, - y: y, - width: panel.frame.width, - height: panel.frame.height, - }), - }) - }) - } - return { - panelContent: newPanelContent, - } -} - -export function updateSizeOfPanel( - panelsData: PanelState, - canvasSize: Size, - menuOrPane: Menu | Pane, - currentPanel: PanelName, - direction: Direction, - width: number, - height: number, -): PanelState { - if (direction === 'left' || direction === 'right') { - const newPanelsData = modify( - panelStateToColumnOptic(currentPanel).compose(traverseArray()), - (data) => { - return { - ...data, - frame: windowRectangle({ - x: data.frame.x, - y: data.frame.y, - width: width, - height: data.frame.height, - }), - } - }, - panelsData, - ) - const withAdjustedPositions = updatePanelPositionsBasedOnLocationAndSize( - newPanelsData, - canvasSize, - ) - return withAdjustedPositions - } else { - const newPanelsData = modify( - panelStateToColumnOptic(currentPanel), - (panelColumn) => { - return panelColumn.map((data) => { - if (menuOrPane === data.name) { - return { - ...data, - frame: windowRectangle({ - x: data.frame.x, - y: data.frame.y, - width: data.frame.width, - height: height, - }), - } - } else { - // fill remaining space - const remainingSpace = - canvasSize.height - height - (panelColumn.length + 1) * GapBetweenPanels - const newHeight = remainingSpace / (panelColumn.length - 1) - return { - ...data, - frame: windowRectangle({ - x: data.frame.x, - y: data.frame.y, - width: data.frame.width, - height: newHeight, - }), - } - } - }) - }, - panelsData, - ) - const withAdjustedPositions = updatePanelPositionsBasedOnLocationAndSize( - newPanelsData, - canvasSize, - ) - return withAdjustedPositions - } -} - -export function findDropAreaInsideColumn( - newPosition: WindowPoint, - panelState: PanelState, -): PanelName | null { - for (const [location, panels] of Object.entries(panelState.panelContent)) { - for (const panel of panels) { - if (rectContainsPoint(panel.frame, newPosition)) { - return location as PanelName - } - } - } - return null -} - -export function shouldSwitchColumnsOnDropOutsideColumn( - dropTargetPanel: PanelName | null, - draggedPanel: PanelName, - panelState: PanelState, -): PanelName | null { - // maybe switch with existing panels if dragging to edge or when dropped to the side of a column - - if (dropTargetPanel != null) { - const leftColumn = panelState.panelContent['leftMenu1'] ?? [] - const leftColumn2 = panelState.panelContent['leftMenu2'] ?? [] - const rightColumn = panelState.panelContent['rightMenu1'] ?? [] - const rightColumn2 = panelState.panelContent['rightMenu2'] ?? [] - if ( - dropTargetPanel === 'leftMenu1' && - leftColumn.length > 0 && - (leftColumn2.length === 0 || (draggedPanel === 'leftMenu2' && leftColumn2.length === 1)) - ) { - // dragging to far left when something is already there - return 'leftMenu2' - } - if ( - dropTargetPanel === 'leftMenu2' && - leftColumn2.length > 0 && - (leftColumn.length === 0 || (draggedPanel === 'leftMenu1' && leftColumn.length === 1)) - ) { - // dragging to the right of the left panel 2 - return 'leftMenu1' - } - - if ( - dropTargetPanel === 'rightMenu1' && - rightColumn.length > 0 && - (rightColumn2.length === 0 || (draggedPanel === 'rightMenu2' && rightColumn2.length === 1)) - ) { - // dragging to the left of the right panel 1 - return 'rightMenu2' - } - - if ( - dropTargetPanel === 'rightMenu2' && - rightColumn2.length > 0 && - (rightColumn.length === 0 || (draggedPanel === 'rightMenu1' && rightColumn.length === 1)) - ) { - // dragging to far right when something is already there - return 'rightMenu1' - } - } - return null -} - -export function findDropAreaBeforeAfterColumn( - newPosition: WindowPoint, - panelState: PanelState, - canvasSize: Size, - draggedPanel: PanelName, -): { newPanel: PanelName; switchWithPanel: PanelName | null } | null { - const EdgeDropAreaWidth = 60 - const leftColumn = panelState.panelContent['leftMenu1'] ?? [] - const leftColumn2 = panelState.panelContent['leftMenu2'] ?? [] - const rightColumn = panelState.panelContent['rightMenu1'] ?? [] - const rightColumn2 = panelState.panelContent['rightMenu2'] ?? [] - - const leftColumnFrame = - boundingRectangleArray(leftColumn.map((data) => data.frame)) ?? zeroWindowRect - const leftColumn2Frame = - boundingRectangleArray(leftColumn2.map((data) => data.frame)) ?? zeroWindowRect - const rightColumnFrame = - boundingRectangleArray(rightColumn.map((data) => data.frame)) ?? - windowRectangle({ x: canvasSize.width, y: 0, width: 0, height: canvasSize.height }) - const rightColumn2Frame = - boundingRectangleArray(rightColumn2.map((data) => data.frame)) ?? - windowRectangle({ x: canvasSize.width, y: 0, width: 0, height: canvasSize.height }) - - const outerDropAreas: Array<{ - targetPanel: PanelName - frame: WindowRectangle - }> = [ - // between the left edge and the first left menu - { - targetPanel: 'leftMenu1', - frame: windowRectangle({ - x: 0, - y: 0, - width: EdgeDropAreaWidth, //GapBetweenPanels, - height: canvasSize.height, - }), - }, - // right side of the left menu - { - targetPanel: 'leftMenu2', - frame: windowRectangle({ - x: leftColumnFrame.x + leftColumnFrame.width, - y: 0, - width: 80, - height: canvasSize.height, - }), - }, - // left side of the right menu - { - targetPanel: 'rightMenu1', - frame: windowRectangle({ - x: rightColumnFrame.x - 80, - y: 0, - width: 80, - height: canvasSize.height, - }), - }, - // between the right edge and the last right menu - { - targetPanel: 'rightMenu2', - frame: windowRectangle({ - x: rightColumn2Frame.x + rightColumn2Frame.width - EdgeDropAreaWidth, //GapBetweenPanels, - y: 0, - width: canvasSize.width - rightColumn2Frame.x + rightColumn2Frame.width + EdgeDropAreaWidth, //GapBetweenPanels, - height: canvasSize.height, - }), - }, - ] - - const dropTargetOutside = - outerDropAreas.find((area) => rectContainsPointInclusive(area.frame, newPosition)) - ?.targetPanel ?? null - if (dropTargetOutside != null) { - return { - newPanel: dropTargetOutside, - switchWithPanel: shouldSwitchColumnsOnDropOutsideColumn( - dropTargetOutside, - draggedPanel, - panelState, - ), - } - } - return null -} - -export function dragPaneToNewPosition( - currentPanelsData: PanelState, - canvasSize: Size, - draggedMenuOrPane: Menu | Pane, - draggedPanel: PanelName, - newPosition: WindowPoint, -): PanelState { - // Determine where the panel has landed. - const droppedToOutsideOfAColumn = findDropAreaBeforeAfterColumn( - newPosition, - currentPanelsData, - canvasSize, - draggedPanel, - ) - let newPanel: PanelName | null = null - let switchWithPanel: PanelName | null = null - if (droppedToOutsideOfAColumn == null) { - newPanel = findDropAreaInsideColumn(newPosition, currentPanelsData) - } else { - newPanel = droppedToOutsideOfAColumn.newPanel - switchWithPanel = droppedToOutsideOfAColumn.switchWithPanel - } - const currentPanelData = eitherToMaybe( - toFirst(panelStateToNamedPanelOptic(draggedMenuOrPane), currentPanelsData), - ) - - // If there are any changes to make... - if ( - currentPanelData != null && - newPanel != null && - (draggedPanel != newPanel || switchWithPanel != null) - ) { - let updatedPanelState: PanelState = currentPanelsData - // Move what is dragged. - const draggedContent = eitherToMaybe( - toFirst(panelStateToNamedPanelOptic(draggedMenuOrPane), updatedPanelState), - ) - if (switchWithPanel != null) { - // Swapping this content out with another panel. - let updatedPanelContent: PanelContent = { ...updatedPanelState.panelContent } - const toSwitchWithCurrentColumn = updatedPanelContent[switchWithPanel] ?? [] - updatedPanelContent[switchWithPanel] = updatedPanelContent[newPanel] ?? [] - updatedPanelContent[newPanel] = toSwitchWithCurrentColumn - updatedPanelState = set(fromField('panelContent'), updatedPanelContent, updatedPanelState) - } - if (draggedContent != null) { - // Remove the entry from where it was... - updatedPanelState = modify( - panelStateToPanelColumns, - (panelColumn) => { - return panelColumn.filter((content) => content !== draggedContent) - }, - updatedPanelState, - ) - // ...Add the entry to where it should be. - updatedPanelState = modify( - panelStateToColumnOptic(newPanel), - (panelColumn) => { - return [...panelColumn, draggedContent] - }, - updatedPanelState, - ) - } - - const panelDataWithPositionsUpdated = updatePanelPositionsBasedOnLocationAndSize( - updatePanelsToDefaultSizes(updatedPanelState, canvasSize), - canvasSize, - ) - return panelDataWithPositionsUpdated - } else { - return currentPanelsData - } -} - -export function getOrderedPanelsForRendering(panelState: PanelState): Array { - let result: Array = [] - function addPanel(panelName: PanelData['name']): void { - for (const [panelColumnKey, panelColumnValue] of Object.entries(panelState.panelContent)) { - for (const panel of panelColumnValue) { - if (panel.name === panelName) { - result.push({ - ...panel, - location: panelColumnKey as PanelName, - column: panelColumnValue, - }) - return - } - } - } - } - for (const panelName of allMenusAndPanels) { - addPanel(panelName) - } - return result -} diff --git a/editor/src/components/canvas/floating-panels.tsx b/editor/src/components/canvas/floating-panels.tsx deleted file mode 100644 index 0bb1d4f90b84..000000000000 --- a/editor/src/components/canvas/floating-panels.tsx +++ /dev/null @@ -1,444 +0,0 @@ -import React from 'react' -import Draggable from 'react-draggable' -import type { DraggableEventHandler } from 'react-draggable' -import { CodeEditorPane, ResizableRightPane } from './design-panel-root' -import { LeftPaneComponent } from '../navigator/left-pane' -import { - AlwaysTrue, - atomWithPubSub, - usePubSubAtom, - usePubSubAtomReadOnly, -} from '../../core/shared/atom-with-pub-sub' -import type { WindowPoint, WindowRectangle } from '../../core/shared/math-utils' -import { - boundingRectangleArray, - rectanglesEqual, - windowPoint, - windowRectangle, - zeroWindowRect, -} from '../../core/shared/math-utils' -import { CanvasSizeAtom } from '../editor/store/editor-state' -import { mapDropNulls, stripNulls } from '../../core/shared/array-utils' -import { useColorTheme } from '../../uuiui' -import { when } from '../../utils/react-conditionals' -import type { Direction } from 're-resizable/lib/resizer' -import { usePrevious } from '../editor/hook-utils' -import { TitleHeight } from '../titlebar/title-bar' -import type { Menu, Pane, PanelColumn, PanelName, PanelState } from './floating-panels-state' -import { getOrderedPanelsForRendering } from './floating-panels-state' -import { findDropAreaBeforeAfterColumn } from './floating-panels-state' -import { dragPaneToNewPosition } from './floating-panels-state' -import { updateSizeOfPanel } from './floating-panels-state' -import { updatePanelPositionsBasedOnLocationAndSize } from './floating-panels-state' -import { isMenuContainingPanel } from './floating-panels-state' -import { updatePanelsToDefaultSizes } from './floating-panels-state' -import { DefaultPanels } from './floating-panels-state' -import { DefaultSizes } from './floating-panels-state' -import { GapBetweenPanels, SizeConstraints } from './floating-panels-state' - -export const FloatingPanelSizesAtom = atomWithPubSub({ - key: 'FloatingPanelSizesAtom', - defaultValue: DefaultSizes, -}) - -export const FloatingPanelsContainer = React.memo(() => { - const [panelsData, setPanelsData] = React.useState(DefaultPanels) - const [highlight, setHighlight] = React.useState(null) - - const canvasSize = usePubSubAtomReadOnly(CanvasSizeAtom, AlwaysTrue) - const prevCanvasSize = usePrevious(canvasSize) - - const [columnFrames, setColumnFrames] = usePubSubAtom<{ left: number; right: number }>( - FloatingPanelSizesAtom, - ) - const colorTheme = useColorTheme() - - // sets the left and right edges for other canvas elements like canvas toolbar - const setColumnFramesFromPanelsData = React.useCallback( - (latestPanelsData: PanelState) => { - const leftColumnFrame = - boundingRectangleArray( - (latestPanelsData.panelContent['leftMenu1'] ?? []).map((data) => data.frame), - ) ?? zeroWindowRect - const left2ColumnFrame = - boundingRectangleArray( - (latestPanelsData.panelContent['leftMenu2'] ?? []).map((data) => data.frame), - ) ?? zeroWindowRect - const rightColumnFrame = - boundingRectangleArray( - (latestPanelsData.panelContent['rightMenu1'] ?? []).map((data) => data.frame), - ) ?? zeroWindowRect - const right2ColumnFrame = - boundingRectangleArray( - (latestPanelsData.panelContent['rightMenu2'] ?? []).map((data) => data.frame), - ) ?? zeroWindowRect - setColumnFrames({ - left: leftColumnFrame.x + leftColumnFrame.width + left2ColumnFrame.width + GapBetweenPanels, - right: rightColumnFrame.width + right2ColumnFrame.width + GapBetweenPanels, - }) - }, - [setColumnFrames], - ) - - // update panel size to default, for example on drop and on browser resize - const getUpdatedPanelSizes = React.useCallback( - (currentPanelsData: PanelState) => { - return updatePanelsToDefaultSizes(currentPanelsData, canvasSize) - }, - [canvasSize], - ) - - // update menu positions based on their sizes and locations - const getUpdatedPanelPositions = React.useCallback( - (currentPanelsData: PanelState) => { - return updatePanelPositionsBasedOnLocationAndSize(currentPanelsData, canvasSize) - }, - [canvasSize], - ) - - const getUpdatedPanelSizesAndPositions = React.useCallback( - (currentPanelsData: PanelState) => { - const withPanelSizeUpdates = getUpdatedPanelSizes(currentPanelsData) - const withPositionsUpdated = getUpdatedPanelPositions(withPanelSizeUpdates) - return withPositionsUpdated - }, - [getUpdatedPanelSizes, getUpdatedPanelPositions], - ) - - // when a menu is dropped in or next to a column - const updateColumn = React.useCallback( - (draggedMenuOrPane: Menu | Pane, draggedPanel: PanelName, newPosition: WindowPoint) => { - setHighlight(null) - const panelDataWithPositionsUpdated = dragPaneToNewPosition( - panelsData, - canvasSize, - draggedMenuOrPane, - draggedPanel, - newPosition, - ) - setPanelsData(panelDataWithPositionsUpdated) - setColumnFramesFromPanelsData(panelDataWithPositionsUpdated) - }, - [canvasSize, panelsData, setPanelsData, setColumnFramesFromPanelsData], - ) - - // when resizing a menu it effects other elements in the same column and the top/left position in the next column - const updateSize = React.useCallback( - ( - menuOrPane: Menu | Pane, - currentPanel: PanelName, - direction: Direction, - width: number, - height: number, - ) => { - return updateSizeOfPanel( - panelsData, - canvasSize, - menuOrPane, - currentPanel, - direction, - width, - height, - ) - }, - [panelsData, canvasSize], - ) - - // TODO this is really not ready here, only works when dragging from left menu to outside of left menu - const showHighlight = React.useCallback( - (newPosition: WindowPoint, draggedPanel: PanelName) => { - const shouldDropOutsideOfColumn = findDropAreaBeforeAfterColumn( - newPosition, - panelsData, - canvasSize, - draggedPanel, - ) - - if ( - shouldDropOutsideOfColumn != null && - draggedPanel === 'leftMenu1' && - shouldDropOutsideOfColumn.newPanel === 'leftMenu1' && - shouldDropOutsideOfColumn.switchWithPanel != null - ) { - const targetFrame = panelsData.panelContent[shouldDropOutsideOfColumn.newPanel][0]?.frame - if (targetFrame != null) { - const x = shouldDropOutsideOfColumn.newPanel.includes('1') - ? targetFrame.x - : targetFrame.x + targetFrame.width - setHighlight( - windowRectangle({ - x: x - GapBetweenPanels / 2, - y: GapBetweenPanels, - width: 2, - height: canvasSize.height - GapBetweenPanels, - }), - ) - return - } - } - - // TODO fix this! - // const isAboveColumn = findDropAreaInsideColumn(newPosition, panelsData) - // if (isAboveColumn != null && draggedPanel != isAboveColumn) { - // const lastPanel = panelsData - // .filter((v) => v.location === isAboveColumn) - // .sort((first, second) => second.locationInColumn - first.locationInColumn)[0] - // if (lastPanel != null) { - // setHighlight( - // windowRectangle({ - // x: lastPanel.frame.x, - // y: lastPanel.frame.y + lastPanel.frame.height + GapBetweenPanels / 2, - // width: lastPanel.frame.width, - // height: 2, - // }), - // ) - // } - // } - }, - [panelsData, canvasSize, setHighlight], - ) - - // menus fill the available space - React.useEffect(() => { - if ( - prevCanvasSize?.width !== canvasSize.width || - prevCanvasSize?.height !== canvasSize.height - ) { - const updatedPanelsData = getUpdatedPanelSizesAndPositions(panelsData) - function shouldUpdatePanelsData(): boolean { - for (const panelColumnName of Object.keys(updatedPanelsData.panelContent)) { - const updatedColumn = updatedPanelsData.panelContent[panelColumnName] - const oldColumn = panelsData.panelContent[panelColumnName] ?? [] - if (updatedColumn.length !== oldColumn.length) { - return true - } - for (let columnIndex = 0; columnIndex < updatedColumn.length; columnIndex++) { - const updatedPanel = updatedColumn[columnIndex] - const oldPanel = oldColumn[columnIndex] - if (updatedPanel.frame == null || oldPanel.frame == null) { - return true - } - if (!rectanglesEqual(oldPanel.frame, updatedPanel.frame)) { - return true - } - } - } - return false - } - if (shouldUpdatePanelsData()) { - setPanelsData(updatedPanelsData) - setColumnFramesFromPanelsData(updatedPanelsData) - } - } - }, [ - canvasSize, - prevCanvasSize, - panelsData, - setPanelsData, - getUpdatedPanelSizesAndPositions, - setColumnFramesFromPanelsData, - ]) - - const orderedPanels = React.useMemo(() => { - return getOrderedPanelsForRendering(panelsData) - }, [panelsData]) - - return ( - <> - {orderedPanels.map((pane) => { - return ( - - ) - })} - {when( - highlight != null, -
, - )} - - ) -}) - -interface FloatingPanelProps { - name: Menu | Pane - type: 'menu' | 'pane' - frame: WindowRectangle - panelLocation: PanelName - updateColumn: (menuOrPane: Menu | Pane, currentPanel: PanelName, newPosition: WindowPoint) => void - onResize: ( - menuOrPane: Menu | Pane, - currentPanel: PanelName, - direction: Direction, - width: number, - height: number, - ) => void - onMenuDrag: (newPosition: WindowPoint, draggedPanel: PanelName) => void - columnData: PanelColumn -} - -export const FloatingPanel = React.memo((props) => { - const { columnData, panelLocation, name, frame, updateColumn, onResize, onMenuDrag } = props - const canvasSize = usePubSubAtomReadOnly(CanvasSizeAtom, AlwaysTrue) - - const [isDraggingOrResizing, setIsDraggingOrResizing] = React.useState(null) - const onDragStart = React.useCallback<(menuOrPane: Menu | Pane) => DraggableEventHandler>( - (menuOrPane: Menu | Pane) => { - return (e, data) => { - setIsDraggingOrResizing(menuOrPane) - } - }, - [setIsDraggingOrResizing], - ) - const onDrag = React.useCallback<(menuOrPane: Menu | Pane) => DraggableEventHandler>( - (menuOrPane: Menu | Pane) => { - return (e, data) => onMenuDrag(windowPoint({ x: data.x, y: data.y }), panelLocation) - }, - [onMenuDrag, panelLocation], - ) - - const onDragStop = React.useCallback<(menuOrPane: Menu | Pane) => DraggableEventHandler>( - (menuOrPane: Menu | Pane) => { - return (e, data) => { - updateColumn( - menuOrPane, - panelLocation, - windowPoint({ x: (e as any).clientX, y: (e as any).clientY }), - ) - setIsDraggingOrResizing(null) - } - }, - [panelLocation, updateColumn, setIsDraggingOrResizing], - ) - - const resizeMinMaxSnap = React.useMemo(() => { - const possibleConstraints = mapDropNulls((v) => SizeConstraints[v.name].resize, columnData) - const minWidth = Math.max(...possibleConstraints.map((v) => v.minWidth), 20) - const maxWidth = Math.min(...possibleConstraints.map((v) => v.maxWidth), canvasSize.width) - const snap = stripNulls(possibleConstraints.map((v) => v.snap))[0] // TODO what happens if there are multiple conflicting snapping menus - - return { minWidth, maxWidth, snap } - }, [columnData, canvasSize]) - - const resizeEventHandler = React.useCallback< - (menuOrPane: Menu | Pane, direction: Direction, width: number, height: number) => void - >( - (menuOrPane: Menu | Pane, direction: Direction, width: number, height: number) => - onResize(menuOrPane, panelLocation, direction, width, height), - [panelLocation, onResize], - ) - - const defaultMenuHeight = React.useMemo( - () => (canvasSize.height - (columnData.length + 1) * GapBetweenPanels) / columnData.length, - [columnData, canvasSize], - ) - - const alignment = React.useMemo( - () => (panelLocation === 'leftMenu1' || panelLocation === 'leftMenu2' ? 'left' : 'right'), - [panelLocation], - ) - - const resizeConfig = React.useMemo(() => { - return { - enable: { - left: alignment === 'right', - right: alignment === 'left', - bottom: !isMenuContainingPanel(columnData) || columnData.length > 1, - }, - minWidth: resizeMinMaxSnap.minWidth, - maxWidth: resizeMinMaxSnap.maxWidth, - minHeight: TitleHeight, - maxHeight: - !isMenuContainingPanel(columnData) || columnData.length > 1 - ? canvasSize.height - (columnData.length * TitleHeight) / 2 // what happens here? - : undefined, - snap: resizeMinMaxSnap.snap, - } - }, [alignment, canvasSize, resizeMinMaxSnap, columnData]) - - const draggablePanelComponent = (() => { - switch (name) { - case 'code-editor': - return ( - - ) - case 'inspector': - return ( - - ) - case 'navigator': - return ( - - ) - default: - return null - } - })() - - return ( - <> - -
- {draggablePanelComponent} -
-
- {when( - isDraggingOrResizing != null, - , - )} - - ) -}) diff --git a/editor/src/components/canvas/grid-panel.tsx b/editor/src/components/canvas/grid-panel.tsx new file mode 100644 index 000000000000..b34e46aab826 --- /dev/null +++ b/editor/src/components/canvas/grid-panel.tsx @@ -0,0 +1,145 @@ +import React from 'react' +import { NO_OP } from '../../core/shared/utils' +import { UtopiaTheme, colorTheme } from '../../uuiui' +import { LeftPanelMinWidth } from '../editor/store/editor-state' +import { LeftPaneComponent } from '../navigator/left-pane' +import { CodeEditorPane, ResizableRightPane } from './design-panel-root' +import { useGridPanelDragInfo, useGridPanelDropArea } from './grid-panels-dnd' +import type { GridPanelData, LayoutUpdate, StoredPanel } from './grid-panels-state' +import { + GridPanelHorizontalGapHalf, + GridPanelVerticalGapHalf, + GridPanelsNumberOfRows, +} from './grid-panels-state' + +interface GridPanelProps { + onDrop: (itemToMove: StoredPanel, newPosition: LayoutUpdate) => void + canDrop: (itemToMove: StoredPanel, newPosition: LayoutUpdate) => boolean + pane: GridPanelData +} + +export const GridPanel = React.memo((props) => { + const { onDrop, canDrop } = props + const { panel, index, span, order } = props.pane + + const { isDragActive, draggedPanel } = useGridPanelDragInfo() + + const dropAboveElement: LayoutUpdate = React.useMemo( + () => ({ + type: 'before-index', + indexInColumn: order, + columnIndex: index, + }), + [order, index], + ) + + const dropBelowElement: LayoutUpdate = React.useMemo( + () => ({ + type: 'after-index', + indexInColumn: order, + columnIndex: index, + }), + [order, index], + ) + + const canDropAbove = draggedPanel != null && canDrop(draggedPanel, dropAboveElement) + const canDropBelow = draggedPanel != null && canDrop(draggedPanel, dropBelowElement) + + const { drop: dropBefore, isOver: isOverBefore } = useGridPanelDropArea( + React.useCallback( + (itemToMove: StoredPanel) => onDrop(itemToMove, dropAboveElement), + [onDrop, dropAboveElement], + ), + ) + const { drop: dropAfter, isOver: isOverAfter } = useGridPanelDropArea( + React.useCallback( + (itemToMove: StoredPanel) => { + onDrop(itemToMove, dropBelowElement) + }, + [onDrop, dropBelowElement], + ), + ) + + const draggablePanelComponent = (() => { + switch (panel.name) { + case 'code-editor': + return ( + + ) + case 'inspector': + return + case 'navigator': + return + default: + return null + } + })() + + return ( +
-1 ? index + 1 : index}`, + gridRow: `span ${span}`, + order: order, + display: 'flex', + flexDirection: 'column', + contain: 'layout', + paddingLeft: GridPanelHorizontalGapHalf, + paddingRight: GridPanelHorizontalGapHalf, + paddingTop: GridPanelVerticalGapHalf, + paddingBottom: GridPanelVerticalGapHalf, + }} + > + {draggablePanelComponent} +
+
+
+
+
+
+
+ ) +}) diff --git a/editor/src/components/canvas/grid-panels-container.tsx b/editor/src/components/canvas/grid-panels-container.tsx new file mode 100644 index 000000000000..0899646b565f --- /dev/null +++ b/editor/src/components/canvas/grid-panels-container.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import { accumulate } from '../../core/shared/array-utils' +import { GridPanel } from './grid-panel' +import { ColumnDragTargets } from './grid-panels-drag-targets' +import type { LayoutUpdate, StoredPanel } from './grid-panels-state' +import { + GridHorizontalExtraPadding, + GridMenuDefaultPanels, + GridMenuWidth, + GridPaneWidth, + GridPanelHorizontalGapHalf, + GridPanelVerticalGapHalf, + GridVerticalExtraPadding, + NumberOfColumns, + storedLayoutToResolvedPanels, + updateLayout, + wrapAroundColIndex, +} from './grid-panels-state' +import { CanvasFloatingToolbars } from './canvas-floating-toolbars' + +export const GridPanelsContainer = React.memo(() => { + const [panelState, setPanelState] = React.useState(GridMenuDefaultPanels) + + const orderedPanels = React.useMemo(() => { + return storedLayoutToResolvedPanels(panelState) + }, [panelState]) + + const nonEmptyColumns = React.useMemo(() => { + return Array.from( + accumulate(new Set(), (acc: Set) => { + // we always include the first and last columns + acc.add(0) + acc.add(NumberOfColumns - 1) + + panelState.forEach((column, colIndex) => { + if (column.length > 0) { + acc.add(wrapAroundColIndex(colIndex)) + } + }) + }), + ) + }, [panelState]) + + const onDrop = React.useCallback( + (itemToMove: StoredPanel, newPosition: LayoutUpdate) => { + setPanelState((panels) => updateLayout(panels, itemToMove, newPosition)) + }, + [setPanelState], + ) + + const canDrop = React.useCallback( + (itemToMove: StoredPanel, newPosition: LayoutUpdate) => { + return true // for now, just enable all drop areas while we are tweaking the behavior + const wouldBePanelState = updateLayout(panelState, itemToMove, newPosition) + const wouldBePanelStateEqualsCurrentPanelState = panelState.every((column, colIndex) => + column.every( + (item, itemIndex) => item.name === wouldBePanelState[colIndex]?.[itemIndex]?.name, + ), + ) + + if (wouldBePanelStateEqualsCurrentPanelState) { + // if the drop results in no change, we don't allow it + return false + } + + return true + }, + [panelState], + ) + + const columnWidths: Array = React.useMemo( + () => + panelState.map((column) => { + if (column.length === 0) { + return `0px` + } else if (column.some((p) => p.type === 'menu')) { + return `${GridMenuWidth + GridPanelHorizontalGapHalf * 2}px` + } else { + return `${GridPaneWidth + GridPanelHorizontalGapHalf * 2}px` + } + }), + [panelState], + ) + + return ( +
+ + + + + {/* All future Panels need to be explicitly listed here */} + {nonEmptyColumns.map((columnIndex) => ( + + ))} +
+ ) +}) diff --git a/editor/src/components/canvas/grid-panels-dnd.tsx b/editor/src/components/canvas/grid-panels-dnd.tsx new file mode 100644 index 000000000000..47508ae683ef --- /dev/null +++ b/editor/src/components/canvas/grid-panels-dnd.tsx @@ -0,0 +1,70 @@ +import type { ConnectDragPreview, ConnectDragSource, ConnectDropTarget } from 'react-dnd' +import { useDrag, useDragLayer, useDrop } from 'react-dnd' +import { magnitude, windowPoint } from '../../core/shared/math-utils' +import type { StoredPanel } from './grid-panels-state' + +const FloatingPanelTitleBarType = 'floating-panel-title-bar' + +type FloatingPanelDragItem = { draggedPanel: StoredPanel } + +export function useGridPanelDraggable(draggedPanel: StoredPanel): { + drag: ConnectDragSource + dragPreview: ConnectDragPreview +} { + const [{ isDragging }, drag, dragPreview] = useDrag( + () => ({ + // "type" is required. It is used by the "accept" specification of drop targets. + type: FloatingPanelTitleBarType, + item: { draggedPanel: draggedPanel } as FloatingPanelDragItem, + // The collect function utilizes a "monitor" instance (see https://react-dnd.github.io/react-dnd/docs/overview/ for what this is) + // to pull important pieces of state from the DnD system. + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }), + [draggedPanel], + ) + + return { drag, dragPreview } +} + +export function useGridPanelDropArea(onDrop: (itemToMove: StoredPanel) => void): { + drop: ConnectDropTarget + isOver: boolean +} { + const [{ isOver }, drop] = useDrop( + () => ({ + // The type (or types) to accept - strings or symbols + accept: FloatingPanelTitleBarType, + drop: (droppedItem: FloatingPanelDragItem) => onDrop(droppedItem.draggedPanel), + // Props to collect + collect: (monitor) => ({ + isOver: monitor.isOver(), + }), + }), + [onDrop], + ) + + return { drop, isOver } +} + +const MinDragThreshold = 3 + +export function useGridPanelDragInfo(): { + isDragActive: boolean + draggedPanel: StoredPanel | undefined +} { + const { isDragActive, draggedPanel } = useDragLayer((monitor) => { + const dragVector = monitor.getDifferenceFromInitialOffset() + return { + isDragActive: + monitor.isDragging() && + monitor.getItemType() === FloatingPanelTitleBarType && + dragVector != null && + magnitude(windowPoint(dragVector)) > MinDragThreshold, + draggedPanel: monitor.getItem()?.draggedPanel, + } + }) + + return { isDragActive, draggedPanel } +} diff --git a/editor/src/components/canvas/grid-panels-drag-targets.tsx b/editor/src/components/canvas/grid-panels-drag-targets.tsx new file mode 100644 index 000000000000..065d1ea5c15c --- /dev/null +++ b/editor/src/components/canvas/grid-panels-drag-targets.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import { colorTheme } from '../../uuiui' +import { useGridPanelDragInfo, useGridPanelDropArea } from './grid-panels-dnd' +import type { LayoutUpdate, StoredPanel } from './grid-panels-state' +import { ExtraHorizontalDropTargetPadding, GridPanelHorizontalGapHalf } from './grid-panels-state' + +export const ColumnDragTargets = React.memo( + (props: { + columnIndex: number + canDrop: (itemToMove: StoredPanel, newPosition: LayoutUpdate) => void + onDrop: (itemToMove: StoredPanel, newPosition: LayoutUpdate) => void + }) => { + const { columnIndex, onDrop, canDrop } = props + const { isDragActive, draggedPanel } = useGridPanelDragInfo() + + const dropBeforeColumn: LayoutUpdate = React.useMemo( + () => ({ + type: 'before-column', + columnIndex: columnIndex, + }), + [columnIndex], + ) + + const dropAfterColumn: LayoutUpdate = React.useMemo( + () => ({ + type: 'after-column', + columnIndex: columnIndex, + }), + [columnIndex], + ) + + const canDropBefore = draggedPanel != null && canDrop(draggedPanel, dropBeforeColumn) + const canDropAfter = draggedPanel != null && canDrop(draggedPanel, dropAfterColumn) + + const { drop: dropBefore, isOver: isOverBefore } = useGridPanelDropArea( + React.useCallback( + (itemToMove: StoredPanel) => onDrop(itemToMove, dropBeforeColumn), + [onDrop, dropBeforeColumn], + ), + ) + + const { drop: dropAfter, isOver: isOverAfter } = useGridPanelDropArea( + React.useCallback( + (itemToMove: StoredPanel) => { + onDrop(itemToMove, dropAfterColumn) + }, + [onDrop, dropAfterColumn], + ), + ) + + return ( + <> +
-1 ? columnIndex + 1 : columnIndex} / span 1`, + display: isDragActive && canDropBefore ? 'block' : 'none', + width: 2 * ExtraHorizontalDropTargetPadding + 2 * GridPanelHorizontalGapHalf, + height: '100%', + left: -(ExtraHorizontalDropTargetPadding + GridPanelHorizontalGapHalf), + }} + > +
+
+
-1 ? columnIndex + 1 : columnIndex} / span 1`, + display: isDragActive && canDropAfter ? 'block' : 'none', + width: 2 * ExtraHorizontalDropTargetPadding + 2 * GridPanelHorizontalGapHalf, + height: '100%', + right: -(ExtraHorizontalDropTargetPadding + GridPanelHorizontalGapHalf), + }} + > +
+
+ + ) + }, +) diff --git a/editor/src/components/canvas/grid-panels-state.tsx b/editor/src/components/canvas/grid-panels-state.tsx new file mode 100644 index 000000000000..8c78a2decaf1 --- /dev/null +++ b/editor/src/components/canvas/grid-panels-state.tsx @@ -0,0 +1,225 @@ +import findLastIndex from 'lodash.findlastindex' +import { v4 as UUID } from 'uuid' +import { accumulate, insert, removeAll, removeIndexFromArray } from '../../core/shared/array-utils' +import { mod } from '../../core/shared/math-utils' +import { assertNever } from '../../core/shared/utils' + +export const GridMenuWidth = 260 +export const GridPaneWidth = 500 + +export const NumberOfColumns = 4 +export const IndexOfCanvas = 2 + +export const GridPanelVerticalGapHalf = 6 +export const GridVerticalExtraPadding = 4 +export const GridPanelHorizontalGapHalf = 6 +export const GridHorizontalExtraPadding = 4 + +export const ExtraHorizontalDropTargetPadding = 45 + +export const GridPanelsNumberOfRows = 12 + +export type Menu = 'inspector' | 'navigator' +export type Pane = 'code-editor' + +export const allMenusAndPanels: Array = [ + 'navigator', + 'code-editor', + 'inspector', + // 'preview', // Does this exist? +] + +export interface GridPanelData { + panel: StoredPanel + span: number + index: number + order: number +} + +export type PanelName = Menu | Pane + +export interface StoredPanel { + name: PanelName + type: 'menu' | 'pane' + uid: string +} + +function storedPanel({ name, type }: { name: PanelName; type: 'menu' | 'pane' }): StoredPanel { + return { + name: name, + type: type, + uid: UUID(), + } +} + +type StoredLayout = Array> + +type BeforeColumn = { + type: 'before-column' + columnIndex: number +} +type AfterColumn = { + type: 'after-column' + columnIndex: number +} +type ColumnUpdate = BeforeColumn | AfterColumn + +type BeforeIndex = { + type: 'before-index' + columnIndex: number + indexInColumn: number +} +type AfterIndex = { + type: 'after-index' + columnIndex: number + indexInColumn: number +} +type RowUpdate = BeforeIndex | AfterIndex +export type LayoutUpdate = ColumnUpdate | RowUpdate + +export const GridMenuDefaultPanels: StoredLayout = [ + [ + storedPanel({ name: 'navigator', type: 'menu' }), + storedPanel({ name: 'code-editor', type: 'pane' }), + ], + [], + [], + [storedPanel({ name: 'inspector', type: 'menu' })], +] + +export function storedLayoutToResolvedPanels(stored: StoredLayout): { + [index in PanelName]: GridPanelData +} { + const panels = accumulate<{ [index in PanelName]: GridPanelData }>({} as any, (acc) => { + stored.forEach((column, colIndex) => { + const panelsForColumn = column.length + column.forEach((panel, panelIndex) => { + acc[panel.name] = { + panel: panel, + span: GridPanelsNumberOfRows / panelsForColumn, // TODO introduce resize function + index: colIndex, + order: panelIndex, + } + }) + }) + }) + + return panels +} + +/** + * Returns the index in the wraparound annotation, currently the values are -2, -1, 0, 1 + */ +export function wrapAroundColIndex(index: number): number { + const normalized = normalizeColIndex(index) + if (normalized >= IndexOfCanvas) { + return normalized - NumberOfColumns + } else { + return normalized + } +} + +/** + * Normalizes the index to 0,1,2,3 + */ +function normalizeColIndex(index: number): number { + return mod(index, NumberOfColumns) +} + +export function updateLayout( + stored: StoredLayout, + paneToMove: StoredPanel, + update: LayoutUpdate, +): StoredLayout { + const panelToInsert = storedPanel(paneToMove) + + function insertPanel(layout: StoredLayout) { + if (update.type === 'before-column' || update.type === 'after-column') { + const atLeastOneEmptyColumn = layout.some((col) => col.length === 0) + if (!atLeastOneEmptyColumn) { + // the user wants to create a new column and fill it with the moved Panel. + // if there's zero empty columns, it means we cannot create a new column, so we must bail out + + return layout // BAIL OUT! TODO we should show a Toast + } + const newColumn: Array = [panelToInsert] + + const normalizedIndex = normalizeColIndex(update.columnIndex) + + const indexInArray = update.type === 'before-column' ? normalizedIndex : normalizedIndex + 1 + + const rightHandSide = normalizedIndex >= IndexOfCanvas + + const withElementInserted = insert(indexInArray, newColumn, layout) + const withOldPanelRemoved = removeOldPanel(withElementInserted) + + const indexOfFirstEmptyColumn = rightHandSide + ? findLastIndex(withOldPanelRemoved, (col) => col.length === 0) + : withOldPanelRemoved.findIndex((col) => col.length === 0) + + return removeIndexFromArray(indexOfFirstEmptyColumn, withOldPanelRemoved) + } + if (update.type === 'before-index') { + const working = [...layout] + + // insert + working[update.columnIndex] = insert( + update.indexInColumn, + panelToInsert, + working[update.columnIndex], + ) + + return removeOldPanel(working) + } + if (update.type === 'after-index') { + const working = [...layout] + + // insert + working[update.columnIndex] = insert( + update.indexInColumn + 1, + panelToInsert, + working[update.columnIndex], + ) + + return removeOldPanel(working) + } + + assertNever(update) + } + + function removeOldPanel(layout: StoredLayout) { + return layout.map((column) => { + return removeAll(column, [paneToMove], (l, r) => l.uid === r.uid) + }) + } + + function floatColumnsTowardsEdges(layout: StoredLayout) { + const leftSide = layout.slice(0, IndexOfCanvas) + const rightSideReversed = layout.slice(IndexOfCanvas).reverse() + const leftSideFixed = accumulate(new Array(leftSide.length).fill([]), (acc) => { + let indexInAccumulator = 0 + leftSide.forEach((column) => { + if (column.length > 0) { + acc[indexInAccumulator] = column + indexInAccumulator++ + } + }) + }) + const rightSideFixed = accumulate(new Array(rightSideReversed.length).fill([]), (acc) => { + let indexInAccumulator = rightSideReversed.length - 1 + rightSideReversed.forEach((column) => { + if (column.length > 0) { + acc[indexInAccumulator] = column + indexInAccumulator-- + } + }) + }) + return [...leftSideFixed, ...rightSideFixed] + } + + const withPanelInserted = insertPanel(stored) + const withEmptyColumnsInMiddle = floatColumnsTowardsEdges(withPanelInserted) + + // TODO we need to fix the sizes too! + return withEmptyColumnsInMiddle +} diff --git a/editor/src/components/navigator/left-pane/index.tsx b/editor/src/components/navigator/left-pane/index.tsx index e92ca66cece8..ee680c39de39 100644 --- a/editor/src/components/navigator/left-pane/index.tsx +++ b/editor/src/components/navigator/left-pane/index.tsx @@ -29,11 +29,12 @@ import { SettingsPane } from './settings-pane' import { NavigatorComponent } from '../navigator' import { usePubSubAtom } from '../../../core/shared/atom-with-pub-sub' import type { ResizeCallback } from 're-resizable' -import type { Menu, Pane } from '../../canvas/floating-panels-state' +import type { Menu, Pane } from '../../canvas/grid-panels-state' import type { Direction } from 're-resizable/lib/resizer' import { isFeatureEnabled } from '../../../utils/feature-switches' import { when } from '../../../utils/react-conditionals' import { TitleBarProjectTitle } from '../../titlebar/title-bar' +import type { StoredPanel } from '../../canvas/grid-panels-state' export interface LeftPaneProps { editorState: EditorState @@ -47,15 +48,10 @@ export const LeftPaneComponentId = 'left-pane' export const LeftPaneOverflowScrollId = 'left-pane-overflow-scroll' interface LeftPaneComponentProps { - width: number - height: number - onResize: (menuName: 'navigator', direction: Direction, width: number, height: number) => void - setIsResizing: React.Dispatch> - resizableConfig: ResizableProps + panelData: StoredPanel } export const LeftPaneComponent = React.memo((props) => { - const { onResize, setIsResizing, width, height } = props const selectedTab = useEditorState( Substores.restOfEditor, (store) => store.editor.leftMenu.selectedTab, @@ -98,30 +94,10 @@ export const LeftPaneComponent = React.memo((props) => { const [leftPanelWidth, setLeftPanelWidth] = usePubSubAtom(LeftPanelWidthAtom) const onLeftPanelResizeStop = React.useCallback( (_event, _direction, _ref, delta) => { - if (isFeatureEnabled('Draggable Floating Panels')) { - onResize('navigator', _direction, _ref?.clientWidth, _ref?.clientHeight) - setIsResizing(null) - } else { - setLeftPanelWidth((currentWidth) => currentWidth + delta.width) - } + setLeftPanelWidth((currentWidth) => currentWidth + delta.width) }, - [onResize, setIsResizing, setLeftPanelWidth], + [setLeftPanelWidth], ) - const onLeftPanelResize = React.useCallback( - (_event, _direction, _ref, delta) => { - if (isFeatureEnabled('Draggable Floating Panels')) { - const newWidth = _ref?.clientWidth - const newHeight = _ref?.clientHeight - if (newWidth != null && newHeight != null) { - onResize('navigator', _direction, newWidth, newHeight) - } - } - }, - [onResize], - ) - const onResizeStart = React.useCallback(() => { - setIsResizing('navigator') - }, [setIsResizing]) const leftMenuExpanded = useEditorState( Substores.restOfEditor, @@ -136,16 +112,15 @@ export const LeftPaneComponent = React.memo((props) => { return ( ((props) => { flexDirection: 'column', overflow: 'hidden', }} - {...props.resizableConfig} > - {when(isFeatureEnabled('Draggable Floating Panels'), )} + {when( + isFeatureEnabled('Draggable Floating Panels'), + , + )}
> = ({ ch ) } export const TitleHeight = 40 -export const TitleBarProjectTitle = React.memo(() => { +export const TitleBarProjectTitle = React.memo((props: { panelData: StoredPanel }) => { + const { drag } = useGridPanelDraggable(props.panelData) + const dispatch = useDispatch() const theme = useColorTheme() const projectName = useEditorState( @@ -120,6 +124,7 @@ export const TitleBarProjectTitle = React.memo(() => { return (
{ ) }) -export const TitleBarUserProfile = React.memo(() => { +export const TitleBarUserProfile = React.memo((props: { panelData: StoredPanel }) => { + const { drag } = useGridPanelDraggable(props.panelData) + const theme = useColorTheme() const { loginState } = useEditorState( Substores.restOfStore, @@ -239,6 +246,7 @@ export const TitleBarUserProfile = React.memo(() => { return (
{ ) }) -export const TitleBarEmpty = React.memo(() => { +export const TitleBarEmpty = React.memo((props: { panelData: StoredPanel }) => { + const { drag } = useGridPanelDraggable(props.panelData) const theme = useColorTheme() return (
{ const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`726`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`728`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -127,7 +127,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`771`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`773`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -183,7 +183,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`574`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`575`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) @@ -249,7 +249,7 @@ describe('React Render Count Tests -', () => { const renderCountAfter = renderResult.getNumberOfRenders() // if this breaks, GREAT NEWS but update the test please :) - expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`646`) + expect(renderCountAfter - renderCountBefore).toMatchInlineSnapshot(`647`) expect(renderResult.getRenderInfo()).toMatchSnapshot() }) }) diff --git a/editor/src/core/shared/array-utils.ts b/editor/src/core/shared/array-utils.ts index 4b886d1102b0..b67993ce41fc 100644 --- a/editor/src/core/shared/array-utils.ts +++ b/editor/src/core/shared/array-utils.ts @@ -445,6 +445,11 @@ export function arrayAccumulate(callback: (acc: Array) => void): ReadonlyA return accumulator } +export function accumulate(accumulator: T, callback: (acc: T) => void): Readonly { + callback(accumulator) + return accumulator +} + export function zip(one: A[], other: B[], make: (a: A, b: B) => C): C[] { const doZip = (oneInner: A[], otherInner: B[]) => oneInner.map((elem, idx) => make(elem, otherInner[idx])) diff --git a/editor/src/uuiui/widgets/layout/resizable-flex-components.tsx b/editor/src/uuiui/widgets/layout/resizable-flex-components.tsx index 4bda8086f897..06c6213bc2ab 100644 --- a/editor/src/uuiui/widgets/layout/resizable-flex-components.tsx +++ b/editor/src/uuiui/widgets/layout/resizable-flex-components.tsx @@ -22,6 +22,7 @@ export const ResizableFlexColumn: React.FunctionComponent< bottomRight: false, bottomLeft: false, topLeft: false, + ...props.enable, }} {...props} />