Files
mdg-filius/packages/editor/src/desktop/DesktopWindow.tsx
T
julian.kiedaischandClaude Opus 5.5 f0e7c73ec7 feat(editor): F4.6c the software installation on a computer's desktop
A new oracle, InstallRun, installs each of Java's programs on new
computers, all of them on one, each installed and removed again, and a
DNS and an e-mail server installed again after their removal; Java saves
the result as a project. A new program is switched off (a computer's
firewall is on), makes the folders and files it lacks (the web server's
start page in FILIUS' language and its picture, /peer2peer,
/mailserver/konten.txt, /dns/hosts), and removing it takes its settings
and leaves its files; a server installed again reads its records or
accounts from them. Java installs in the order of its list and starts
the program at once.

The document's setSoftware does the same, checked against Java's saved
installation. The core knows what a program makes in the files and
installs or removes a program while the simulation runs; the room
watches the document's programs and hands changes to the engine, so a
new command line works at once. The desktop offers Java's dialog with
two lists, arrows, double click and apply changes.

Also: Java's file may name a number it rolled by the result of any
getter (an idref, here a thread's priority); GetterDefaults now
measures the priority of the saved thread classes, which the import
read as 0 before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:03:35 +00:00

186 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// A computer's desktop in the simulation, after Java's GUIDesktopWindow and
// GUIDesktopPanel: a window titled with the computer's label and address
// (the label alone where it is the address), Java's background, the
// software installation's icon first and then the installed programs in the
// order of Java's list, a grey taskbar with "home screen" in the middle and,
// on the right, the network icon — lit while the computer's cable carries a
// frame — that shows its network as it runs (NetworkInfoPanel). Java's panel
// has a "change" button under read-only fields, which writes back what they
// show; it is left out.
//
// On a wide screen the window lies over the surface and moves by its title
// bar, as Java's windows do; on a phone it fills the surface. A program's
// icon opens its window over the desktop (ProgramFrame), the home button
// leaves it; so far the command line has one (F4.6b), the others follow.
import { useCallback, useEffect, useState, type PointerEvent as ReactPointerEvent } from 'react';
import type * as Y from 'yjs';
import { desktopView } from '@mdg-filius/doc';
import background from '../assets/desktop/hintergrundbild.png';
import networkOff from '../assets/desktop/netzwek_aus.png';
import networkOn from '../assets/desktop/netzwek_c.png';
import { ICON_URLS } from '../canvas/icons.js';
import { useLanguage, useTranslate } from '../i18n/i18n.js';
import type { Simulation } from '../simulation/useSimulation.js';
import { ProgramFrame } from './ProgramFrame.js';
import { desktopPrograms, INSTALLATION_ICON, JAVA_PROGRAMS, type DesktopProgram } from './programs.js';
import { InstallationPanel } from './InstallationPanel.js';
import { TerminalPanel } from './TerminalPanel.js';
import './desktop.css';
export interface DesktopWindowProps {
readonly doc: Y.Doc;
readonly id: string;
/** The room: the computer's network as it runs, its programs' windows. */
readonly simulation: Simulation;
/** Whether its cable carried a frame at the last tick. */
readonly active: boolean;
/** The window's top left corner on the surface. */
readonly x: number;
readonly y: number;
/** Its place in the stack of desktops. */
readonly z: number;
/** The window open on it — a program's or the installation's —, kept by the editor over a return to the design (Java's desktop keeps it). */
readonly program: DesktopWindowName | null;
readonly onProgram: (program: DesktopWindowName | null) => void;
/** The person's origin, for what the installation changes in the document. */
readonly origin: object;
readonly onMove: (x: number, y: number) => void;
readonly onFront: () => void;
readonly onClose: () => void;
}
/** What can be open on a desktop: a program's window or the software installation. */
export type DesktopWindowName = DesktopProgram | 'installation';
/** The programs whose windows are built so far. */
const WITH_WINDOW: ReadonlySet<DesktopProgram> = new Set(['commandPrompt']);
/** The document's desktop of a computer, read anew after every change (a program installed, the label changed). */
function useDesktopView(doc: Y.Doc, id: string) {
const [view, setView] = useState(() => desktopView(doc, id));
useEffect(() => {
setView(desktopView(doc, id));
const update = () => setView(desktopView(doc, id));
doc.on('update', update);
return () => doc.off('update', update);
}, [doc, id]);
return view;
}
export function DesktopWindow({ doc, id, simulation, active, x, y, z, program: open, onProgram: setOpen, origin, onMove, onFront, onClose }: DesktopWindowProps) {
const t = useTranslate();
const language = useLanguage();
const view = useDesktopView(doc, id);
const { watch } = simulation;
useEffect(() => watch([`node:${id}`]), [watch, id]);
const settings = simulation.nodes.get(id);
const [info, setInfo] = useState(false);
const home = useCallback(() => setOpen(null), [setOpen]);
const [drag, setDrag] = useState<{ readonly pointer: number; readonly dx: number; readonly dy: number; moved: boolean } | null>(null);
if (view === undefined) return null;
const programs = desktopPrograms(view.programs, language);
// A program removed while its window was open: the desktop shows.
const shown = open === 'installation' || (open !== null && programs.includes(open)) ? open : null;
const address = settings?.ipAddress ?? '';
const title = view.addressAsName ? view.label : `${view.label} - ${address}`;
// The title bar moves the window; the pointer is captured once it moves (pointer-capture trap).
const onPointerDown = (event: ReactPointerEvent<HTMLElement>) => {
if ((event.target as Element).closest('button') !== null) return;
setDrag({ pointer: event.pointerId, dx: event.clientX - x, dy: event.clientY - y, moved: false });
};
const onPointerMove = (event: ReactPointerEvent<HTMLElement>) => {
if (drag === null || drag.pointer !== event.pointerId) return;
if (!drag.moved) {
drag.moved = true;
event.currentTarget.setPointerCapture(event.pointerId);
}
onMove(Math.max(0, event.clientX - drag.dx), Math.max(0, event.clientY - drag.dy));
};
const onPointerUp = () => setDrag(null);
const rows: readonly [string, string][] = [
[t('network.ipAddress'), address],
[t('network.subnetMask'), settings?.subnetMask ?? ''],
[t('network.dnsServer'), settings?.dnsServer ?? ''],
[t('network.gateway'), settings?.defaultGateway ?? ''],
[t('network.macAddress'), settings?.macAddress ?? ''],
];
return (
<section className="desktop-window" role="dialog" aria-label={title} data-desktop={id} style={{ left: x, top: y, zIndex: 5 + z }} onPointerDownCapture={onFront}>
<header className="desktop-titlebar" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} onPointerCancel={onPointerUp}>
<img src={ICON_URLS[view.kind === 'notebook' ? 'laptop' : 'server']} alt="" />
<h2>{title}</h2>
<button type="button" className="desktop-close" aria-label={t('sheet.close')} title={t('sheet.close')} onClick={onClose}>
×
</button>
</header>
<div className="desktop-screen" style={{ backgroundImage: `url(${background})` }}>
<ul className="desktop-icons">
<li>
<button type="button" className="desktop-icon" data-program="installation" title={t('desktop.installation')} onClick={() => setOpen('installation')}>
<img src={INSTALLATION_ICON} alt="" />
<span>{t('desktop.installation')}</span>
</button>
</li>
{programs.map((program) => (
<li key={program}>
<button
type="button"
className="desktop-icon"
data-program={program}
aria-disabled={WITH_WINDOW.has(program) ? undefined : 'true'}
title={t(`program.${program}`)}
onClick={() => WITH_WINDOW.has(program) && setOpen(program)}
>
<img src={JAVA_PROGRAMS[program].icon} alt="" />
<span>{t(`program.${program}`)}</span>
</button>
</li>
))}
</ul>
{shown === 'installation' ? (
<ProgramFrame icon={INSTALLATION_ICON} title={t('desktop.installation')} program="installation">
<InstallationPanel doc={doc} node={id} installed={view.programs} origin={origin} onDone={home} />
</ProgramFrame>
) : shown === null ? null : (
<ProgramFrame icon={JAVA_PROGRAMS[shown].icon} title={t(`program.${shown}`)} program={shown}>
{shown === 'commandPrompt' ? <TerminalPanel node={id} simulation={simulation} onExit={home} /> : null}
</ProgramFrame>
)}
{info ? (
// A press beside the panel closes it, as in Java.
<div className="desktop-info-layer" onPointerDown={(event) => event.target === event.currentTarget && setInfo(false)}>
<dl className="desktop-network" aria-label={t('desktop.network')}>
{rows.map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
</div>
) : null}
</div>
<footer className="desktop-taskbar">
<span />
<button
type="button"
className="desktop-home"
onClick={() => {
setInfo(false);
setOpen(null);
}}
>
{t('desktop.home')}
</button>
<button type="button" className="desktop-network-icon" data-active={active ? 'true' : undefined} aria-label={t('desktop.network')} aria-expanded={info} title={address} onClick={() => setInfo(!info)}>
<img src={active ? networkOn : networkOff} alt="" />
</button>
</footer>
</section>
);
}