Added modals
This commit is contained in:
parent
2695e78344
commit
af29c8bc5e
18 changed files with 315 additions and 15 deletions
15
src/App.jsx
15
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 (
|
||||
<VirtualRootProvider>
|
||||
<WindowsManagerProvider>
|
||||
|
|
@ -14,6 +28,7 @@ function App() {
|
|||
<div className={styles.App}>
|
||||
<Taskbar/>
|
||||
<WindowsView/>
|
||||
<ModalsView/>
|
||||
<Desktop/>
|
||||
</div>
|
||||
</SettingsProvider>
|
||||
|
|
|
|||
|
|
@ -6,4 +6,9 @@
|
|||
height: 100%;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.App > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import styles from "./Calculator.module.css";
|
||||
// import styles from "./Calculator.module.css";
|
||||
|
||||
export function Calculator() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={styles.Container} style={{ backgroundImage: `url(${wallpaper})` }}/>
|
||||
);
|
||||
const onContextMenu = (event) => {
|
||||
event.preventDefault();
|
||||
modalsManager.open(new Modal(ContextMenu).setPosition(new Vector2(event.clientX, event.clientY)));
|
||||
};
|
||||
|
||||
return (<>
|
||||
<div className={styles.Container} style={{ backgroundImage: `url(${wallpaper})` }} onContextMenu={onContextMenu}>
|
||||
<ModalsView modalsManager={modalsManager} modals={modals}/>
|
||||
</div>
|
||||
</>);
|
||||
}
|
||||
17
src/components/modals/ModalView.jsx
Normal file
17
src/components/modals/ModalView.jsx
Normal file
|
|
@ -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 (
|
||||
<OutsideClickListener onOutsideClick={() => { modal.close(); }}>
|
||||
<div className={styles.Container} style={{ "--position-x": modal.position.x, "--position-y": modal.position.y }}>
|
||||
<modal.element modal={modal} {...modal.props}/>
|
||||
</div>
|
||||
</OutsideClickListener>
|
||||
);
|
||||
}
|
||||
14
src/components/modals/ModalView.module.css
Normal file
14
src/components/modals/ModalView.module.css
Normal file
|
|
@ -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);
|
||||
}
|
||||
18
src/components/modals/ModalsView.jsx
Normal file
18
src/components/modals/ModalsView.jsx
Normal file
|
|
@ -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 (
|
||||
<div>
|
||||
{modals?.map((modal) =>
|
||||
<ModalView key={modal.id} modal={modal}/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
src/components/modals/context-menu/ContextMenu.jsx
Normal file
5
src/components/modals/context-menu/ContextMenu.jsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import styles from "./ContextMenu.module.css";
|
||||
|
||||
export function ContextMenu() {
|
||||
return <p className={styles.Container} onContextMenu={(event) => { event.preventDefault(); }}>This is a context menu!</p>;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.Container {
|
||||
margin: 0;
|
||||
padding: 0.5rem;
|
||||
border-top-left-radius: 0;
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 (<div>
|
||||
{sortedWindows.map(({ id, app, size, position, options }) =>
|
||||
<Window
|
||||
<WindowView
|
||||
onInteract={() => { windowsManager.focus(id); }}
|
||||
id={id}
|
||||
key={id}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
99
src/features/modals/modal.js
Normal file
99
src/features/modals/modal.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
86
src/features/modals/modals.js
Normal file
86
src/features/modals/modals.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
19
src/hooks/modals/ModalsContext.js
Normal file
19
src/hooks/modals/ModalsContext.js
Normal file
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import reportWebVitals from "./reportWebVitals";
|
|||
const root = ReactDOM.createRoot(document.getElementById("root"));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue