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.
- package/README.md +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/appcontext.js +8 -0
- package/src/appearance.js +36 -0
- package/src/{cocoa → backend}/context2d.js +27 -7
- package/src/capabilities.js +99 -1
- package/src/cocoa/app.js +17 -9
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/glarea.js +48 -10
- package/src/cocoa/overlay.js +2 -2
- package/src/cocoa/panewindow.js +2 -2
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/surface.js +3 -3
- package/src/cocoa/window.js +23 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +21 -7
- package/src/idle.js +59 -1
- package/src/index.d.ts +41 -0
- package/src/index.js +30 -3
- package/src/launcher.js +17 -8
- package/src/launcherhooks.js +24 -10
- package/src/node.d.ts +1 -1
- package/src/nodes/cascade.js +9 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/window/hints.js +21 -2
- package/src/nodes/window/window.js +2 -2
- package/src/notifications.js +39 -14
- package/src/taskbarhooks.js +164 -0
- package/src/transfer.js +20 -1
- package/src/trayhooks.js +1 -1
- package/src/types/capabilities.d.ts +32 -3
- package/src/types/elements.d.ts +23 -1
- package/src/types/events.d.ts +16 -0
- package/src/types/launcher.d.ts +20 -6
- package/src/types/taskbar.d.ts +79 -0
- package/src/wayland/context2d.js +1 -1
- package/src/win32/a11y.js +604 -0
- package/src/win32/app.js +768 -0
- package/src/win32/bezels.js +158 -0
- package/src/win32/dnd.js +283 -0
- package/src/win32/fonts.js +497 -0
- package/src/win32/glarea.js +548 -0
- package/src/win32/ime.js +267 -0
- package/src/win32/keymap.js +116 -0
- package/src/win32/native.js +54 -0
- package/src/win32/panehost.js +106 -0
- package/src/win32/panewindow.js +343 -0
- package/src/win32/shell.js +426 -0
- package/src/win32/surface.js +192 -0
- package/src/win32/window.js +659 -0
- package/src/windowid.js +66 -0
package/src/win32/ime.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// The input method, as the tree sees it.
|
|
2
|
+
//
|
|
3
|
+
// The bridge answers the IMM32 messages and hands over four events
|
|
4
|
+
// (windows/src/ime.cc); this turns them into the composition events the
|
|
5
|
+
// renderer already has — `CompositionStart`, `CompositionUpdate`,
|
|
6
|
+
// `CompositionEnd` — so that a `<textinput>` on Windows composes through
|
|
7
|
+
// exactly the machinery a `<textinput>` on Wayland composes through, and an
|
|
8
|
+
// application that handles one handles both.
|
|
9
|
+
//
|
|
10
|
+
// The order is the one `src/wayland/textinput.js` `_apply` fixes, because it
|
|
11
|
+
// is the order the events are defined in rather than a detail of either
|
|
12
|
+
// protocol: **the commit first, then the new preedit.** One
|
|
13
|
+
// WM_IME_COMPOSITION can carry both, and together they mean "this text is
|
|
14
|
+
// settled, and this is what is still being typed".
|
|
15
|
+
//
|
|
16
|
+
// The other half is telling the IME where to put its candidate list. The
|
|
17
|
+
// preedit is drawn in the field (src/nodes/preedit.js), so the candidate list
|
|
18
|
+
// is the only thing the IME still draws, and it has to sit at the caret
|
|
19
|
+
// rather than in the corner of the screen. `sync()` is what keeps it there:
|
|
20
|
+
// the frame loop calls it after each tick, when the layout the caret is read
|
|
21
|
+
// off is the one just painted.
|
|
22
|
+
import { runWithPriority, DiscreteEventPriority } from '../priority.js';
|
|
23
|
+
|
|
24
|
+
const TRACE = process.env.REACT_X11_TRACE_IME === '1';
|
|
25
|
+
|
|
26
|
+
/** How many code points `units` UTF-16 code units cover.
|
|
27
|
+
*
|
|
28
|
+
* The bridge counts in code units because that is what IMM32 hands it, and
|
|
29
|
+
* the tree counts in code points. JS strings are UTF-16, so slicing at the
|
|
30
|
+
* bridge's offset and counting the result is the whole conversion — and it is
|
|
31
|
+
* done here rather than there because here is where the string is. */
|
|
32
|
+
function codePointsIn(text, units) {
|
|
33
|
+
if (!(units > 0)) return 0;
|
|
34
|
+
return Array.from(text.slice(0, units)).length;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class Win32InputMethod {
|
|
38
|
+
constructor(app) {
|
|
39
|
+
this.app = app;
|
|
40
|
+
this._native = app._native;
|
|
41
|
+
/** The field the IME is enabled for: `{ wnd, node, rect }`, or null. */
|
|
42
|
+
this.active = null;
|
|
43
|
+
/** Whether the tree has heard a `CompositionStart` it is owed an end for. */
|
|
44
|
+
this.composing = false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- what the tree says ---------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The focused text control of a window, or null.
|
|
51
|
+
*
|
|
52
|
+
* The duck type is `defaultComposition` + `textCaretRect`, which is what
|
|
53
|
+
* `<textinput>` and `<textarea>` implement and what a registered element
|
|
54
|
+
* with a caret of its own would implement to be reached the same way —
|
|
55
|
+
* the same test `src/wayland/textinput.js` makes, deliberately.
|
|
56
|
+
*
|
|
57
|
+
* A `sensitive` field answers null: a password is not offered to an input
|
|
58
|
+
* method, whose word history is one more place it must not reach. Windows
|
|
59
|
+
* does the same for its own password boxes.
|
|
60
|
+
*/
|
|
61
|
+
_focusedTextControl(wnd) {
|
|
62
|
+
const node = wnd._reactX11Node?.events?.focusManager?.focused ?? null;
|
|
63
|
+
if (!node || node.destroyed) return null;
|
|
64
|
+
if (node.root?.window !== wnd) return null;
|
|
65
|
+
if (typeof node.defaultComposition !== 'function') return null;
|
|
66
|
+
if (typeof node.textCaretRect !== 'function') return null;
|
|
67
|
+
if (node.composes === false) return null;
|
|
68
|
+
if (node.props?.sensitive) return null;
|
|
69
|
+
return node;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where the caret is, in the window's client pixels — which is the space
|
|
74
|
+
* the tree is already in on this backend (a mouse event's `x` reaches the
|
|
75
|
+
* tree exactly as the bridge sent it), so there is no scale to divide out
|
|
76
|
+
* and no frame inset to add. A field whose text has no layout yet offers
|
|
77
|
+
* its content box, which at least puts the list under the field.
|
|
78
|
+
*/
|
|
79
|
+
_caretRect(wnd, node) {
|
|
80
|
+
let rect = null;
|
|
81
|
+
try {
|
|
82
|
+
const chars = Array.from(node.value ?? '');
|
|
83
|
+
const caret = Number.isInteger(node._caret)
|
|
84
|
+
? Math.max(0, Math.min(node._caret, chars.length))
|
|
85
|
+
: chars.length;
|
|
86
|
+
rect = node.textCaretRect(caret);
|
|
87
|
+
} catch {
|
|
88
|
+
rect = null;
|
|
89
|
+
}
|
|
90
|
+
if (!rect) {
|
|
91
|
+
const box = node.contentBox?.() ?? node.abs;
|
|
92
|
+
if (!box) return null;
|
|
93
|
+
rect = { x: box.x, y: box.y, width: 0, height: box.height };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
x: Math.round(rect.x),
|
|
97
|
+
y: Math.round(rect.y),
|
|
98
|
+
width: Math.max(1, Math.round(rect.width ?? 0)),
|
|
99
|
+
height: Math.max(1, Math.round(rect.height ?? 0)),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Bring the IME up to date about a window's focused field.
|
|
105
|
+
*
|
|
106
|
+
* Called at the end of every frame the window ran, which is cheap when
|
|
107
|
+
* nothing changed: one duck-type check and four comparisons. Nothing is
|
|
108
|
+
* sent to the UI thread unless the field or the caret actually moved.
|
|
109
|
+
*/
|
|
110
|
+
sync(wnd) {
|
|
111
|
+
if (this._destroyed || !wnd || wnd._destroyed) return;
|
|
112
|
+
const node = this._focusedTextControl(wnd);
|
|
113
|
+
if (node !== this.active?.node) {
|
|
114
|
+
if (this.active) this._disable();
|
|
115
|
+
if (node) this._enable(wnd, node);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (node) this._place(wnd, node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_enable(wnd, node) {
|
|
122
|
+
this.active = { wnd, node, rect: null };
|
|
123
|
+
this.composing = false;
|
|
124
|
+
this._native.imeEnable(wnd.id, true);
|
|
125
|
+
this._place(wnd, node);
|
|
126
|
+
if (TRACE) trace(`enable <${node.kind}>`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Take the IME off this window, and end the composition the tree is owed.
|
|
131
|
+
*
|
|
132
|
+
* The field's own blur has already cleared the preedit it was drawing; what
|
|
133
|
+
* is owed is the `CompositionEnd` an application that saw the start is
|
|
134
|
+
* waiting for, with no data — which is how an abandoned composition has
|
|
135
|
+
* always ended.
|
|
136
|
+
*/
|
|
137
|
+
_disable() {
|
|
138
|
+
const active = this.active;
|
|
139
|
+
this.active = null;
|
|
140
|
+
if (!active) return;
|
|
141
|
+
if (this.composing) {
|
|
142
|
+
this.composing = false;
|
|
143
|
+
const node = active.node;
|
|
144
|
+
const events = node.destroyed ? null : node.root?.events;
|
|
145
|
+
if (events) {
|
|
146
|
+
runWithPriority(DiscreteEventPriority, () =>
|
|
147
|
+
events._composition('End', node, '', null),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this._native.imeEnable(active.wnd.id, false);
|
|
152
|
+
if (TRACE) trace('disable');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Move the candidate list to the caret, if it is not already there. */
|
|
156
|
+
_place(wnd, node) {
|
|
157
|
+
const rect = this._caretRect(wnd, node);
|
|
158
|
+
const was = this.active?.rect;
|
|
159
|
+
if (!rect) return;
|
|
160
|
+
if (
|
|
161
|
+
was &&
|
|
162
|
+
was.x === rect.x &&
|
|
163
|
+
was.y === rect.y &&
|
|
164
|
+
was.width === rect.width &&
|
|
165
|
+
was.height === rect.height
|
|
166
|
+
) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (this.active) this.active.rect = rect;
|
|
170
|
+
this._native.imeCaret(wnd.id, rect.x, rect.y, rect.width, rect.height);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- what the input method says -------------------------------------------
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* One `ime-*` event from the bridge.
|
|
177
|
+
*
|
|
178
|
+
* Every one is dispatched at `DiscreteEventPriority`: a composition is a
|
|
179
|
+
* keystroke's worth of user intent and must not be batched behind a
|
|
180
|
+
* transition, exactly as the Wayland transport dispatches its own.
|
|
181
|
+
*/
|
|
182
|
+
handle(event, wnd) {
|
|
183
|
+
const active = this.active;
|
|
184
|
+
if (!active || active.wnd !== wnd) return;
|
|
185
|
+
const node = active.node;
|
|
186
|
+
if (node.destroyed) return;
|
|
187
|
+
const events = node.root?.events;
|
|
188
|
+
if (!events) return;
|
|
189
|
+
|
|
190
|
+
// There is no key behind an input method's commit, which is what a
|
|
191
|
+
// handler reading `nativeEvent` needs to be told.
|
|
192
|
+
const native = { type: 'ime' };
|
|
193
|
+
|
|
194
|
+
switch (event.type) {
|
|
195
|
+
case 'ime-start':
|
|
196
|
+
// The renderer's `CompositionStart` is raised lazily, with the first
|
|
197
|
+
// text — the same as every other backend, so that a composition that
|
|
198
|
+
// begins and ends with nothing in it never reaches an application.
|
|
199
|
+
// This only clears a flag a previous session could have left.
|
|
200
|
+
if (TRACE) trace('start');
|
|
201
|
+
return;
|
|
202
|
+
|
|
203
|
+
case 'ime-commit': {
|
|
204
|
+
const text = event.text ?? '';
|
|
205
|
+
if (!text) return;
|
|
206
|
+
if (TRACE) trace(`commit ${JSON.stringify(text)}`);
|
|
207
|
+
runWithPriority(DiscreteEventPriority, () => {
|
|
208
|
+
if (!this.composing) events._composition('Start', node, '', native);
|
|
209
|
+
this.composing = false;
|
|
210
|
+
// `defaultComposition` inserts it the way a typed character is
|
|
211
|
+
// inserted — through `_insert`, with `maxLength`, `onChange` and
|
|
212
|
+
// the undo run all in play.
|
|
213
|
+
events._composition('End', node, text, native);
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case 'ime-preedit': {
|
|
219
|
+
const text = event.text ?? '';
|
|
220
|
+
if (TRACE)
|
|
221
|
+
trace(`preedit ${JSON.stringify(text)} ${event.a}..${event.b}`);
|
|
222
|
+
runWithPriority(DiscreteEventPriority, () => {
|
|
223
|
+
if (text) {
|
|
224
|
+
if (!this.composing) {
|
|
225
|
+
events._composition('Start', node, '', native);
|
|
226
|
+
this.composing = true;
|
|
227
|
+
}
|
|
228
|
+
// The clause the IME is converting, which is what it wants drawn
|
|
229
|
+
// as the selection inside the preedit.
|
|
230
|
+
const cursor = {
|
|
231
|
+
cursorBegin: codePointsIn(text, event.a),
|
|
232
|
+
cursorEnd: codePointsIn(text, event.b),
|
|
233
|
+
};
|
|
234
|
+
events._composition('Update', node, text, native, cursor);
|
|
235
|
+
} else if (this.composing) {
|
|
236
|
+
this.composing = false;
|
|
237
|
+
events._composition('End', node, '', native);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'ime-end':
|
|
244
|
+
// Usually nothing to do: the commit or an emptied preedit has already
|
|
245
|
+
// ended it. An IME that cancels without either lands here.
|
|
246
|
+
if (!this.composing) return;
|
|
247
|
+
if (TRACE) trace('end');
|
|
248
|
+
runWithPriority(DiscreteEventPriority, () => {
|
|
249
|
+
this.composing = false;
|
|
250
|
+
events._composition('End', node, '', native);
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
|
|
254
|
+
default:
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
destroy() {
|
|
260
|
+
this._destroyed = true;
|
|
261
|
+
this.active = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function trace(line) {
|
|
266
|
+
process.stderr.write(`react-x11 win32: ime ${line}\n`);
|
|
267
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Windows key events -> the renderer's keysym vocabulary (src/keysyms.js).
|
|
2
|
+
//
|
|
3
|
+
// The rule mirrors X's own, as the Cocoa map does: a Latin-1 keysym is its
|
|
4
|
+
// code point, anything else printable is 0x01000000 + code point, and the
|
|
5
|
+
// editing and navigation keys have fixed keysyms looked up here by virtual
|
|
6
|
+
// key code.
|
|
7
|
+
//
|
|
8
|
+
// The bridge has already done the part only it can do — asking the active
|
|
9
|
+
// layout what the key types, once with the modifiers that are down and once
|
|
10
|
+
// with none (src/win32.cc, EmitKey). That gives the two character roles
|
|
11
|
+
// events.js reads:
|
|
12
|
+
//
|
|
13
|
+
// baseCodepoint (nothing held) -> baseKeysym — what chords match
|
|
14
|
+
// codepoint (Shift/AltGr held) -> keysym / codepoint — what was typed
|
|
15
|
+
//
|
|
16
|
+
// A key that types nothing reports 0 for both, and the table below is what
|
|
17
|
+
// gives it a keysym at all. Tab is the one to keep an eye on: it *does* type
|
|
18
|
+
// a character on Windows (U+0009), and letting the character rule have it
|
|
19
|
+
// would make the keysym 9 instead of XK_Tab — which is why the table wins.
|
|
20
|
+
import { keysymOf, MOD } from '../keysyms.js';
|
|
21
|
+
|
|
22
|
+
// Virtual key codes -> X keysyms, for keys whose character is a control code
|
|
23
|
+
// or nothing at all.
|
|
24
|
+
const VK_KEYSYMS = new Map([
|
|
25
|
+
[0x08, 0xff08], // Backspace
|
|
26
|
+
[0x09, 0xff09], // Tab
|
|
27
|
+
[0x0d, 0xff0d], // Return
|
|
28
|
+
[0x13, 0xff13], // Pause
|
|
29
|
+
[0x14, 0xffe5], // Caps Lock
|
|
30
|
+
[0x1b, 0xff1b], // Escape
|
|
31
|
+
[0x21, 0xff55], // Page Up
|
|
32
|
+
[0x22, 0xff56], // Page Down
|
|
33
|
+
[0x23, 0xff57], // End
|
|
34
|
+
[0x24, 0xff50], // Home
|
|
35
|
+
[0x25, 0xff51], // Left
|
|
36
|
+
[0x26, 0xff52], // Up
|
|
37
|
+
[0x27, 0xff53], // Right
|
|
38
|
+
[0x28, 0xff54], // Down
|
|
39
|
+
[0x2c, 0xfd1d], // Print Screen -> XK_3270_PrintScreen
|
|
40
|
+
[0x2d, 0xff63], // Insert
|
|
41
|
+
[0x2e, 0xffff], // Delete
|
|
42
|
+
[0x5b, 0xffeb], // Left Super
|
|
43
|
+
[0x5c, 0xffec], // Right Super
|
|
44
|
+
[0x5d, 0xff67], // Menu
|
|
45
|
+
[0x90, 0xff7f], // Num Lock
|
|
46
|
+
[0x91, 0xff14], // Scroll Lock
|
|
47
|
+
[0xa0, 0xffe1], // Left Shift
|
|
48
|
+
[0xa1, 0xffe2], // Right Shift
|
|
49
|
+
[0xa2, 0xffe3], // Left Control
|
|
50
|
+
[0xa3, 0xffe4], // Right Control
|
|
51
|
+
[0xa4, 0xffe9], // Left Alt
|
|
52
|
+
[0xa5, 0xffea], // Right Alt
|
|
53
|
+
// The keypad, which has keysyms of its own so that a caret can tell
|
|
54
|
+
// KP_Left from Left even when Num Lock is off.
|
|
55
|
+
[0x6a, 0xffaa], // KP_Multiply
|
|
56
|
+
[0x6b, 0xffab], // KP_Add
|
|
57
|
+
[0x6d, 0xffad], // KP_Subtract
|
|
58
|
+
[0x6e, 0xffae], // KP_Decimal
|
|
59
|
+
[0x6f, 0xffaf], // KP_Divide
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// F1..F24 are contiguous on both sides, so they are a range rather than 24
|
|
63
|
+
// table entries.
|
|
64
|
+
const VK_F1 = 0x70;
|
|
65
|
+
const XK_F1 = 0xffbe;
|
|
66
|
+
|
|
67
|
+
// The keypad digits, likewise: VK_NUMPAD0..9 -> XK_KP_0..9.
|
|
68
|
+
const VK_NUMPAD0 = 0x60;
|
|
69
|
+
const XK_KP_0 = 0xffb0;
|
|
70
|
+
|
|
71
|
+
function fixedKeysym(vk) {
|
|
72
|
+
const known = VK_KEYSYMS.get(vk);
|
|
73
|
+
if (known) return known;
|
|
74
|
+
if (vk >= VK_F1 && vk <= VK_F1 + 23) return XK_F1 + (vk - VK_F1);
|
|
75
|
+
if (vk >= VK_NUMPAD0 && vk <= VK_NUMPAD0 + 9)
|
|
76
|
+
return XK_KP_0 + (vk - VK_NUMPAD0);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const keysymFromCodepoint = (cp) =>
|
|
81
|
+
cp && cp >= 0x20 ? keysymOf(String.fromCodePoint(cp)) : 0;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The three key facts events.js reads off a native key event, from the
|
|
85
|
+
* bridge's payload: `a` the virtual key, `b` what it typed, `c` what it
|
|
86
|
+
* would type with nothing held.
|
|
87
|
+
*/
|
|
88
|
+
export function decodeKey(event) {
|
|
89
|
+
const vk = event.a;
|
|
90
|
+
const fixed = fixedKeysym(vk);
|
|
91
|
+
if (fixed) {
|
|
92
|
+
// A key with a fixed keysym reports no code point even when Windows
|
|
93
|
+
// says it types one. Tab and Return type U+0009 and U+000D, and letting
|
|
94
|
+
// those through would insert a control character into every text field
|
|
95
|
+
// the moment a caret was in one.
|
|
96
|
+
return { keysym: fixed, baseKeysym: fixed, codepoint: undefined };
|
|
97
|
+
}
|
|
98
|
+
const keysym = keysymFromCodepoint(event.b);
|
|
99
|
+
const baseKeysym = keysymFromCodepoint(event.c) || keysym;
|
|
100
|
+
return {
|
|
101
|
+
keysym: keysym || baseKeysym,
|
|
102
|
+
baseKeysym,
|
|
103
|
+
codepoint: event.b ? event.b : undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The bridge's modifier bits -> the X-style state mask events carry. */
|
|
108
|
+
export function modifierMask(bits) {
|
|
109
|
+
let mask = 0;
|
|
110
|
+
if (bits & 1) mask |= MOD.Shift;
|
|
111
|
+
if (bits & 2) mask |= MOD.Control;
|
|
112
|
+
if (bits & 4) mask |= MOD.Alt;
|
|
113
|
+
if (bits & 8) mask |= MOD.Super;
|
|
114
|
+
if (bits & 16) mask |= MOD.Lock;
|
|
115
|
+
return mask;
|
|
116
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The bridge to @windowkit/win32 — the Windows backend's native half.
|
|
2
|
+
// Resolved lazily so that importing react-x11 anywhere else costs nothing;
|
|
3
|
+
// `createRoot` only walks in here once the backend decision has landed on
|
|
4
|
+
// 'win32'. That laziness is what lets the package be an *optional* dependency:
|
|
5
|
+
// off Windows npm skips it (its `os` field is win32-only) and nothing here
|
|
6
|
+
// ever asks for it.
|
|
7
|
+
//
|
|
8
|
+
// Resolution order: `REACT_X11_WIN32_PATH` (a checkout, for development), then
|
|
9
|
+
// the installed `@windowkit/win32`. This mirrors src/cocoa/native.js down to
|
|
10
|
+
// the loader, including why `createRequire` is made from `process.execPath`
|
|
11
|
+
// when there is no module URL — a single-executable build (docs/packaging.md
|
|
12
|
+
// tier 3) is CommonJS, and esbuild leaves `import.meta` an empty object there.
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const PACKAGE = '@windowkit/win32';
|
|
16
|
+
|
|
17
|
+
let require = null;
|
|
18
|
+
function load(spec) {
|
|
19
|
+
require ??= createRequire(import.meta.url ?? process.execPath);
|
|
20
|
+
return require(spec);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let cached = null;
|
|
24
|
+
|
|
25
|
+
export function loadNative() {
|
|
26
|
+
if (cached) return cached;
|
|
27
|
+
if (process.platform !== 'win32') {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"react-x11: the 'win32' backend is Windows-only. On this platform use " +
|
|
30
|
+
'the X11 backend (the default when DISPLAY is set), or pass ' +
|
|
31
|
+
"createRoot({ backend: 'x11' }).",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const tried = [];
|
|
35
|
+
const path = process.env.REACT_X11_WIN32_PATH;
|
|
36
|
+
for (const spec of [path, PACKAGE].filter(Boolean)) {
|
|
37
|
+
try {
|
|
38
|
+
const mod = load(spec);
|
|
39
|
+
cached = mod.native ?? mod;
|
|
40
|
+
return cached;
|
|
41
|
+
} catch (err) {
|
|
42
|
+
tried.push(` ${spec}: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw new Error(
|
|
46
|
+
`react-x11: the win32 backend needs the ${PACKAGE} native bridge and ` +
|
|
47
|
+
'none could be loaded:\n' +
|
|
48
|
+
tried.join('\n') +
|
|
49
|
+
`\nInstall it with \`npm install ${PACKAGE}\` (Windows; prebuilds ship ` +
|
|
50
|
+
'for x64 and ARM64, and building from source needs Visual Studio with ' +
|
|
51
|
+
'the "Desktop development with C++" workload). To use X11 instead, set ' +
|
|
52
|
+
"DISPLAY and pass createRoot({ backend: 'x11' }).",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// The host's half of a Windows frame pane: one visual over the window's
|
|
2
|
+
// content — the same stacking a `<glarea>` gets — whose content is the
|
|
3
|
+
// buffer the pane process is drawing into. The pane owns its buffers and its
|
|
4
|
+
// drawing; this side owns the visual, the layout and the input. CPU
|
|
5
|
+
// offloading, not isolation (docs/frame.md).
|
|
6
|
+
//
|
|
7
|
+
// The seam is `setRect` / `present` / `destroy`, the same three methods the
|
|
8
|
+
// Cocoa host implements, and the difference between the two backends lives
|
|
9
|
+
// entirely inside `present`. There a Cocoa host is told a **different**
|
|
10
|
+
// IOSurface every frame and points its layer at each one; here it is told
|
|
11
|
+
// the **same** composition surface handle every time, because the pane's
|
|
12
|
+
// swapchain flips inside that one handle. So this one attaches once and
|
|
13
|
+
// recognises the handle ever after — after which the pane's frames arrive
|
|
14
|
+
// with nothing at all crossing the process boundary.
|
|
15
|
+
export class Win32PaneHost {
|
|
16
|
+
constructor(app, wnd) {
|
|
17
|
+
this.app = app;
|
|
18
|
+
this.wnd = wnd;
|
|
19
|
+
this._native = app._native;
|
|
20
|
+
this.destroyed = false;
|
|
21
|
+
/** The bridge's id for the attached visual; 0 until the first present,
|
|
22
|
+
* because there is no buffer to attach before the pane names one. */
|
|
23
|
+
this._view = 0;
|
|
24
|
+
/** The handle that view was opened from, so the same one arriving again
|
|
25
|
+
* is recognised rather than re-opened. */
|
|
26
|
+
this._handle = 0;
|
|
27
|
+
this._rect = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Where the pane goes, in the host window's device pixels — the node's
|
|
32
|
+
* `abs`. The size is a **clip**, not a scale: the pane draws at whatever
|
|
33
|
+
* size it was last told over the channel, and the two disagree for the
|
|
34
|
+
* length of a round trip whenever the box changes. Bounding it here is
|
|
35
|
+
* what keeps a pane that has not caught up from painting over its
|
|
36
|
+
* neighbours (`paneSetRect`, windows/src/pane.cc).
|
|
37
|
+
*/
|
|
38
|
+
setRect(rect) {
|
|
39
|
+
if (this.destroyed || !rect) return;
|
|
40
|
+
const prev = this._rect;
|
|
41
|
+
if (
|
|
42
|
+
prev &&
|
|
43
|
+
prev.x === rect.x &&
|
|
44
|
+
prev.y === rect.y &&
|
|
45
|
+
prev.width === rect.width &&
|
|
46
|
+
prev.height === rect.height
|
|
47
|
+
) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this._rect = { ...rect };
|
|
51
|
+
this._place();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_place() {
|
|
55
|
+
const rect = this._rect;
|
|
56
|
+
if (!this._view || !rect) return;
|
|
57
|
+
this._native.paneSetRect(
|
|
58
|
+
this._view,
|
|
59
|
+
Math.round(rect.x),
|
|
60
|
+
Math.round(rect.y),
|
|
61
|
+
Math.max(0, Math.round(rect.width)),
|
|
62
|
+
Math.max(0, Math.round(rect.height)),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The pane named the buffer its frames are in.
|
|
68
|
+
*
|
|
69
|
+
* On the first one this opens the handle and hangs it in the window's
|
|
70
|
+
* visual tree; on every one after it there is nothing to do, and saying so
|
|
71
|
+
* quickly is the point — the compositor is already scanning out of
|
|
72
|
+
* whatever the pane last presented, so a frame reaches the screen without
|
|
73
|
+
* this method being called at all.
|
|
74
|
+
*
|
|
75
|
+
* A *different* handle means a different pane behind the same host, which
|
|
76
|
+
* is what a restart looks like from here: drop the old view and open the
|
|
77
|
+
* new one, rather than leaving the window showing a buffer whose process
|
|
78
|
+
* has gone.
|
|
79
|
+
*/
|
|
80
|
+
present(handle) {
|
|
81
|
+
if (this.destroyed || !handle) return;
|
|
82
|
+
if (handle === this._handle && this._view) return;
|
|
83
|
+
if (this._view) {
|
|
84
|
+
this._native.paneDetach(this._view);
|
|
85
|
+
this._view = 0;
|
|
86
|
+
}
|
|
87
|
+
const view = this._native.paneAttach(this.wnd.id, handle);
|
|
88
|
+
if (!view) {
|
|
89
|
+
// Nothing was shown and nothing was lost; the pane will say so again
|
|
90
|
+
// with its next pane-rect, and a host window not yet composed is the
|
|
91
|
+
// one way this happens.
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
this._handle = handle;
|
|
95
|
+
this._view = view;
|
|
96
|
+
this._place();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
destroy() {
|
|
100
|
+
if (this.destroyed) return;
|
|
101
|
+
this.destroyed = true;
|
|
102
|
+
if (this._view) this._native.paneDetach(this._view);
|
|
103
|
+
this._view = 0;
|
|
104
|
+
this._handle = 0;
|
|
105
|
+
}
|
|
106
|
+
}
|