Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 39 additions & 6 deletions shells/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ resolution, AppInstance isolation, scheduling and native composition.

## Themes

Pocket Shell Desktop ships three System UI themes: Classic 98, Windows XP and Aqua.
Pocket Shell Desktop ships three System UI themes: Classic 98, XP and Aqua.
Each is a period desktop rebuilt from PocketJS-native drawing — no bitmaps of
the originals, no theme-specific code paths outside the theme's own
definition. **All three run on the same System manifest, AppInstances and
native compositor.**

| Classic 98 | Windows XP | Aqua |
| Classic 98 | XP | Aqua |
|---|---|---|
| ![Pocket Shell Desktop classic theme](docs/classic-theme.png) | ![Pocket Shell Desktop XP theme](docs/xp-theme.png) | ![Pocket Shell Desktop Aqua theme](docs/aqua-theme.png) |

- **Classic 98** — hard two-ring bevels, 18px captions, a 28px taskbar with
the Start rail menu, the W95FA bitmap face, native 32px pixel-art desktop
icons and Explorer's coolbar and "Folders" pane in the file manager.
- **Windows XP** — Luna chrome: three-stop gel gradients under 1px
- **XP** — Luna chrome: three-stop gel gradients under 1px
highlight and seat strips, top-rounded window frames, the two-column Start
panel (user header, pinned programs, places, Turn Off Computer), a baked
green Start pill, softly shaded Luna-style vector icons, and the Explorer
Expand All @@ -47,6 +47,15 @@ themes keeps every client rectangle, caret and compositor surface exact. The
Pocket app icon is the PocketJS favicon mark, cut from Aqua silver-and-blue
or Luna silver per theme.

The same contract covers what a program draws inside its window: list boxes
and their rows, a scroll mark, group frames, value wells, radio marks,
progress bars and push buttons. The Shut Down dialog draws its radio marks
from it. A Pocket app that imports `src/system-ui/theme.ts` draws its own
window from the same parts, and bakes the five theme font slots and the icon
set into its own pack the way the System UI does (`src/system-ui/pak.json`,
`src/system-ui/icons/`). The host tells the app which theme is active; the
`theme` lines below carry it.

Choose a theme from **Start → Settings** (the logo menu on Aqua), or press
**Cmd+Shift+T** to cycle while testing.

Expand Down Expand Up @@ -76,11 +85,35 @@ Aqua screen bar with no window focused, the About dialog and its menu entries,
and the welcome note show it, so a product that installs this System UI under
its own `pocket.system.json` gets its own name in each of them.

The About dialog's text comes from `pocket.about.json` beside the System
manifest, because the manifest schema has no field for product copy:

```json
{
"body": ["One line of text.", "Another line."],
"link": "example.org/product"
}
```

`body` holds one text line per entry; the dialog does not wrap them and its
fixed size holds four above the link. `link` is shown as text under the body.
Both fields are optional. This repository's file describes the shell; a
product that bundles the System UI replaces the file with its own.

The host and the System UI exchange JSON lines over the `system-ui` companion
service; `src/system-ui/svc.ts` lists every line. Besides input, the host can
send `{t:"open", package}` to open an installed app by package id. When that
app's window is already open, the line restores it if it is minimized, raises
it and focuses it. A package id outside the installed catalog is ignored.
send two lines:

- `{t:"open", package}` opens an installed app by package id. When that app's
window is already open, the line restores it if it is minimized, raises it
and focuses it. A package id outside the installed catalog is ignored.
- `{t:"theme", id}` selects the theme with that id: `classic`, `xp` or `aqua`.
An id that names no theme is ignored.

The System UI sends `{t:"theme", id}` to the host once after boot and after
every frame that ends in another theme, whether the Settings menu, the cycle
chord or the host's line changed it. A host that keeps the user's choice or
passes the theme on to its apps reads this line.

The experimental framework implementation is pinned directly in
`vendor/pocketjs` from [PocketJS PR #399](https://github.com/pocket-nexus/pocketjs/pull/399),
Expand Down
9 changes: 9 additions & 0 deletions shells/desktop/pocket.about.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"body": [
"A desktop compositor demo on the portable Rust backend.",
"SolidJS JSX over the same DrawList the",
"consoles boot; windows, menus and shortcuts",
"live in the guest."
],
"link": "github.com/pocket-nexus/pocket-shell"
}
29 changes: 25 additions & 4 deletions shells/desktop/src/system-ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
// ⌘` cycles, ⌘N opens Notepad, ⌘Esc toggles Start, ⌘A/C/X edit the
// focused Notepad. Pocket app input and scheduling are owned by the native
// compositor using the focused surface fact emitted by this shell. The host
// can also open an installed app by package id (the svc "open" line).
// can also open an installed app by package id (the svc "open" line) and
// select the theme (the svc "theme" line); the shell reports the active
// theme to the host the same way.
//
// Without the System UI companion (sim, goldens, consoles) the app boots a
// static arrangement and just renders it — the unmodified-app base case.
Expand Down Expand Up @@ -125,6 +127,7 @@ import {
} from "./pocket-apps.ts";
import {
CLASSIC_THEME,
isThemeId,
nextThemeId,
THEMES,
themeById,
Expand Down Expand Up @@ -325,6 +328,8 @@ export default function App() {
let lastClick = { key: "", t: -1, x: 0, y: 0 };
let lastCaret = { x: -1, y: -1, h: 0 };
let minesStart = 0;
/** Theme the host was last told about; null until the first report. */
let reportedTheme: ThemeId | null = null;

const byId = (id: number) => wins().find((w) => w.id === id);
const focused = () => byId(focusId());
Expand Down Expand Up @@ -717,7 +722,7 @@ export default function App() {
case "drivec":
return [
{ icon: "folder", name: "Program Files", size: "", type: "File Folder" },
{ icon: "folder", name: "Windows", size: "", type: "File Folder" },
{ icon: "folder", name: "System", size: "", type: "File Folder" },
{
icon: "folder",
name: "My Documents",
Expand All @@ -727,7 +732,7 @@ export default function App() {
navigate(w, "documents");
},
},
{ icon: "file", name: "AUTOEXEC.BAT", size: "1 KB", type: "MS-DOS Batch File" },
{ icon: "file", name: "AUTOEXEC.BAT", size: "1 KB", type: "Batch File" },
{ icon: "file", name: "CONFIG.SYS", size: "1 KB", type: "System file" },
{
icon: "notepad",
Expand Down Expand Up @@ -855,7 +860,10 @@ export default function App() {
};
const w = createWin({
kind: "shutdown",
title: "Shut Down Windows",
// No name after the verb: the dialog is 300px wide, and Aqua's
// centered caption has room for about twenty characters between its
// control cluster and the balancing spacer.
title: "Shut Down",
icon: "shutdown",
geo: centered(SHUTDOWN_GEO.w, SHUTDOWN_GEO.h),
buttons: ["close"],
Expand Down Expand Up @@ -2162,6 +2170,12 @@ export default function App() {
}
break;
}
case "theme": {
// The host selects a theme, as the Settings menu does. An id that
// names no theme is ignored.
if (isThemeId(ev.id)) setTheme(ev.id);
break;
}
}
}

Expand Down Expand Up @@ -2222,6 +2236,13 @@ export default function App() {
}
}

// Theme report: once after boot, then after every frame that ends in
// another theme than the one last reported, whatever changed it (the
// Settings menu, the cycle chord or the host's own theme line).
if (svc && reportedTheme !== themeId()) {
reportedTheme = themeId();
svc.send({ t: "theme", id: reportedTheme });
}
});

// ---- render -------------------------------------------------------------
Expand Down
38 changes: 36 additions & 2 deletions shells/desktop/src/system-ui/pocket-apps.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-3.0-only
// The desktop's name and its visible application catalog come from the System
// manifest. The native host receives separately resolved complete package
// plans; this module carries only System-owned presentation data.
// manifest, and the About dialog's text from pocket.about.json beside it. The
// native host receives separately resolved complete package plans; this
// module carries only System-owned presentation data.

import type { PocketSystemV1 } from "@pocketjs/framework/manifest";
import aboutJson from "../../pocket.about.json";
import systemJson from "../../pocket.system.json";

const system = systemJson as unknown as PocketSystemV1;
Expand All @@ -15,6 +17,38 @@ const installedPackages = new Set(system.installation.installedPackages);
* under its own manifest gets its own name in all of them. */
export const DESKTOP_NAME: string = system.title;

/** What the About dialog shows under the desktop's name. */
export interface DesktopAbout {
/** One text line each. The dialog does not wrap them, and its fixed size
* holds four above the link. */
body: readonly string[];
/** Shown as text under the body; "" leaves the row out. */
link: string;
}

/** Read a pocket.about.json value. The System manifest schema has no field
* for product copy, so the text lives in this file beside the manifest. Both
* fields are optional: a missing `body` gives no lines and a missing `link`
* no link row. Entries of `body` that are not strings are dropped. */
export function readAbout(value: unknown): DesktopAbout {
const record =
value !== null && typeof value === "object"
? (value as { body?: unknown; link?: unknown })
: {};
return {
body: Array.isArray(record.body)
? record.body.filter((line): line is string => typeof line === "string")
: [],
link: typeof record.link === "string" ? record.link : "",
};
}

/** The About text of the product that installs this System UI. This
* repository's file describes the shell; a product that bundles the System
* UI under its own pocket.system.json puts its own pocket.about.json beside
* that manifest. */
export const DESKTOP_ABOUT: DesktopAbout = readAbout(aboutJson);

export interface PocketAppSpec {
/** Stable package id used by the native compositor surface registry. */
package: string;
Expand Down
27 changes: 14 additions & 13 deletions shells/desktop/src/system-ui/programs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
type VSeg,
} from "./notepad.ts";
import { MINES_W, type Cell } from "./mines.ts";
import { DESKTOP_NAME } from "./pocket-apps.ts";
import { DESKTOP_ABOUT, DESKTOP_NAME } from "./pocket-apps.ts";
import {
PLACES,
type AboutData,
Expand Down Expand Up @@ -680,15 +680,16 @@ export function AboutView(props: {
<View class={props.theme.popupSeparatorDark} />
<View class={props.theme.popupSeparatorLight} />
</View>
<UiText theme={props.theme} t="A desktop compositor demo on the portable Rust backend." />
<UiText theme={props.theme} t="SolidJS JSX over the same DrawList the" />
<UiText theme={props.theme} t="consoles boot; windows, menus and shortcuts" />
<UiText theme={props.theme} t="live in the guest." />
<UiText
theme={props.theme}
cls={props.theme.mutedText}
t="github.com/pocket-nexus/pocket-shell"
/>
{DESKTOP_ABOUT.body.map((line) => (
<UiText theme={props.theme} t={line} />
))}
{DESKTOP_ABOUT.link !== "" ? (
<UiText
theme={props.theme}
cls={props.theme.mutedText}
t={DESKTOP_ABOUT.link}
/>
) : null}
<View class="flex-1" />
<View class="flex-row justify-end">
<DialogButton
Expand Down Expand Up @@ -731,10 +732,10 @@ export function ShutdownView(props: {
}) {
const radio = (i: number, label: string) => (
<View class="h-[20] flex-row items-center gap-[6]">
<View class="w-[12] h-[12] rounded-full bg-[#808080] flex-col justify-center items-center">
<View class="w-[10] h-[10] rounded-full bg-[#ffffff] flex-col justify-center items-center">
<View class={props.theme.radioRing(props.data.choice() === i)}>
<View class={props.theme.radioFace(props.data.choice() === i)}>
{props.data.choice() === i ? (
<View class="w-[4] h-[4] rounded-full bg-[#000000]" />
<View class={props.theme.radioDot} />
) : null}
</View>
</View>
Expand Down
16 changes: 14 additions & 2 deletions shells/desktop/src/system-ui/svc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@
// when its window is already open, restore it
// from the task strip, raise it and focus it.
// An id outside the installed catalog is ignored
// {t:"theme", id} select the theme with that id ("classic",
// "xp" or "aqua"). An id that names no theme
// is ignored
//
// guest → host intents:
// {t:"theme", id} the active theme: sent once after boot and
// after every frame that ends in another theme,
// whether the Settings menu, the cycle chord or
// the host's theme line changed it
// {t:"quit"} Shut Down
// {t:"copy", text} put text on the system clipboard
// {t:"paste-req"} ask for the clipboard (host answers {t:"paste"})
Expand All @@ -30,6 +37,7 @@
// grabbing|ew|ns|nwse|nesw

import { getOps } from "@pocketjs/framework";
import type { ThemeId } from "./theme.ts";

export interface HostEvent {
t:
Expand All @@ -41,7 +49,8 @@ export interface HostEvent {
| "scroll"
| "paste"
| "ime"
| "open";
| "open"
| "theme";
w?: number;
h?: number;
epoch?: number;
Expand All @@ -64,6 +73,8 @@ export interface HostEvent {
c?: number | null;
/** Package id of the installed app an "open" line names. */
package?: string;
/** Theme id a "theme" line selects. */
id?: string;
}

export type CursorKind =
Expand All @@ -86,7 +97,8 @@ export interface Svc {
| { t: "copy"; text: string }
| { t: "paste-req" }
| { t: "caret"; x: number; y: number; h: number }
| { t: "cursor"; k: CursorKind },
| { t: "cursor"; k: CursorKind }
| { t: "theme"; id: ThemeId },
): void;
}

Expand Down
Loading
Loading