Isolated q3 module
Some checks are pending
Tests / tests (push) Waiting to run

This commit is contained in:
Dustin Brett 2025-01-18 23:39:50 -08:00
parent 2d16d6edb4
commit c00d4285da
11 changed files with 153 additions and 100 deletions

View File

@ -6,6 +6,7 @@
"alpha-value-notation": "percentage",
"color-function-notation": "legacy",
"hue-degree-notation": "number",
"no-empty-source": null,
"order/properties-alphabetical-order": true,
"value-keyword-case": [
"lower",

View File

@ -1,14 +0,0 @@
import styled from "styled-components";
const StyledQuake3 = styled.div`
display: flex;
place-content: center;
canvas {
background-color: #000;
height: 100%;
width: 100%;
}
`;
export default StyledQuake3;

View File

@ -1,10 +1,9 @@
import StyledQuake3 from "components/apps/Quake3/StyledQuake3";
import useQuake3 from "components/apps/Quake3/useQuake3";
import AppContainer from "components/system/Apps/AppContainer";
import { type ComponentProcessProps } from "components/system/Apps/RenderComponent";
const Quake3: FC<ComponentProcessProps> = ({ id }) => (
<AppContainer StyledComponent={StyledQuake3} id={id} useHook={useQuake3} />
<AppContainer id={id} useHook={useQuake3} />
);
export default Quake3;

View File

@ -1,12 +1,13 @@
import { useTheme } from "styled-components";
import { useEffect, useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { type ContainerHookProps } from "components/system/Apps/AppContainer";
import useEmscriptenMount from "components/system/Files/FileManager/useEmscriptenMount";
import { type EmscriptenFS } from "contexts/fileSystem/useAsyncFs";
import { useProcesses } from "contexts/process";
import { useSession } from "contexts/session";
import { TRANSITIONS_IN_MILLISECONDS } from "utils/constants";
import { PREVENT_SCROLL, TRANSITIONS_IN_MILLISECONDS } from "utils/constants";
import { loadFiles, pxToNum } from "utils/functions";
import useIsolatedContentWindow from "hooks/useIsolatedContentWindow";
declare global {
interface Window {
@ -22,7 +23,7 @@ declare global {
exit: () => void;
exitHandler: (error: Error | null) => void;
setCanvasSize: (width: number, height: number) => void;
viewport: HTMLDivElement | null;
viewport: HTMLElement | null;
};
}
}
@ -52,70 +53,104 @@ const useQuake3 = ({
const wasMaximized = useRef(false);
const mountEmFs = useEmscriptenMount();
const { size } = windowState || {};
const focusCanvas = useCallback((focusedWindow: Window) => {
if (focusedWindow?.ioq3?.canvas) {
focusedWindow.ioq3.canvas.focus(PREVENT_SCROLL);
} else {
requestAnimationFrame(() => focusCanvas(focusedWindow));
}
}, []);
const getContentWindow = useIsolatedContentWindow(
id,
containerRef,
focusCanvas,
`
body { display: flex; place-content: center; place-items: center; }
canvas { background-color: #000; height: 100%; width: 100%; }
canvas:focus-visible { outline: none; }
`
);
const [contentWindow, setContentWindow] = useState<Window>();
useEffect(() => {
if (loading) {
loadFiles(libs).then(() => {
if (!window.ioq3) return;
const newContentWindow = getContentWindow?.();
window.ioq3.viewport = containerRef.current;
window.ioq3.elementPointerLock = true;
window.ioq3.callMain([]);
if (!newContentWindow) return;
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "Quake3");
});
loadFiles(libs, undefined, undefined, undefined, newContentWindow).then(
() => {
if (!newContentWindow.ioq3) return;
newContentWindow.ioq3.viewport = newContentWindow.document.body;
newContentWindow.ioq3.elementPointerLock = true;
newContentWindow.ioq3.callMain([]);
setLoading(false);
mountEmFs(window.FS as EmscriptenFS, "Quake3");
setContentWindow(newContentWindow);
}
);
}
}, [containerRef, libs, loading, mountEmFs, setLoading]);
}, [getContentWindow, libs, loading, mountEmFs, setLoading]);
useEffect(() => {
if (!window.ioq3) return;
if (!contentWindow?.ioq3) return;
const updateSize = (): void => {
if (!contentWindow.ioq3?.canvas) return;
wasMaximized.current = maximized;
const { height, width } =
(!maximized && size) || componentWindow?.getBoundingClientRect() || {};
if (!height || !width) return;
const aspectRatio = defaultSize
? pxToNum(defaultSize.width) / pxToNum(defaultSize.height)
: 4 / 3;
const numWidth = pxToNum(width);
const hasGreaterWidth = numWidth > pxToNum(height) - titleBar.height;
const newWidth =
maximized && hasGreaterWidth ? numWidth / aspectRatio : numWidth;
const newHeight = newWidth / aspectRatio;
if (newHeight > 0 && newWidth > 0) {
contentWindow.ioq3.setCanvasSize(newWidth, newHeight);
contentWindow.ioq3.canvas.setAttribute(
"style",
`object-fit: ${hasGreaterWidth ? "contain" : "scale-down"}`
);
}
};
setTimeout(
() => {
wasMaximized.current = maximized;
const { height, width } =
(!maximized && size) ||
componentWindow?.getBoundingClientRect() ||
{};
if (!height || !width) return;
const aspectRatio = defaultSize
? pxToNum(defaultSize.width) / pxToNum(defaultSize.height)
: 4 / 3;
const numWidth = pxToNum(width);
const hasGreaterWidth = numWidth > pxToNum(height) - titleBar.height;
const newWidth =
maximized && hasGreaterWidth ? numWidth / aspectRatio : numWidth;
const newHeight = newWidth / aspectRatio;
if (newHeight > 0 && newWidth > 0 && window.ioq3?.canvas) {
window.ioq3.setCanvasSize(newWidth, newHeight);
window.ioq3.canvas.setAttribute(
"style",
`object-fit: ${hasGreaterWidth ? "contain" : "scale-down"}`
);
}
},
updateSize,
maximized || wasMaximized.current
? TRANSITIONS_IN_MILLISECONDS.WINDOW + 10
: 0
);
}, [componentWindow, defaultSize, maximized, size, titleBar.height]);
}, [
componentWindow,
contentWindow,
defaultSize,
maximized,
size,
titleBar.height,
]);
useEffect(
() => () => {
try {
window.ioq3?.exit();
contentWindow?.ioq3?.exit();
} catch {
// Ignore error on exit
}
window.AL?.contexts.forEach(({ ctx }) => ctx.close());
contentWindow?.AL?.contexts.forEach(({ ctx }) => ctx.close());
},
[]
[contentWindow]
);
};

View File

@ -1,12 +0,0 @@
import styled from "styled-components";
const StyledTic80 = styled.div`
iframe {
background-color: #1a1c2c;
border: 0;
height: 100%;
width: 100%;
}
`;
export default StyledTic80;

View File

@ -1,10 +1,9 @@
import useTic80 from "components/apps/Tic80/useTic80";
import StyledTic80 from "components/apps/Tic80/StyledTic80";
import AppContainer from "components/system/Apps/AppContainer";
import { type ComponentProcessProps } from "components/system/Apps/RenderComponent";
const Tic80: FC<ComponentProcessProps> = ({ id }) => (
<AppContainer StyledComponent={StyledTic80} id={id} useHook={useTic80} />
<AppContainer id={id} useHook={useTic80} />
);
export default Tic80;

View File

@ -17,7 +17,13 @@ const useTic80 = ({
const { readFile } = useFileSystem();
const loadedUrl = useRef<string>(undefined);
const { appendFileToTitle } = useTitle(id);
const getContentWindow = useIsolatedContentWindow(id, containerRef, true);
const getContentWindow = useIsolatedContentWindow(
id,
containerRef,
undefined,
"canvas { image-rendering: pixelated; }",
true
);
const loadComputer = useCallback(
async (fileUrl?: string) => {
const loadApp = async (blobUrl?: string): Promise<void> => {

View File

@ -1,5 +1,5 @@
import { memo, useMemo, useRef, useState } from "react";
import { type IStyledComponent } from "styled-components";
import styled, { type IStyledComponent } from "styled-components";
import { type FastOmit } from "styled-components/dist/types";
import StyledLoading from "components/system/Files/FileManager/StyledLoading";
import useFileDrop from "components/system/Files/FileManager/useFileDrop";
@ -16,7 +16,7 @@ export type ContainerHookProps = {
type ContainerHook = (props: ContainerHookProps) => void;
type AppContainerProps = {
StyledComponent: IStyledComponent<
StyledComponent?: IStyledComponent<
"web",
FastOmit<
React.DetailedHTMLProps<
@ -30,6 +30,8 @@ type AppContainerProps = {
useHook: ContainerHook;
};
const StyledAppContainer = styled.div``;
const AppContainer: FC<AppContainerProps> = ({
id,
useHook,
@ -48,19 +50,16 @@ const AppContainer: FC<AppContainerProps> = ({
}),
[loading]
);
const StyledWrapper = StyledComponent || StyledAppContainer;
useHook({ containerRef, id, loading, setLoading, url });
return (
<>
{loading && <StyledLoading />}
<StyledComponent
ref={containerRef}
style={style}
{...useFileDrop({ id })}
>
<StyledWrapper ref={containerRef} style={style} {...useFileDrop({ id })}>
{children}
</StyledComponent>
</StyledWrapper>
</>
);
};

View File

@ -79,14 +79,15 @@ const useFileDrop = ({
[id, mkdirRecursive, updateFolder, url, writeFile]
);
const { openTransferDialog } = useTransferDialog();
return {
onDragLeave,
onDragOver: (event) => {
const onDragOverThenHaltEvent = useCallback(
(event: DragEvent | React.DragEvent<HTMLElement>): void => {
onDragOver?.(event);
haltEvent(event);
},
onDrop: (event) => {
[onDragOver]
);
const onDrop = useCallback(
(event: DragEvent | React.DragEvent<HTMLElement>): void => {
if (MOUNTABLE_EXTENSIONS.has(getExtension(directory))) return;
if (updatePositions && event.target instanceof HTMLElement) {
@ -189,6 +190,25 @@ const useFileDrop = ({
hasUpdateId
);
},
[
callback,
directory,
exists,
iconPositions,
id,
openTransferDialog,
processesRef,
setIconPositions,
sortOrders,
updatePositions,
updateProcessUrl,
]
);
return {
onDragLeave,
onDragOver: onDragOverThenHaltEvent,
onDrop,
};
};

View File

@ -1,4 +1,10 @@
import { useState, useEffect, useCallback, useLayoutEffect } from "react";
import {
useState,
useEffect,
useCallback,
useLayoutEffect,
useMemo,
} from "react";
import useFileDrop from "components/system/Files/FileManager/useFileDrop";
import { PREVENT_SCROLL } from "utils/constants";
import { useProcesses } from "contexts/process";
@ -19,18 +25,28 @@ const createCanvas = (hostDocument: Document): HTMLCanvasElement => {
const createIframe = (
id: string,
container: HTMLDivElement
container: HTMLDivElement,
styles?: string
): HTMLIFrameElement => {
const iframe = document.createElement("iframe");
iframe.title = id;
iframe.style.backgroundColor = "transparent";
iframe.style.border = "0";
iframe.style.width = "100%";
iframe.style.height = "100%";
container.append(iframe);
const contentDocument = iframe.contentDocument as Document;
contentDocument.open();
contentDocument.write("<!DOCTYPE html><head /><body />");
contentDocument.write(`
<!DOCTYPE html>
${styles ? `<head><style>${styles}</style></head>` : "<head />"}
<body />
`);
contentDocument.close();
const contentWindow = iframe.contentWindow as Window;
@ -51,6 +67,8 @@ type IsolatedContentWindow = (() => Window | undefined) | undefined;
const useIsolatedContentWindow = (
id: string,
containerRef: React.RefObject<HTMLDivElement | null>,
focusFunction?: (window: Window) => void,
styles?: string,
withCanvas = false
): IsolatedContentWindow => {
const [container, setContainer] = useState<HTMLDivElement>();
@ -63,7 +81,7 @@ const useIsolatedContentWindow = (
container.querySelector("iframe")?.remove();
const iframe = createIframe(id, container);
const iframe = createIframe(id, container, styles);
const newContentWindow = iframe.contentWindow as Window;
let canvas: HTMLCanvasElement;
@ -86,22 +104,21 @@ const useIsolatedContentWindow = (
setContentWindow(newContentWindow);
return newContentWindow;
}, [container, id, onDragOver, onDrop, setForegroundId, withCanvas]);
}, [container, id, onDragOver, onDrop, setForegroundId, styles, withCanvas]);
useLayoutEffect(() => {
if (contentWindow && foregroundId === id) {
requestAnimationFrame(() => {
if (withCanvas) {
if (focusFunction) focusFunction(contentWindow);
else if (withCanvas) {
contentWindow.document
.querySelector<HTMLCanvasElement>("canvas")
?.focus(PREVENT_SCROLL);
} else {
contentWindow.focus();
}
} else contentWindow.focus();
});
}
// eslint-disable-next-line react-hooks-addons/no-unused-deps
}, [contentWindow, foregroundId, id, maximized, withCanvas]);
}, [contentWindow, focusFunction, foregroundId, id, maximized, withCanvas]);
useEffect(() => {
if (!container) {
@ -119,7 +136,10 @@ const useIsolatedContentWindow = (
}
}, [container, containerRef]);
return container ? createContentWindow : undefined;
return useMemo(
() => (container ? createContentWindow : undefined),
[container, createContentWindow]
);
};
export default useIsolatedContentWindow;

File diff suppressed because one or more lines are too long