react-x11 2.16.1 → 2.17.1

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.
Files changed (55) hide show
  1. package/README.md +38 -23
  2. package/package.json +3 -1
  3. package/src/Reconciler.js +82 -23
  4. package/src/a11y.js +18 -1
  5. package/src/appcontext.js +8 -0
  6. package/src/appearance.js +36 -0
  7. package/src/{cocoa → backend}/context2d.js +27 -7
  8. package/src/capabilities.js +99 -1
  9. package/src/cocoa/app.js +17 -9
  10. package/src/cocoa/fonts.js +1 -1
  11. package/src/cocoa/glarea.js +48 -10
  12. package/src/cocoa/overlay.js +2 -2
  13. package/src/cocoa/panewindow.js +2 -2
  14. package/src/cocoa/presenter.js +2 -2
  15. package/src/cocoa/surface.js +3 -3
  16. package/src/cocoa/window.js +23 -2
  17. package/src/events.js +21 -0
  18. package/src/foreignnodes.js +8 -3
  19. package/src/frame/index.js +30 -4
  20. package/src/glnodes.js +21 -7
  21. package/src/idle.js +59 -1
  22. package/src/index.d.ts +41 -0
  23. package/src/index.js +30 -3
  24. package/src/launcher.js +17 -8
  25. package/src/launcherhooks.js +24 -10
  26. package/src/node.d.ts +1 -1
  27. package/src/nodes/cascade.js +9 -0
  28. package/src/nodes/node.js +6 -1
  29. package/src/nodes/window/hints.js +21 -2
  30. package/src/nodes/window/window.js +2 -2
  31. package/src/notifications.js +39 -14
  32. package/src/taskbarhooks.js +164 -0
  33. package/src/transfer.js +20 -1
  34. package/src/trayhooks.js +1 -1
  35. package/src/types/capabilities.d.ts +32 -3
  36. package/src/types/elements.d.ts +23 -1
  37. package/src/types/events.d.ts +16 -0
  38. package/src/types/launcher.d.ts +20 -6
  39. package/src/types/taskbar.d.ts +79 -0
  40. package/src/wayland/context2d.js +1 -1
  41. package/src/win32/a11y.js +604 -0
  42. package/src/win32/app.js +768 -0
  43. package/src/win32/bezels.js +158 -0
  44. package/src/win32/dnd.js +283 -0
  45. package/src/win32/fonts.js +497 -0
  46. package/src/win32/glarea.js +548 -0
  47. package/src/win32/ime.js +267 -0
  48. package/src/win32/keymap.js +116 -0
  49. package/src/win32/native.js +54 -0
  50. package/src/win32/panehost.js +106 -0
  51. package/src/win32/panewindow.js +343 -0
  52. package/src/win32/shell.js +426 -0
  53. package/src/win32/surface.js +192 -0
  54. package/src/win32/window.js +659 -0
  55. package/src/windowid.js +66 -0
@@ -0,0 +1,158 @@
1
+ // Native control bezels — the visual styles engine's own pixels for the core
2
+ // controls, cached as surfaces the ordinary 2d paint path blits. This is the
3
+ // Windows counterpart of src/cocoa/bezels.js, and it answers the same
4
+ // `app.nativeBezels` contract: interaction, focus and keyboard stay the shared
5
+ // component implementation, and only the *bezel* is asked of the system.
6
+ //
7
+ // ## Light only, and why that is the honest answer
8
+ //
9
+ // Windows exposes the **dark** common controls through undocumented uxtheme
10
+ // ordinals. The public route is `SetWindowTheme(hwnd, 'DarkMode_CFD')`, and
11
+ // measured on Windows 11 build 26200 it reaches COMBOBOX and nothing else:
12
+ // BUTTON, the checkbox, the radio and the trackbar thumb all draw
13
+ // pixel-identical to their light selves (test/bezels.js in the bridge is that
14
+ // measurement, and it fails if a future build changes the answer).
15
+ //
16
+ // A light button on a dark window is worse than a drawn one, so this store is
17
+ // installed only while the desktop is light. In dark the widget set draws its
18
+ // own bezels — which still follow the system, because the palette's accent is
19
+ // the accent the appearance ladder read out of DWM.
20
+ //
21
+ // ## Nothing here is asked twice
22
+ //
23
+ // A bezel is a surface keyed by everything that changes its pixels. The Cocoa
24
+ // store has a `_drawLater` path because its bridge answers on another thread;
25
+ // here every draw is synchronous on the JS thread, so `get` always answers and
26
+ // the `onReady` callback is never needed.
27
+
28
+ /** The kinds the widget set asks for. `switch` is absent on purpose: the
29
+ * theme engine has no toggle-switch part — the WinUI one is not in it — so
30
+ * the component keeps drawing its own rather than being handed something that
31
+ * is not a switch. */
32
+ const KINDS = ['push', 'checkbox', 'radio', 'popup', 'slider'];
33
+
34
+ /**
35
+ * The heights a control is laid out at. A checkbox and a radio have a real
36
+ * intrinsic size and the theme reports it (13x13 at 96 dpi), so those are
37
+ * asked. A button and a combo box size to their content, and the theme's
38
+ * answer for them is a *minimum* — 13x11 for a push button, which as a
39
+ * footprint would be a button nobody could read — so the conventional Windows
40
+ * metrics are used instead.
41
+ */
42
+ const HEIGHTS = {
43
+ push: { regular: 24, small: 20 },
44
+ popup: { regular: 24, small: 20 },
45
+ slider: { regular: 22, small: 18 },
46
+ };
47
+
48
+ export class Win32Bezels {
49
+ constructor(native, scale = 1) {
50
+ this._native = native;
51
+ this._scale = scale;
52
+ /** key -> { surface, sx, sy, sw, sh } */
53
+ this._cache = new Map();
54
+ /** `${kind}:${controlSize}` -> { width, height } | null */
55
+ this._natural = new Map();
56
+ }
57
+
58
+ /** Dropped when the appearance changes, because every cached bezel was
59
+ * drawn in the old one. `cascade.js` calls this. */
60
+ clear() {
61
+ for (const entry of this._cache.values()) {
62
+ this._native.releaseSurface(entry.surface);
63
+ }
64
+ this._cache.clear();
65
+ this._natural.clear();
66
+ }
67
+
68
+ natural(kind, controlSize = 'regular') {
69
+ const key = `${kind}:${controlSize}`;
70
+ if (this._natural.has(key)) return this._natural.get(key);
71
+
72
+ let answer = null;
73
+ if (KINDS.includes(kind)) {
74
+ const themed = this._native.bezelNatural(kind, false);
75
+ if (themed) {
76
+ const height = HEIGHTS[kind]?.[controlSize];
77
+ answer = height
78
+ ? { width: themed.width, height }
79
+ : { width: themed.width, height: themed.height };
80
+ }
81
+ }
82
+ this._natural.set(key, answer);
83
+ return answer;
84
+ }
85
+
86
+ /** Windows parts carry no translucent shadow band above or below the body,
87
+ * the way an AppKit push button does — the bezel is its own footprint. */
88
+ shadow() {
89
+ return { top: 0, bottom: 0 };
90
+ }
91
+
92
+ /**
93
+ * The bezel for these parameters at this size, as a surface and the rect
94
+ * inside it. Answers null for a kind with no part, which the component reads
95
+ * as "draw your own".
96
+ */
97
+ get(params, width, height) {
98
+ const w = Math.max(1, Math.round(width));
99
+ const h = Math.max(1, Math.round(height));
100
+ if (!KINDS.includes(params.kind)) return null;
101
+ // A dark request should never reach here — the store is uninstalled in
102
+ // dark — but a palette pinned dark under a light desktop can ask, and a
103
+ // light bezel is not the answer to it.
104
+ if (params.appearance === 'dark') return null;
105
+
106
+ const key = [
107
+ params.kind,
108
+ params.controlSize ?? 'regular',
109
+ params.state ? 1 : 0,
110
+ params.pressed ? 1 : 0,
111
+ params.enabled === false ? 0 : 1,
112
+ params.isDefault ? 1 : 0,
113
+ w,
114
+ h,
115
+ ].join(':');
116
+ const cached = this._cache.get(key);
117
+ if (cached) return cached;
118
+
119
+ const surface = this._native.createSurface(w, h, 1);
120
+ const drawn = this._native.bezelDraw(
121
+ surface,
122
+ params.kind,
123
+ {
124
+ enabled: params.enabled !== false,
125
+ pressed: Boolean(params.pressed),
126
+ checked: Boolean(params.state),
127
+ isDefault: Boolean(params.isDefault),
128
+ dark: false,
129
+ },
130
+ 0,
131
+ 0,
132
+ w,
133
+ h,
134
+ );
135
+ if (!drawn) {
136
+ this._native.releaseSurface(surface);
137
+ this._cache.set(key, null);
138
+ return null;
139
+ }
140
+
141
+ const entry = { surface, sx: 0, sy: 0, sw: w, sh: h };
142
+ this._cache.set(key, entry);
143
+ return entry;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * The store, or null where native bezels would be worse than drawn ones:
149
+ * under a classic theme with no visual styles at all, and in dark, where the
150
+ * parts this backend can reach are light-only (see the header).
151
+ */
152
+ export function createBezels(native, { colorScheme, scale = 1 } = {}) {
153
+ if (typeof native.themesActive !== 'function' || !native.themesActive()) {
154
+ return null;
155
+ }
156
+ if (colorScheme === 'dark') return null;
157
+ return new Win32Bezels(native, scale);
158
+ }
@@ -0,0 +1,283 @@
1
+ // Drag and drop on the win32 backend — the OLE transport over
2
+ // @windowkit/win32: `IDropTarget` on every window that has a `dropAccept`
3
+ // under it, `DoDragDrop` out of one. The `dropAccept` / `onDrag*` / `dragData`
4
+ // prop contract is the tree's (src/dnd.js); what this file owns is the
5
+ // translation between the shell's vocabulary and `DropSession`/`DragSession`'s,
6
+ // the same job src/cocoa/dnd.js does for AppKit and src/wayland/dnd.js for the
7
+ // data device.
8
+ //
9
+ // ## The answer, and when it is given
10
+ //
11
+ // AppKit asks its questions on the thread the tree lives on and wants the
12
+ // answer from inside the callback. The shell asks on the bridge's UI thread
13
+ // and wants it before `IDropTarget::DragOver` returns — and this tree is on
14
+ // another thread, so "from inside the callback" is not available.
15
+ //
16
+ // The bridge splits the difference by what each question is for (src/dnd.cc):
17
+ // a motion's answer picks a cursor and is allowed to be one motion stale, so
18
+ // the native returns the previous answer and never waits; a drop's answer
19
+ // decides whether the source deletes its original, so the native waits for
20
+ // this file to call `dropResponse`. Both arrive here as ordinary events and
21
+ // both are answered the same way — the difference is entirely on the other
22
+ // side, and the only thing it asks of this file is that the drop is answered
23
+ // **synchronously in the event handler**, which `DropSession.localDrop`
24
+ // already is.
25
+ //
26
+ // ## Types
27
+ //
28
+ // The shell has no registration step: a window that is a drop target is
29
+ // offered everything, and what it accepts is decided per motion. So
30
+ // `refreshTypes` has nothing to register — it turns the target on when any
31
+ // `dropAccept` exists under the window and off when the last one goes, which
32
+ // is what keeps the shell from offering a cursor over a window that would
33
+ // refuse everything.
34
+ //
35
+ // Clipboard formats map to MIME in the bridge. `text/uri-list` is CF_HDROP,
36
+ // which is what a Explorer file drag arrives as, and anything the table does
37
+ // not name is a registered format named by the MIME string — two react-x11
38
+ // apps agree on it for free, like an X11 atom, and nothing else will.
39
+ //
40
+ // ## The source
41
+ //
42
+ // `beginDrag` resolves every type's data **up front** and hands the bytes
43
+ // over, then `DoDragDrop` runs its modal loop on the UI thread. That is a
44
+ // real difference from the cocoa transport, which keeps a thunk as a promise
45
+ // the bridge redeems when the receiver asks: here a thunk is called at the
46
+ // start of the drag instead. The reason is the thread split — a data object
47
+ // that asked JS for its bytes from inside the modal loop would be reaching
48
+ // across it at the one moment the UI thread cannot wait — and the cost is
49
+ // that a `dragData` thunk is not lazy on this backend.
50
+ //
51
+ // What the split buys back is the thing it was built for: JS keeps rendering
52
+ // for the whole drag. On the cocoa backend AppKit owns the thread from the
53
+ // threshold to the drop and no frame clock ticks in between.
54
+ const DEBUG = process.env.REACT_X11_WIN32_DEBUG === '1';
55
+
56
+ import {
57
+ TEXT_TARGETS,
58
+ TYPE_GROUPS,
59
+ parseUriList,
60
+ resolveType,
61
+ } from '../transfer.js';
62
+
63
+ /** Bytes for the wire: a string goes as UTF-8, anything else as it is. */
64
+ function wire(value) {
65
+ if (value == null) return '';
66
+ if (typeof value === 'string') return value;
67
+ if (Buffer.isBuffer(value)) return value;
68
+ if (ArrayBuffer.isView(value)) {
69
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
70
+ }
71
+ return String(value);
72
+ }
73
+
74
+ /**
75
+ * What `beginDrag` hands the bridge: every type with its bytes already
76
+ * resolved, and the actions the gesture allows.
77
+ */
78
+ export function dragSpec(session) {
79
+ const items = [];
80
+ for (const type of session.types) {
81
+ const raw = session._resolve(type);
82
+ if (raw == null) continue;
83
+ items.push({ type, data: wire(raw) });
84
+ }
85
+ return { items, actions: session.actions ?? ['copy'] };
86
+ }
87
+
88
+ /**
89
+ * One window's drop side: the four events turned into `DropSession` calls,
90
+ * with the answer sent back before the handler returns.
91
+ */
92
+ export class Win32DropTransport {
93
+ constructor(wnd, session, node) {
94
+ this.wnd = wnd;
95
+ this.session = session;
96
+ this.node = node;
97
+ this._enabled = null;
98
+ this.refreshTypes();
99
+ }
100
+
101
+ /**
102
+ * The shell registers no types, so this is the on/off switch: a window with
103
+ * nothing that would accept a drop is not a drop target at all, and the
104
+ * shell shows the "no" cursor over it without asking anybody.
105
+ */
106
+ refreshTypes() {
107
+ const wanted =
108
+ (this.node._dndConcreteTypes?.() ?? []).length > 0 ||
109
+ this.node._dnd != null;
110
+ if (wanted === this._enabled) return;
111
+ this._enabled = wanted;
112
+ if (DEBUG) console.error(`[win32] dropTarget ${this.wnd.id} -> ${wanted}`);
113
+ this.wnd._native.dropTargetEnable(this.wnd.id, wanted);
114
+ }
115
+
116
+ /**
117
+ * Register again now that there is an HWND to register on.
118
+ *
119
+ * The tree mounts its `dropAccept`s in the turn the window is asked for,
120
+ * and this bridge creates a window asynchronously — `createWindow` answers
121
+ * with an id and the HWND appears later. So the first registration lands
122
+ * on a window that does not exist yet and does nothing, quietly, and the
123
+ * only symptom is a window the shell never offers a drag to.
124
+ */
125
+ reattach() {
126
+ this._enabled = null;
127
+ this.refreshTypes();
128
+ }
129
+
130
+ handle(ev) {
131
+ if (DEBUG)
132
+ console.error(`[win32] ${ev.type} ${ev.a},${ev.b} "${ev.text ?? ''}"`);
133
+ switch (ev.type) {
134
+ case 'drag-enter':
135
+ case 'drag-over':
136
+ return this._over(ev);
137
+ case 'drag-leave':
138
+ return this._leave();
139
+ case 'drag-drop':
140
+ return this._drop(ev);
141
+ default:
142
+ return undefined;
143
+ }
144
+ }
145
+
146
+ /** The live drag in this process, when this drop is our own gesture coming
147
+ * back over one of our windows. */
148
+ _local() {
149
+ return this.wnd.app._activeDrag ?? null;
150
+ }
151
+
152
+ _types(ev) {
153
+ return String(ev.text ?? '')
154
+ .split('\n')
155
+ .filter(Boolean);
156
+ }
157
+
158
+ /** What the shell says the source allows, in the tree's vocabulary. The
159
+ * order is the preference: a source offering both copy and move means
160
+ * move, which is what every other transport reports. */
161
+ _actions(effects) {
162
+ const allowed = [];
163
+ if (effects & 2) allowed.push('move');
164
+ if (effects & 1) allowed.push('copy');
165
+ if (effects & 4) allowed.push('link');
166
+ return allowed;
167
+ }
168
+
169
+ _offer(ev) {
170
+ const drag = this._local();
171
+ if (drag) return drag._offer();
172
+ const actions = this._actions(ev.c ?? 0);
173
+ return {
174
+ types: this._types(ev),
175
+ action: actions[0] ?? 'copy',
176
+ source: 'external',
177
+ };
178
+ }
179
+
180
+ /**
181
+ * The answer, and which question it answers.
182
+ *
183
+ * `forDrop` is not a detail: the bridge's UI thread is blocked inside
184
+ * `IDropTarget::Drop` waiting for one, and the answers to the last few
185
+ * motions are still arriving behind it — JS is a queue or two back by
186
+ * then. Without the flag the first of those would release the wait, and
187
+ * the drop would be decided by a motion's answer.
188
+ */
189
+ _answer(answer, forDrop = false) {
190
+ this.wnd._native.dropResponse(
191
+ this.wnd.id,
192
+ Boolean(answer.accepted ?? answer.handled),
193
+ answer.action ?? 'copy',
194
+ forDrop,
195
+ );
196
+ }
197
+
198
+ _over(ev) {
199
+ const drag = this._local();
200
+ const offer = this._offer(ev);
201
+ const answer = this.session.localOver(ev.a, ev.b, offer, Date.now());
202
+ if (drag) {
203
+ drag.accepted = answer.accepted;
204
+ if (answer.accepted) drag.currentAction = answer.action;
205
+ }
206
+ this._answer(answer);
207
+ }
208
+
209
+ _leave() {
210
+ const drag = this._local();
211
+ this.session.localLeave();
212
+ if (drag) drag.accepted = false;
213
+ }
214
+
215
+ _drop(ev) {
216
+ const drag = this._local();
217
+ const offer = this._offer(ev);
218
+ // The bridge read every format while the data object was alive; this asks
219
+ // it for the ones the tree wants. A local drop keeps `e.items` by
220
+ // reference instead, so an in-app drop never round-trips through bytes.
221
+ const extras = drag ? drag._dropExtras() : this._payload(offer.types);
222
+ let outcome = { handled: false, action: null };
223
+ try {
224
+ outcome = this.session.localDrop(offer, extras, Date.now());
225
+ } catch (err) {
226
+ if (DEBUG) console.error(`[win32] localDrop threw: ${err?.stack ?? err}`);
227
+ throw err;
228
+ } finally {
229
+ // Answered in a `finally` because the bridge's UI thread is blocked
230
+ // inside `IDropTarget::Drop` waiting for it. A handler that throws must
231
+ // not leave it there for the whole two-second backstop — the pointer is
232
+ // captured for as long as the drop is open.
233
+ this._answer(outcome, true);
234
+ }
235
+ if (DEBUG) console.error(`[win32] drop outcome ${JSON.stringify(outcome)}`);
236
+ if (drag && outcome.handled) drag.currentAction = outcome.action;
237
+ }
238
+
239
+ /**
240
+ * What the bridge read at the drop, in the shape `localDrop` takes — the
241
+ * same four fields the cocoa transport builds from a pasteboard.
242
+ *
243
+ * Textual types come back as strings and everything else as the bytes the
244
+ * source put on the clipboard: a `text/uri-list` that was decoded from
245
+ * CF_HDROP is text by the time it gets here, and an
246
+ * `application/x-myapp-thing` is whatever its owner wrote.
247
+ */
248
+ _payload(types) {
249
+ const values = {};
250
+ for (const type of types) {
251
+ const bytes = this.wnd._native.dropData(type);
252
+ if (!bytes) continue;
253
+ values[type] = this._textual(type) ? bytes.toString('utf8') : bytes;
254
+ }
255
+ if (
256
+ values['text/plain;charset=utf-8'] !== undefined &&
257
+ values['text/plain'] === undefined
258
+ ) {
259
+ values['text/plain'] = values['text/plain;charset=utf-8'];
260
+ }
261
+ const best = TEXT_TARGETS.find((t) => values[t] !== undefined);
262
+ return {
263
+ items: values,
264
+ files: values['text/uri-list']
265
+ ? parseUriList(values['text/uri-list'])
266
+ : [],
267
+ text: best ? values[best] : undefined,
268
+ // A promise, like the cocoa transport's: the payload is already in
269
+ // hand, and answering synchronously here would make this the one
270
+ // backend where `await e.getData(…)` was not needed.
271
+ getData: (type) =>
272
+ Promise.resolve(values[resolveType(type, types)] ?? null),
273
+ };
274
+ }
275
+
276
+ _textual(type) {
277
+ return (
278
+ TEXT_TARGETS.includes(type) ||
279
+ TYPE_GROUPS.files.includes(type) ||
280
+ type.startsWith('text/')
281
+ );
282
+ }
283
+ }