Skip to content

Commit

Permalink
feat: split layouts and initial bounds
Browse files Browse the repository at this point in the history
  • Loading branch information
thenick775 committed Feb 23, 2025
1 parent ee0d0d7 commit 1a2c2d1
Show file tree
Hide file tree
Showing 16 changed files with 293 additions and 152 deletions.
23 changes: 13 additions & 10 deletions gbajs3/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AppErrorBoundary } from './components/shared/error-boundary.tsx';
import { ToasterWithDefaults } from './components/toast/toaster.tsx';
import { AuthProvider } from './context/auth/auth-provider.tsx';
import { EmulatorContextProvider } from './context/emulator/emulator-context-provider.tsx';
import { InitialBoundsProvider } from './context/initial-bounds/initial-bounds-provider.tsx';
import { LayoutProvider } from './context/layout/layout-provider.tsx';
import { ModalProvider } from './context/modal/modal-provider.tsx';
import { GbaDarkTheme } from './context/theme/theme.tsx';
Expand All @@ -24,16 +25,18 @@ export const App = () => {
<ToasterWithDefaults />
<AuthProvider>
<EmulatorContextProvider>
<LayoutProvider>
<ModalProvider>
<PwaPrompt />
<NavigationMenu />
<Screen />
<ControlPanel />
<VirtualControls />
<ModalContainer />
</ModalProvider>
</LayoutProvider>
<InitialBoundsProvider>
<LayoutProvider>
<ModalProvider>
<PwaPrompt />
<NavigationMenu />
<Screen />
<ControlPanel />
<VirtualControls />
<ModalContainer />
</ModalProvider>
</LayoutProvider>
</InitialBoundsProvider>
</EmulatorContextProvider>
</AuthProvider>
</AppErrorBoundary>
Expand Down
90 changes: 58 additions & 32 deletions gbajs3/src/components/controls/control-panel.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,16 @@ describe('<ControlPanel />', () => {
];

beforeEach(async () => {
const { useLayoutContext: original } = await vi.importActual<
const { useInitialBoundsContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...original(),
layouts: {
...original().layouts,
screen: { initialBounds: { left: 0, bottom: 0 } as DOMRect }
}
}));
vi.spyOn(contextHooks, 'useInitialBoundsContext').mockImplementation(
() => ({
...original(),
initialBounds: { screen: { left: 0, bottom: 0 } as DOMRect }
})
);
});

it('renders panel controls', async () => {
Expand Down Expand Up @@ -95,30 +94,30 @@ describe('<ControlPanel />', () => {
});

it('sets initial bounds when rendered', async () => {
const setLayoutSpy = vi.fn();
const setInitialBoundSpy = vi.fn();

const { useLayoutContext: originalLayout } = await vi.importActual<
const { useInitialBoundsContext: originalBounds } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
setLayout: setLayoutSpy
}));
vi.spyOn(contextHooks, 'useInitialBoundsContext').mockImplementation(
() => ({
...originalBounds(),
setInitialBound: setInitialBoundSpy
})
);

renderWithContext(<ControlPanel />);

expect(setLayoutSpy).toHaveBeenCalledWith('controlPanel', {
initialBounds: {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0
}
expect(setInitialBoundSpy).toHaveBeenCalledWith('controlPanel', {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0
});
});

Expand All @@ -134,9 +133,7 @@ describe('<ControlPanel />', () => {

vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...originalLayout(),
setLayout: setLayoutSpy,
hasSetLayout: true,
layouts: { screen: { initialBounds: new DOMRect() } }
setLayout: setLayoutSpy
}));

renderWithContext(<ControlPanel />);
Expand All @@ -149,9 +146,24 @@ describe('<ControlPanel />', () => {
initialPos
);
fireEvent.mouseMove(document, movements[0]);
fireEvent.mouseUp(document, movements[1]);

expect(setLayoutSpy).toHaveBeenCalledOnce();
expect(setLayoutSpy).toHaveBeenCalledWith('controlPanel', {
originalBounds: {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0
}
});

fireEvent.mouseUp(document, movements[1]);

expect(setLayoutSpy).toHaveBeenCalledTimes(2);
expect(setLayoutSpy).toHaveBeenCalledWith('controlPanel', {
position: {
x: movements[1].clientX,
Expand All @@ -176,8 +188,7 @@ describe('<ControlPanel />', () => {
clearLayouts: vi.fn(),
setLayout: setLayoutSpy,
setLayouts: vi.fn(),
hasSetLayout: true,
layouts: { screen: { initialBounds: new DOMRect() } }
layouts: {}
};

vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(
Expand All @@ -193,9 +204,24 @@ describe('<ControlPanel />', () => {
// simulate mouse events on a resize handle
fireEvent.mouseDown(screen.getAllByTestId('gripper-handle')[0], initialPos);
fireEvent.mouseMove(document, movements[0]);
fireEvent.mouseUp(document, movements[1]);

expect(setLayoutSpy).toHaveBeenCalledOnce();
expect(setLayoutSpy).toHaveBeenCalledWith('controlPanel', {
originalBounds: {
bottom: 0,
height: 0,
left: 0,
right: 0,
top: 0,
width: 0,
x: 0,
y: 0
}
});

fireEvent.mouseUp(document, movements[1]);

expect(setLayoutSpy).toHaveBeenCalledTimes(2);
expect(setLayoutSpy).toHaveBeenCalledWith('controlPanel', {
position: {
x: expect.anything(),
Expand Down
38 changes: 29 additions & 9 deletions gbajs3/src/components/controls/control-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useMediaQuery } from '@mui/material';
import { useLocalStorage } from '@uidotdev/usehooks';
import { useCallback, useId, useState } from 'react';
import { useCallback, useId, useRef, useState } from 'react';
import { IconContext } from 'react-icons';
import { AiOutlineFastForward, AiOutlineForward } from 'react-icons/ai';
import {
Expand All @@ -22,6 +22,7 @@ import {
import {
useDragContext,
useEmulatorContext,
useInitialBoundsContext,
useLayoutContext,
useResizeContext,
useRunningContext
Expand Down Expand Up @@ -81,7 +82,8 @@ export const ControlPanel = () => {
const { isRunning } = useRunningContext();
const { areItemsDraggable, setAreItemsDraggable } = useDragContext();
const { areItemsResizable, setAreItemsResizable } = useResizeContext();
const { layouts, setLayout, hasSetLayout } = useLayoutContext();
const { layouts, setLayout } = useLayoutContext();
const { initialBounds, setInitialBound } = useInitialBoundsContext();
const theme = useTheme();
const isLargerThanPhone = useMediaQuery(theme.isLargerThanPhone);
const isMobileLandscape = useMediaQuery(theme.isMobileLandscape);
Expand All @@ -97,22 +99,26 @@ export const ControlPanel = () => {
emulatorFFMultiplierLocalStorageKey,
1
);
const rndRef = useRef<Rnd | null>();

// pause emulator when document is not visible,
// resumes if applicable when document is visible
useBackgroundEmulator({ isPaused });

const refSetLayout = useCallback(
(node: Rnd | null) => {
if (!hasSetLayout && node)
setLayout('controlPanel', {
initialBounds: node.resizableElement.current?.getBoundingClientRect()
});
if (!initialBounds?.controlPanel && node)
setInitialBound(
'controlPanel',
node.resizableElement.current?.getBoundingClientRect()
);

rndRef.current = node;
},
[setLayout, hasSetLayout]
[initialBounds?.controlPanel, setInitialBound]
);

const canvasBounds = layouts?.screen?.initialBounds;
const canvasBounds = layouts?.screen?.originalBounds ?? initialBounds?.screen;

if (!canvasBounds) return null;

Expand Down Expand Up @@ -265,10 +271,24 @@ export const ControlPanel = () => {
cancel=".noDrag"
position={position}
size={size}
onDragStart={() => {
if (!layouts?.controlPanel?.originalBounds)
setLayout('controlPanel', {
originalBounds:
rndRef.current?.resizableElement.current?.getBoundingClientRect()
});
}}
onDragStop={(_, data) => {
setLayout('controlPanel', { position: { x: data.x, y: data.y } });
}}
onResizeStart={() => setIsResizing(true)}
onResizeStart={() => {
setIsResizing(true);
if (!layouts?.controlPanel?.originalBounds)
setLayout('controlPanel', {
originalBounds:
rndRef.current?.resizableElement.current?.getBoundingClientRect()
});
}}
onResizeStop={(_1, _2, ref, _3, position) => {
setLayout('controlPanel', {
size: { width: ref.clientWidth, height: ref.clientHeight },
Expand Down
18 changes: 10 additions & 8 deletions gbajs3/src/components/controls/virtual-controls.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ import type { GBAEmulator } from '../../emulator/mgba/mgba-emulator.tsx';

describe('<VirtualControls />', () => {
beforeEach(async () => {
const { useLayoutContext: original } = await vi.importActual<
const { useInitialBoundsContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({
...original(),
layouts: {
...original().layouts,
controlPanel: { initialBounds: { left: 0, bottom: 0 } as DOMRect }
}
}));
vi.spyOn(contextHooks, 'useInitialBoundsContext').mockImplementation(
() => ({
...original(),
initialBounds: {
controlPanel: { left: 0, bottom: 0 } as DOMRect,
screen: { left: 0, bottom: 0 } as DOMRect
}
})
);
});

it('renders opad and default virtual controls on mobile', () => {
Expand Down
13 changes: 8 additions & 5 deletions gbajs3/src/components/controls/virtual-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import { OPad } from './o-pad.tsx';
import { VirtualButton } from './virtual-button.tsx';
import {
useEmulatorContext,
useLayoutContext,
useAuthContext,
useModalContext,
useRunningContext
useRunningContext,
useInitialBoundsContext,
useLayoutContext
} from '../../hooks/context.tsx';
import { useAddCallbacks } from '../../hooks/emulator/use-add-callbacks.tsx';
import { useQuickReload } from '../../hooks/emulator/use-quick-reload.tsx';
Expand Down Expand Up @@ -62,6 +63,7 @@ export const VirtualControls = () => {
const { isAuthenticated } = useAuthContext();
const { setModalContent, setIsModalOpen } = useModalContext();
const { layouts } = useLayoutContext();
const { initialBounds } = useInitialBoundsContext();
const virtualControlToastId = useId();
const quickReload = useQuickReload();
const { syncActionIfEnabled } = useAddCallbacks();
Expand All @@ -73,10 +75,11 @@ export const VirtualControls = () => {
AreVirtualControlsEnabledProps | undefined
>(virtualControlsLocalStorageKey);

const controlPanelBounds = layouts?.controlPanel?.initialBounds;
const canvasBounds = layouts?.screen?.initialBounds;
const controlPanelBounds =
layouts?.controlPanel?.originalBounds ?? initialBounds?.controlPanel;
const canvasBounds = layouts?.screen?.originalBounds ?? initialBounds?.screen;

if (!controlPanelBounds) return null;
if (!controlPanelBounds || !canvasBounds) return null;

const shouldShowVirtualControl = (virtualControlEnabled?: boolean) => {
return (
Expand Down
14 changes: 12 additions & 2 deletions gbajs3/src/components/modals/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { VirtualControlsForm } from './controls/virtual-controls-form.tsx';
import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { useLayoutContext, useModalContext } from '../../hooks/context.tsx';
import {
useInitialBoundsContext,
useLayoutContext,
useModalContext
} from '../../hooks/context.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand Down Expand Up @@ -72,6 +76,7 @@ const ControlTabs = ({
setIsSuccessfulSubmit
}: ControlTabsProps) => {
const { clearLayouts } = useLayoutContext();
const { clearInitialBounds } = useInitialBoundsContext();
const [value, setValue] = useState(0);

const tabIndexToFormId = (tabIndex: number) => {
Expand All @@ -95,6 +100,11 @@ const ControlTabs = ({

const onAfterSubmit = () => setIsSuccessfulSubmit(true);

const clearLayoutsAndInitialBounds = () => {
clearInitialBounds();
clearLayouts();
};

return (
<>
<TabsWithBorder
Expand All @@ -116,7 +126,7 @@ const ControlTabs = ({
<Button
id={resetPositionsButtonId}
sx={{ marginTop: '10px' }}
onClick={clearLayouts}
onClick={clearLayoutsAndInitialBounds}
>
Reset All Positions
</Button>
Expand Down
Loading

0 comments on commit 1a2c2d1

Please sign in to comment.