diff --git a/src/App.jsx b/src/App.jsx index 68d515d..b704074 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,8 +5,22 @@ import { WindowsView } from "./components/windows/WindowsView.jsx"; import { VirtualRootProvider } from "./hooks/virtual-drive/VirtualRootContext.js"; import { Desktop } from "./components/desktop/Desktop.jsx"; import { SettingsProvider } from "./hooks/settings/SettingsContext.js"; +import { ModalsView } from "./components/modals/ModalsView.jsx"; +import { useEffect } from "react"; function App() { + useEffect(() => { + const onContextMenu = (event) => { + event.preventDefault(); + }; + + document.addEventListener("contextmenu", onContextMenu); + + return () => { + document.removeEventListener("contextmenu", onContextMenu); + }; + }, []); + return ( @@ -14,6 +28,7 @@ function App() {
+
diff --git a/src/App.module.css b/src/App.module.css index 0be3495..9b79409 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -6,4 +6,9 @@ height: 100%; text-align: center; overflow: hidden; + pointer-events: none; +} + +.App > * { + pointer-events: auto; } diff --git a/src/components/applications/calculator/Calculator.jsx b/src/components/applications/calculator/Calculator.jsx index cfff69e..0ef4897 100644 --- a/src/components/applications/calculator/Calculator.jsx +++ b/src/components/applications/calculator/Calculator.jsx @@ -1,4 +1,4 @@ -import styles from "./Calculator.module.css"; +// import styles from "./Calculator.module.css"; export function Calculator() { return ( diff --git a/src/components/desktop/Desktop.jsx b/src/components/desktop/Desktop.jsx index 53d6138..744e02f 100644 --- a/src/components/desktop/Desktop.jsx +++ b/src/components/desktop/Desktop.jsx @@ -3,10 +3,16 @@ import { SettingsManager } from "../../features/settings/settings.js"; import { useSettings } from "../../hooks/settings/SettingsContext.js"; import styles from "./Desktop.module.css"; import { useEffect } from "react"; +import { useModals } from "../../hooks/modals/ModalsContext.js"; +import { ModalsView } from "../modals/ModalsView.jsx"; +import Vector2 from "../../features/math/vector2.js"; +import { ContextMenu } from "../modals/context-menu/ContextMenu.jsx"; +import Modal from "../../features/modals/modal.js"; export function Desktop() { const settingsManager = useSettings(); const [wallpaper, setWallpaper] = useState(null); + const [modalsManager, modals] = useModals(); useEffect(() => { (async () => { @@ -15,7 +21,14 @@ export function Desktop() { })(); }, [settingsManager]); - return ( -
- ); + const onContextMenu = (event) => { + event.preventDefault(); + modalsManager.open(new Modal(ContextMenu).setPosition(new Vector2(event.clientX, event.clientY))); + }; + + return (<> +
+ +
+ ); } \ No newline at end of file diff --git a/src/components/modals/ModalView.jsx b/src/components/modals/ModalView.jsx new file mode 100644 index 0000000..db21615 --- /dev/null +++ b/src/components/modals/ModalView.jsx @@ -0,0 +1,17 @@ +import { Modal as ModalType } from "../../features/modals/modal.js"; +import OutsideClickListener from "../../hooks/utils/outsideClick.js"; +import styles from "./ModalView.module.css"; + +/** + * @param {object} root + * @param {ModalType} root.modal + */ +export function ModalView({ modal }) { + return ( + { modal.close(); }}> +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/components/modals/ModalView.module.css b/src/components/modals/ModalView.module.css new file mode 100644 index 0000000..8863b1a --- /dev/null +++ b/src/components/modals/ModalView.module.css @@ -0,0 +1,14 @@ +.Container { + --position-x: 0; + --position-y: 0; + + position: fixed; + top: calc(var(--position-y) * 1px); + left: calc(var(--position-x) * 1px); +} + +.Container > * { + background-color: var(--background-color-a); + border-radius: 0.5rem; + box-shadow: var(--window-box-shadow); +} \ No newline at end of file diff --git a/src/components/modals/ModalsView.jsx b/src/components/modals/ModalsView.jsx new file mode 100644 index 0000000..3b29b11 --- /dev/null +++ b/src/components/modals/ModalsView.jsx @@ -0,0 +1,18 @@ +import { Modal } from "../../features/modals/modal.js"; +import ModalsManager from "../../features/modals/modals.js"; +import { ModalView } from "./ModalView.jsx"; + +/** + * @param {object} root + * @param {ModalsManager} root.modalsManager + * @param {Modal[]} root.modals + */ +export function ModalsView({ modalsManager, modals }) { + return ( +
+ {modals?.map((modal) => + + )} +
+ ); +} \ No newline at end of file diff --git a/src/components/modals/context-menu/ContextMenu.jsx b/src/components/modals/context-menu/ContextMenu.jsx new file mode 100644 index 0000000..dff6eea --- /dev/null +++ b/src/components/modals/context-menu/ContextMenu.jsx @@ -0,0 +1,5 @@ +import styles from "./ContextMenu.module.css"; + +export function ContextMenu() { + return

{ event.preventDefault(); }}>This is a context menu!

; +} \ No newline at end of file diff --git a/src/components/modals/context-menu/ContextMenu.module.css b/src/components/modals/context-menu/ContextMenu.module.css new file mode 100644 index 0000000..ca3a344 --- /dev/null +++ b/src/components/modals/context-menu/ContextMenu.module.css @@ -0,0 +1,5 @@ +.Container { + margin: 0; + padding: 0.5rem; + border-top-left-radius: 0; +} \ No newline at end of file diff --git a/src/components/windows/Window.jsx b/src/components/windows/WindowView.jsx similarity index 96% rename from src/components/windows/Window.jsx rename to src/components/windows/WindowView.jsx index b71cfea..f23f60b 100644 --- a/src/components/windows/Window.jsx +++ b/src/components/windows/WindowView.jsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import styles from "./Window.module.css"; +import styles from "./WindowView.module.css"; import { faMinus, faWindowMaximize as fasWindowMaximize, faXmark } from "@fortawesome/free-solid-svg-icons"; import { ReactSVG } from "react-svg"; import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js"; @@ -20,7 +20,7 @@ import utilStyles from "../../styles/utils.module.css"; * @param {Function} props.onInteract * @param {object} props.options */ -export const Window = memo(function Window({ id, app, size, position, focused = false, onInteract, options }) { +export const WindowView = memo(function Window({ id, app, size, position, focused = false, onInteract, options }) { const windowsManager = useWindowsManager(); const nodeRef = useRef(null); diff --git a/src/components/windows/Window.module.css b/src/components/windows/WindowView.module.css similarity index 89% rename from src/components/windows/Window.module.css rename to src/components/windows/WindowView.module.css index bead889..a4946b6 100644 --- a/src/components/windows/Window.module.css +++ b/src/components/windows/WindowView.module.css @@ -4,10 +4,6 @@ } .Window-container { - --shadow-size: 0.3rem; - --shadow-opacity: 35%; - --shadow-spread: 3; - position: absolute; display: flex; flex-direction: column; @@ -15,7 +11,7 @@ min-height: 150px; background-color: var(--background-color-c); border-radius: 0.5rem; - box-shadow: calc(var(--shadow-size) * -1) var(--shadow-size) calc(var(--shadow-size) * var(--shadow-spread)) 0px rgba(0, 0, 0, var(--shadow-opacity)); + box-shadow: var(--window-box-shadow); resize: both; overflow: hidden; } diff --git a/src/components/windows/WindowsView.jsx b/src/components/windows/WindowsView.jsx index 39b88fa..70256ea 100644 --- a/src/components/windows/WindowsView.jsx +++ b/src/components/windows/WindowsView.jsx @@ -1,7 +1,7 @@ -import { Window } from "./Window.jsx"; import { useWindows } from "../../hooks/windows/WindowsContext.js"; import { useWindowsManager } from "../../hooks/windows/WindowsManagerContext.js"; import { useEffect, useState } from "react"; +import { WindowView } from "./WindowView.jsx"; export function WindowsView() { const windows = useWindows(); @@ -18,7 +18,7 @@ export function WindowsView() { return (
{sortedWindows.map(({ id, app, size, position, options }) => - { windowsManager.focus(id); }} id={id} key={id} diff --git a/src/features/applications/applications.js b/src/features/applications/applications.js index 93a40ce..d79a078 100644 --- a/src/features/applications/applications.js +++ b/src/features/applications/applications.js @@ -5,7 +5,7 @@ import { WebView } from "../../components/applications/.templates/WebView.jsx"; import { Terminal } from "../../components/applications/terminal/Terminal.jsx"; import { TextEditor } from "../../components/applications/text-editor/TextEditor.jsx"; import { Settings } from "../../components/applications/settings/Settings.jsx"; -import { Calculator } from "../../components/applications/calculator/Calculator.jsx"; +// import { Calculator } from "../../components/applications/calculator/Calculator.jsx"; import Vector2 from "../math/vector2.js"; export default class ApplicationsManager { diff --git a/src/features/modals/modal.js b/src/features/modals/modal.js new file mode 100644 index 0000000..6a0fece --- /dev/null +++ b/src/features/modals/modal.js @@ -0,0 +1,99 @@ +import Vector2 from "../math/vector2.js"; +import ModalsManager from "./modals.js"; + +export default class Modal { + /** @type {Vector2} */ + size = new Vector2(400, 200); + /** @type {Vector2} */ + position = new Vector2(300, 300); + /** @type {string | null} */ + icon = null; + /** @type {title | null} */ + title = null; + /** @type {ModalsManager} */ + modalsManager = null; + /** @type {import("react").ReactElement | null} */ + element = null; + /** @type {object} */ + props = {}; + /** @type {Function | null} */ + callback = null; + /** @type {number | null} */ + id = null; + /** @type {boolean} */ + closeOnOutsideClick = true; + + /** + * @param {import("react").ReactElement} element + * @param {Function} callback + */ + constructor(element, callback) { + this.element = element; + this.callback = callback; + this.focus(); + } + + /** + * @param {string} icon + * @returns {Modal} + */ + setIcon(icon) { + this.icon = icon; + return this; + } + + /** + * @param {string} title + * @returns {Modal} + */ + setTitle(title) { + this.title = title; + return this; + } + + /** + * @param {Vector2} position + * @returns {Modal} + */ + setPosition(position) { + this.position = position; + return this; + } + + /** + * @param {object} props + * @returns {Modal} + */ + setProps(props) { + this.props = props; + return this; + } + + /** + * @param {boolean} closeOnOutsideClick + * @returns {Modal} + */ + setCloseOnOutsideClick(closeOnOutsideClick) { + this.closeOnOutsideClick = closeOnOutsideClick; + return this; + } + + focus() { + this.lastInteraction = new Date().valueOf(); + } + + /** + * @param {...any} args + */ + finish(...args) { + if (this.modalsManager == null || this.id == null) + return; + + this.modalsManager.close(this.id); + this.callback?.(...args); + } + + close() { + this.finish(); + } +} \ No newline at end of file diff --git a/src/features/modals/modals.js b/src/features/modals/modals.js new file mode 100644 index 0000000..777ba06 --- /dev/null +++ b/src/features/modals/modals.js @@ -0,0 +1,86 @@ +import Modal from "./modal.js"; + +export default class ModalsManager { + /** + * @type {Modal} + */ + modals = {}; + + /** + * @type {Function} + */ + updateModals = () => {}; + + /** + * @param {Modal} modal + * @param {boolean} closeOthers + */ + open(modal, closeOthers = true) { + if (closeOthers) { + this.modalIds.forEach((id) => { + this.close(id, false); + }); + } + + let id = 0; + while (this.modalIds.includes(id.toString())) { + id++; + } + + modal.id = id; + modal.modalsManager = this; + + this.modals[id] = modal; + this.updateModals(this.modals); + } + + /** + * @param {string} modalId + * @param {boolean} sendModalsUpdate + */ + close(modalId, sendModalsUpdate = true) { + modalId = modalId.toString(); + + if (!this.modalIds.includes(modalId)) { + console.log(`Failed to close modal ${modalId}: modal not found`); + return; + } + + console.log(`Closing window ${modalId}`); + delete this.modals[modalId]; + + if (sendModalsUpdate) + this.updateModals(this.modals); + } + + /** + * @param {string} modalId + */ + focus(modalId) { + modalId = modalId.toString(); + + if (!this.modalIds.includes(modalId)) { + console.log(`Failed to focus modal ${modalId}: modal not found`); + return; + } + + const modal = this.modals[modalId]; + modal.focus(); + + this.updateModals(this.modals); + } + + /** + * @param {Function} updateModals + */ + setUpdateModals(updateModals) { + this.updateModals = updateModals; + } + + /** + * @returns {string[]} + */ + get modalIds() { + return Object.keys(this.modals); + } +} \ No newline at end of file diff --git a/src/hooks/modals/ModalsContext.js b/src/hooks/modals/ModalsContext.js new file mode 100644 index 0000000..bdcb67f --- /dev/null +++ b/src/hooks/modals/ModalsContext.js @@ -0,0 +1,19 @@ +import { useCallback, useState } from "react"; +import ModalsManager from "../../features/modals/modals.js"; +import Modal from "../../features/modals/modal.js"; + +/** + * @returns {[ModalsManager, Modal[]]} + */ +export function useModals() { + const modalsManager = new ModalsManager(); + const [modals, setModals] = useState([]); + + const updateModals = useCallback((updatedModals) => { + setModals(Object.values(updatedModals)); + }, []); + + modalsManager.setUpdateModals(updateModals); + + return [modalsManager, modals]; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index dda47e4..0e3cc9d 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,7 @@ import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - + ); diff --git a/src/styles/global.css b/src/styles/global.css index d0ba01c..5b83c60 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -37,11 +37,19 @@ --background-color-d: var(--dark-grey-e); --scrollbar-color: hsla(211, 29%, 40%, 25%); + + --window-shadow-size: 0.3rem; + --window-shadow-opacity: 35%; + --window-shadow-spread: 3; + --window-box-shadow: calc(var(--window-shadow-size) * -1) var(--window-shadow-size) + calc(var(--window-shadow-size) * var(--window-shadow-spread)) 0px + rgba(0, 0, 0, var(--window-shadow-opacity)); } html, body, #root { width: 100%; height: 100%; + pointer-events: none; } html {