diff --git a/gbajs3/src/components/controls/control-panel.spec.tsx b/gbajs3/src/components/controls/control-panel.spec.tsx
index f151a763..380d92f6 100644
--- a/gbajs3/src/components/controls/control-panel.spec.tsx
+++ b/gbajs3/src/components/controls/control-panel.spec.tsx
@@ -32,9 +32,9 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...original(),
- initialBounds: {
- ...original().initialBounds,
- screen: { left: 0, bottom: 0 } as DOMRect
+ layouts: {
+ ...original().layouts,
+ screen: { initialBounds: { left: 0, bottom: 0 } as DOMRect }
}
}));
});
@@ -95,7 +95,7 @@ describe('', () => {
});
it('sets initial bounds when rendered', async () => {
- const setInitialBoundSpy = vi.fn();
+ const setLayoutSpy = vi.fn();
const { useLayoutContext: originalLayout } = await vi.importActual<
typeof contextHooks
@@ -103,20 +103,22 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
- setInitialBound: setInitialBoundSpy
+ setLayout: setLayoutSpy
}));
renderWithContext();
- expect(setInitialBoundSpy).toHaveBeenCalledWith('controlPanel', {
- bottom: 0,
- height: 0,
- left: 0,
- right: 0,
- top: 0,
- width: 0,
- x: 0,
- y: 0
+ expect(setLayoutSpy).toHaveBeenCalledWith('controlPanel', {
+ initialBounds: {
+ bottom: 0,
+ height: 0,
+ left: 0,
+ right: 0,
+ top: 0,
+ width: 0,
+ x: 0,
+ y: 0
+ }
});
});
@@ -133,11 +135,14 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
setLayout: setLayoutSpy,
- initialBounds: { screen: new DOMRect() }
+ hasSetLayout: true,
+ layouts: { screen: { initialBounds: new DOMRect() } }
}));
renderWithContext();
+ setLayoutSpy.mockClear(); // clear calls from initial render
+
// simulate mouse events on wrapper
fireEvent.mouseDown(
screen.getByTestId('control-panel-wrapper'),
@@ -168,12 +173,11 @@ describe('', () => {
// needs to be a consistent object
const testLayout = {
- clearLayoutsAndBounds: vi.fn(),
+ clearLayouts: vi.fn(),
setLayout: setLayoutSpy,
setLayouts: vi.fn(),
- setInitialBound: vi.fn(),
- layouts: {},
- initialBounds: { screen: new DOMRect() }
+ hasSetLayout: true,
+ layouts: { screen: { initialBounds: new DOMRect() } }
};
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(
@@ -182,6 +186,8 @@ describe('', () => {
renderWithContext();
+ setLayoutSpy.mockClear(); // clear calls from initial render
+
fireEvent.resize(screen.getByTestId('control-panel-wrapper'));
// simulate mouse events on a resize handle
diff --git a/gbajs3/src/components/controls/control-panel.tsx b/gbajs3/src/components/controls/control-panel.tsx
index 28f3aecb..d4bc1e7c 100644
--- a/gbajs3/src/components/controls/control-panel.tsx
+++ b/gbajs3/src/components/controls/control-panel.tsx
@@ -81,8 +81,7 @@ export const ControlPanel = () => {
const { isRunning } = useRunningContext();
const { areItemsDraggable, setAreItemsDraggable } = useDragContext();
const { areItemsResizable, setAreItemsResizable } = useResizeContext();
- const { layouts, setLayout, initialBounds, setInitialBound } =
- useLayoutContext();
+ const { layouts, setLayout, hasSetLayout } = useLayoutContext();
const theme = useTheme();
const isLargerThanPhone = useMediaQuery(theme.isLargerThanPhone);
const isMobileLandscape = useMediaQuery(theme.isMobileLandscape);
@@ -105,16 +104,15 @@ export const ControlPanel = () => {
const refSetLayout = useCallback(
(node: Rnd | null) => {
- if (!initialBounds?.controlPanel && node)
- setInitialBound(
- 'controlPanel',
- node?.resizableElement.current?.getBoundingClientRect()
- );
+ if (!hasSetLayout && node)
+ setLayout('controlPanel', {
+ initialBounds: node.resizableElement.current?.getBoundingClientRect()
+ });
},
- [initialBounds?.controlPanel, setInitialBound]
+ [setLayout, hasSetLayout]
);
- const canvasBounds = initialBounds?.screen;
+ const canvasBounds = layouts?.screen?.initialBounds;
if (!canvasBounds) return null;
diff --git a/gbajs3/src/components/controls/virtual-controls.spec.tsx b/gbajs3/src/components/controls/virtual-controls.spec.tsx
index 274b87b0..f415950c 100644
--- a/gbajs3/src/components/controls/virtual-controls.spec.tsx
+++ b/gbajs3/src/components/controls/virtual-controls.spec.tsx
@@ -22,9 +22,9 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...original(),
- initialBounds: {
- ...original().initialBounds,
- controlPanel: { left: 0, bottom: 0 } as DOMRect
+ layouts: {
+ ...original().layouts,
+ controlPanel: { initialBounds: { left: 0, bottom: 0 } as DOMRect }
}
}));
});
diff --git a/gbajs3/src/components/controls/virtual-controls.tsx b/gbajs3/src/components/controls/virtual-controls.tsx
index 8b9f94ce..6f30fb97 100644
--- a/gbajs3/src/components/controls/virtual-controls.tsx
+++ b/gbajs3/src/components/controls/virtual-controls.tsx
@@ -61,7 +61,7 @@ export const VirtualControls = () => {
const { isRunning } = useRunningContext();
const { isAuthenticated } = useAuthContext();
const { setModalContent, setIsModalOpen } = useModalContext();
- const { initialBounds } = useLayoutContext();
+ const { layouts } = useLayoutContext();
const virtualControlToastId = useId();
const quickReload = useQuickReload();
const { syncActionIfEnabled } = useAddCallbacks();
@@ -73,8 +73,8 @@ export const VirtualControls = () => {
AreVirtualControlsEnabledProps | undefined
>(virtualControlsLocalStorageKey);
- const controlPanelBounds = initialBounds?.controlPanel;
- const canvasBounds = initialBounds?.screen;
+ const controlPanelBounds = layouts?.controlPanel?.initialBounds;
+ const canvasBounds = layouts?.screen?.initialBounds;
if (!controlPanelBounds) return null;
diff --git a/gbajs3/src/components/modals/controls.spec.tsx b/gbajs3/src/components/modals/controls.spec.tsx
index 63ae7519..c2792805 100644
--- a/gbajs3/src/components/modals/controls.spec.tsx
+++ b/gbajs3/src/components/modals/controls.spec.tsx
@@ -95,14 +95,14 @@ describe('', () => {
});
it('resets movable control layouts', async () => {
- const clearLayoutsAndBoundsSpy = vi.fn();
+ const clearLayoutsSpy = vi.fn();
const { useLayoutContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...original(),
- clearLayoutsAndBounds: clearLayoutsAndBoundsSpy
+ clearLayouts: clearLayoutsSpy
}));
renderWithContext();
@@ -115,7 +115,7 @@ describe('', () => {
await userEvent.click(resetPositionsButton);
- expect(clearLayoutsAndBoundsSpy).toHaveBeenCalledOnce();
+ expect(clearLayoutsSpy).toHaveBeenCalledOnce();
});
it('closes modal using the close button', async () => {
diff --git a/gbajs3/src/components/modals/controls.tsx b/gbajs3/src/components/modals/controls.tsx
index f54371e6..6b405a8f 100644
--- a/gbajs3/src/components/modals/controls.tsx
+++ b/gbajs3/src/components/modals/controls.tsx
@@ -71,7 +71,7 @@ const ControlTabs = ({
resetPositionsButtonId,
setIsSuccessfulSubmit
}: ControlTabsProps) => {
- const { clearLayoutsAndBounds } = useLayoutContext();
+ const { clearLayouts } = useLayoutContext();
const [value, setValue] = useState(0);
const tabIndexToFormId = (tabIndex: number) => {
@@ -116,7 +116,7 @@ const ControlTabs = ({
diff --git a/gbajs3/src/components/navigation-menu/navigation-menu.spec.tsx b/gbajs3/src/components/navigation-menu/navigation-menu.spec.tsx
index 0923bffe..ecf42f9e 100644
--- a/gbajs3/src/components/navigation-menu/navigation-menu.spec.tsx
+++ b/gbajs3/src/components/navigation-menu/navigation-menu.spec.tsx
@@ -470,7 +470,8 @@ describe('', () => {
position: {
x: movements[1].clientX,
y: movements[1].clientY
- }
+ },
+ standalone: true
});
});
diff --git a/gbajs3/src/components/navigation-menu/navigation-menu.tsx b/gbajs3/src/components/navigation-menu/navigation-menu.tsx
index 54182cb7..dd0257bd 100644
--- a/gbajs3/src/components/navigation-menu/navigation-menu.tsx
+++ b/gbajs3/src/components/navigation-menu/navigation-menu.tsx
@@ -193,7 +193,10 @@ export const NavigationMenu = () => {
position={layouts?.menuButton?.position ?? { x: 0, y: 0 }}
disabled={!areItemsDraggable}
onStop={(_, data) =>
- setLayout('menuButton', { position: { x: 0, y: data.y } })
+ setLayout('menuButton', {
+ position: { x: 0, y: data.y },
+ standalone: true
+ })
}
>
', () => {
});
it('sets initial bounds when rendered', async () => {
- const setInitialBoundSpy = vi.fn();
+ const setLayoutSpy = vi.fn();
const { useLayoutContext: originalLayout } = await vi.importActual<
typeof contextHooks
@@ -49,20 +49,22 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
- setInitialBound: setInitialBoundSpy
+ setLayout: setLayoutSpy
}));
renderWithContext();
- expect(setInitialBoundSpy).toHaveBeenCalledWith('screen', {
- bottom: 0,
- height: 0,
- left: 0,
- right: 0,
- top: 0,
- width: 0,
- x: 0,
- y: 0
+ expect(setLayoutSpy).toHaveBeenCalledWith('screen', {
+ initialBounds: {
+ bottom: 0,
+ height: 0,
+ left: 0,
+ right: 0,
+ top: 0,
+ width: 0,
+ x: 0,
+ y: 0
+ }
});
});
@@ -129,7 +131,9 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
- setLayout: setLayoutSpy
+ setLayout: setLayoutSpy,
+ hasSetLayout: true,
+ layouts: { screen: { initialBounds: new DOMRect() } }
}));
renderWithContext();
@@ -163,7 +167,8 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
setLayout: setLayoutSpy,
- initialBounds: { screen: new DOMRect() }
+ hasSetLayout: true,
+ layouts: { screen: { initialBounds: new DOMRect() } }
}));
renderWithContext();
diff --git a/gbajs3/src/components/screen/screen.tsx b/gbajs3/src/components/screen/screen.tsx
index 4a4be74b..df85d514 100644
--- a/gbajs3/src/components/screen/screen.tsx
+++ b/gbajs3/src/components/screen/screen.tsx
@@ -1,5 +1,6 @@
import { useMediaQuery } from '@mui/material';
-import { useCallback, useRef } from 'react';
+import { useOrientation } from '@uidotdev/usehooks';
+import { useCallback, useLayoutEffect, useRef } from 'react';
import { Rnd, type Props as RndProps } from 'react-rnd';
import { styled, useTheme } from 'styled-components';
@@ -83,31 +84,37 @@ export const Screen = () => {
const { setCanvas } = useEmulatorContext();
const { areItemsDraggable } = useDragContext();
const { areItemsResizable } = useResizeContext();
- const { layouts, setLayout, initialBounds, setInitialBound } =
- useLayoutContext();
+ const { layouts, setLayout, hasSetLayout } = useLayoutContext();
const screenWrapperXStart = isLargerThanPhone ? NavigationMenuWidth + 10 : 0;
const screenWrapperYStart = isLargerThanPhone && !isMobileLandscape ? 15 : 0;
const rndRef = useRef();
+ const orientation = useOrientation();
const refUpdateDefaultPosition = useCallback(
(node: Rnd | null) => {
- if (!layouts?.screen) {
+ if (!hasSetLayout) {
node?.resizableElement?.current?.style?.removeProperty('width');
node?.resizableElement?.current?.style?.removeProperty('height');
}
- if (!initialBounds?.screen && node) {
- setInitialBound(
- 'screen',
- node.resizableElement.current?.getBoundingClientRect()
- );
- }
+ if (!hasSetLayout && node)
+ setLayout('screen', {
+ initialBounds: node.resizableElement.current?.getBoundingClientRect()
+ });
if (!rndRef.current) rndRef.current = node;
},
- [initialBounds?.screen, layouts?.screen, setInitialBound]
+ [hasSetLayout, setLayout]
);
+ useLayoutEffect(() => {
+ if (!hasSetLayout && [0, 90, 270].includes(orientation.angle))
+ setLayout('screen', {
+ initialBounds:
+ rndRef.current?.resizableElement?.current?.getBoundingClientRect()
+ });
+ }, [hasSetLayout, isMobileLandscape, setLayout, orientation.angle]);
+
const refSetCanvas = useCallback(
(node: HTMLCanvasElement | null) => setCanvas(node),
[setCanvas]
diff --git a/gbajs3/src/context/layout/layout-context.tsx b/gbajs3/src/context/layout/layout-context.tsx
index 2ed27ccc..9cca8b6c 100644
--- a/gbajs3/src/context/layout/layout-context.tsx
+++ b/gbajs3/src/context/layout/layout-context.tsx
@@ -3,21 +3,19 @@ import { createContext } from 'react';
export type Layout = {
position?: { x: number; y: number };
size?: { width: string | number; height: string | number };
+ initialBounds?: DOMRect;
+ /** indicates whether or not this layout is standalone, and considered in any relative calculations or counts */
+ standalone?: boolean;
};
export type Layouts = {
[key: string]: Layout;
};
-export type InitialBounds = {
- [key: string]: DOMRect | undefined;
-};
-
export type LayoutContextProps = {
layouts: Layouts;
- clearLayoutsAndBounds: () => void;
- initialBounds?: InitialBounds;
- setInitialBound: (key: string, bounds?: DOMRect) => void;
+ hasSetLayout: boolean;
+ clearLayouts: () => void;
setLayout: (layoutKey: string, layout: Layout) => void;
setLayouts: (layouts: Layouts) => void;
};
diff --git a/gbajs3/src/context/layout/layout-provider.tsx b/gbajs3/src/context/layout/layout-provider.tsx
index bc78fa1a..4a16f53a 100644
--- a/gbajs3/src/context/layout/layout-provider.tsx
+++ b/gbajs3/src/context/layout/layout-provider.tsx
@@ -1,50 +1,39 @@
-import { useOrientation } from '@uidotdev/usehooks';
-import { useCallback, useEffect, useState, type ReactNode } from 'react';
+import { useCallback, useEffect, type ReactNode } from 'react';
import { LayoutContext } from './layout-context.tsx';
import { useLayouts } from '../../hooks/use-layouts.tsx';
-import type { InitialBounds, Layout } from './layout-context.tsx';
+import type { Layout } from './layout-context.tsx';
type LayoutProviderProps = { children: ReactNode };
export const LayoutProvider = ({ children }: LayoutProviderProps) => {
- const { layouts, setLayouts, clearLayouts } = useLayouts();
- const [initialBounds, setInitialBounds] = useState();
- const orientation = useOrientation();
+ const { layouts, setLayouts, hasSetLayout, clearLayouts } = useLayouts();
const setLayout = useCallback(
(layoutKey: string, layout: Layout) =>
- setLayouts((prevState) => ({
- ...prevState,
- [layoutKey]: { ...prevState?.[layoutKey], ...layout }
- })),
+ setLayouts((prevState) => {
+ return {
+ ...prevState,
+ [layoutKey]: { ...prevState?.[layoutKey], ...layout }
+ };
+ }),
[setLayouts]
);
- const setInitialBound = useCallback(
- (key: string, bounds?: DOMRect) =>
- setInitialBounds((prevState) => ({ ...prevState, [key]: bounds })),
- []
- );
-
- const clearLayoutsAndBounds = useCallback(() => {
- clearLayouts();
- setInitialBounds({});
- }, [clearLayouts]);
-
useEffect(() => {
- if (orientation.angle !== null && [0, 90, 270].includes(orientation.angle))
- setInitialBounds({});
- }, [setInitialBounds, orientation.angle]);
+ if (!hasSetLayout) clearLayouts();
+
+ // clears the initial bounds if no actual layouts are set on initial render only
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
return (
{
const clearLayouts = useCallback(() => setLayouts({}), [setLayouts]);
- return { layouts, setLayouts, clearLayouts };
+ const hasSetLayout = useMemo(
+ () =>
+ !!Object.values(layouts).some(
+ (layout) => (!!layout?.position || !!layout?.size) && !layout.standalone
+ ),
+ [layouts]
+ );
+
+ return { layouts, setLayouts, hasSetLayout, clearLayouts };
};