Added modals

This commit is contained in:
Prozilla 2023-08-11 15:27:27 +02:00
parent 2695e78344
commit af29c8bc5e
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
18 changed files with 315 additions and 15 deletions

View file

@ -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>

View file

@ -6,4 +6,9 @@
height: 100%;
text-align: center;
overflow: hidden;
pointer-events: none;
}
.App > * {
pointer-events: auto;
}

View file

@ -1,4 +1,4 @@
import styles from "./Calculator.module.css";
// import styles from "./Calculator.module.css";
export function Calculator() {
return (

View file

@ -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>
</>);
}

View 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>
);
}

View 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);
}

View 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>
);
}

View 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>;
}

View file

@ -0,0 +1,5 @@
.Container {
margin: 0;
padding: 0.5rem;
border-top-left-radius: 0;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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}

View file

@ -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 {

View 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();
}
}

View 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);
}
}

View 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];
}

View file

@ -7,7 +7,7 @@ import reportWebVitals from "./reportWebVitals";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
<App/>
</React.StrictMode>
);

View file

@ -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 {