react-x11 2.16.0 → 2.17.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/README.md +38 -23
- package/package.json +3 -1
- package/src/Reconciler.js +82 -23
- package/src/a11y.js +18 -1
- package/src/acceleratorhooks.js +40 -6
- package/src/anchor.js +20 -2
- 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 +204 -6
- package/src/cocoa/fonts.js +1 -1
- 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 +2 -2
- package/src/events.js +21 -0
- package/src/foreignnodes.js +8 -3
- package/src/frame/index.js +30 -4
- package/src/glnodes.js +12 -1
- package/src/idle.js +59 -1
- package/src/index.d.ts +51 -1
- package/src/index.js +30 -3
- package/src/keysymchars.js +47 -0
- package/src/keysyms.d.ts +19 -1
- package/src/keysyms.js +107 -8
- 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/screens.js +159 -24
- 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 +21 -0
- package/src/types/filedialog.d.ts +3 -1
- 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/wayland/xkb.js +170 -59
- 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 +128 -20
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
// An ntk-window-shaped object over an HWND — the contract WindowNode realizes
|
|
2
|
+
// against (src/testing/mock-app.js is the reference shape; this file is that
|
|
3
|
+
// shape with DirectComposition behind it).
|
|
4
|
+
//
|
|
5
|
+
// Units: everything crossing this object's boundary is device pixels, like an
|
|
6
|
+
// X window. The window is per-monitor-v2 on the bridge's UI thread, so the
|
|
7
|
+
// HWND's own client area is already in device pixels and there is no divide.
|
|
8
|
+
//
|
|
9
|
+
// The frame is taken through `presentFrame` rather than through `getContext` +
|
|
10
|
+
// `present`, and that is the one structural difference from the Cocoa window.
|
|
11
|
+
// A DirectComposition surface is not a persistent bitmap that gets flipped: it
|
|
12
|
+
// hands back a drawing context per `BeginDraw`, valid until `EndDraw`, and
|
|
13
|
+
// every pixel inside the rect is repainted while every pixel outside it is
|
|
14
|
+
// kept. So the frame has to be *driven* from the damage list rather than
|
|
15
|
+
// painted into a surface that was already there — which `presentFrame` is
|
|
16
|
+
// exactly the hook for, and which makes one BeginDraw per damage rect fall out
|
|
17
|
+
// for free.
|
|
18
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
19
|
+
import { Win32DropTransport, dragSpec } from './dnd.js';
|
|
20
|
+
|
|
21
|
+
// REACT_X11_WIN32_DEBUG=1 reports every frame and what it drew into. A window
|
|
22
|
+
// that stays blank on this backend has a short list of causes — no frame
|
|
23
|
+
// asked for, a BeginDraw refused, a damage rect outside the surface — and they
|
|
24
|
+
// are indistinguishable from the outside, because none of them throws.
|
|
25
|
+
const DEBUG = process.env.REACT_X11_WIN32_DEBUG === '1';
|
|
26
|
+
|
|
27
|
+
export class Win32Window {
|
|
28
|
+
constructor(app, attributes = {}) {
|
|
29
|
+
this.app = app;
|
|
30
|
+
this._native = app._native;
|
|
31
|
+
this.attributes = attributes;
|
|
32
|
+
|
|
33
|
+
this.width = Math.max(1, Math.round(attributes.width ?? 800));
|
|
34
|
+
this.height = Math.max(1, Math.round(attributes.height ?? 600));
|
|
35
|
+
this.x = attributes.x ?? 0;
|
|
36
|
+
this.y = attributes.y ?? 0;
|
|
37
|
+
this.title = attributes.title ?? '';
|
|
38
|
+
this.mapped = false;
|
|
39
|
+
this.destroyed = false;
|
|
40
|
+
this.parent = null;
|
|
41
|
+
this.cursor = null;
|
|
42
|
+
|
|
43
|
+
// The handle the verb table draws through, non-zero only inside a
|
|
44
|
+
// BeginDraw/EndDraw pair, and the generation that tells BackendContext2D
|
|
45
|
+
// its sticky state has to be pushed into a fresh one.
|
|
46
|
+
this._surface = 0;
|
|
47
|
+
this._gen = 0;
|
|
48
|
+
this._ctx = null;
|
|
49
|
+
|
|
50
|
+
this._handlers = {};
|
|
51
|
+
this._ready = false;
|
|
52
|
+
this._composed = false;
|
|
53
|
+
this._pendingFrames = [];
|
|
54
|
+
this._owesFullPaint = false;
|
|
55
|
+
this._dirty = false;
|
|
56
|
+
|
|
57
|
+
// `overrideRedirect` is how a `<popup>` says what it is — a menu, a
|
|
58
|
+
// select's list, a tooltip. On X11 it means "the window manager does not
|
|
59
|
+
// manage this"; here it means WS_POPUP: no frame, no taskbar button, and
|
|
60
|
+
// no stealing activation from the window it belongs to.
|
|
61
|
+
this.popup = attributes.overrideRedirect === true;
|
|
62
|
+
|
|
63
|
+
this.id = this._native.createWindow({
|
|
64
|
+
title: this.title,
|
|
65
|
+
width: this.width,
|
|
66
|
+
height: this.height,
|
|
67
|
+
// A popup is placed by anchor.js against the monitor's work area, and
|
|
68
|
+
// that placement *is* the contract — a menu created at the default
|
|
69
|
+
// position is a menu in the wrong place.
|
|
70
|
+
x: attributes.x,
|
|
71
|
+
y: attributes.y,
|
|
72
|
+
popup: this.popup,
|
|
73
|
+
// A shaped window needs a surface with an alpha channel, or the pixels
|
|
74
|
+
// outside its shape composite as a dark fringe — which on a rounded
|
|
75
|
+
// popup is a dark edge along every corner.
|
|
76
|
+
transparent: attributes.transparent === true,
|
|
77
|
+
// A `<popup dragPreview>` follows the pointer, so it is **under** the
|
|
78
|
+
// pointer for the whole gesture — and the window under the pointer is
|
|
79
|
+
// the one the shell asks about when it looks for somewhere to drop.
|
|
80
|
+
// Unanswered, the preview answers for itself, and it is not a drop
|
|
81
|
+
// target: the list underneath never sees the drop. Cocoa spells the
|
|
82
|
+
// same thing `ignoresMouseEvents` (src/cocoa/window.js).
|
|
83
|
+
clickThrough: attributes.dragPreview === true,
|
|
84
|
+
});
|
|
85
|
+
app._register(this);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- events --------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
on(name, fn) {
|
|
91
|
+
(this._handlers[name] ??= []).push(fn);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The other half of `on`. Without it a component that subscribes for as
|
|
95
|
+
* long as it is mounted has no way to stop, and the handler outlives it. */
|
|
96
|
+
off(name, fn) {
|
|
97
|
+
const list = this._handlers[name];
|
|
98
|
+
if (!list) return;
|
|
99
|
+
const at = list.indexOf(fn);
|
|
100
|
+
if (at >= 0) list.splice(at, 1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
emit(name, ev) {
|
|
104
|
+
for (const fn of this._handlers[name] ?? []) fn(ev);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The bridge says the HWND exists. Composition is set up here and not at
|
|
109
|
+
* construction because there is no HWND to target until now — the command
|
|
110
|
+
* queue is one way and a window is created asynchronously.
|
|
111
|
+
*
|
|
112
|
+
* Then `draw`, which is the frame clock's "the backing store is invalid,
|
|
113
|
+
* repaint everything" and is what the window node listens to. It is not
|
|
114
|
+
* optional: the tree mounts, lays out and paints in the same turn the window
|
|
115
|
+
* is asked for, so the first frames land before there is anything to paint
|
|
116
|
+
* into and are dropped. Without this the window stays whatever
|
|
117
|
+
* WS_EX_NOREDIRECTIONBITMAP shows when nothing was ever committed, which is
|
|
118
|
+
* black, and nothing else would ever ask again.
|
|
119
|
+
*/
|
|
120
|
+
_onReady(originX, originY) {
|
|
121
|
+
this._ready = true;
|
|
122
|
+
// The HWND exists now, which is the first moment a drop target can be
|
|
123
|
+
// registered on it — the tree mounted its `dropAccept`s before this.
|
|
124
|
+
this._dropTransport?.reattach();
|
|
125
|
+
// And the first moment there is a window for the accessibility mirror to
|
|
126
|
+
// be keyed by. The tree mounted and committed before this, so without
|
|
127
|
+
// this the first push would have had nowhere to go and a screen reader
|
|
128
|
+
// attaching to an idle application would find an empty window.
|
|
129
|
+
this.app._a11y?.windowReady(this);
|
|
130
|
+
// The identity goes on **before `show`** below: the taskbar reads the
|
|
131
|
+
// window's AppUserModelID when it makes the button, so an id arriving
|
|
132
|
+
// afterwards leaves that button grouped where it already was.
|
|
133
|
+
if (this.attributes?.appId != null) this.setClass(this.attributes.appId);
|
|
134
|
+
if (Number.isFinite(originX)) this._noteOrigin(originX, originY);
|
|
135
|
+
if (!this._composed) {
|
|
136
|
+
this._native.compose(this.id);
|
|
137
|
+
this._composed = true;
|
|
138
|
+
}
|
|
139
|
+
if (this.mapped) this._native.show(this.id, true);
|
|
140
|
+
this.emit('draw', {});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- geometry ------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Where the client area is on the virtual screen. `anchor.js` reads
|
|
147
|
+
* `_screenOrigin` to turn a node's rect into the screen rect a `<popup>` is
|
|
148
|
+
* placed against, and falls back to `x`/`y` — so a window that never reports
|
|
149
|
+
* its position anchors every menu as though it were at the screen's origin,
|
|
150
|
+
* which puts a select's list a whole window-offset away from the select.
|
|
151
|
+
*/
|
|
152
|
+
_noteOrigin(x, y) {
|
|
153
|
+
this.x = x;
|
|
154
|
+
this.y = y;
|
|
155
|
+
this._screenOrigin = { x, y };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
map() {
|
|
159
|
+
this.mapped = true;
|
|
160
|
+
if (this._ready) this._native.show(this.id, true);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
unmap() {
|
|
164
|
+
this.mapped = false;
|
|
165
|
+
if (this._ready) this._native.show(this.id, false);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
destroy() {
|
|
169
|
+
if (this.destroyed) return;
|
|
170
|
+
this.destroyed = true;
|
|
171
|
+
this._native.destroyWindow?.(this.id);
|
|
172
|
+
this.app._unregister(this);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
resize(width, height) {
|
|
176
|
+
this.width = Math.max(1, Math.round(width));
|
|
177
|
+
this.height = Math.max(1, Math.round(height));
|
|
178
|
+
this._native.resizeWindow?.(this.id, this.width, this.height);
|
|
179
|
+
// Unconditionally, not only once composed: an auto-sized window is
|
|
180
|
+
// measured and resized before its HWND exists, and the surface compose()
|
|
181
|
+
// makes later is made at whatever size the bridge last recorded.
|
|
182
|
+
this._native.resize(this.id, this.width, this.height);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
move(x, y) {
|
|
186
|
+
this.x = x;
|
|
187
|
+
this.y = y;
|
|
188
|
+
this._native.moveWindow?.(this.id, x, y);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
setTitle(title) {
|
|
192
|
+
this.title = title;
|
|
193
|
+
this._native.setTitle?.(this.id, String(title ?? ''));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The size limits Windows asks for synchronously in WM_GETMINMAXINFO, so
|
|
197
|
+
* they are pushed ahead rather than answered on demand — docs/windows.md
|
|
198
|
+
* "What Windows asks synchronously, and JS can know in advance". */
|
|
199
|
+
setSizeHints(hints = {}) {
|
|
200
|
+
this._native.setSizeHints?.(this.id, hints);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `<window appId>`: the AppUserModelID, which is what Windows means by
|
|
205
|
+
* "which application is this" — the taskbar groups buttons by it, pinning
|
|
206
|
+
* pins it, and a jump list belongs to it.
|
|
207
|
+
*
|
|
208
|
+
* X11 carries an instance and a class; this takes the **class**, the half
|
|
209
|
+
* that names the application rather than the window, which is the same
|
|
210
|
+
* choice `windowAttributes` makes for every single-id backend
|
|
211
|
+
* (src/nodes/window/hints.js).
|
|
212
|
+
*
|
|
213
|
+
* Set per window rather than per process:
|
|
214
|
+
* `SetCurrentProcessExplicitAppUserModelID` has to be called before the
|
|
215
|
+
* process creates any UI, which a library cannot promise of an embedder. A
|
|
216
|
+
* window's own id overrides the process's anyway, so this is both the more
|
|
217
|
+
* flexible form and the only one that can be guaranteed.
|
|
218
|
+
*/
|
|
219
|
+
setClass(instance, className) {
|
|
220
|
+
const id = className ?? instance;
|
|
221
|
+
this._native.windowAppId?.(this.id, id == null ? null : String(id));
|
|
222
|
+
this._setRelaunch(id == null ? null : String(id));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What a **pinned** tile starts, and what it is called while pinned.
|
|
227
|
+
*
|
|
228
|
+
* An id on its own is half the story. It makes the taskbar group this
|
|
229
|
+
* window under an identity of its own — and then a user who pins that
|
|
230
|
+
* button gets a shortcut to whatever the shell can work out by itself,
|
|
231
|
+
* which for `node app.js` is node.exe, under node's name and icon. The
|
|
232
|
+
* relaunch properties are the other half, and Microsoft's guidance is that
|
|
233
|
+
* an application setting the id sets these too.
|
|
234
|
+
*
|
|
235
|
+
* Derived rather than asked for, because every part of it is already known
|
|
236
|
+
* and a second Windows-only prop to make the first one work is a bad trade:
|
|
237
|
+
*
|
|
238
|
+
* - **the command** is the one that started this process, quoted — argv as
|
|
239
|
+
* it was, so the relaunch is the launch;
|
|
240
|
+
* - **the name** is the window's title, falling back to the id. It is what
|
|
241
|
+
* the pin menu and the button's tooltip show;
|
|
242
|
+
* - **the icon** is the executable's own, which is what the shell would
|
|
243
|
+
* have used anyway — named explicitly so the pinned tile keeps it rather
|
|
244
|
+
* than resolving it again from a shortcut that may not exist.
|
|
245
|
+
*
|
|
246
|
+
* Clearing the id clears all three: a window with no identity of its own
|
|
247
|
+
* should not keep claiming how to relaunch one.
|
|
248
|
+
*/
|
|
249
|
+
_setRelaunch(id) {
|
|
250
|
+
if (typeof this._native.windowRelaunch !== 'function') return;
|
|
251
|
+
if (id == null) {
|
|
252
|
+
this._native.windowRelaunch(this.id, {
|
|
253
|
+
command: null,
|
|
254
|
+
displayName: null,
|
|
255
|
+
icon: null,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const quote = (arg) =>
|
|
260
|
+
/[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
|
|
261
|
+
const argv = process.argv.slice(1).map(quote).join(' ');
|
|
262
|
+
this._native.windowRelaunch(this.id, {
|
|
263
|
+
command: `${quote(process.execPath)}${argv ? ` ${argv}` : ''}`,
|
|
264
|
+
displayName: this.title || id,
|
|
265
|
+
icon: `${process.execPath},0`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Still X11's names, still accepted and dropped: each is optional
|
|
270
|
+
// decoration, and an app that sets one should not fail to open a window.
|
|
271
|
+
setWindowType() {}
|
|
272
|
+
setActions() {}
|
|
273
|
+
setTransientFor() {}
|
|
274
|
+
setAlwaysOnTop(on = true) {
|
|
275
|
+
this._native.windowState(this.id, 'above', on !== false);
|
|
276
|
+
}
|
|
277
|
+
setProperty() {
|
|
278
|
+
return Promise.resolve(this);
|
|
279
|
+
}
|
|
280
|
+
deleteProperty() {
|
|
281
|
+
return Promise.resolve(this);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* One of `<window>`'s EWMH state names, applied with the Windows call that
|
|
286
|
+
* means it: `ShowWindow` for maximized and minimized, a remembered frame
|
|
287
|
+
* and the monitor's rect for fullscreen, `SetWindowPos` for `above`.
|
|
288
|
+
*
|
|
289
|
+
* Resolves false for a name this platform has no answer for, which is what
|
|
290
|
+
* `useWindowState()` reads to know the request went nowhere — never for a
|
|
291
|
+
* call that was made and did not take. `focused` is the one that can be
|
|
292
|
+
* refused after the fact: the shell will not let a background process take
|
|
293
|
+
* the foreground, and that refusal is the documented behaviour rather than
|
|
294
|
+
* a failure, so it reports true and the state simply does not change.
|
|
295
|
+
*/
|
|
296
|
+
setWmState(names, action = 'add') {
|
|
297
|
+
// A name or a list of them, which is the contract the other backends
|
|
298
|
+
// keep (src/cocoa/window.js) even though the renderer's own caller sends
|
|
299
|
+
// them one at a time.
|
|
300
|
+
const list = Array.isArray(names) ? names : [names];
|
|
301
|
+
// A bridge too old to have the verb answers false for every name rather
|
|
302
|
+
// than throwing: `useWindowState()` reads that false to know the request
|
|
303
|
+
// went nowhere, and a throw from a state a window never had is not a
|
|
304
|
+
// failure an application can do anything with.
|
|
305
|
+
if (typeof this._native.windowState !== 'function') {
|
|
306
|
+
return Promise.resolve(false);
|
|
307
|
+
}
|
|
308
|
+
const held = action === 'toggle' ? new Set(this.getWmStatesNow()) : null;
|
|
309
|
+
let honoured = true;
|
|
310
|
+
for (const name of list) {
|
|
311
|
+
const on = held ? !held.has(name) : action !== 'remove';
|
|
312
|
+
if (!this._native.windowState(this.id, name, on)) honoured = false;
|
|
313
|
+
}
|
|
314
|
+
return Promise.resolve(honoured);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The live states, synchronously — what a toggle has to read first. */
|
|
318
|
+
getWmStatesNow() {
|
|
319
|
+
if (typeof this._native.windowStates !== 'function') return [];
|
|
320
|
+
return this._native.windowStates(this.id) ?? [];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
getWmStates() {
|
|
324
|
+
return Promise.resolve(this.getWmStatesNow());
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// --- drag and drop -------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The window's drop side, which the tree installs when it first mounts a
|
|
331
|
+
* `dropAccept` under this window (src/nodes/window/droptarget.js). Its
|
|
332
|
+
* presence is what tells the tree this backend has drop machinery of its
|
|
333
|
+
* own and no XDND property to write.
|
|
334
|
+
*/
|
|
335
|
+
attachDropTransport(session, node) {
|
|
336
|
+
this._dropTransport = new Win32DropTransport(this, session, node);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** A `dropAccept` came or went under this window. The shell registers no
|
|
340
|
+
* types, so all this decides is whether the window is a target at all. */
|
|
341
|
+
dropTargetsChanged() {
|
|
342
|
+
this._dropTransport?.refreshTypes();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Backend events for the drag in progress, routed by the app. */
|
|
346
|
+
_routeDrag(event) {
|
|
347
|
+
this._dropTransport?.handle(event);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The source side: hand a `DragSession`'s gesture to the shell. Returns at
|
|
352
|
+
* once — `DoDragDrop` runs its modal loop on the bridge's UI thread, and
|
|
353
|
+
* the gesture reports back as `drag-session-moved` and
|
|
354
|
+
* `drag-session-ended`.
|
|
355
|
+
*
|
|
356
|
+
* And nothing stops here. That is the whole point of the thread split: the
|
|
357
|
+
* frame clock keeps ticking, React keeps committing and a `<popup
|
|
358
|
+
* dragPreview>` mounted by `onDragStart` is painted while the shell owns
|
|
359
|
+
* the pointer — where the cocoa backend's pump does not return until the
|
|
360
|
+
* drop.
|
|
361
|
+
*/
|
|
362
|
+
beginDrag(session) {
|
|
363
|
+
if (this.destroyed) return null;
|
|
364
|
+
return this._native.beginDrag(this.id, dragSpec(session));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
setCursor(name) {
|
|
368
|
+
this.cursor = name;
|
|
369
|
+
this._native.setCursor?.(this.id, name);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* What `<popup grab>` asks for, by the effect rather than the mechanism.
|
|
374
|
+
*
|
|
375
|
+
* There is no cross-application pointer grab on Windows: `SetCapture` sends
|
|
376
|
+
* a window the mouse only while a button is already down, so a *press* that
|
|
377
|
+
* starts over another window never arrives here. What the grab is actually
|
|
378
|
+
* for is one thing — "tell me when the user pressed somewhere else, so the
|
|
379
|
+
* menu can close" — and that is observable without it:
|
|
380
|
+
*
|
|
381
|
+
* - a press delivered to **another window of this application**, which is
|
|
382
|
+
* a click in the owner behind the menu, or in a second window, or in a
|
|
383
|
+
* menu this one opened from;
|
|
384
|
+
* - this application **losing activation**, which is a press in another
|
|
385
|
+
* application or on the desktop. A popup is `WS_EX_NOACTIVATE`, so it
|
|
386
|
+
* never takes activation itself and opening one raises no blur —
|
|
387
|
+
* measured, because the whole rule rests on it.
|
|
388
|
+
*
|
|
389
|
+
* The app watches both and answers with the press the tree expects
|
|
390
|
+
* (src/win32/app.js `_dismissOutsidePopups`), the way the Wayland backend
|
|
391
|
+
* answers `xdg_popup.popup_done` with one (wayland/backendwindow.js).
|
|
392
|
+
*
|
|
393
|
+
* **What it does not catch**: a press on the *non-client* area of one of our
|
|
394
|
+
* own windows — a title bar, a resize border. The bridge does not report
|
|
395
|
+
* those, so a menu left open while the user drags the window behind it stays
|
|
396
|
+
* open. X11's grab covers that case and this does not; it is the one gap,
|
|
397
|
+
* and it is narrower than the one it replaces.
|
|
398
|
+
*
|
|
399
|
+
* The callback reports success because the behaviour it stands for is here
|
|
400
|
+
* now. It used to say the same thing while nothing was watching, which is
|
|
401
|
+
* the worst of both: a caller that checked was told a grab it did not have
|
|
402
|
+
* had been taken.
|
|
403
|
+
*/
|
|
404
|
+
grabPointer(options, cb) {
|
|
405
|
+
this.app._dismissOnOutside?.add(this);
|
|
406
|
+
cb?.(null, 0);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
ungrabPointer() {
|
|
410
|
+
this.app._dismissOnOutside?.delete(this);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* A press that landed outside this window, as the tree hears it.
|
|
415
|
+
*
|
|
416
|
+
* Negative coordinates are what make it a dismissal rather than a click:
|
|
417
|
+
* `_pressOutside` in src/events.js compares against the window's own bounds,
|
|
418
|
+
* and answers anything outside them with `onDismiss`. The same made-up press
|
|
419
|
+
* the Wayland backend sends for `popup_done`, for the same reason — the
|
|
420
|
+
* platform kept the real one.
|
|
421
|
+
*/
|
|
422
|
+
_dismissFromOutside() {
|
|
423
|
+
if (this.destroyed) return;
|
|
424
|
+
this.emit('mousedown', {
|
|
425
|
+
x: -1,
|
|
426
|
+
y: -1,
|
|
427
|
+
rootx: -1,
|
|
428
|
+
rooty: -1,
|
|
429
|
+
keycode: 1,
|
|
430
|
+
buttons: 0,
|
|
431
|
+
dismissed: true,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// The keyboard's half has no Windows mechanism at all and nothing in the
|
|
436
|
+
// tree reads its result, so it stays honest about doing nothing: a popup
|
|
437
|
+
// that asked for keys gets them only while it is the foreground window.
|
|
438
|
+
grabKeyboard(options, cb) {
|
|
439
|
+
cb?.(null, 0);
|
|
440
|
+
}
|
|
441
|
+
ungrabKeyboard() {}
|
|
442
|
+
|
|
443
|
+
selectXI2() {
|
|
444
|
+
return Promise.resolve(false);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// --- painting ------------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
getContext() {
|
|
450
|
+
if (this._ctx) return this._ctx;
|
|
451
|
+
this._ctx = new BackendContext2D(
|
|
452
|
+
this._native,
|
|
453
|
+
() => this._surface,
|
|
454
|
+
() => this._gen,
|
|
455
|
+
);
|
|
456
|
+
// Reading a *window* is not reading a surface. The surface handle the
|
|
457
|
+
// context draws through belongs to one BeginDraw and is write-only
|
|
458
|
+
// anyway; what a caller asking a window for its pixels means is "what is
|
|
459
|
+
// on screen", and only DWM has that (`windowPixels`, src/win32.cc).
|
|
460
|
+
//
|
|
461
|
+
// The callback form is the contract — the X backend's read is a round
|
|
462
|
+
// trip and every caller is written for one (examples/configurator reads
|
|
463
|
+
// its own UI this way to put it on the laptop's screen). Here the answer
|
|
464
|
+
// is already in hand, so it is delivered on a microtask rather than
|
|
465
|
+
// pretended to be slower than it is.
|
|
466
|
+
this._ctx.getImageData = (x, y, width, height, cb) => {
|
|
467
|
+
const read = () => {
|
|
468
|
+
const w = Math.round(width);
|
|
469
|
+
const h = Math.round(height);
|
|
470
|
+
const bytes = this._native.windowPixels(
|
|
471
|
+
this.id,
|
|
472
|
+
Math.round(x),
|
|
473
|
+
Math.round(y),
|
|
474
|
+
w,
|
|
475
|
+
h,
|
|
476
|
+
);
|
|
477
|
+
if (!bytes) throw new Error('react-x11: the window could not be read');
|
|
478
|
+
if (DEBUG) {
|
|
479
|
+
let lit = 0;
|
|
480
|
+
for (let i = 0; i < bytes.length; i += 4) {
|
|
481
|
+
if (bytes[i] || bytes[i + 1] || bytes[i + 2]) lit++;
|
|
482
|
+
}
|
|
483
|
+
console.error(
|
|
484
|
+
`[win32] getImageData ${x},${y} ${w}x${h} -> ${lit}/${bytes.length / 4} lit`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
data: new Uint8ClampedArray(
|
|
489
|
+
bytes.buffer,
|
|
490
|
+
bytes.byteOffset,
|
|
491
|
+
bytes.length,
|
|
492
|
+
),
|
|
493
|
+
width: w,
|
|
494
|
+
height: h,
|
|
495
|
+
};
|
|
496
|
+
};
|
|
497
|
+
if (typeof cb !== 'function') return read();
|
|
498
|
+
let result;
|
|
499
|
+
let failure;
|
|
500
|
+
try {
|
|
501
|
+
result = read();
|
|
502
|
+
} catch (err) {
|
|
503
|
+
failure = err;
|
|
504
|
+
}
|
|
505
|
+
queueMicrotask(() => cb(failure ?? null, result));
|
|
506
|
+
return undefined;
|
|
507
|
+
};
|
|
508
|
+
return this._ctx;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* The frame. One `BeginDraw` per damage rect, which is the X11 damage model
|
|
513
|
+
* verbatim — every pixel inside the rect is repainted, every pixel outside
|
|
514
|
+
* it is kept by DirectComposition — and one `Commit` for the lot, which is
|
|
515
|
+
* the atomic frame the node model relies on.
|
|
516
|
+
*
|
|
517
|
+
* The generation is bumped per rect because each BeginDraw hands back a
|
|
518
|
+
* *different* Direct2D context with none of the previous one's state: the
|
|
519
|
+
* context wrapper reads the bump and pushes its sticky state back in.
|
|
520
|
+
*/
|
|
521
|
+
presentFrame(node, damage) {
|
|
522
|
+
if (DEBUG) {
|
|
523
|
+
console.error(
|
|
524
|
+
`[win32] presentFrame window=${this.id} composed=${this._composed} ` +
|
|
525
|
+
`size=${this.width}x${this.height} rects=${damage ? damage.length : 'full'}` +
|
|
526
|
+
(this._owesFullPaint ? ' (owes full)' : ''),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
if (this.destroyed) return;
|
|
530
|
+
if (!this._composed) {
|
|
531
|
+
// Nothing to paint into yet. The frame is not merely skipped: what it
|
|
532
|
+
// would have covered is remembered, because the damage it carried is
|
|
533
|
+
// gone once this returns and the next frame's bound is whatever has been
|
|
534
|
+
// claimed since — which, on a tree with an animation in it, is a handful
|
|
535
|
+
// of small rects. That is exactly how this window came up transparent
|
|
536
|
+
// with a perfectly healthy-looking frame log.
|
|
537
|
+
this._owesFullPaint = true;
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const ctx = this.getContext();
|
|
541
|
+
// REACT_X11_WIN32_FULL_REPAINT=1 throws the damage away and paints the
|
|
542
|
+
// whole window every frame. Slow on purpose: it is the control for
|
|
543
|
+
// "is this a damage bug", which no amount of reading the rects settles.
|
|
544
|
+
if (process.env.REACT_X11_WIN32_FULL_REPAINT === '1') damage = null;
|
|
545
|
+
if (this._owesFullPaint) {
|
|
546
|
+
this._owesFullPaint = false;
|
|
547
|
+
damage = null;
|
|
548
|
+
}
|
|
549
|
+
const rects = damage ?? [null];
|
|
550
|
+
let painted = false;
|
|
551
|
+
for (const rect of rects) {
|
|
552
|
+
const r = rect ?? { x: 0, y: 0, width: this.width, height: this.height };
|
|
553
|
+
const w = Math.min(this.width - Math.max(0, r.x), Math.ceil(r.width));
|
|
554
|
+
const h = Math.min(this.height - Math.max(0, r.y), Math.ceil(r.height));
|
|
555
|
+
if (!(w > 0 && h > 0)) continue;
|
|
556
|
+
const handle = this._native.beginDraw(
|
|
557
|
+
this.id,
|
|
558
|
+
Math.max(0, Math.floor(r.x)),
|
|
559
|
+
Math.max(0, Math.floor(r.y)),
|
|
560
|
+
w,
|
|
561
|
+
h,
|
|
562
|
+
);
|
|
563
|
+
if (DEBUG) {
|
|
564
|
+
console.error(
|
|
565
|
+
`[win32] rect ${Math.floor(r.x)},${Math.floor(r.y)} ${w}x${h} ` +
|
|
566
|
+
`-> surface ${handle}`,
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
if (!handle) continue;
|
|
570
|
+
this._surface = handle;
|
|
571
|
+
this._gen++;
|
|
572
|
+
try {
|
|
573
|
+
node._paintRegion(ctx, rect, this.width, this.height);
|
|
574
|
+
painted = true;
|
|
575
|
+
} finally {
|
|
576
|
+
this._surface = 0;
|
|
577
|
+
this._native.endDraw(this.id);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (painted) this._native.commit();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The scroll-blit fast path: `IDCompositionSurface::Scroll`, which moves the
|
|
585
|
+
* surviving band inside the surface on the GPU. The exposed strip is
|
|
586
|
+
* repainted by the frame's own damage, exactly as on X11.
|
|
587
|
+
*/
|
|
588
|
+
scrollRegion(rect, dx, dy) {
|
|
589
|
+
if (!this._composed || this.destroyed) return false;
|
|
590
|
+
if (DEBUG) {
|
|
591
|
+
console.error(
|
|
592
|
+
`[win32] scrollRegion ${Math.round(rect.x)},${Math.round(rect.y)} ` +
|
|
593
|
+
`${Math.round(rect.width)}x${Math.round(rect.height)} by ${dx},${dy}`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return this._native.scrollRegion(
|
|
597
|
+
this.id,
|
|
598
|
+
Math.round(rect.x),
|
|
599
|
+
Math.round(rect.y),
|
|
600
|
+
Math.round(rect.width),
|
|
601
|
+
Math.round(rect.height),
|
|
602
|
+
Math.round(dx),
|
|
603
|
+
Math.round(dy),
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** DirectComposition does not hold a frame back the way an X server's fence
|
|
608
|
+
* or a WindowServer's buffer does: Commit is asynchronous and the surface
|
|
609
|
+
* retains its own pixels. So there is never a frame in flight to wait for. */
|
|
610
|
+
frameInFlight() {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
requestAnimationFrame(cb) {
|
|
615
|
+
return this.app._requestFrame(cb, this);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
present() {
|
|
619
|
+
// Nothing to flip: presentFrame committed. Kept because the frame loop
|
|
620
|
+
// calls it on every window it paced.
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** A DirectComposition surface is write-only, so a window's pixels are not
|
|
624
|
+
* readable the way an IOSurface's or an X drawable's are. docs/windows.md
|
|
625
|
+
* answers this with PrintWindow once the commit has completed; not bound
|
|
626
|
+
* yet, and it says so rather than answering with something wrong. */
|
|
627
|
+
/**
|
|
628
|
+
* The window's pixels, as RGBA. PrintWindow is the door DWM opens on a
|
|
629
|
+
* composed window — see `windowPixels` in src/win32.cc for which flags are
|
|
630
|
+
* the ones that work and what the others answer instead.
|
|
631
|
+
*/
|
|
632
|
+
snapshot() {
|
|
633
|
+
const width = Math.round(this.width);
|
|
634
|
+
const height = Math.round(this.height);
|
|
635
|
+
const bytes = this._native.windowPixels(this.id, 0, 0, width, height);
|
|
636
|
+
if (!bytes) {
|
|
637
|
+
return Promise.reject(
|
|
638
|
+
new Error(
|
|
639
|
+
'react-x11: the window could not be read — PrintWindow refused it. ' +
|
|
640
|
+
'A window that is minimised or not yet composed has nothing to ' +
|
|
641
|
+
'read; draw into an offscreen surface and read that instead.',
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
return Promise.resolve({
|
|
646
|
+
data: new Uint8ClampedArray(bytes.buffer, bytes.byteOffset, bytes.length),
|
|
647
|
+
width,
|
|
648
|
+
height,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** How many damage rects a frame may carry before it collapses to their
|
|
653
|
+
* box. Each is a BeginDraw with a fixed cost, so the right number is a fact
|
|
654
|
+
* about DirectComposition rather than a choice — 16 is Cocoa's, kept until
|
|
655
|
+
* it is measured here. */
|
|
656
|
+
get damageRectCap() {
|
|
657
|
+
return 16;
|
|
658
|
+
}
|
|
659
|
+
}
|