diff --git a/gbajs3/src/components/controls/control-panel.spec.tsx b/gbajs3/src/components/controls/control-panel.spec.tsx
index f151a763..6cb56550 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,13 @@ 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: expect.anything()
});
});
@@ -133,11 +126,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 +164,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 +177,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..256f0908 100644
--- a/gbajs3/src/components/navigation-menu/navigation-menu.spec.tsx
+++ b/gbajs3/src/components/navigation-menu/navigation-menu.spec.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import * as toast from 'react-hot-toast';
import { describe, expect, it, vi } from 'vitest';
@@ -433,66 +433,4 @@ describe('', () => {
}
);
});
-
- describe('menu button', () => {
- const initialPos = {
- clientX: 0,
- clientY: 0
- };
- const movements = [
- { clientX: 0, clientY: 220 },
- { clientX: 0, clientY: 120 }
- ];
-
- it('sets layout on drag', async () => {
- const setLayoutSpy = vi.fn();
- const { useLayoutContext: originalLayout, useDragContext: originalDrag } =
- await vi.importActual('../../hooks/context.tsx');
-
- vi.spyOn(contextHooks, 'useDragContext').mockImplementation(() => ({
- ...originalDrag(),
- areItemsDraggable: true
- }));
-
- vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
- ...originalLayout(),
- setLayout: setLayoutSpy
- }));
-
- renderWithContext();
-
- fireEvent.mouseDown(screen.getByLabelText('Menu Toggle'), initialPos);
- fireEvent.mouseMove(document, movements[0]);
- fireEvent.mouseUp(document, movements[1]);
-
- expect(setLayoutSpy).toHaveBeenCalledOnce();
- expect(setLayoutSpy).toHaveBeenCalledWith('menuButton', {
- position: {
- x: movements[1].clientX,
- y: movements[1].clientY
- }
- });
- });
-
- it('renders with existing layout', async () => {
- const { useLayoutContext: originalLayout, useDragContext: originalDrag } =
- await vi.importActual('../../hooks/context.tsx');
-
- vi.spyOn(contextHooks, 'useDragContext').mockImplementation(() => ({
- ...originalDrag(),
- areItemsDraggable: true
- }));
-
- vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
- ...originalLayout(),
- layouts: { menuButton: { position: { x: 0, y: 200 } } }
- }));
-
- renderWithContext();
-
- expect(screen.getByLabelText('Menu Toggle')).toHaveStyle({
- transform: 'translate(0px,200px)'
- });
- });
- });
});
diff --git a/gbajs3/src/components/navigation-menu/navigation-menu.tsx b/gbajs3/src/components/navigation-menu/navigation-menu.tsx
index 54182cb7..9de98cd7 100644
--- a/gbajs3/src/components/navigation-menu/navigation-menu.tsx
+++ b/gbajs3/src/components/navigation-menu/navigation-menu.tsx
@@ -1,6 +1,5 @@
import { useMediaQuery } from '@mui/material';
-import { useId, useRef, useState } from 'react';
-import Draggable from 'react-draggable';
+import { useId, useState } from 'react';
import toast from 'react-hot-toast';
import {
BiInfoCircle,
@@ -32,9 +31,7 @@ import {
useEmulatorContext,
useAuthContext,
useModalContext,
- useRunningContext,
- useDragContext,
- useLayoutContext
+ useRunningContext
} from '../../hooks/context.tsx';
import { useQuickReload } from '../../hooks/emulator/use-quick-reload.tsx';
import { useLogout } from '../../hooks/use-logout.tsx';
@@ -111,9 +108,7 @@ const MenuItemWrapper = styled.ul`
}
`;
-const HamburgerButton = styled(ButtonBase)<
- ExpandableComponentProps & { $areItemsDraggable: boolean }
->`
+const HamburgerButton = styled(ButtonBase)`
background-color: ${({ theme }) => theme.mediumBlack};
color: ${({ theme }) => theme.pureWhite};
z-index: 200;
@@ -122,7 +117,6 @@ const HamburgerButton = styled(ButtonBase)<
top: 12px;
transition: 0.4s ease-in-out;
-webkit-transition: 0.4s ease-in-out;
- transition-property: left;
cursor: pointer;
border-radius: 0.25rem;
border: none;
@@ -143,14 +137,6 @@ const HamburgerButton = styled(ButtonBase)<
outline: 0;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
-
- ${({ $areItemsDraggable = false, theme }) =>
- $areItemsDraggable &&
- `
- outline-color: ${theme.gbaThemeBlue};
- outline-style: dashed;
- outline-width: 2px;
- `}
`;
const NavigationMenuClearDismiss = styled.button`
@@ -165,14 +151,11 @@ const NavigationMenuClearDismiss = styled.button`
export const NavigationMenu = () => {
const [isExpanded, setIsExpanded] = useState(true);
- const menuButtonRef = useRef(null);
const { setModalContent, setIsModalOpen } = useModalContext();
const { isAuthenticated } = useAuthContext();
const { canvas, emulator } = useEmulatorContext();
const { isRunning } = useRunningContext();
const { execute: executeLogout } = useLogout();
- const { areItemsDraggable } = useDragContext();
- const { layouts, setLayout } = useLayoutContext();
const theme = useTheme();
const isLargerThanPhone = useMediaQuery(theme.isLargerThanPhone);
const isMobileLandscape = useMediaQuery(theme.isMobileLandscape);
@@ -186,29 +169,16 @@ export const NavigationMenu = () => {
return (
<>
-
- setLayout('menuButton', { position: { x: 0, y: data.y } })
- }
+
+
+
', () => {
});
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,13 @@ 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: expect.anything()
});
});
@@ -129,7 +122,9 @@ describe('', () => {
vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
- setLayout: setLayoutSpy
+ setLayout: setLayoutSpy,
+ hasSetLayout: true,
+ layouts: { screen: { initialBounds: new DOMRect() } }
}));
renderWithContext();
@@ -163,7 +158,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..5604bd93 100644
--- a/gbajs3/src/context/layout/layout-context.tsx
+++ b/gbajs3/src/context/layout/layout-context.tsx
@@ -3,21 +3,17 @@ import { createContext } from 'react';
export type Layout = {
position?: { x: number; y: number };
size?: { width: string | number; height: string | number };
+ initialBounds?: DOMRect;
};
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..031a8b9d 100644
--- a/gbajs3/src/context/layout/layout-provider.tsx
+++ b/gbajs3/src/context/layout/layout-provider.tsx
@@ -1,50 +1,40 @@
-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
+ ),
+ [layouts]
+ );
+
+ return { layouts, setLayouts, hasSetLayout, clearLayouts };
};