From de0a3306db7352f0e6048661de3cdc568ceb9873 Mon Sep 17 00:00:00 2001 From: "Yifeng \"Evan\" Wang" <7312949+doodlewind@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:38:56 +0800 Subject: [PATCH] refactor(desk98): author the Vue Vapor desktop in JSX instead of SFCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same framework, different authoring surface: the 18 .vue single-file components collapse back into the original three-module shape — chrome.tsx (T98, caption controls, taskbar, popups, Start menu, icons), programs.tsx (program views + content-local hit helpers back in one file) and app.tsx (the compositor, with the window shell + content dispatch) — compiled through vue-jsx-vapor like apps/hero-vue-vapor and the app.vue-vapor.tsx variants. Reactivity, state modules, the desk dialect, tests and the build wiring are untouched; pocket.json only moves the entry to main.tsx. JSX-specific rules honored: lists are plain .map() (the repo's vapor JSX idiom), reactive branches live in JSX child positions (a bare ternary returned from a component body evaluates once — MinesCell keeps its hidden/revealed swap inside a stable 16×16 wrapper), and imports use the clean @pocketjs/framework/* subpaths the framework resolves per-framework. Co-Authored-By: Claude Fable 5 --- apps/desk98/AboutView.vue | 31 - apps/desk98/Button98.vue | 21 - apps/desk98/CaptionButtons.vue | 39 - apps/desk98/Counter.vue | 21 - apps/desk98/DesktopIcons.vue | 23 - apps/desk98/Digit.vue | 39 - apps/desk98/FolderView.vue | 57 -- apps/desk98/MinesCell.vue | 55 -- apps/desk98/MinesView.vue | 48 -- apps/desk98/NotepadView.vue | 78 -- apps/desk98/PopupPanel.vue | 64 -- apps/desk98/ShutdownView.vue | 43 - apps/desk98/StartMenu.vue | 63 -- apps/desk98/T98.vue | 17 - apps/desk98/Taskbar.vue | 54 -- apps/desk98/Window98.vue | 55 -- apps/desk98/app.tsx | 1488 ++++++++++++++++++++++++++++++++ apps/desk98/app.vue | 1341 ---------------------------- apps/desk98/chrome.tsx | 252 ++++++ apps/desk98/main.ts | 8 - apps/desk98/main.tsx | 5 + apps/desk98/pocket.json | 2 +- apps/desk98/programs.ts | 97 --- apps/desk98/programs.tsx | 446 ++++++++++ apps/desk98/screenshot.png | Bin 63444 -> 63462 bytes docs/BACKENDS.md | 2 +- 26 files changed, 2193 insertions(+), 2156 deletions(-) delete mode 100644 apps/desk98/AboutView.vue delete mode 100644 apps/desk98/Button98.vue delete mode 100644 apps/desk98/CaptionButtons.vue delete mode 100644 apps/desk98/Counter.vue delete mode 100644 apps/desk98/DesktopIcons.vue delete mode 100644 apps/desk98/Digit.vue delete mode 100644 apps/desk98/FolderView.vue delete mode 100644 apps/desk98/MinesCell.vue delete mode 100644 apps/desk98/MinesView.vue delete mode 100644 apps/desk98/NotepadView.vue delete mode 100644 apps/desk98/PopupPanel.vue delete mode 100644 apps/desk98/ShutdownView.vue delete mode 100644 apps/desk98/StartMenu.vue delete mode 100644 apps/desk98/T98.vue delete mode 100644 apps/desk98/Taskbar.vue delete mode 100644 apps/desk98/Window98.vue create mode 100644 apps/desk98/app.tsx delete mode 100644 apps/desk98/app.vue create mode 100644 apps/desk98/chrome.tsx delete mode 100644 apps/desk98/main.ts create mode 100644 apps/desk98/main.tsx delete mode 100644 apps/desk98/programs.ts create mode 100644 apps/desk98/programs.tsx diff --git a/apps/desk98/AboutView.vue b/apps/desk98/AboutView.vue deleted file mode 100644 index 35c83bcc..00000000 --- a/apps/desk98/AboutView.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/apps/desk98/Button98.vue b/apps/desk98/Button98.vue deleted file mode 100644 index d677555f..00000000 --- a/apps/desk98/Button98.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/apps/desk98/CaptionButtons.vue b/apps/desk98/CaptionButtons.vue deleted file mode 100644 index 20118a41..00000000 --- a/apps/desk98/CaptionButtons.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/apps/desk98/Counter.vue b/apps/desk98/Counter.vue deleted file mode 100644 index ca37cec8..00000000 --- a/apps/desk98/Counter.vue +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/apps/desk98/DesktopIcons.vue b/apps/desk98/DesktopIcons.vue deleted file mode 100644 index 2d0eb6db..00000000 --- a/apps/desk98/DesktopIcons.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/apps/desk98/Digit.vue b/apps/desk98/Digit.vue deleted file mode 100644 index 5de1f583..00000000 --- a/apps/desk98/Digit.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/apps/desk98/FolderView.vue b/apps/desk98/FolderView.vue deleted file mode 100644 index ed95df73..00000000 --- a/apps/desk98/FolderView.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/apps/desk98/MinesCell.vue b/apps/desk98/MinesCell.vue deleted file mode 100644 index 32f832ac..00000000 --- a/apps/desk98/MinesCell.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - diff --git a/apps/desk98/MinesView.vue b/apps/desk98/MinesView.vue deleted file mode 100644 index 3672ac2f..00000000 --- a/apps/desk98/MinesView.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/apps/desk98/NotepadView.vue b/apps/desk98/NotepadView.vue deleted file mode 100644 index e74386cc..00000000 --- a/apps/desk98/NotepadView.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - diff --git a/apps/desk98/PopupPanel.vue b/apps/desk98/PopupPanel.vue deleted file mode 100644 index 707a2d9e..00000000 --- a/apps/desk98/PopupPanel.vue +++ /dev/null @@ -1,64 +0,0 @@ - - - diff --git a/apps/desk98/ShutdownView.vue b/apps/desk98/ShutdownView.vue deleted file mode 100644 index 85242e8b..00000000 --- a/apps/desk98/ShutdownView.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - diff --git a/apps/desk98/StartMenu.vue b/apps/desk98/StartMenu.vue deleted file mode 100644 index 4eb83282..00000000 --- a/apps/desk98/StartMenu.vue +++ /dev/null @@ -1,63 +0,0 @@ - - - diff --git a/apps/desk98/T98.vue b/apps/desk98/T98.vue deleted file mode 100644 index 1a828722..00000000 --- a/apps/desk98/T98.vue +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/apps/desk98/Taskbar.vue b/apps/desk98/Taskbar.vue deleted file mode 100644 index 23a263ea..00000000 --- a/apps/desk98/Taskbar.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/apps/desk98/Window98.vue b/apps/desk98/Window98.vue deleted file mode 100644 index 587be263..00000000 --- a/apps/desk98/Window98.vue +++ /dev/null @@ -1,55 +0,0 @@ - - - diff --git a/apps/desk98/app.tsx b/apps/desk98/app.tsx new file mode 100644 index 00000000..1daaaa4a --- /dev/null +++ b/apps/desk98/app.tsx @@ -0,0 +1,1488 @@ +// apps/desk98/app.tsx — PocketJS 98: a Windows 98 desktop compositor as a +// PocketJS app on the gpui macOS host — Vue Vapor, authored in JSX +// (vue-jsx-vapor, the same path as apps/hero-vue-vapor). +// +// The compositor owns ALL input: the host forwards raw mouse/keyboard over +// the desk svc dialect (svc.ts), and this file routes every event itself — +// window drags, resizes, caption buttons, menus, text selection, program +// content — against the same geometry the chrome renders (wm.ts + +// programs.tsx helpers). The framework's focus/onPress pipeline is never +// engaged; a window manager IS its own hit tester. Window moves ride +// paint-only translate props, raises ride zIndex, so a drag never relayouts +// and an idle desktop hashes stable for the demand-render governor. +// +// Shortcuts are macOS-style: the host forwards ⌘ chords as cmd-flagged key +// lines (⌘Q quits and ⌘V pastes host-side) — ⌘W closes, ⌘M minimizes, +// ⌘` cycles, ⌘N opens Notepad, ⌘Esc toggles Start, ⌘A/C/X edit the +// focused Notepad. +// +// Without the desk companion (sim, goldens, consoles) the app boots a +// static arrangement and just renders it — the unmodified-app base case. + +import { ref, shallowRef, triggerRef } from "vue"; +import { Image, View } from "@pocketjs/framework/components"; +import { onFrame } from "@pocketjs/framework/lifecycle"; +import { virtualNow } from "@pocketjs/framework/clock"; +import { connectSvc, type CursorKind, type HostEvent } from "./svc.ts"; +import { + cascadePos, + clampMove, + contentTop, + cursorForDir, + hitRegion, + maximizedGeo, + resizeGeo, + type CaptionButton, + type Dir, + type Geo, + type Region, +} from "./wm.ts"; +import { + createWin, + type AboutData, + type DeskIcon, + type FolderData, + type FolderRow, + type MinesData, + type PadData, + type Popup, + type PopupItem, + type ShutdownData, + type TaskEntry, + type WinCtl, +} from "./state.ts"; +import { + ABOUT_GEO, + AboutView, + aboutHit, + FolderView, + folderRowAt, + measure, + MINES_GEO, + MinesView, + minesHit, + NotepadView, + PAD_LINE_H, + SHUTDOWN_GEO, + ShutdownView, + shutdownHit, +} from "./programs.tsx"; +import { + applyMove, + backspace, + colFromX, + del, + deleteSel, + hasSel, + insertText, + selectAll, + selectedText, + wordRangeAt, + type CaretMove, + type Doc, +} from "./notepad.ts"; +import { newMines, reveal, toggleFlag } from "./mines.ts"; +import { CAPTION_ACTIVE, CAPTION_INACTIVE, FRAME, TASK_H } from "./theme.ts"; +import { CaptionButtons, DesktopIcons, PopupPanel, StartMenu, T98, Taskbar } from "./chrome.tsx"; + +const WELCOME = [ + "Welcome to PocketJS 98.", + "", + "This desktop is one PocketJS guest: the windows,", + "the taskbar, the Start menu and this Notepad are", + "Vue Vapor JSX over the same DrawList contract", + "the consoles boot, painted by the gpui backend.", + "", + "Things to try:", + " - drag windows by the title bar", + " - drag any edge or corner to resize", + " - double-click a title bar to maximize", + " - drag-select this text; Cmd+C/X/V, right-click", + " - right-click the desktop or Minesweeper", + " - Cmd+` cycles windows, Cmd+W closes them", + " - Cmd+Esc opens the Start menu", + "", + "The font is W95FA, baked to the same atlas", + "format every other PocketJS target reads.", +]; + +type Drag = + | { type: "move"; id: number; sx: number; sy: number; orig: Geo } + | { type: "resize"; id: number; dir: Dir; sx: number; sy: number; orig: Geo } + | { type: "capbtn"; id: number; btn: CaptionButton } + | { type: "textsel"; id: number } + | { type: "minehold"; id: number } + | { type: "smiley"; id: number } + | { type: "dialogbtn"; id: number; tag: string } + | null; + +// Program data bags (typed views of w.data for render + routing). +const padOf = (w: WinCtl) => w.data as PadData; +const minesOf = (w: WinCtl) => w.data as MinesData; +const folderOf = (w: WinCtl) => w.data as FolderData; +const aboutOf = (w: WinCtl) => w.data as AboutData; +const shutdownOf = (w: WinCtl) => w.data as ShutdownData; + +/** One window: raised frame, caption gradient, controls, menu bar, and the + * program content dispatched on the (static) window kind. Position/size + * ride the style prop — translate moves are paint-only, and zIndex raises + * without reordering siblings (a reorder would rebuild the layout tree). */ +function Window98(props: { win: WinCtl; active: boolean }) { + const w = props.win; + return ( + + + + + + + + + {w.menus !== null ? ( + + {(w.menus ?? []).map((menu, i) => ( + + + + ))} + + ) : null} + + {w.kind === "notepad" ? ( + + ) : w.kind === "mines" ? ( + + ) : w.kind === "folder" ? ( + + ) : w.kind === "about" ? ( + + ) : ( + + )} + + + ); +} + +export default function App() { + const svc = connectSvc(); + + const vp = shallowRef<{ w: number; h: number }>({ w: 800, h: 600 }); + const wins = shallowRef([]); + const focusId = ref(-1); + const iconSel = ref(-1); + const startOpen = ref(false); + const startHover = ref(-1); + const startFly = shallowRef<{ index: number; popup: Popup } | null>(null); + const flyHover = ref(-1); + const popup = shallowRef<{ popup: Popup; winId?: number } | null>(null); + const popupHover = ref(-1); + const clock = ref("--:--"); + + // Non-reactive input state (nothing renders from these directly). + let stack: number[] = []; // window ids, bottom → top + let drag: Drag = null; + let mx = 0; + let my = 0; + let prevDown = false; + let epoch = 0; // wall ms at hello, anchored to virtualNow() then + let epochAt = 0; + let lastCursor: CursorKind = "default"; + let lastClick = { key: "", t: -1, x: 0, y: 0 }; + let lastCaret = { x: -1, y: -1, h: 0 }; + let minesStart = 0; + + const byId = (id: number) => wins.value.find((w) => w.id === id); + const focused = () => byId(focusId.value); + + // ---- window management ---------------------------------------------------- + + function applyZ() { + stack.forEach((id, i) => { + const w = byId(id); + if (w) w.z.value = i + 1; + }); + } + + function raise(id: number) { + stack = stack.filter((x) => x !== id).concat(id); + applyZ(); + focusId.value = id; + const w = byId(id); + if (w?.minimized.value) w.minimized.value = false; + } + + function addWin(w: WinCtl) { + wins.value = wins.value.concat(w); + stack = stack.concat(w.id); + applyZ(); + focusId.value = w.id; + } + + function closeWin(id: number) { + wins.value = wins.value.filter((w) => w.id !== id); + stack = stack.filter((x) => x !== id); + applyZ(); + focusId.value = stack.length > 0 ? stack[stack.length - 1] : -1; + } + + function minimize(id: number) { + const w = byId(id); + if (w) w.minimized.value = true; + const next = stack.filter((x) => x !== id && !byId(x)?.minimized.value); + focusId.value = next.length > 0 ? next[next.length - 1] : -1; + } + + function toggleMax(w: WinCtl) { + if (!w.resizable) return; + if (w.maximized.value) { + w.maximized.value = false; + if (w.restoreGeo) w.geo.value = w.restoreGeo; + } else { + w.restoreGeo = w.geo.value; + w.maximized.value = true; + w.geo.value = maximizedGeo(vp.value.w, vp.value.h); + } + } + + function cycleWindows() { + const visible = stack.filter((id) => !byId(id)?.minimized.value); + if (visible.length < 2) return; + raise(visible[0]); // bottom-most visible comes up — repeated ⌘` cycles + } + + // ---- clipboard (notepad selection ↔ host) --------------------------------- + + function focusedPad(): { w: WinCtl; d: PadData } | null { + const w = focused(); + return w?.kind === "notepad" ? { w, d: padOf(w) } : null; + } + + function copySel(): void { + const p = focusedPad(); + if (!p) return; + const text = selectedText(p.d.doc.value); + if (text !== "" && svc) svc.send({ t: "copy", text }); + } + + function cutSel(): void { + const p = focusedPad(); + if (!p || !hasSel(p.d.doc.value)) return; + copySel(); + p.d.doc.value = deleteSel(p.d.doc.value); + scrollCaretIntoView(p.w); + } + + function pasteReq(): void { + // The clipboard lives host-side; the paste line answers next frame. + svc?.send({ t: "paste-req" }); + } + + function selectAllIn(w: WinCtl): void { + const d = padOf(w); + d.doc.value = selectAll(d.doc.value); + } + + // ---- programs --------------------------------------------------------------- + + function openNotepad(title: string, content: string[]) { + const existing = wins.value.find((w) => w.kind === "notepad" && w.title === title); + if (existing) return raise(existing.id); + const data: PadData = { + kind: "notepad", + doc: shallowRef({ + lines: content.length > 0 ? content : [""], + caret: { row: 0, col: 0 }, + }), + scroll: ref(0), + preedit: ref<{ s: string; c: number } | null>(null), + }; + const w = createWin({ + kind: "notepad", + title, + icon: "icons/notepad-16.svg", + geo: cascadePos(wins.value.length, vp.value.w, vp.value.h, 400, 300), + minW: 220, + minH: 140, + menus: [ + { + label: "File", + width: measure("File") + 12, + items: () => [ + { + label: "New", + act: () => { + data.doc.value = { lines: [""], caret: { row: 0, col: 0 } }; + }, + }, + { sep: true, label: "" }, + { + label: "Exit", + shortcut: "Cmd+W", + act: () => { + closeWin(w.id); + }, + }, + ], + }, + { + label: "Edit", + width: measure("Edit") + 12, + items: () => [ + { label: "Cut", shortcut: "Cmd+X", disabled: !hasSel(data.doc.value), act: cutSel }, + { label: "Copy", shortcut: "Cmd+C", disabled: !hasSel(data.doc.value), act: copySel }, + { label: "Paste", shortcut: "Cmd+V", act: pasteReq }, + { + label: "Select All", + shortcut: "Cmd+A", + act: () => { + selectAllIn(w); + }, + }, + { sep: true, label: "" }, + { + label: "Time/Date", + shortcut: "F5", + act: () => { + insertTimeDate(w); + }, + }, + { sep: true, label: "" }, + { label: "Word Wrap", disabled: true }, + ], + }, + { + label: "Help", + width: measure("Help") + 12, + items: () => [{ label: "About PocketJS 98", act: openAbout }], + }, + ], + data, + }); + addWin(w); + } + + function openMines() { + const existing = wins.value.find((w) => w.kind === "mines"); + if (existing) return raise(existing.id); + const data: MinesData = { + kind: "mines", + board: shallowRef(newMines((virtualNow() * 1000) | 0)), + held: ref(-1), + smileyHeld: ref(false), + elapsed: ref(0), + }; + const w = createWin({ + kind: "mines", + title: "Minesweeper", + icon: "icons/mines-16.svg", + geo: { ...cascadePos(wins.value.length, vp.value.w, vp.value.h, MINES_GEO.w, MINES_GEO.h) }, + buttons: ["min", "close"], + resizable: false, + menus: [ + { + label: "Game", + width: measure("Game") + 12, + items: () => [ + { + label: "New", + shortcut: "F2", + act: () => { + minesNew(w); + }, + }, + { sep: true, label: "" }, + { + label: "Exit", + shortcut: "Cmd+W", + act: () => { + closeWin(w.id); + }, + }, + ], + }, + { + label: "Help", + width: measure("Help") + 12, + items: () => [{ label: "About PocketJS 98", act: openAbout }], + }, + ], + data, + }); + addWin(w); + } + + function minesNew(w: WinCtl) { + const d = minesOf(w); + d.board.value = newMines((virtualNow() * 1000) | 0); + d.elapsed.value = 0; + minesStart = 0; + } + + function openFolder(title: string, icon: string, rows: FolderRow[], geoW = 420, geoH = 280) { + const existing = wins.value.find((w) => w.kind === "folder" && w.title === title); + if (existing) return raise(existing.id); + const data: FolderData = { kind: "folder", rows, selected: ref(-1) }; + const w = createWin({ + kind: "folder", + title, + icon, + geo: cascadePos(wins.value.length, vp.value.w, vp.value.h, geoW, geoH), + minW: 260, + minH: 160, + data, + }); + addWin(w); + } + + function openMyComputer() { + openFolder("My Computer", "icons/computer-16.svg", [ + { + icon: "icons/drive-16.svg", + name: "(C:)", + size: "", + type: "Local Disk", + open: () => { + openDriveC(); + }, + }, + { icon: "icons/cdrom-16.svg", name: "(D:)", size: "", type: "CD-ROM Disc" }, + { icon: "icons/folder-16.svg", name: "Control Panel", size: "", type: "System Folder" }, + { icon: "icons/folder-16.svg", name: "Printers", size: "", type: "System Folder" }, + ]); + } + + function openDriveC() { + openFolder("(C:)", "icons/drive-16.svg", [ + { icon: "icons/folder-16.svg", name: "Program Files", size: "", type: "File Folder" }, + { icon: "icons/folder-16.svg", name: "Windows", size: "", type: "File Folder" }, + { icon: "icons/file-16.svg", name: "AUTOEXEC.BAT", size: "1 KB", type: "MS-DOS Batch File" }, + { icon: "icons/file-16.svg", name: "CONFIG.SYS", size: "1 KB", type: "System file" }, + { + icon: "icons/notepad-16.svg", + name: "README.TXT", + size: "2 KB", + type: "Text Document", + open: () => { + openNotepad("README.TXT - Notepad", WELCOME); + }, + }, + ]); + } + + function openDocuments() { + openFolder("My Documents", "icons/folder-16.svg", [ + { + icon: "icons/notepad-16.svg", + name: "welcome.txt", + size: "1 KB", + type: "Text Document", + open: () => { + openNotepad("welcome.txt - Notepad", WELCOME); + }, + }, + ]); + } + + function openRecycle() { + openFolder("Recycle Bin", "icons/recycle-16.svg", []); + } + + function openAbout() { + const existing = wins.value.find((w) => w.kind === "about"); + if (existing) return raise(existing.id); + const data: AboutData = { kind: "about", armed: ref(null) }; + const w = createWin({ + kind: "about", + title: "About PocketJS 98", + icon: "icons/computer-16.svg", + geo: centered(ABOUT_GEO.w, ABOUT_GEO.h), + buttons: ["close"], + resizable: false, + data, + }); + addWin(w); + } + + function openShutdown() { + const existing = wins.value.find((w) => w.kind === "shutdown"); + if (existing) return raise(existing.id); + const data: ShutdownData = { + kind: "shutdown", + choice: ref(0), + armed: ref(null), + }; + const w = createWin({ + kind: "shutdown", + title: "Shut Down Windows", + icon: "icons/shutdown-16.svg", + geo: centered(SHUTDOWN_GEO.w, SHUTDOWN_GEO.h), + buttons: ["close"], + resizable: false, + data, + }); + addWin(w); + } + + function centered(w: number, h: number): Geo { + return { + x: Math.max(0, Math.round((vp.value.w - w) / 2)), + y: Math.max(0, Math.round((vp.value.h - TASK_H - h) / 2)), + w, + h, + }; + } + + function restartSession() { + for (const w of wins.value.slice()) closeWin(w.id); + boot(); + } + + function insertTimeDate(w: WinCtl) { + const d = padOf(w); + const t = new Date(epoch + (virtualNow() - epochAt) * 1000); + const stamp = `${pad2(t.getHours())}:${pad2(t.getMinutes())} ${pad2(t.getMonth() + 1)}/${pad2(t.getDate())}/${t.getFullYear()}`; + d.doc.value = insertText(d.doc.value, stamp); + } + + // ---- desktop icons + start menu ---------------------------------------------- + + const icons: DeskIcon[] = [ + { icon: "icons/computer.svg", label: "My Computer", open: openMyComputer }, + { icon: "icons/documents.svg", label: "My Documents", open: openDocuments }, + { icon: "icons/recycle.svg", label: "Recycle Bin", open: openRecycle }, + { + icon: "icons/notepad.svg", + label: "Notepad", + open: () => { + openNotepad("Untitled - Notepad", [""]); + }, + }, + { icon: "icons/mines.svg", label: "Minesweeper", open: openMines }, + ]; + const ICON_X = 8; + const ICON_W = 74; + const ICON_STRIDE = 58; + const ICON_CELL_H = 48; + + function iconAt(x: number, y: number): number { + if (x < ICON_X || x >= ICON_X + ICON_W) return -1; + const rel = y - 8; + if (rel < 0 || rel % ICON_STRIDE >= ICON_CELL_H) return -1; + const i = Math.floor(rel / ICON_STRIDE); + return i >= 0 && i < icons.length ? i : -1; + } + + const startItems = (): PopupItem[] => [ + { + label: "Programs", + icon: "icons/folder-16.svg", + sub: [ + { + label: "Notepad", + icon: "icons/notepad-16.svg", + act: () => { + openNotepad("Untitled - Notepad", [""]); + }, + }, + { label: "Minesweeper", icon: "icons/mines-16.svg", act: openMines }, + ], + }, + { + label: "Documents", + icon: "icons/folder-16.svg", + sub: [ + { + label: "welcome.txt", + icon: "icons/notepad-16.svg", + act: () => { + openNotepad("welcome.txt - Notepad", WELCOME); + }, + }, + ], + }, + { label: "Settings", icon: "icons/settings-16.svg", disabled: true }, + { label: "Find", icon: "icons/find-16.svg", disabled: true }, + { label: "Help", icon: "icons/help-16.svg", act: openAbout }, + { label: "Run...", icon: "icons/run-16.svg", disabled: true }, + { sep: true, label: "" }, + { label: "Shut Down...", icon: "icons/shutdown-16.svg", act: openShutdown }, + ]; + + const START_ROW = 26; + const START_SEP = 8; + const startH = () => + 2 + startItems().reduce((a, it) => a + (it.sep ? START_SEP : START_ROW), 0); + const startY = () => vp.value.h - TASK_H - startH(); + + function startItemAt(x: number, y: number): number { + const items = startItems(); + if (x < 2 + 25 || x >= 2 + 182 - 1) return -1; + let oy = startY() + 1; + for (let i = 0; i < items.length; i++) { + const h = items[i].sep ? START_SEP : START_ROW; + if (y >= oy && y < oy + h) return items[i].sep ? -1 : i; + oy += h; + } + return -1; + } + + function buildPopup(x: number, y: number, items: PopupItem[]): Popup { + let w = 0; + for (const it of items) { + if (it.sep) continue; + w = Math.max( + w, + 26 + measure(it.label) + (it.shortcut ? 20 + measure(it.shortcut) : 0) + (it.sub ? 14 : 0) + 14, + ); + } + const h = 2 + items.reduce((a, it) => a + (it.sep ? START_SEP : 18), 0); + return { + x: Math.min(x, vp.value.w - w - 2), + y: Math.min(y, vp.value.h - TASK_H - h), + w: Math.max(w, 120), + items, + }; + } + + function popupItemAt(p: Popup, x: number, y: number): number { + if (x < p.x + 1 || x >= p.x + p.w - 1) return -1; + let oy = p.y + 1; + for (let i = 0; i < p.items.length; i++) { + const h = p.items[i].sep ? START_SEP : 18; + if (y >= oy && y < oy + h) return p.items[i].sep ? -1 : i; + oy += h; + } + return -1; + } + + function closeMenus() { + startOpen.value = false; + startFly.value = null; + popup.value = null; + startHover.value = -1; + flyHover.value = -1; + popupHover.value = -1; + for (const w of wins.value) w.openMenu.value = -1; + } + + function toggleStart() { + popup.value = null; + startOpen.value = !startOpen.value; + if (!startOpen.value) { + startFly.value = null; + startHover.value = -1; + flyHover.value = -1; + } + } + + // ---- helpers ------------------------------------------------------------------ + + function chromeOpts(w: WinCtl) { + return { + buttons: w.buttons, + resizable: w.resizable, + maximized: w.maximized.value, + menuWidths: (w.menus ?? []).map((m) => m.width), + }; + } + + /** Topmost visible window under the point, with its chrome region. */ + function hitWindows(x: number, y: number): { win: WinCtl; region: Region } | null { + for (let i = stack.length - 1; i >= 0; i--) { + const w = byId(stack[i]); + if (!w || w.minimized.value) continue; + const region = hitRegion(w.geo.value, chromeOpts(w), x, y); + if (region) return { win: w, region }; + } + return null; + } + + function isDblClick(key: string): boolean { + const now = virtualNow(); + const hit = + lastClick.key === key && + now - lastClick.t < 0.4 && + Math.abs(mx - lastClick.x) < 4 && + Math.abs(my - lastClick.y) < 4; + lastClick = hit ? { key: "", t: -1, x: 0, y: 0 } : { key, t: now, x: mx, y: my }; + return hit; + } + + function pad2(n: number): string { + return String(n).padStart(2, "0"); + } + + function sendCursor(k: CursorKind) { + if (svc && k !== lastCursor) { + lastCursor = k; + svc.send({ t: "cursor", k }); + } + } + + /** Notepad caret position for a content-local point (row clamped). */ + function padCaretAt(d: PadData, cx: number, cy: number): { row: number; col: number } { + const doc = d.doc.value; + const row = Math.max( + 0, + Math.min(doc.lines.length - 1, Math.floor((cy - 3 + d.scroll.value) / PAD_LINE_H)), + ); + const col = colFromX(doc.lines[row], cx - 3, (s) => measure(s)); + return { row, col }; + } + + // ---- input routing -------------------------------------------------------------- + + function onPrimaryDown(shift: boolean) { + // Open menus swallow the click (classic: outside-click only dismisses). + if (startOpen.value) { + const fly = startFly.value; + if (fly) { + const i = popupItemAt(fly.popup, mx, my); + if (i >= 0) { + const item = fly.popup.items[i]; + if (!item.disabled && item.act) { + item.act(); + closeMenus(); + } + return; + } + } + const i = startItemAt(mx, my); + if (i >= 0) { + const item = startItems()[i]; + if (item.sub) return; // hover already opened the flyout; stay open + if (!item.disabled && item.act) { + item.act(); + closeMenus(); + } + return; + } + // Outside click (the Start button included) dismisses and is consumed. + closeMenus(); + return; + } + const pop = popup.value; + if (pop) { + const i = popupItemAt(pop.popup, mx, my); + closeMenus(); + if (i >= 0) { + const item = pop.popup.items[i]; + if (!item.disabled && item.act) item.act(); + } + return; + } + + // Taskbar. + if (my >= vp.value.h - TASK_H) { + if (mx >= 2 && mx < 58) { + startOpen.value = !startOpen.value; + return; + } + const entry = taskEntryAt(mx, my); + if (entry !== -1) { + const id = wins.value[entry].id; + const w = byId(id); + if (!w) return; + if (focusId.value === id && !w.minimized.value) minimize(id); + else raise(id); + } + return; + } + + // Windows, top-down. + const hit = hitWindows(mx, my); + if (hit) { + const { win: w, region } = hit; + raise(w.id); + if (region.kind === "button") { + drag = { type: "capbtn", id: w.id, btn: region.button }; + w.pressedBtn.value = region.button; + return; + } + if (region.kind === "caption") { + if (isDblClick(`cap:${w.id}`)) { + toggleMax(w); + return; + } + if (!w.maximized.value) { + drag = { type: "move", id: w.id, sx: mx, sy: my, orig: w.geo.value }; + } + return; + } + if (region.kind === "resize") { + drag = { type: "resize", id: w.id, dir: region.dir, sx: mx, sy: my, orig: w.geo.value }; + return; + } + if (region.kind === "menu") { + const open = w.openMenu.value === region.index ? -1 : region.index; + w.openMenu.value = open; + if (open >= 0 && w.menus) { + const mxs = w.menus.slice(0, open).reduce((a, m) => a + m.width, 0); + const g = w.geo.value; + popup.value = { + popup: buildPopup(g.x + FRAME + mxs, g.y + FRAME + 18 + 1 + 18, w.menus[open].items()), + winId: w.id, + }; + } + return; + } + routeContentDown(w, region.cx, region.cy, shift); + return; + } + + // Desktop: select an icon (double-click opens), deactivate windows. + const icon = iconAt(mx, my); + iconSel.value = icon; + focusId.value = -1; + if (icon >= 0 && isDblClick(`icon:${icon}`)) icons[icon].open(); + } + + function routeContentDown(w: WinCtl, cx: number, cy: number, shift: boolean) { + if (w.kind === "notepad") { + const d = padOf(w); + const doc = d.doc.value; + const { row, col } = padCaretAt(d, cx, cy); + if (isDblClick(`pad:${w.id}`)) { + // Double-click: select the word under the pointer. + const r = wordRangeAt(doc.lines[row], col); + d.doc.value = { lines: doc.lines, caret: { row, col: r.to }, anchor: { row, col: r.from } }; + return; + } + // Click places the caret; shift-click extends; dragging selects. + const anchor = shift ? (doc.anchor ?? doc.caret) : { row, col }; + d.doc.value = { lines: doc.lines, caret: { row, col }, anchor }; + drag = { type: "textsel", id: w.id }; + return; + } + if (w.kind === "mines") { + const d = minesOf(w); + const hit = minesHit(cx, cy); + if (hit?.type === "cell") { + drag = { type: "minehold", id: w.id }; + d.held.value = hit.i; + } else if (hit?.type === "smiley") { + drag = { type: "smiley", id: w.id }; + d.smileyHeld.value = true; + } + return; + } + if (w.kind === "folder") { + const d = folderOf(w); + const row = folderRowAt(cy, d.rows.length); + d.selected.value = row; + if (row >= 0 && isDblClick(`row:${w.id}:${row}`)) d.rows[row].open?.(); + return; + } + if (w.kind === "about") { + const g = w.geo.value; + const hit = aboutHit(g.w - FRAME * 2, g.h - FRAME - contentTop({ menuWidths: [] }), cx, cy); + if (hit === "ok") { + drag = { type: "dialogbtn", id: w.id, tag: "ok" }; + aboutOf(w).armed.value = "ok"; + } + return; + } + if (w.kind === "shutdown") { + const g = w.geo.value; + const d = shutdownOf(w); + const hit = shutdownHit(g.w - FRAME * 2, g.h - FRAME - contentTop({ menuWidths: [] }), cx, cy); + if (hit === "radio0") d.choice.value = 0; + else if (hit === "radio1") d.choice.value = 1; + else if (hit === "ok" || hit === "cancel") { + drag = { type: "dialogbtn", id: w.id, tag: hit }; + d.armed.value = hit; + } + } + } + + function onRightDown() { + if (startOpen.value || popup.value) { + closeMenus(); + return; + } + const hit = hitWindows(mx, my); + if (hit) { + const { win: w, region } = hit; + if (region.kind === "content" && w.kind === "mines") { + raise(w.id); + const d = minesOf(w); + const cell = minesHit(region.cx, region.cy); + if (cell?.type === "cell") { + d.board.value = toggleFlag(d.board.value, cell.i); + triggerRef(d.board); + } + return; + } + if (region.kind === "content" && w.kind === "notepad") { + raise(w.id); + const d = padOf(w); + const has = hasSel(d.doc.value); + popup.value = { + popup: buildPopup(mx, my, [ + { label: "Cut", shortcut: "Cmd+X", disabled: !has, act: cutSel }, + { label: "Copy", shortcut: "Cmd+C", disabled: !has, act: copySel }, + { label: "Paste", shortcut: "Cmd+V", act: pasteReq }, + { sep: true, label: "" }, + { + label: "Select All", + shortcut: "Cmd+A", + act: () => { + selectAllIn(w); + }, + }, + ]), + }; + return; + } + if (region.kind === "caption") { + raise(w.id); + popup.value = { + popup: buildPopup(mx, my, [ + { + label: "Minimize", + shortcut: "Cmd+M", + act: () => { + minimize(w.id); + }, + }, + { + label: w.maximized.value ? "Restore" : "Maximize", + disabled: !w.resizable, + act: () => { + toggleMax(w); + }, + }, + { sep: true, label: "" }, + { + label: "Close", + shortcut: "Cmd+W", + act: () => { + closeWin(w.id); + }, + }, + ]), + winId: w.id, + }; + } + return; + } + if (my < vp.value.h - TASK_H) { + const icon = iconAt(mx, my); + iconSel.value = icon; + popup.value = { + popup: buildPopup(mx, my, [ + { label: "Arrange Icons", act: () => {} }, + { label: "Refresh", act: () => {} }, + { sep: true, label: "" }, + { + label: "New Text Document", + act: () => { + openNotepad("Untitled - Notepad", [""]); + }, + }, + { sep: true, label: "" }, + { label: "Properties", disabled: true }, + ]), + }; + } + } + + function onMove() { + // Menu hover states. + if (startOpen.value) { + const i = startItemAt(mx, my); + startHover.value = i; + if (i >= 0) { + const item = startItems()[i]; + if (item.sub) { + if (startFly.value?.index !== i) { + let oy = startY() + 1; + for (let k = 0; k < i; k++) oy += startItems()[k].sep ? START_SEP : START_ROW; + startFly.value = { index: i, popup: buildPopup(2 + 182 - 3, oy, item.sub) }; + flyHover.value = -1; + } + } else if (startFly.value) { + startFly.value = null; + } + } + const fly = startFly.value; + if (fly) flyHover.value = popupItemAt(fly.popup, mx, my); + } + const pop = popup.value; + if (pop) { + popupHover.value = popupItemAt(pop.popup, mx, my); + // A menu-bar dropdown follows the hovered menu title (classic). + if (pop.winId !== undefined) { + const w = byId(pop.winId); + if (w?.menus) { + const r = hitRegion(w.geo.value, chromeOpts(w), mx, my); + if (r?.kind === "menu" && r.index !== w.openMenu.value) { + w.openMenu.value = r.index; + const mxs = w.menus.slice(0, r.index).reduce((a, m) => a + m.width, 0); + const g = w.geo.value; + popup.value = { + popup: buildPopup( + g.x + FRAME + mxs, + g.y + FRAME + 18 + 1 + 18, + w.menus[r.index].items(), + ), + winId: w.id, + }; + } + } + } + } + + // Drags. + if (drag?.type === "move") { + const w = byId(drag.id); + if (w) { + w.geo.value = clampMove( + { ...drag.orig, x: drag.orig.x + (mx - drag.sx), y: drag.orig.y + (my - drag.sy) }, + vp.value.w, + vp.value.h, + ); + } + return; + } + if (drag?.type === "resize") { + const w = byId(drag.id); + if (w) { + w.geo.value = resizeGeo(drag.orig, drag.dir, mx - drag.sx, my - drag.sy, w.minW, w.minH); + } + sendCursor(cursorForDir(drag.dir)); + return; + } + if (drag?.type === "textsel") { + const w = byId(drag.id); + if (w) { + const d = padOf(w); + const g = w.geo.value; + const cx = mx - g.x - FRAME; + const cy = my - g.y - contentTop(chromeOpts(w)); + const doc = d.doc.value; + const { row, col } = padCaretAt(d, Math.max(0, cx), cy); + if (row !== doc.caret.row || col !== doc.caret.col) { + d.doc.value = { + lines: doc.lines, + caret: { row, col }, + anchor: doc.anchor ?? doc.caret, + }; + } + } + sendCursor("text"); + return; + } + if (drag?.type === "capbtn") { + const w = byId(drag.id); + if (w) { + const r = hitRegion(w.geo.value, chromeOpts(w), mx, my); + w.pressedBtn.value = r?.kind === "button" && r.button === drag.btn ? drag.btn : null; + } + return; + } + if (drag?.type === "minehold") { + const w = byId(drag.id); + if (w) { + const d = minesOf(w); + const r = hitRegion(w.geo.value, chromeOpts(w), mx, my); + const cell = r?.kind === "content" ? minesHit(r.cx, r.cy) : null; + d.held.value = cell?.type === "cell" ? cell.i : -1; + } + return; + } + if (drag?.type === "dialogbtn") { + const w = byId(drag.id); + if (w) { + const r = hitRegion(w.geo.value, chromeOpts(w), mx, my); + const g = w.geo.value; + const cw = g.w - FRAME * 2; + const chh = g.h - FRAME - contentTop({ menuWidths: [] }); + let over: string | null = null; + if (r?.kind === "content") { + over = + w.kind === "about" ? aboutHit(cw, chh, r.cx, r.cy) : shutdownHit(cw, chh, r.cx, r.cy); + } + const armed = w.kind === "about" ? aboutOf(w).armed : shutdownOf(w).armed; + armed.value = over === drag.tag ? drag.tag : null; + } + return; + } + + // Hover cursor shape. + let k: CursorKind = "default"; + const hover = hitWindows(mx, my); + if (hover) { + if (hover.region.kind === "resize") k = cursorForDir(hover.region.dir); + else if (hover.region.kind === "content" && hover.win.kind === "notepad") k = "text"; + } + sendCursor(k); + } + + function onPrimaryUp() { + const d = drag; + drag = null; + if (!d) return; + if (d.type === "capbtn") { + const w = byId(d.id); + if (w && w.pressedBtn.value === d.btn) { + w.pressedBtn.value = null; + if (d.btn === "close") closeWin(d.id); + else if (d.btn === "min") minimize(d.id); + else if (d.btn === "max") toggleMax(w); + } else if (w) w.pressedBtn.value = null; + return; + } + if (d.type === "minehold") { + const w = byId(d.id); + if (w) { + const md = minesOf(w); + const i = md.held.value; + md.held.value = -1; + if (i >= 0) { + const was = md.board.value.phase; + md.board.value = reveal(md.board.value, i); + triggerRef(md.board); + if (was === "ready" && md.board.value.phase === "playing") minesStart = virtualNow(); + } + } + return; + } + if (d.type === "smiley") { + const w = byId(d.id); + if (w) { + minesOf(w).smileyHeld.value = false; + const r = hitRegion(w.geo.value, chromeOpts(w), mx, my); + if (r?.kind === "content" && minesHit(r.cx, r.cy)?.type === "smiley") minesNew(w); + } + return; + } + if (d.type === "dialogbtn") { + const w = byId(d.id); + if (w) { + const armedRef = w.kind === "about" ? aboutOf(w).armed : shutdownOf(w).armed; + const armed = armedRef.value; + armedRef.value = null; + if (armed === d.tag) { + if (w.kind === "about") closeWin(w.id); + else if (w.kind === "shutdown") { + const choice = shutdownOf(w).choice.value; + if (d.tag === "cancel") closeWin(w.id); + else if (choice === 0) svc?.send({ t: "quit" }); + else restartSession(); + } + } + } + return; + } + if (d.type === "resize") sendCursor("default"); + } + + /** macOS-style ⌘ chords (host forwards them cmd-flagged, raw lowercase k). */ + function onCmd(k: string) { + switch (k) { + case "escape": + toggleStart(); + return; + case "`": + cycleWindows(); + return; + case "n": + openNotepad("Untitled - Notepad", [""]); + return; + case "w": { + const w = focused(); + if (w) closeWin(w.id); + return; + } + case "m": { + const w = focused(); + if (w) minimize(w.id); + return; + } + case "a": { + const p = focusedPad(); + if (p) selectAllIn(p.w); + return; + } + case "c": + copySel(); + return; + case "x": + cutSel(); + return; + } + } + + function onKey(ev: HostEvent) { + const k = ev.k ?? ""; + if (ev.cmd) { + onCmd(k); + return; + } + if (k === "Escape") { + if (startOpen.value || popup.value) closeMenus(); + return; + } + const w = focused(); + if (!w) return; + if (w.kind === "mines" && k === "F2") { + minesNew(w); + return; + } + if (w.kind === "notepad") { + if (k === "F5") { + insertTimeDate(w); + return; + } + const d = padOf(w); + const doc = d.doc.value; + switch (k) { + case "Enter": + d.doc.value = insertText(doc, "\n"); + break; + case "Backspace": + d.doc.value = backspace(doc); + break; + case "Delete": + d.doc.value = del(doc); + break; + case "Tab": + d.doc.value = insertText(doc, " "); + break; + case "Left": + case "Right": + case "Up": + case "Down": + case "Home": + case "End": + d.doc.value = applyMove(doc, k as CaretMove, ev.sh ?? false); + break; + default: + return; + } + scrollCaretIntoView(w); + return; + } + if (w.kind === "shutdown" && k === "Enter") { + const choice = shutdownOf(w).choice.value; + if (choice === 0) svc?.send({ t: "quit" }); + else restartSession(); + return; + } + if (w.kind === "about" && k === "Enter") closeWin(w.id); + } + + function padViewH(w: WinCtl): number { + return w.geo.value.h - FRAME - contentTop(chromeOpts(w)) - 2; + } + + function scrollCaretIntoView(w: WinCtl) { + const d = padOf(w); + const y = d.doc.value.caret.row * PAD_LINE_H; + const viewH = padViewH(w); + if (y - d.scroll.value < 0) d.scroll.value = Math.max(0, y); + else if (y - d.scroll.value > viewH - PAD_LINE_H) d.scroll.value = y - viewH + PAD_LINE_H; + } + + function typeInto(w: WinCtl, s: string) { + const d = padOf(w); + d.doc.value = insertText(d.doc.value, s); + scrollCaretIntoView(w); + } + + // ---- taskbar -------------------------------------------------------------------- + + const taskEntries = (): TaskEntry[] => + wins.value + .filter((w) => w.kind !== "shutdown") + .map((w) => ({ id: w.id, title: w.title, icon: w.icon })); + const taskButtonW = () => { + const n = Math.max(1, taskEntries().length); + return Math.min(160, Math.floor((vp.value.w - 70 - 60 - n * 3) / n)); + }; + + function taskEntryAt(x: number, y: number): number { + if (y < vp.value.h - TASK_H + 3) return -1; + const entries = taskEntries(); + const w = taskButtonW(); + const x0 = 2 + 54 + 3 + 1 + 3; + for (let i = 0; i < entries.length; i++) { + const bx = x0 + i * (w + 3); + if (x >= bx && x < bx + w) { + return wins.value.findIndex((win) => win.id === entries[i].id); + } + } + return -1; + } + + // ---- frame pump ------------------------------------------------------------------- + + function handleEvent(ev: HostEvent) { + switch (ev.t) { + case "hello": { + vp.value = { w: ev.w ?? 800, h: ev.h ?? 600 }; + epoch = ev.epoch ?? 0; + epochAt = virtualNow(); + break; + } + case "resize": { + const w = ev.w ?? vp.value.w; + const h = ev.h ?? vp.value.h; + vp.value = { w, h }; + for (const win of wins.value) { + if (win.maximized.value) win.geo.value = maximizedGeo(w, h); + else win.geo.value = clampMove(win.geo.value, w, h); + } + break; + } + case "mouse": { + if (ev.b === 2) { + if (ev.d) { + mx = ev.x ?? mx; + my = ev.y ?? my; + onRightDown(); + } + break; + } + mx = ev.x ?? mx; + my = ev.y ?? my; + const down = ev.d ?? false; + if (down && !prevDown) onPrimaryDown(ev.sh ?? false); + else if (!down && prevDown) { + onMove(); + onPrimaryUp(); + } else onMove(); + prevDown = down; + break; + } + case "key": + onKey(ev); + break; + case "ch": { + const w = focused(); + if (w?.kind === "notepad" && ev.s) typeInto(w, ev.s); + break; + } + case "paste": { + const w = focused(); + if (w?.kind === "notepad" && ev.text) typeInto(w, ev.text); + break; + } + case "ime": { + const w = focused(); + if (w?.kind === "notepad") { + const d = padOf(w); + // Composition replaces the selection the moment it starts. + if (ev.s && hasSel(d.doc.value)) d.doc.value = deleteSel(d.doc.value); + d.preedit.value = ev.s ? { s: ev.s, c: ev.c ?? ev.s.length } : null; + } + break; + } + case "scroll": { + const hover = hitWindows(mx, my); + if (hover?.win.kind === "notepad") { + const d = padOf(hover.win); + const contentH = d.doc.value.lines.length * PAD_LINE_H + 6; + const maxY = Math.max(0, contentH - padViewH(hover.win)); + d.scroll.value = Math.max(0, Math.min(maxY, d.scroll.value + (ev.dy ?? 0))); + } + break; + } + } + } + + function boot() { + openNotepad("welcome.txt - Notepad", WELCOME); + if (!svc) { + // Standalone (sim, goldens): a lively static arrangement. + openMines(); + openMyComputer(); + } + } + + boot(); + + onFrame(() => { + if (svc) for (const ev of svc.poll()) handleEvent(ev); + + // Taskbar clock (minute precision, anchored at the hello epoch). + if (epoch > 0) { + const t = new Date(epoch + (virtualNow() - epochAt) * 1000); + const s = `${pad2(t.getHours())}:${pad2(t.getMinutes())}`; + if (s !== clock.value) clock.value = s; + } + + // Minesweeper timer. + const mw = wins.value.find((w) => w.kind === "mines"); + if (mw) { + const d = minesOf(mw); + if (d.board.value.phase === "playing" && minesStart > 0) { + const e = Math.min(999, Math.floor(virtualNow() - minesStart)); + if (e !== d.elapsed.value) d.elapsed.value = e; + } + } + + // IME candidate window docking: report the focused notepad caret. + const fw = focused(); + if (svc && fw?.kind === "notepad") { + const d = padOf(fw); + const g = fw.geo.value; + const doc = d.doc.value; + const line = doc.lines[doc.caret.row] ?? ""; + const x = g.x + FRAME + 4 + measure(line.slice(0, doc.caret.col)); + const y = g.y + contentTop(chromeOpts(fw)) + 3 + doc.caret.row * PAD_LINE_H - d.scroll.value; + if (x !== lastCaret.x || y !== lastCaret.y) { + lastCaret = { x, y, h: PAD_LINE_H }; + svc.send({ t: "caret", x, y, h: PAD_LINE_H }); + } + } + }); + + // ---- render ------------------------------------------------------------- + + return ( + + + {wins.value.map((w) => ( + + ))} + {startOpen.value ? ( + + ) : null} + {startOpen.value && startFly.value ? ( + + ) : null} + {popup.value ? : null} + + + ); +} diff --git a/apps/desk98/app.vue b/apps/desk98/app.vue deleted file mode 100644 index 267fe5dd..00000000 --- a/apps/desk98/app.vue +++ /dev/null @@ -1,1341 +0,0 @@ - - - diff --git a/apps/desk98/chrome.tsx b/apps/desk98/chrome.tsx new file mode 100644 index 00000000..54e6e034 --- /dev/null +++ b/apps/desk98/chrome.tsx @@ -0,0 +1,252 @@ +// apps/desk98/chrome.tsx — presentational Windows 98 chrome, Vue Vapor JSX +// (vue-jsx-vapor; lists are plain .map() like every vapor JSX app in the +// repo). Every component here only paints; the compositor (app.tsx) owns hit +// testing and routes all pointer/keyboard input itself off the svc mouse +// stream, so nothing in this file registers a handler. Geometry mirrors +// wm.ts via theme.ts constants. +// +// Class strings are FULL literals throughout — the style table compiles at +// build time and template-interpolated fragments are a compile error, so the +// bevel recipes repeat verbatim instead of riding shared constants. + +import { Image, Text, View } from "@pocketjs/framework/components"; +import { FONT, FONT_B, FONT_XL } from "./theme.ts"; +import type { CaptionButton } from "./wm.ts"; +import type { DeskIcon, Popup, TaskEntry, WinCtl } from "./state.ts"; + +/** W95FA text. Slots 19/20/21 ride the style prop — the class table never + * sees them (baked per-app via pak.json, docs in gen-assets.ts). Text rides + * the `t` prop; `cls` replaces the class attr so nothing falls through to + * a user component's attrs. */ +export function T98(props: { t: string; cls?: string; bold?: boolean; xl?: boolean }) { + return ( + + {props.t} + + ); +} + +const BTN_ICON: Record = { + min: "icons/cap-min.svg", + max: "icons/cap-max.svg", + close: "icons/cap-close.svg", +}; + +/** Caption controls, flush right and flush against each other (wm.ts + * captionButtonXs mirrors this row). Press feedback inverts the bevel and + * nudges the glyph one px — app.tsx drives win.pressedBtn off the raw + * pointer stream. */ +export function CaptionButtons(props: { win: WinCtl }) { + const w = props.win; + return ( + + {w.buttons.map((btn) => ( + + + + ))} + + ); +} + +/** The taskbar: the Start button (PocketJS favicon mark, gen-icons + * start-logo), one button per window, the sunken clock tray. */ +export function Taskbar(props: { + entries: TaskEntry[]; + activeId: number; + startOpen: boolean; + clock: string; + buttonW: number; +}) { + return ( + + + + + + + + {props.entries.map((entry) => ( + + + + + + + ))} + + + + + + ); +} + +/** Generic popup menu panel (context menus, dropdowns, start flyouts). */ +export function PopupPanel(props: { popup: Popup; hover: number }) { + return ( + + {props.popup.items.map((item, i) => + item.sep ? ( + + + + + ) : ( + + {item.icon ? ( + + ) : ( + + )} + + + + {item.shortcut ? ( + + ) : null} + {item.sub ? : null} + + ), + )} + + ); +} + +/** The Start menu: a plain navy gradient banner strip + 26px rows (flyouts + * render as PopupPanels). */ +export function StartMenu(props: { + x: number; + y: number; + h: number; + items: Popup["items"]; + hover: number; +}) { + return ( + + + + {props.items.map((item, i) => + item.sep ? ( + + + + + ) : ( + + + + + + {item.sub ? : null} + + ), + )} + + + ); +} + +/** Desktop icons: a left column of 32px art + label, teal behind. */ +export function DesktopIcons(props: { icons: DeskIcon[]; selected: number }) { + return ( + + {props.icons.map((icon, i) => ( + + + + + + + ))} + + ); +} diff --git a/apps/desk98/main.ts b/apps/desk98/main.ts deleted file mode 100644 index a127ab9a..00000000 --- a/apps/desk98/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -// apps/desk98/main.ts — PocketJS 98 entry point. Only the mount lives here: -// the framework-runtime import stays confined to this file (app-check -// typechecks the entry's whole graph; the prelude's globalThis reshaping -// belongs to the runtime, not to app modules). -import { mount } from "@pocketjs/framework/vue-vapor"; -import App from "./app.vue"; - -mount(App); diff --git a/apps/desk98/main.tsx b/apps/desk98/main.tsx new file mode 100644 index 00000000..94613228 --- /dev/null +++ b/apps/desk98/main.tsx @@ -0,0 +1,5 @@ +// apps/desk98/main.tsx — PocketJS 98 entry point (Vue Vapor, JSX). +import { mount } from "@pocketjs/framework"; +import App from "./app.tsx"; + +mount(() => ); diff --git a/apps/desk98/pocket.json b/apps/desk98/pocket.json index aba01973..9eb3b192 100644 --- a/apps/desk98/pocket.json +++ b/apps/desk98/pocket.json @@ -21,7 +21,7 @@ } }, "app": { - "entry": "apps/desk98/main.ts", + "entry": "apps/desk98/main.tsx", "output": "desk98-main", "framework": "vue-vapor", "companions": [ diff --git a/apps/desk98/programs.ts b/apps/desk98/programs.ts deleted file mode 100644 index 3a8fca25..00000000 --- a/apps/desk98/programs.ts +++ /dev/null @@ -1,97 +0,0 @@ -// apps/desk98/programs.ts — pure program geometry: content-local hit helpers -// and metrics for Notepad, Minesweeper, the folder view and the dialogs. The -// matching views live in the *.vue components; render geometry and hit -// geometry stay in one place by sharing these constants. Content coordinates -// are (cx, cy) from wm.ts hitRegion — origin at the frame's inner top-left, -// below caption (and menu bar if present). - -import { getOps } from "@pocketjs/framework/host"; -import { FONT } from "./theme.ts"; -import { MINES_W } from "./mines.ts"; - -export function measure(s: string): number { - const ops = getOps(); - return ops.measureText ? ops.measureText(s, FONT) : s.length * 7; -} - -// --------------------------------------------------------------------------- -// Notepad -// --------------------------------------------------------------------------- - -export const PAD_LINE_H = 16; -export const PAD_PAD = 3; // inset of the text from the white well - -// --------------------------------------------------------------------------- -// Minesweeper — fixed-size window; all metrics in content-local px. -// --------------------------------------------------------------------------- - -export const MINES_GEO = { w: 166, h: 227 } as const; -const M_PAD = 5; // content padding -const M_HEADER_H = 36; -const M_FIELD_TOP = M_PAD + M_HEADER_H + 6; // header + gap -const M_CELL = 16; -const M_CELLS_X = M_PAD + 3; // field bevel-w-[3] ring -const M_CELLS_Y = M_FIELD_TOP + 3; - -export type MinesHit = { type: "cell"; i: number } | { type: "smiley" } | null; - -export function minesHit(cx: number, cy: number): MinesHit { - const sx = 160 / 2 - 13; - if (cx >= sx && cx < sx + 26 && cy >= M_PAD + 5 && cy < M_PAD + 5 + 26) { - return { type: "smiley" }; - } - const x = Math.floor((cx - M_CELLS_X) / M_CELL); - const y = Math.floor((cy - M_CELLS_Y) / M_CELL); - if (x >= 0 && x < 9 && y >= 0 && y < 9) return { type: "cell", i: y * MINES_W + x }; - return null; -} - -// --------------------------------------------------------------------------- -// Folder (Explorer details view) -// --------------------------------------------------------------------------- - -export const FOLDER_HEADER_H = 17; -export const FOLDER_ROW_H = 17; -export const FOLDER_STATUS_H = 20; - -/** Row index for a content-local click inside the list, -1 none. */ -export function folderRowAt(cy: number, rowCount: number): number { - const i = Math.floor((cy - 1 - FOLDER_HEADER_H) / FOLDER_ROW_H); - return i >= 0 && i < rowCount ? i : -1; -} - -// --------------------------------------------------------------------------- -// About + Shut Down dialogs -// --------------------------------------------------------------------------- - -export const ABOUT_GEO = { w: 340, h: 216 } as const; -export const SHUTDOWN_GEO = { w: 300, h: 176 } as const; - -/** Content-local button hits for the About dialog. */ -export function aboutHit(contentW: number, contentH: number, cx: number, cy: number): "ok" | null { - const x = contentW - 10 - 75; - const y = contentH - 10 - 23; - return cx >= x && cx < x + 75 && cy >= y && cy < y + 23 ? "ok" : null; -} - -export type ShutdownHit = "ok" | "cancel" | "radio0" | "radio1" | null; - -export function shutdownHit( - contentW: number, - contentH: number, - cx: number, - cy: number, -): ShutdownHit { - const by = contentH - 10 - 23; - const cancelX = contentW - 10 - 75; - const okX = cancelX - 6 - 75; - if (cy >= by && cy < by + 23) { - if (cx >= okX && cx < okX + 75) return "ok"; - if (cx >= cancelX && cx < cancelX + 75) return "cancel"; - } - for (const i of [0, 1]) { - const ry = 46 + i * 20; - if (cx >= 56 && cx < 220 && cy >= ry && cy < ry + 18) return i === 0 ? "radio0" : "radio1"; - } - return null; -} diff --git a/apps/desk98/programs.tsx b/apps/desk98/programs.tsx new file mode 100644 index 00000000..fe347fb1 --- /dev/null +++ b/apps/desk98/programs.tsx @@ -0,0 +1,446 @@ +// apps/desk98/programs.tsx — the window contents: Notepad (with selection), +// Minesweeper, the Explorer-style folder view, About and Shut Down dialogs. +// Vue Vapor JSX, presentational like chrome.tsx; each program also exports +// the content-local hit helpers app.tsx routes clicks through, so render +// geometry and hit geometry sit in one file. Content coordinates are +// (cx, cy) from wm.ts hitRegion — origin at the frame's inner top-left, +// below caption (and menu bar if present). + +import { computed } from "vue"; +import { Image, View } from "@pocketjs/framework/components"; +import { getOps } from "@pocketjs/framework/host"; +import { T98 } from "./chrome.tsx"; +import { FONT } from "./theme.ts"; +import { rowSelSpan } from "./notepad.ts"; +import { MINES_W, type Cell } from "./mines.ts"; +import type { AboutData, FolderData, MinesData, PadData, ShutdownData } from "./state.ts"; + +export function measure(s: string): number { + const ops = getOps(); + return ops.measureText ? ops.measureText(s, FONT) : s.length * 7; +} + +// --------------------------------------------------------------------------- +// Notepad +// --------------------------------------------------------------------------- + +export const PAD_LINE_H = 16; +export const PAD_PAD = 3; // inset of the text from the white well + +/** Row text split at the selection edges; one segment when unselected. */ +function segs(d: PadData, line: string, row: number): { t: string; sel: boolean }[] { + const span = rowSelSpan(d.doc.value, row); + if (!span) return [{ t: line, sel: false }]; + return [ + { t: line.slice(0, span.from), sel: false }, + { t: line.slice(span.from, span.to), sel: true }, + { t: line.slice(span.to), sel: false }, + ]; +} + +export function NotepadView(props: { data: PadData; active: boolean }) { + const d = props.data; + const caretRow = () => d.doc.value.caret.row; + const caretX = () => { + const doc = d.doc.value; + const line = doc.lines[doc.caret.row] ?? ""; + const pre = d.preedit.value; + const head = line.slice(0, doc.caret.col); + return measure(pre ? head + pre.s.slice(0, pre.c) : head); + }; + return ( + + + + {d.doc.value.lines.map((line, row) => ( + + {row === caretRow() && d.preedit.value ? ( + [ + , + + + + , + , + ] + ) : ( + segs(d, line, row).map((seg) => + seg.sel ? ( + + + + ) : ( + + ), + ) + )} + + ))} + + {props.active ? ( + + ) : null} + + + ); +} + +// --------------------------------------------------------------------------- +// Minesweeper — fixed-size window; all metrics in content-local px. +// --------------------------------------------------------------------------- + +export const MINES_GEO = { w: 166, h: 227 } as const; +const M_PAD = 5; // content padding +const M_HEADER_H = 36; +const M_FIELD_TOP = M_PAD + M_HEADER_H + 6; // header + gap +const M_CELL = 16; +const M_CELLS_X = M_PAD + 3; // field bevel-w-[3] ring +const M_CELLS_Y = M_FIELD_TOP + 3; + +export type MinesHit = { type: "cell"; i: number } | { type: "smiley" } | null; + +export function minesHit(cx: number, cy: number): MinesHit { + const sx = 160 / 2 - 13; + if (cx >= sx && cx < sx + 26 && cy >= M_PAD + 5 && cy < M_PAD + 5 + 26) { + return { type: "smiley" }; + } + const x = Math.floor((cx - M_CELLS_X) / M_CELL); + const y = Math.floor((cy - M_CELLS_Y) / M_CELL); + if (x >= 0 && x < 9 && y >= 0 && y < 9) return { type: "cell", i: y * MINES_W + x }; + return null; +} + +const NUM_COLORS = [ + "", // 0 unused + "text-[#0000ff]", + "text-[#008000]", + "text-[#ff0000]", + "text-[#000080]", + "text-[#800000]", + "text-[#008080]", + "text-[#000000]", + "text-[#808080]", +]; + +const SEGS: Record = { + "0": 0b0111111, + "1": 0b0000110, + "2": 0b1011011, + "3": 0b1001111, + "4": 0b1100110, + "5": 0b1101101, + "6": 0b1111101, + "7": 0b0000111, + "8": 0b1111111, + "9": 0b1101111, + "-": 0b1000000, + " ": 0, +}; + +/** One 7-seg digit, 13×23, red on black. + * a + * f b segments: bit 0..6 = a b c d e f g + * g + * e c + * d + */ +function Digit(props: { ch: string }) { + const on = (bit: number) => (((SEGS[props.ch] ?? 0) >> bit) & 1) === 1; + return ( + + + + + + + + + + ); +} + +/** Three-digit 7-seg counter (mine count / timer), clamped to -99..999. */ +function Counter(props: { value: number }) { + const text = computed(() => { + const v = Math.max(-99, Math.min(999, Math.round(props.value))); + return v < 0 ? "-" + String(-v).padStart(2, "0") : String(v).padStart(3, "0"); + }); + return ( + + + + + + ); +} + +/** One field cell: raised while hidden, flat when revealed (red on the bust + * mine), flag/mine art, colored adjacency digit. */ +function MinesCell(props: { data: MinesData; i: number }) { + const c = (): Cell => props.data.board.value.cells[props.i]; + const heldDown = () => props.data.held.value === props.i && c().state === "hidden"; + // The hidden/revealed swap must sit in a JSX child position — a bare + // ternary returned from the component body evaluates once at setup. + return ( + + {c().state !== "revealed" ? ( + + {c().state === "flag" ? : null} + + ) : ( + + {c().mine ? ( + + ) : c().adj > 0 ? ( + + ) : null} + + )} + + ); +} + +const ROWS9 = Array.from({ length: MINES_W }, (_, i) => i); + +/** Minesweeper content: sunken header (mine counter, smiley, timer) over the + * 9×9 field. The grid rides static index arrays — the board ref retriggers + * cell reads, rows never move. */ +export function MinesView(props: { data: MinesData }) { + const d = props.data; + const smiley = () => { + if (d.smileyHeld.value) return "icons/smile.svg"; + const m = d.board.value; + if (m.phase === "lost") return "icons/smile-dead.svg"; + if (m.phase === "won") return "icons/smile-cool.svg"; + if (d.held.value >= 0) return "icons/smile-ooh.svg"; + return "icons/smile.svg"; + }; + return ( + + + + + + + + + + + {ROWS9.map((ry) => ( + + {ROWS9.map((rx) => ( + + ))} + + ))} + + + ); +} + +// --------------------------------------------------------------------------- +// Folder (Explorer details view) +// --------------------------------------------------------------------------- + +export const FOLDER_HEADER_H = 17; +export const FOLDER_ROW_H = 17; +export const FOLDER_STATUS_H = 20; + +/** Row index for a content-local click inside the list, -1 none. */ +export function folderRowAt(cy: number, rowCount: number): number { + const i = Math.floor((cy - 1 - FOLDER_HEADER_H) / FOLDER_ROW_H); + return i >= 0 && i < rowCount ? i : -1; +} + +export function FolderView(props: { data: FolderData; resizable: boolean }) { + const d = props.data; + return ( + + + + + + + + + + + + + + {d.rows.map((row, i) => ( + + + + + + + + + + + + + ))} + {d.rows.length === 0 ? ( + + + + ) : null} + + + + + + {props.resizable ? : null} + + + ); +} + +// --------------------------------------------------------------------------- +// About + Shut Down dialogs +// --------------------------------------------------------------------------- + +export const ABOUT_GEO = { w: 340, h: 216 } as const; +export const SHUTDOWN_GEO = { w: 300, h: 176 } as const; + +/** Dialog push button; armed = pressed bevel + 1px content nudge. */ +function Button98(props: { label: string; armed: boolean }) { + return ( + + + + + + ); +} + +/** Content-local button hits for the About dialog. */ +export function aboutHit(contentW: number, contentH: number, cx: number, cy: number): "ok" | null { + const x = contentW - 10 - 75; + const y = contentH - 10 - 23; + return cx >= x && cx < x + 75 && cy >= y && cy < y + 23 ? "ok" : null; +} + +export function AboutView(props: { data: AboutData }) { + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +export type ShutdownHit = "ok" | "cancel" | "radio0" | "radio1" | null; + +export function shutdownHit( + contentW: number, + contentH: number, + cx: number, + cy: number, +): ShutdownHit { + const by = contentH - 10 - 23; + const cancelX = contentW - 10 - 75; + const okX = cancelX - 6 - 75; + if (cy >= by && cy < by + 23) { + if (cx >= okX && cx < okX + 75) return "ok"; + if (cx >= cancelX && cx < cancelX + 75) return "cancel"; + } + for (const i of [0, 1]) { + const ry = 46 + i * 20; + if (cx >= 56 && cx < 220 && cy >= ry && cy < ry + 18) return i === 0 ? "radio0" : "radio1"; + } + return null; +} + +export function ShutdownView(props: { data: ShutdownData }) { + const radio = (i: number, label: string) => ( + + + + {props.data.choice.value === i ? ( + + ) : null} + + + + + ); + return ( + + + + + + + + + + {radio(0, "Shut down")} + {radio(1, "Restart")} + + + + + + + + ); +} diff --git a/apps/desk98/screenshot.png b/apps/desk98/screenshot.png index 93522dd2b65568390b91ad14f6a5328960db5320..725f5a3cb6675a6df0e1c659293f6d32f9415995 100644 GIT binary patch delta 23356 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 Bbm0I1 diff --git a/docs/BACKENDS.md b/docs/BACKENDS.md index 37bd234a..2ca00a64 100644 --- a/docs/BACKENDS.md +++ b/docs/BACKENDS.md @@ -123,7 +123,7 @@ compositor owns its shortcuts (⌘W close, ⌘M minimize, ⌘` cycle windows, ⌘Esc Start menu, ⌘A/C/X editing). Plain typing arrives only through the IME input handler (`insertText:` → one `ch` line per keypress). -`apps/desk98` — a Windows 98 desktop compositor written in Vue Vapor SFCs +`apps/desk98` — a Windows 98 desktop compositor written in Vue Vapor JSX — is the reference consumer: the guest owns every window (drag, resize, z-order, menus, text selection, Minesweeper) by hit-testing the raw pointer stream itself, window moves ride paint-only translate props, and