diff --git a/editor/src/components/canvas/design-panel-root.tsx b/editor/src/components/canvas/design-panel-root.tsx index 51083fd8419c..1545a9ab563b 100644 --- a/editor/src/components/canvas/design-panel-root.tsx +++ b/editor/src/components/canvas/design-panel-root.tsx @@ -38,7 +38,7 @@ 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' -import { TitleBarEmpty, TitleBarUserProfile } from '../titlebar/title-bar' +import { TitleBarCode, TitleBarUserProfile } from '../titlebar/title-bar' import type { EditorAction } from '../editor/action-types' import { SettingsPane } from '../navigator/left-pane/settings-pane' import { MenuTab } from '../../uuiui/menu-tab' @@ -430,7 +430,7 @@ export const CodeEditorPane = React.memo((props) => { > {when( isFeatureEnabled('Draggable Floating Panels'), - , + , )}
{ 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.panels.every( @@ -116,6 +119,7 @@ export const GridPanelsContainer = React.memo(() => { style={{ position: 'absolute', gridColumn: 'canvas / span 1', gridRow: '1 / -1' }} /> {/* All future Panels need to be explicitly listed here */} + {nonEmptyColumns.map((columnIndex) => ( void + onDrop: (itemToMove: StoredPanel, newPosition: LayoutUpdate) => void + }) => { + const { onDrop, canDrop } = props + const { isDragActive, draggedPanel } = useGridPanelDragInfo() + + const dropBeforeColumn: LayoutUpdate = React.useMemo( + () => ({ + type: 'before-column', + columnIndex: IndexOfCanvas - 1, + }), + [], + ) + + const dropAfterColumn: LayoutUpdate = React.useMemo( + () => ({ + type: 'after-column', + columnIndex: IndexOfCanvas - 1, + }), + [], + ) + + 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 ( + <> +
+
+
+
+
+
+ + ) + }, +) + export const GridColumnResizeHandle = React.memo( (props: { columnIndex: number diff --git a/editor/src/components/canvas/grid-panels-state.tsx b/editor/src/components/canvas/grid-panels-state.tsx index da4b2a3dc720..26b42f2a8e24 100644 --- a/editor/src/components/canvas/grid-panels-state.tsx +++ b/editor/src/components/canvas/grid-panels-state.tsx @@ -10,8 +10,9 @@ import { } from '../inspector/common/inspector-utils' import invariant from '../../third-party/remix/invariant' import { useKeepShallowReferenceEquality } from '../../utils/react-performance' -import { atom, useAtom, useAtomValue } from 'jotai' +import { atom, useAtom, useAtomValue, useSetAtom, useStore } from 'jotai' import immutableUpdate from 'immutability-helper' +import { deepFreeze } from '../../utils/deep-freeze' export const GridMenuWidth = 268 export const GridMenuMinWidth = 200 @@ -23,9 +24,9 @@ export const NumberOfColumns = 4 export const IndexOfCanvas = 2 export const GridPanelVerticalGapHalf = 6 -export const GridVerticalExtraPadding = 4 +export const GridVerticalExtraPadding = -4 export const GridPanelHorizontalGapHalf = 6 -export const GridHorizontalExtraPadding = 4 +export const GridHorizontalExtraPadding = -4 export const ExtraHorizontalDropTargetPadding = 45 @@ -160,6 +161,8 @@ export function updateLayout( const panelToInsert = storedPanel(paneToMove) function insertPanel(layout: StoredLayout) { + deepFreeze(layout) + if (update.type === 'before-column' || update.type === 'after-column') { const atLeastOneEmptyColumn = layout.some((col) => col.panels.length === 0) if (!atLeastOneEmptyColumn) { @@ -189,11 +192,10 @@ export function updateLayout( const working = [...layout] // insert - working[update.columnIndex].panels = insert( - update.indexInColumn, - panelToInsert, - working[update.columnIndex].panels, - ) + working[update.columnIndex] = { + ...working[update.columnIndex], + panels: insert(update.indexInColumn, panelToInsert, working[update.columnIndex].panels), + } return removeOldPanel(working) } @@ -201,11 +203,10 @@ export function updateLayout( const working = [...layout] // insert - working[update.columnIndex].panels = insert( - update.indexInColumn + 1, - panelToInsert, - working[update.columnIndex].panels, - ) + working[update.columnIndex] = { + ...working[update.columnIndex], + panels: insert(update.indexInColumn + 1, panelToInsert, working[update.columnIndex].panels), + } return removeOldPanel(working) } @@ -259,6 +260,65 @@ export function updateLayout( return withEmptyColumnsInMiddle } +export function useUpdateGridPanelLayout(): (panelName: PanelName, update: LayoutUpdate) => void { + const setStoredState = useSetAtom(GridPanelsStateAtom) + + return React.useCallback( + (panelName: PanelName, update: LayoutUpdate) => { + setStoredState((stored) => { + const paneToMove: StoredPanel = (() => { + for (const column of stored) { + for (const panel of column.panels) { + if (panel.name === panelName) { + return panel + } + } + } + throw new Error(`Invariant error: we should have found a panel by now: '${panelName}'`) + })() + return updateLayout(stored, paneToMove, update) + }) + }, + [setStoredState], + ) +} + +export function useUpdateGridPanelLayoutPutCodeEditorBelowNavigator(): () => void { + const setStoredState = useSetAtom(GridPanelsStateAtom) + + return React.useCallback(() => { + setStoredState((stored) => { + const codeEditorPane: StoredPanel = (() => { + for (const column of stored) { + for (const panel of column.panels) { + if (panel.name === 'code-editor') { + return panel + } + } + } + throw new Error('Invariant error: we should have found a code-editor panel by now') + })() + const update: LayoutUpdate = (() => { + for (let columnIndex = 0; columnIndex < stored.length; columnIndex++) { + const column = stored[columnIndex] + for (let indexInColumn = 0; indexInColumn < column.panels.length; indexInColumn++) { + const panel = column.panels[indexInColumn] + if (panel.name === 'navigator') { + return { + type: 'after-index', + columnIndex: columnIndex, + indexInColumn: indexInColumn, + } + } + } + } + throw new Error('Invariant error: we should have found a navigator panel by now') + })() + return updateLayout(stored, codeEditorPane, update) + }) + }, [setStoredState]) +} + export function useColumnWidths(): [ Array, (columnIndex: number, newWidth: number) => void, diff --git a/editor/src/components/navigator/navigator-drag-layer.tsx b/editor/src/components/navigator/navigator-drag-layer.tsx index f5bf6b751223..7f99b644f195 100644 --- a/editor/src/components/navigator/navigator-drag-layer.tsx +++ b/editor/src/components/navigator/navigator-drag-layer.tsx @@ -2,7 +2,10 @@ import React from 'react' import { useDragLayer } from 'react-dnd' import type { RegularNavigatorEntry } from '../editor/store/editor-state' import { navigatorEntryToKey, regularNavigatorEntry } from '../editor/store/editor-state' -import type { NavigatorItemDragAndDropWrapperProps } from './navigator-item/navigator-item-dnd-container' +import { + NavigatorItemDragType, + type NavigatorItemDragAndDropWrapperProps, +} from './navigator-item/navigator-item-dnd-container' import type { WindowPoint } from '../../core/shared/math-utils' import { windowPoint, zeroPoint } from '../../core/shared/math-utils' import { ItemLabel } from './navigator-item/item-label' @@ -35,7 +38,8 @@ export const NavigatorDragLayer = React.memo(() => { [item?.elementPath], ) - const hidden = item == null + const draggedItemIsNavigatorItem = item != null && item.type === NavigatorItemDragType + const hidden = !draggedItemIsNavigatorItem const entryNavigatorDepth = useEditorState( Substores.metadata, diff --git a/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx b/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx index 344010baeb5b..6ca4e96cf00e 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item-dnd-container.tsx @@ -91,7 +91,10 @@ export interface DragSelection { index: number } +export const NavigatorItemDragType = 'navigator-item-drag-item' as const + export interface NavigatorItemDragAndDropWrapperPropsBase { + type: typeof NavigatorItemDragType index: number entryDepth: number appropriateDropTargetHint: DropTargetHint | null diff --git a/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx b/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx index ade1a40863d6..0e8ddae4ba90 100644 --- a/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx +++ b/editor/src/components/navigator/navigator-item/navigator-item-wrapper.tsx @@ -46,6 +46,7 @@ import { ConditionalClauseNavigatorItemContainer, ErrorNavigatorItemContainer, NavigatorItemContainer, + NavigatorItemDragType, SyntheticNavigatorItemContainer, } from './navigator-item-dnd-container' import { navigatorDepth } from '../navigator-utils' @@ -317,6 +318,7 @@ export const NavigatorItemWrapper: React.FunctionComponent< ) const navigatorItemProps: NavigatorItemDragAndDropWrapperPropsBase = { + type: NavigatorItemDragType, index: props.index, editorDispatch: dispatch, entryDepth: entryDepth, diff --git a/editor/src/components/titlebar/title-bar.tsx b/editor/src/components/titlebar/title-bar.tsx index d3912a9a0025..425964b1c2b9 100644 --- a/editor/src/components/titlebar/title-bar.tsx +++ b/editor/src/components/titlebar/title-bar.tsx @@ -11,8 +11,10 @@ import { Icons, LargerIcons, SimpleFlexRow, + SquareButton, UNSAFE_getIconURL, useColorTheme, + UtopiaTheme, } from '../../uuiui' import type { LoginState } from '../../uuiui-deps' import type { EditorAction } from '../editor/action-types' @@ -29,7 +31,11 @@ import { Substores, useEditorState } from '../editor/store/store-hook' import { RoundButton } from './buttons' import { TestMenu } from './test-menu' import { useGridPanelDraggable } from '../canvas/grid-panels-dnd' -import type { StoredPanel } from '../canvas/grid-panels-state' +import { + useUpdateGridPanelLayout, + type StoredPanel, + useUpdateGridPanelLayoutPutCodeEditorBelowNavigator, +} from '../canvas/grid-panels-state' interface ProjectTitleProps {} @@ -334,6 +340,56 @@ export const TitleBarEmpty = React.memo((props: { panelData: StoredPanel }) => { ) }) +export const TitleBarCode = React.memo((props: { panelData: StoredPanel }) => { + const { drag } = useGridPanelDraggable(props.panelData) + const theme = useColorTheme() + + const updatePanelLayout = useUpdateGridPanelLayout() + const onMaximize = React.useCallback(() => { + updatePanelLayout('code-editor', { type: 'before-column', columnIndex: 0 }) + }, [updatePanelLayout]) + + const onMinimize = useUpdateGridPanelLayoutPutCodeEditorBelowNavigator() + + return ( +
+
+
+ Code +
+ ) +}) + const TitleBar = React.memo(() => { const dispatch = useDispatch() const { loginState } = useEditorState( @@ -516,6 +572,7 @@ const TitleBar = React.memo(() => { paddingRight: 8, background: colorTheme.dynamicBlue.value, color: colorTheme.bg1.value, + borderRadius: 20, }} onClick={onClickLoginNewTab} > diff --git a/editor/src/core/shared/array-utils.ts b/editor/src/core/shared/array-utils.ts index b67993ce41fc..c33743a28ee9 100644 --- a/editor/src/core/shared/array-utils.ts +++ b/editor/src/core/shared/array-utils.ts @@ -385,7 +385,7 @@ export function difference( return result } -export function insert(index: number, element: T, array: Array): Array { +export function insert(index: number, element: T, array: ReadonlyArray): Array { const clampedIndex = clamp(0, array.length, index) return [...array.slice(0, clampedIndex), element, ...array.slice(clampedIndex, array.length)] }