@n1h/sweetview 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/LICENSE-0BSD +12 -0
- package/README.md +268 -3
- package/docs/README.md +26 -0
- package/docs/app.md +523 -0
- package/docs/build.md +169 -0
- package/docs/ipc-ws.md +202 -0
- package/docs/ipc.md +323 -0
- package/docs/native.md +1066 -0
- package/docs/page.md +71 -0
- package/docs/settings-store.md +125 -0
- package/docs/webview-library.md +104 -0
- package/docs/window-chrome.md +221 -0
- package/docs/worker.md +72 -0
- package/file-imports.d.ts +19 -0
- package/package.json +66 -4
- package/src/app.ts +350 -0
- package/src/build.ts +118 -0
- package/src/html.ts +71 -0
- package/src/index.ts +8 -0
- package/src/ipc/bridge.ts +41 -0
- package/src/ipc/frame.ts +16 -0
- package/src/ipc/index.ts +16 -0
- package/src/ipc/memory.ts +33 -0
- package/src/ipc/service.ts +75 -0
- package/src/ipc-ws/client.ts +111 -0
- package/src/ipc-ws/index.ts +5 -0
- package/src/ipc-ws/server.ts +133 -0
- package/src/make-app.ts +93 -0
- package/src/messages.ts +16 -0
- package/src/native/app-icon.ts +23 -0
- package/src/native/app-menu.ts +18 -0
- package/src/native/darwin/app-icon.ts +10 -0
- package/src/native/darwin/app-menu.ts +88 -0
- package/src/native/darwin/folder-picker.ts +29 -0
- package/src/native/darwin/image.ts +47 -0
- package/src/native/darwin/menu.ts +89 -0
- package/src/native/darwin/notifier.ts +95 -0
- package/src/native/darwin/objc.ts +246 -0
- package/src/native/darwin/overlay.ts +352 -0
- package/src/native/darwin/tray.ts +129 -0
- package/src/native/darwin/window.ts +64 -0
- package/src/native/folder-picker.ts +15 -0
- package/src/native/hit-regions.ts +162 -0
- package/src/native/icon.ts +530 -0
- package/src/native/index.ts +38 -0
- package/src/native/menu.ts +43 -0
- package/src/native/notifications.ts +40 -0
- package/src/native/overlay.ts +182 -0
- package/src/native/platform.ts +25 -0
- package/src/native/tray.ts +41 -0
- package/src/native/win32/api.ts +234 -0
- package/src/native/win32/app-icon.ts +5 -0
- package/src/native/win32/app-menu.ts +4 -0
- package/src/native/win32/folder-picker.ts +121 -0
- package/src/native/win32/icon.ts +85 -0
- package/src/native/win32/menu.ts +107 -0
- package/src/native/win32/message-window.ts +72 -0
- package/src/native/win32/notifier.ts +28 -0
- package/src/native/win32/overlay.ts +499 -0
- package/src/native/win32/tray.ts +228 -0
- package/src/native/win32/window.ts +77 -0
- package/src/native/window.ts +40 -0
- package/src/page.ts +48 -0
- package/src/ring.ts +110 -0
- package/src/settings-store.ts +101 -0
- package/src/webview-lib.ts +60 -0
- package/src/window-chrome.ts +118 -0
- package/src/worker.ts +72 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// A frameless window whose page draws its own chrome: the title bar, the
|
|
2
|
+
// minimise / maximise / close buttons and the resize handles are the page's,
|
|
3
|
+
// what the user does with them is the OS's. The page tells the UI thread
|
|
4
|
+
// which part was pressed, and the UI thread hands the gesture to the window
|
|
5
|
+
// manager (sweetnative's Overlay.startDrag / startResize), so moving snaps
|
|
6
|
+
// and docks like a real window and resizing lays the page out live.
|
|
7
|
+
//
|
|
8
|
+
// Two halves, one method table between them, both in this module because it
|
|
9
|
+
// imports nothing native (the sweetnative and sweetipc imports are types):
|
|
10
|
+
//
|
|
11
|
+
// UI thread: uiMethods: { ...windowChromeMethods(settingsWindow, { chrome: "page" }), ... }
|
|
12
|
+
// page: const chrome = windowChromeControls(webviewTransport());
|
|
13
|
+
// <div onPointerDown={chrome.onTitlePointerDown}>…</div>
|
|
14
|
+
// {resizeHandles().map((h) => <div key={h.edge} style={{ position: "absolute", ...h.style }} onPointerDown={chrome.onHandlePointerDown(h.edge)} />)}
|
|
15
|
+
//
|
|
16
|
+
// The window: createOverlayWindow({ transparent: true, topmost: false, hideFromTaskbar: false, noActivate: false, … })
|
|
17
|
+
// and createWebviewApp({ window: it.handle, transparent: true, … }); the page is see-through outside what it draws.
|
|
18
|
+
|
|
19
|
+
import type { Transport } from "./ipc";
|
|
20
|
+
import type { Overlay, ResizeEdge } from "./native";
|
|
21
|
+
|
|
22
|
+
/** Who draws the frame: the page (frameless, see-through window) or the OS (a plain framed window hosting the same page). */
|
|
23
|
+
export type WindowChromeKind = "page" | "os";
|
|
24
|
+
|
|
25
|
+
export type WindowChromeInfo = { chrome: WindowChromeKind; maximized: boolean };
|
|
26
|
+
|
|
27
|
+
/** The UI-thread methods a page-drawn chrome calls. Add to the app's method map: `type Methods = WindowChromeMethods & { … }`. */
|
|
28
|
+
export type WindowChromeMethods = {
|
|
29
|
+
/** Whether the page draws the chrome ("page") or the OS does ("os"), and whether the window is maximised. */
|
|
30
|
+
"window.info": [void, WindowChromeInfo];
|
|
31
|
+
/** The page's title bar was pressed: the OS moves the window with the cursor until the button comes up. */
|
|
32
|
+
"window.drag": [void, void];
|
|
33
|
+
/** One of the page's resize handles was pressed: the OS resizes from that edge until the button comes up. */
|
|
34
|
+
"window.resize": [ResizeEdge, void];
|
|
35
|
+
"window.minimize": [void, void];
|
|
36
|
+
/** Toggles: maximised <-> previous bounds. */
|
|
37
|
+
"window.maximize": [void, void];
|
|
38
|
+
/** What the page's close button does; `windowChromeMethods` hides the window unless told otherwise. */
|
|
39
|
+
"window.close": [void, void];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type WindowChromeMethodTable = { [K in keyof WindowChromeMethods]: (params: WindowChromeMethods[K][0]) => WindowChromeMethods[K][1] };
|
|
43
|
+
|
|
44
|
+
export type WindowChromeOptions = {
|
|
45
|
+
/** What `window.info` reports; the page renders bare when it says "os". Default "page". */
|
|
46
|
+
chrome?: WindowChromeKind;
|
|
47
|
+
/** What the close button does. Default: hide the window (a tray or a menu shows it again); pass `app.quit` for a main window. */
|
|
48
|
+
close?: () => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** The UI-thread half: the `window.*` methods, answered by the window they are given. Spread into `uiMethods`. */
|
|
52
|
+
export function windowChromeMethods(window: Overlay, options: WindowChromeOptions = {}): WindowChromeMethodTable {
|
|
53
|
+
const chrome = options.chrome ?? "page";
|
|
54
|
+
const close = options.close ?? (() => window.hide());
|
|
55
|
+
return {
|
|
56
|
+
"window.info": () => ({ chrome, maximized: window.maximized() }),
|
|
57
|
+
"window.drag": () => window.startDrag(),
|
|
58
|
+
"window.resize": (edge) => window.startResize(edge),
|
|
59
|
+
"window.minimize": () => window.minimize(),
|
|
60
|
+
"window.maximize": () => window.toggleMaximize(),
|
|
61
|
+
"window.close": close,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The page half: what to call from the chrome's elements. Framework-free; pass `webviewTransport()`. */
|
|
66
|
+
export type WindowChromeControls = {
|
|
67
|
+
/** Whether to draw the chrome at all, and the maximised state (for the restore glyph and square corners). Ask again after `toggleMaximize`. */
|
|
68
|
+
info(): Promise<WindowChromeInfo>;
|
|
69
|
+
/** For the title bar's pointer-down: a primary-button press hands the move to the OS. Other buttons are ignored. */
|
|
70
|
+
onTitlePointerDown(event: { button: number }): void;
|
|
71
|
+
/** For a resize handle's pointer-down, one handler per edge. */
|
|
72
|
+
onHandlePointerDown(edge: ResizeEdge): (event: { button: number }) => void;
|
|
73
|
+
minimize(): void;
|
|
74
|
+
toggleMaximize(): void;
|
|
75
|
+
close(): void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function windowChromeControls(transport: Transport): WindowChromeControls {
|
|
79
|
+
const call = <K extends keyof WindowChromeMethods>(method: K, params: WindowChromeMethods[K][0] | null = null) => transport.notify(method, params);
|
|
80
|
+
return {
|
|
81
|
+
info: () => transport.call("window.info", null) as Promise<WindowChromeInfo>,
|
|
82
|
+
onTitlePointerDown: (e) => {
|
|
83
|
+
if (e.button === 0) call("window.drag");
|
|
84
|
+
},
|
|
85
|
+
onHandlePointerDown: (edge) => (e) => {
|
|
86
|
+
if (e.button === 0) call("window.resize", edge);
|
|
87
|
+
},
|
|
88
|
+
minimize: () => call("window.minimize"),
|
|
89
|
+
toggleMaximize: () => call("window.maximize"),
|
|
90
|
+
close: () => call("window.close"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ResizeHandle = {
|
|
95
|
+
edge: ResizeEdge;
|
|
96
|
+
/** Absolute placement inside the window, CSS pixels, plus the cursor; add `position: "absolute"`. */
|
|
97
|
+
style: { top?: number; right?: number; bottom?: number; left?: number; width?: number; height?: number; cursor: string };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The eight resize handles along the window's edges: four strips `thickness`
|
|
102
|
+
* CSS pixels wide and four corner squares twice that, each with the cursor
|
|
103
|
+
* the OS would show there. Lay them over the page (hidden while maximised).
|
|
104
|
+
*/
|
|
105
|
+
export function resizeHandles(thickness = 6): ResizeHandle[] {
|
|
106
|
+
const t = thickness;
|
|
107
|
+
const c = thickness * 2;
|
|
108
|
+
return [
|
|
109
|
+
{ edge: "top", style: { top: 0, left: t, right: t, height: t, cursor: "ns-resize" } },
|
|
110
|
+
{ edge: "bottom", style: { bottom: 0, left: t, right: t, height: t, cursor: "ns-resize" } },
|
|
111
|
+
{ edge: "left", style: { left: 0, top: t, bottom: t, width: t, cursor: "ew-resize" } },
|
|
112
|
+
{ edge: "right", style: { right: 0, top: t, bottom: t, width: t, cursor: "ew-resize" } },
|
|
113
|
+
{ edge: "top-left", style: { top: 0, left: 0, width: c, height: c, cursor: "nwse-resize" } },
|
|
114
|
+
{ edge: "bottom-right", style: { bottom: 0, right: 0, width: c, height: c, cursor: "nwse-resize" } },
|
|
115
|
+
{ edge: "top-right", style: { top: 0, right: 0, width: c, height: c, cursor: "nesw-resize" } },
|
|
116
|
+
{ edge: "bottom-left", style: { bottom: 0, left: 0, width: c, height: c, cursor: "nesw-resize" } },
|
|
117
|
+
];
|
|
118
|
+
}
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Worker side. The worker owns the Bun event loop and the app's Service.
|
|
2
|
+
// Calls arrive from the UI thread by postMessage; replies and events go back
|
|
3
|
+
// through the shared ring, with webview_dispatch as the wakeup.
|
|
4
|
+
//
|
|
5
|
+
// // app-worker.ts
|
|
6
|
+
// import { hostService } from "@n1h/sweetview/worker";
|
|
7
|
+
// import { createService } from "my-backend";
|
|
8
|
+
// hostService(createService());
|
|
9
|
+
|
|
10
|
+
import { FFIType, dlopen, type Pointer } from "bun:ffi";
|
|
11
|
+
import type { CallFrame, EventMap, Frame, MethodMap, Service } from "./ipc";
|
|
12
|
+
import { WEBVIEW_SESSION, type WorkerMessage } from "./messages";
|
|
13
|
+
import { Ring } from "./ring";
|
|
14
|
+
|
|
15
|
+
declare const self: Worker;
|
|
16
|
+
|
|
17
|
+
export function hostService<M extends MethodMap, E extends EventMap>(service: Service<M, E>): void {
|
|
18
|
+
let ring: Ring | undefined;
|
|
19
|
+
let wake: (() => void) | undefined;
|
|
20
|
+
let beforeInit: Frame[] = [];
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
const ctx = { session: WEBVIEW_SESSION };
|
|
23
|
+
|
|
24
|
+
function send(frame: Frame) {
|
|
25
|
+
if (!ring || !wake) {
|
|
26
|
+
beforeInit.push(frame);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
ring.write(encoder.encode(JSON.stringify(frame)));
|
|
30
|
+
if (ring.claimWakeup()) wake();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
service.onEvent((name, payload, to) => {
|
|
34
|
+
if (to !== undefined && to !== WEBVIEW_SESSION.id) return;
|
|
35
|
+
send({ kind: "event", name, payload });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function handle(frame: CallFrame) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await service.call(frame.method as keyof M & string, frame.params as never, ctx);
|
|
41
|
+
if (frame.seq !== null) send({ kind: "reply", seq: frame.seq, ok: true, result });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
44
|
+
if (frame.seq !== null) send({ kind: "reply", seq: frame.seq, ok: false, error });
|
|
45
|
+
else console.error(`${frame.method} failed:`, error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
self.onmessage = (event: MessageEvent<WorkerMessage>) => {
|
|
50
|
+
const msg = event.data;
|
|
51
|
+
if (msg.kind === "init") {
|
|
52
|
+
ring = new Ring(msg.ring);
|
|
53
|
+
const lib = dlopen(msg.libPath, {
|
|
54
|
+
webview_dispatch: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
|
55
|
+
});
|
|
56
|
+
const handle = msg.webviewHandle as Pointer;
|
|
57
|
+
const fn = msg.drainCallbackPtr as Pointer;
|
|
58
|
+
wake = () => lib.symbols.webview_dispatch(handle, fn, null);
|
|
59
|
+
const queued = beforeInit;
|
|
60
|
+
beforeInit = [];
|
|
61
|
+
for (const frame of queued) {
|
|
62
|
+
try {
|
|
63
|
+
send(frame);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error("sweetview worker: a queued frame could not be sent:", (err as Error).message); // the rest still go
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
void handle(msg);
|
|
71
|
+
};
|
|
72
|
+
}
|