refactor: unifies various ui states into single enum

This commit is contained in:
Mark Mankarious
2023-10-11 18:07:42 +01:00
parent 034a8490e3
commit 6a0a3982b7
9 changed files with 126 additions and 91 deletions

View File

@@ -9,6 +9,7 @@ import {
NodeInput,
ConnectorInput,
RectangleInput,
IsoflowProps,
InitialScene
} from 'src/types';
import { sceneToSceneInput } from 'src/utils';
@@ -22,25 +23,15 @@ import { UiStateProvider, useUiStateStore } from 'src/stores/uiStateStore';
import { INITIAL_SCENE } from 'src/config';
import { useIconCategories } from './hooks/useIconCategories';
interface Props {
initialScene?: InitialScene;
disableInteractions?: boolean;
onSceneUpdated?: (scene: SceneInput) => void;
width?: number | string;
height?: number | string;
debugMode?: boolean;
hideMainMenu?: boolean;
}
const App = ({
initialScene,
width = '100%',
height = '100%',
disableInteractions: disableInteractionsProp,
onSceneUpdated,
debugMode = false,
hideMainMenu = false
}: Props) => {
hideMainMenu = false,
editorMode = 'EDITABLE'
}: IsoflowProps) => {
const prevInitialScene = useRef<SceneInput>(INITIAL_SCENE);
const [isReady, setIsReady] = useState(false);
useWindowUtils();
@@ -59,16 +50,9 @@ const App = ({
const { setIconCategoriesState } = useIconCategories();
useEffect(() => {
uiActions.setHideMainMenu(hideMainMenu);
uiActions.setZoom(initialScene?.zoom ?? 1);
uiActions.setDisableInteractions(Boolean(disableInteractionsProp));
}, [
initialScene?.zoom,
disableInteractionsProp,
sceneActions,
uiActions,
hideMainMenu
]);
uiActions.setEditorMode(editorMode);
}, [initialScene?.zoom, editorMode, sceneActions, uiActions, hideMainMenu]);
useEffect(() => {
if (!initialScene || prevInitialScene.current === initialScene) return;
@@ -114,7 +98,7 @@ const App = ({
);
};
export const Isoflow = (props: Props) => {
export const Isoflow = (props: IsoflowProps) => {
return (
<ThemeProvider theme={theme}>
<SceneProvider>

View File

@@ -1,5 +1,6 @@
import React, { useCallback } from 'react';
import React, { useCallback, useMemo } from 'react';
import { Box, useTheme, Typography } from '@mui/material';
import { EditorModeEnum } from 'src/types';
import { UiElement } from 'components/UiElement/UiElement';
import { toPx } from 'src/utils';
import { SceneLayer } from 'src/components/SceneLayer/SceneLayer';
@@ -12,6 +13,34 @@ import { ZoomControls } from 'src/components/ZoomControls/ZoomControls';
import { useSceneStore } from 'src/stores/sceneStore';
import { DebugUtils } from 'src/components/DebugUtils/DebugUtils';
const ToolsEnum = {
MAIN_MENU: 'MAIN_MENU',
ZOOM_CONTROLS: 'ZOOM_CONTROLS',
TOOL_MENU: 'TOOL_MENU',
ITEM_CONTROLS: 'ITEM_CONTROLS'
} as const;
interface EditorModeMapping {
[k: string]: (keyof typeof ToolsEnum)[];
}
const EDITOR_MODE_MAPPING: EditorModeMapping = {
[EditorModeEnum.EDITABLE]: [
'ITEM_CONTROLS',
'ZOOM_CONTROLS',
'TOOL_MENU',
'MAIN_MENU'
],
[EditorModeEnum.EXPLORABLE_READONLY]: ['ZOOM_CONTROLS'],
[EditorModeEnum.NON_INTERACTIVE]: []
};
const getEditorModeMapping = (editorMode: keyof typeof EditorModeEnum) => {
const availableUiFeatures = EDITOR_MODE_MAPPING[editorMode];
return availableUiFeatures;
};
export const UiOverlay = () => {
const theme = useTheme();
const { appPadding } = theme.customVars;
@@ -21,9 +50,6 @@ export const UiOverlay = () => {
},
[theme]
);
const disableInteractions = useUiStateStore((state) => {
return state.disableInteractions;
});
const debugMode = useUiStateStore((state) => {
return state.debugMode;
});
@@ -39,15 +65,16 @@ export const UiOverlay = () => {
const sceneTitle = useSceneStore((state) => {
return state.title;
});
const hideMainMenu = useUiStateStore((state) => {
return state.hideMainMenu;
const editorMode = useUiStateStore((state) => {
return state.editorMode;
});
if (disableInteractions) return null;
const availableTools = useMemo(() => {
return getEditorModeMapping(editorMode);
}, [editorMode]);
return (
<>
{itemControls && (
{availableTools.includes('ITEM_CONTROLS') && itemControls && (
<UiElement
sx={{
position: 'absolute',
@@ -65,33 +92,38 @@ export const UiOverlay = () => {
</UiElement>
)}
<Box
sx={{
position: 'absolute',
right: appPadding.x,
top: appPadding.y
}}
>
<ToolMenu />
</Box>
{mode.type === 'PLACE_ELEMENT' && mode.icon && (
<SceneLayer>
<DragAndDrop icon={mode.icon} tile={mouse.position.tile} />
</SceneLayer>
{availableTools.includes('TOOL_MENU') && (
<>
<Box
sx={{
position: 'absolute',
right: appPadding.x,
top: appPadding.y
}}
>
<ToolMenu />
</Box>
{mode.type === 'PLACE_ELEMENT' && mode.icon && (
<SceneLayer>
<DragAndDrop icon={mode.icon} tile={mouse.position.tile} />
</SceneLayer>
)}
</>
)}
<Box
sx={{
position: 'absolute',
left: appPadding.x,
bottom: appPadding.y
}}
>
<ZoomControls />
</Box>
{availableTools.includes('ZOOM_CONTROLS') && (
<Box
sx={{
position: 'absolute',
left: appPadding.x,
bottom: appPadding.y
}}
>
<ZoomControls />
</Box>
)}
{!hideMainMenu && (
{availableTools.includes('MAIN_MENU') && (
<Box
sx={{
position: 'absolute',

View File

@@ -49,7 +49,3 @@ export const INITIAL_SCENE: SceneInput = {
textBoxes: [],
rectangles: []
};
export const STARTING_MODE: Mode = {
type: 'PAN',
showCursor: false
};

View File

@@ -49,7 +49,8 @@ export const useInteractionManager = () => {
const onMouseEvent = useCallback(
(e: SlimMouseEvent) => {
if (!rendererRef.current || uiState.disableInteractions) return;
if (!rendererRef.current || uiState.editorMode === 'NON_INTERACTIVE')
return;
const mode = modes[uiState.mode.type];
const modeFunction = getModeFunction(mode, e);

View File

@@ -1,16 +1,21 @@
import React, { createContext, useContext, useRef } from 'react';
import { createStore, useStore } from 'zustand';
import { CoordsUtils, incrementZoom, decrementZoom } from 'src/utils';
import {
CoordsUtils,
incrementZoom,
decrementZoom,
getStartingMode
} from 'src/utils';
import { UiStateStore } from 'src/types';
import { STARTING_MODE } from 'src/config';
const initialState = () => {
return createStore<UiStateStore>((set, get) => {
return {
editorMode: 'EXPLORABLE_READONLY',
mode: getStartingMode('EXPLORABLE_READONLY'),
iconCategoriesState: [],
disableInteractions: false,
hideMainMenu: false,
mode: STARTING_MODE,
isMainMenuOpen: false,
mouse: {
position: { screen: CoordsUtils.zero(), tile: CoordsUtils.zero() },
@@ -27,12 +32,15 @@ const initialState = () => {
zoom: 1,
rendererSize: { width: 0, height: 0 },
actions: {
setEditorMode: (mode) => {
set({ editorMode: mode, mode: getStartingMode(mode) });
},
setIconCategoriesState: (iconCategoriesState) => {
set({ iconCategoriesState });
},
resetUiState: () => {
set({
mode: STARTING_MODE,
mode: getStartingMode(get().editorMode),
scroll: {
position: CoordsUtils.zero(),
offset: CoordsUtils.zero()
@@ -45,9 +53,6 @@ const initialState = () => {
setMode: (mode) => {
set({ mode });
},
setHideMainMenu: (state) => {
set({ hideMainMenu: state });
},
setIsMainMenuOpen: (isMainMenuOpen) => {
set({ isMainMenuOpen, itemControls: null });
},
@@ -77,17 +82,6 @@ const initialState = () => {
setRendererSize: (rendererSize) => {
set({ rendererSize });
},
setDisableInteractions: (isDisabled) => {
set({ disableInteractions: isDisabled });
if (isDisabled) {
set({ mode: { type: 'INTERACTIONS_DISABLED', showCursor: false } });
} else {
set({
mode: STARTING_MODE
});
}
},
setDebugMode: (debugMode) => {
set({ debugMode });
}

View File

@@ -1,9 +1,3 @@
import type { SceneInput } from './inputs';
export type InitialScene = Partial<SceneInput> & {
zoom?: number;
};
export interface Coords {
x: number;
y: number;
@@ -30,3 +24,9 @@ export type SlimMouseEvent = Pick<
MouseEvent,
'clientX' | 'clientY' | 'target' | 'type'
>;
export const EditorModeEnum = {
NON_INTERACTIVE: 'NON_INTERACTIVE',
EXPLORABLE_READONLY: 'EXPLORABLE_READONLY',
EDITABLE: 'EDITABLE'
} as const;

View File

@@ -9,6 +9,7 @@ import {
connectorStyleEnum
} from 'src/validation/sceneItems';
import { sceneInput } from 'src/validation/scene';
import type { EditorModeEnum } from './common';
export type ConnectorStyleEnum = z.infer<typeof connectorStyleEnum>;
export type IconInput = z.infer<typeof iconInput>;
@@ -18,3 +19,17 @@ export type ConnectorInput = z.infer<typeof connectorInput>;
export type TextBoxInput = z.infer<typeof textBoxInput>;
export type RectangleInput = z.infer<typeof rectangleInput>;
export type SceneInput = z.infer<typeof sceneInput>;
export type InitialScene = Partial<SceneInput> & {
zoom?: number;
};
export interface IsoflowProps {
initialScene?: InitialScene;
onSceneUpdated?: (scene: SceneInput) => void;
width?: number | string;
height?: number | string;
debugMode?: boolean;
hideMainMenu?: boolean;
editorMode?: keyof typeof EditorModeEnum;
}

View File

@@ -1,4 +1,4 @@
import { Coords, Size } from './common';
import { Coords, Size, EditorModeEnum } from './common';
import { SceneItem, Connector, SceneItemReference } from './scene';
import { IconInput } from './inputs';
@@ -154,9 +154,8 @@ export type IconCollectionStateWithIcons = IconCollectionState & {
};
export interface UiState {
editorMode: keyof typeof EditorModeEnum;
iconCategoriesState: IconCollectionState[];
disableInteractions: boolean;
hideMainMenu: boolean;
mode: Mode;
isMainMenuOpen: boolean;
itemControls: ItemControls;
@@ -169,10 +168,10 @@ export interface UiState {
}
export interface UiStateActions {
setEditorMode: (mode: keyof typeof EditorModeEnum) => void;
setIconCategoriesState: (iconCategoriesState: IconCollectionState[]) => void;
resetUiState: () => void;
setMode: (mode: Mode) => void;
setHideMainMenu: (state: boolean) => void;
incrementZoom: () => void;
decrementZoom: () => void;
setIsMainMenuOpen: (isOpen: boolean) => void;
@@ -182,7 +181,6 @@ export interface UiStateActions {
setContextMenu: (contextMenu: ContextMenu) => void;
setMouse: (mouse: Mouse) => void;
setRendererSize: (rendererSize: Size) => void;
setDisableInteractions: (isDisabled: boolean) => void;
setDebugMode: (enabled: boolean) => void;
}

View File

@@ -1,5 +1,5 @@
import chroma from 'chroma-js';
import { Icon } from 'src/types';
import { Icon, EditorModeEnum, Mode } from 'src/types';
import { v4 as uuid } from 'uuid';
export const generateId = () => {
@@ -63,3 +63,18 @@ export const categoriseIcons = (icons: Icon[]) => {
return categories;
};
export const getStartingMode = (
editorMode: keyof typeof EditorModeEnum
): Mode => {
switch (editorMode) {
case 'EDITABLE':
return { type: 'CURSOR', showCursor: true, mousedownItem: null };
case 'EXPLORABLE_READONLY':
return { type: 'PAN', showCursor: false };
case 'NON_INTERACTIVE':
return { type: 'INTERACTIONS_DISABLED', showCursor: false };
default:
throw new Error('Invalid editor mode.');
}
};