react-x11 2.18.3 → 2.18.5
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/package.json +1 -1
- package/src/Reconciler.js +8 -1
- package/src/appcontext.js +10 -5
- package/src/backend/context2d.js +65 -18
- package/src/glnodes.js +34 -3
- package/src/globalmenu.js +104 -8
- package/src/gloverlay.js +118 -31
- package/src/index.d.ts +4 -1
- package/src/nodes/invalidate.js +10 -2
- package/src/nodes/node.js +15 -3
- package/src/nodes/scrollblit.js +12 -2
- package/src/nodes/window/flush.js +20 -3
- package/src/statusnotifier.js +49 -7
- package/src/svgnodes.js +76 -15
- package/src/types/elements.d.ts +6 -5
- package/src/win32/a11y.js +66 -6
- package/src/win32/app.js +83 -9
- package/src/win32/fonts.js +26 -7
- package/src/win32/overlay.js +244 -0
- package/src/win32/panewindow.js +6 -0
- package/src/win32/window.js +71 -1
package/src/win32/app.js
CHANGED
|
@@ -16,6 +16,8 @@ import { setScreensForTests } from '../screens.js';
|
|
|
16
16
|
import { createBezels } from './bezels.js';
|
|
17
17
|
import { Win32InputMethod } from './ime.js';
|
|
18
18
|
import { decodeKey, modifierMask } from './keymap.js';
|
|
19
|
+
import { refuseOverlay } from '../gloverlay.js';
|
|
20
|
+
import { Win32OverlayPane } from './overlay.js';
|
|
19
21
|
import { Win32PaneHost } from './panehost.js';
|
|
20
22
|
import { Win32PaneWindow } from './panewindow.js';
|
|
21
23
|
import { Win32Surface } from './surface.js';
|
|
@@ -80,6 +82,8 @@ export class Win32App {
|
|
|
80
82
|
// an in-app drop reads to keep `e.items` by reference (src/dnd.js).
|
|
81
83
|
this._activeDrag = null;
|
|
82
84
|
this._rafQueue = [];
|
|
85
|
+
/** Windows holding input for the next frame (`Win32Window._queueInput`). */
|
|
86
|
+
this._inputHeld = new Set();
|
|
83
87
|
this._frameTimer = null;
|
|
84
88
|
/** A frame has been asked for and not yet delivered — one outstanding
|
|
85
89
|
* request at a time, whether the clock or the timer is answering it. */
|
|
@@ -118,6 +122,7 @@ export class Win32App {
|
|
|
118
122
|
this.nativeBezels = null;
|
|
119
123
|
|
|
120
124
|
this.fonts = new Win32FontManager(native);
|
|
125
|
+
this._installOverlay();
|
|
121
126
|
|
|
122
127
|
const screens = native.listScreens?.() ?? [];
|
|
123
128
|
const primary = screens[0] ?? {
|
|
@@ -275,10 +280,19 @@ export class Win32App {
|
|
|
275
280
|
* A copy of the set is walked because `onDismiss` is what unmounts the
|
|
276
281
|
* popup, and unmounting runs `ungrabPointer` — which deletes from the set
|
|
277
282
|
* this loop would otherwise still be reading.
|
|
283
|
+
*
|
|
284
|
+
* `open` is the popups to judge — for a press, the ones that were open
|
|
285
|
+
* *before* it (`_route`). The press is delivered first, and a press that
|
|
286
|
+
* opens a menu — a `<Select>`, a menu button — opens it and takes its
|
|
287
|
+
* grab synchronously, in the same dispatch; judged afterwards, the new
|
|
288
|
+
* menu heard the press that opened it as a press outside itself and
|
|
289
|
+
* closed at once. Every `<Select>` did that. An X11 grab taken during a
|
|
290
|
+
* press does not see that press either.
|
|
278
291
|
*/
|
|
279
|
-
_dismissOutsidePopups(pressed) {
|
|
292
|
+
_dismissOutsidePopups(pressed, open = null) {
|
|
280
293
|
if (this._dismissOnOutside.size === 0) return;
|
|
281
|
-
for (const wnd of [...this._dismissOnOutside]) {
|
|
294
|
+
for (const wnd of open ?? [...this._dismissOnOutside]) {
|
|
295
|
+
if (!this._dismissOnOutside.has(wnd)) continue;
|
|
282
296
|
if (wnd === pressed) continue;
|
|
283
297
|
if (wnd.destroyed) {
|
|
284
298
|
this._dismissOnOutside.delete(wnd);
|
|
@@ -367,6 +381,29 @@ export class Win32App {
|
|
|
367
381
|
return new Win32PaneHost(this, wnd);
|
|
368
382
|
}
|
|
369
383
|
|
|
384
|
+
/**
|
|
385
|
+
* The pane a `<glarea>`'s children are drawn on (src/gloverlay.js): a
|
|
386
|
+
* composited layer above the window's swap chains, where the children's
|
|
387
|
+
* transparent pixels show the GL frame (src/win32/overlay.js). Having this
|
|
388
|
+
* at all is how the overlay knows it composites — and the X11 fallback it
|
|
389
|
+
* would otherwise reach for, a plain child window, is a GL surface here.
|
|
390
|
+
* Installed in the constructor where the bridge has layers.
|
|
391
|
+
*/
|
|
392
|
+
_installOverlay() {
|
|
393
|
+
if (typeof this._native.layerCreate === 'function') {
|
|
394
|
+
this.createOverlayPane = (attributes) =>
|
|
395
|
+
new Win32OverlayPane(this, attributes);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
refuseOverlay(
|
|
399
|
+
this,
|
|
400
|
+
"this @windowkit/win32 predates layers, which are what a <glarea>'s " +
|
|
401
|
+
'children are drawn on here, and a child window on this backend is ' +
|
|
402
|
+
'a GL surface of its own. Update the bridge, or draw the overlay ' +
|
|
403
|
+
'beside the surface',
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
370
407
|
/**
|
|
371
408
|
* The pane process's end of the frame channel: geometry and input in,
|
|
372
409
|
* presents out. Feature-detected by the pane bootstrap
|
|
@@ -421,7 +458,18 @@ export class Win32App {
|
|
|
421
458
|
|
|
422
459
|
_requestFrame(cb, wnd = null) {
|
|
423
460
|
this._rafQueue.push({ cb, wnd });
|
|
424
|
-
|
|
461
|
+
this._armFrame();
|
|
462
|
+
return this._rafQueue.length;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** A window is holding input for the next frame: there has to be one. */
|
|
466
|
+
_holdInput(wnd) {
|
|
467
|
+
this._inputHeld.add(wnd);
|
|
468
|
+
this._armFrame();
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_armFrame() {
|
|
472
|
+
if (this._framePending || this._closed) return;
|
|
425
473
|
this._framePending = true;
|
|
426
474
|
// One tick, from the compositor if this Windows has a clock to wait on.
|
|
427
475
|
// It answers false when it has not, and then — and only then — a timer.
|
|
@@ -434,7 +482,7 @@ export class Win32App {
|
|
|
434
482
|
this._tickFrames();
|
|
435
483
|
}, CLOCK_WATCHDOG_MS);
|
|
436
484
|
this._frameTimer.unref?.();
|
|
437
|
-
return
|
|
485
|
+
return;
|
|
438
486
|
}
|
|
439
487
|
this._frameTimer = setTimeout(() => {
|
|
440
488
|
this._frameTimer = null;
|
|
@@ -442,10 +490,24 @@ export class Win32App {
|
|
|
442
490
|
this._tickFrames();
|
|
443
491
|
}, FRAME_INTERVAL_MS);
|
|
444
492
|
this._frameTimer.unref?.();
|
|
445
|
-
return this._rafQueue.length;
|
|
446
493
|
}
|
|
447
494
|
|
|
448
495
|
_tickFrames() {
|
|
496
|
+
// The input held for this frame goes first: what it changes is what the
|
|
497
|
+
// frame paints, and the frames its handlers ask for are this one —
|
|
498
|
+
// the callbacks are taken after it.
|
|
499
|
+
if (this._inputHeld.size !== 0) {
|
|
500
|
+
const held = [...this._inputHeld];
|
|
501
|
+
this._inputHeld.clear();
|
|
502
|
+
for (const wnd of held) {
|
|
503
|
+
if (wnd.destroyed) continue;
|
|
504
|
+
try {
|
|
505
|
+
wnd._flushInput();
|
|
506
|
+
} catch (err) {
|
|
507
|
+
if (process.env.NODE_ENV !== 'production') console.error(err);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
449
511
|
const due = this._rafQueue;
|
|
450
512
|
this._rafQueue = [];
|
|
451
513
|
const now = performance.now();
|
|
@@ -515,6 +577,11 @@ export class Win32App {
|
|
|
515
577
|
|
|
516
578
|
const wnd = this._windows.get(event.id);
|
|
517
579
|
if (!wnd) return;
|
|
580
|
+
// Whatever this is happened after the motion and the wheel the window is
|
|
581
|
+
// holding for its next frame, so they go first (`_queueInput`).
|
|
582
|
+
if (event.type !== 'mousemove' && event.type !== 'wheel') {
|
|
583
|
+
wnd._flushInput?.();
|
|
584
|
+
}
|
|
518
585
|
switch (event.type) {
|
|
519
586
|
case 'window-ready':
|
|
520
587
|
wnd._onReady(event.a, event.b);
|
|
@@ -533,7 +600,7 @@ export class Win32App {
|
|
|
533
600
|
break;
|
|
534
601
|
}
|
|
535
602
|
case 'mousemove':
|
|
536
|
-
wnd.
|
|
603
|
+
wnd._queueInput('mousemove', {
|
|
537
604
|
x: event.a,
|
|
538
605
|
y: event.b,
|
|
539
606
|
...rootOf(wnd, event),
|
|
@@ -545,7 +612,7 @@ export class Win32App {
|
|
|
545
612
|
case 'wheel': {
|
|
546
613
|
const deltaX = event.c;
|
|
547
614
|
const deltaY = event.d;
|
|
548
|
-
wnd.
|
|
615
|
+
wnd._queueInput('wheel', {
|
|
549
616
|
name: 'wheel',
|
|
550
617
|
x: event.a,
|
|
551
618
|
y: event.b,
|
|
@@ -564,7 +631,13 @@ export class Win32App {
|
|
|
564
631
|
break;
|
|
565
632
|
}
|
|
566
633
|
case 'mousedown':
|
|
567
|
-
case 'mouseup':
|
|
634
|
+
case 'mouseup': {
|
|
635
|
+
// the menus this press may close: those open before it (see
|
|
636
|
+
// `_dismissOutsidePopups`)
|
|
637
|
+
const open =
|
|
638
|
+
event.type === 'mousedown' && this._dismissOnOutside.size !== 0
|
|
639
|
+
? [...this._dismissOnOutside]
|
|
640
|
+
: null;
|
|
568
641
|
// `keycode` is the button, numbered as X numbers them — the bridge
|
|
569
642
|
// already speaks that vocabulary. It used to be hardcoded to 1, so
|
|
570
643
|
// a right-click arrived as a left-click and no context menu ever
|
|
@@ -579,8 +652,9 @@ export class Win32App {
|
|
|
579
652
|
// A press in one window is a press *outside* every open menu but the
|
|
580
653
|
// one it landed in — which on X11 the grab would have delivered to
|
|
581
654
|
// the menu instead of to this window.
|
|
582
|
-
if (
|
|
655
|
+
if (open) this._dismissOutsidePopups(wnd, open);
|
|
583
656
|
break;
|
|
657
|
+
}
|
|
584
658
|
case 'keydown':
|
|
585
659
|
case 'keyup': {
|
|
586
660
|
// The bridge asked the active layout what the key types before the
|
package/src/win32/fonts.js
CHANGED
|
@@ -187,13 +187,25 @@ class Win32Face {
|
|
|
187
187
|
this.italic = style === 'italic' || style === 'oblique';
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Kept per size: a face's metrics never change, and the bridge answers
|
|
192
|
+
* each call by resolving the family through DirectWrite again. A trimmed
|
|
193
|
+
* `<text>` asks on every placement of its layout (`_trim`), which on a
|
|
194
|
+
* graph of widget cards was every body on every step of a drag.
|
|
195
|
+
*/
|
|
190
196
|
metrics(size) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
this._metrics ??= new Map();
|
|
198
|
+
let m = this._metrics.get(size);
|
|
199
|
+
if (m === undefined) {
|
|
200
|
+
m = this._manager._native.fontMetrics(
|
|
201
|
+
this.family,
|
|
202
|
+
size,
|
|
203
|
+
this.weight,
|
|
204
|
+
this.italic,
|
|
205
|
+
);
|
|
206
|
+
this._metrics.set(size, m);
|
|
207
|
+
}
|
|
208
|
+
return m;
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
/**
|
|
@@ -474,7 +486,14 @@ export class Win32FontManager {
|
|
|
474
486
|
}
|
|
475
487
|
|
|
476
488
|
match(family, options = {}) {
|
|
477
|
-
|
|
489
|
+
// the stack resolved once: a named family is a DirectWrite lookup, and
|
|
490
|
+
// every text layout asks
|
|
491
|
+
this._families ??= new Map();
|
|
492
|
+
let name = this._families.get(family);
|
|
493
|
+
if (name === undefined) {
|
|
494
|
+
name = familyOf(this._native, family);
|
|
495
|
+
this._families.set(family, name);
|
|
496
|
+
}
|
|
478
497
|
const key = `${name}|${weightOf(options.weight)}|${options.style ?? 'normal'}`;
|
|
479
498
|
let face = this._faces.get(key);
|
|
480
499
|
if (!face) {
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// The pane a `<glarea>`'s children are drawn on, on the Windows backend: a
|
|
2
|
+
// DirectComposition visual with a premultiplied surface of its own, above
|
|
3
|
+
// every swap chain in the window's tree (src/gloverlay.js; the bridge's
|
|
4
|
+
// src/layer.cc).
|
|
5
|
+
//
|
|
6
|
+
// Without it a pane was whatever `app.createWindow({ parent })` answers,
|
|
7
|
+
// which here is a GL surface (src/win32/glarea.js) — a WGL context and a
|
|
8
|
+
// swap chain per pane, and no 2D context at all. The children were laid out,
|
|
9
|
+
// claimed and painted nowhere, and a component that drew their cards under
|
|
10
|
+
// them had nothing left on screen where they were.
|
|
11
|
+
//
|
|
12
|
+
// Composited, like the Cocoa pane (src/cocoa/overlay.js): what the children
|
|
13
|
+
// leave transparent shows the GL frame, and a translucent fill, an
|
|
14
|
+
// antialiased edge or a shadow blends with it. Text on it is grayscale
|
|
15
|
+
// rather than ClearType, which Direct2D does on any surface with alpha
|
|
16
|
+
// (docs/windows.md §Text).
|
|
17
|
+
//
|
|
18
|
+
// Drawn like the window it lives in (src/win32/window.js): a DirectComposition
|
|
19
|
+
// surface hands out a context per `BeginDraw`, valid until `EndDraw`, so the
|
|
20
|
+
// overlay's passes come through `paintPasses` — one `BeginDraw` each — rather
|
|
21
|
+
// than through a context that is always there. `present` asks for the commit
|
|
22
|
+
// the window's own frame makes, so a pane and the window land together.
|
|
23
|
+
import { BackendContext2D } from '../backend/context2d.js';
|
|
24
|
+
|
|
25
|
+
export class Win32OverlayPane {
|
|
26
|
+
constructor(app, options) {
|
|
27
|
+
this.app = app;
|
|
28
|
+
this.parent = options.parent;
|
|
29
|
+
this._native = app._native;
|
|
30
|
+
this.destroyed = false;
|
|
31
|
+
this.rect = {
|
|
32
|
+
x: options.x ?? 0,
|
|
33
|
+
y: options.y ?? 0,
|
|
34
|
+
width: options.width ?? 1,
|
|
35
|
+
height: options.height ?? 1,
|
|
36
|
+
};
|
|
37
|
+
// the bridge's layer, made once the window has a composition tree to
|
|
38
|
+
// hang it in — the tree renders before the window is ready
|
|
39
|
+
this._layer = 0;
|
|
40
|
+
this._visible = true;
|
|
41
|
+
// non-zero only between a pass's BeginDraw and its EndDraw
|
|
42
|
+
this._surface = 0;
|
|
43
|
+
this._gen = 0;
|
|
44
|
+
this._ctx = null;
|
|
45
|
+
this._handlers = {};
|
|
46
|
+
this._ensureLayer();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get width() {
|
|
50
|
+
return this.rect.width;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get height() {
|
|
54
|
+
return this.rect.height;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
on(name, fn) {
|
|
58
|
+
(this._handlers[name] ??= []).push(fn);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
emit(name, ev) {
|
|
62
|
+
for (const fn of this._handlers[name] ?? []) fn(ev);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_ensureLayer() {
|
|
66
|
+
if (this._layer || this.destroyed) return this._layer;
|
|
67
|
+
if (!this.parent?._composed || this.parent.destroyed) return 0;
|
|
68
|
+
const { x, y, width, height } = this.rect;
|
|
69
|
+
this._layer = this._native.layerCreate(
|
|
70
|
+
this.parent.id,
|
|
71
|
+
Math.round(x),
|
|
72
|
+
Math.round(y),
|
|
73
|
+
Math.max(1, Math.round(width)),
|
|
74
|
+
Math.max(1, Math.round(height)),
|
|
75
|
+
);
|
|
76
|
+
if (this._layer && !this._visible) {
|
|
77
|
+
this._native.layerSetVisible(this._layer, false);
|
|
78
|
+
}
|
|
79
|
+
return this._layer;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Geometry in device px, the unit the overlay's rects are in. */
|
|
83
|
+
setState(rect) {
|
|
84
|
+
if (this.destroyed) return;
|
|
85
|
+
this.rect = rect;
|
|
86
|
+
if (!this._layer) return;
|
|
87
|
+
this._native.layerSetRect(
|
|
88
|
+
this._layer,
|
|
89
|
+
Math.round(rect.x),
|
|
90
|
+
Math.round(rect.y),
|
|
91
|
+
Math.max(1, Math.round(rect.width)),
|
|
92
|
+
Math.max(1, Math.round(rect.height)),
|
|
93
|
+
);
|
|
94
|
+
this._owe();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
map() {
|
|
98
|
+
this._setVisible(true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
unmap() {
|
|
102
|
+
this._setVisible(false);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_setVisible(on) {
|
|
106
|
+
if (this._visible === on) return;
|
|
107
|
+
this._visible = on;
|
|
108
|
+
if (!this._layer || this.destroyed) return;
|
|
109
|
+
this._native.layerSetVisible(this._layer, on);
|
|
110
|
+
this._owe();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getContext() {
|
|
114
|
+
if (!this._ctx) {
|
|
115
|
+
this._ctx = new BackendContext2D(
|
|
116
|
+
this._native,
|
|
117
|
+
() => this._surface,
|
|
118
|
+
() => this._gen,
|
|
119
|
+
);
|
|
120
|
+
this._ctx._fonts = this.app.fonts;
|
|
121
|
+
}
|
|
122
|
+
return this._ctx;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The overlay's passes, each its own `BeginDraw` — `rect`s in window
|
|
127
|
+
* coordinates, each painted by `paint(ctx, rect)` with the context drawing
|
|
128
|
+
* in window coordinates too. A pass is grown to whole pixels first:
|
|
129
|
+
* everything inside a `BeginDraw` rect is undefined until it is painted,
|
|
130
|
+
* so the rect the painter clears and the rect the surface hands out have
|
|
131
|
+
* to be the same one.
|
|
132
|
+
*/
|
|
133
|
+
paintPasses(passes, paint) {
|
|
134
|
+
if (this.destroyed) return;
|
|
135
|
+
if (!this._ensureLayer()) {
|
|
136
|
+
// Nothing to draw into yet. The overlay believes these passes landed,
|
|
137
|
+
// so it is told they did not: all of the pane is owed, on the next
|
|
138
|
+
// frame, when the window will have a tree to hang the layer in.
|
|
139
|
+
setImmediate(() => {
|
|
140
|
+
if (!this.destroyed) this.emit('draw', {});
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const ctx = this.getContext();
|
|
145
|
+
const ox = Math.round(this.rect.x);
|
|
146
|
+
const oy = Math.round(this.rect.y);
|
|
147
|
+
let drawn = false;
|
|
148
|
+
for (const pass of passes) {
|
|
149
|
+
const x0 = Math.max(Math.floor(pass.x), ox);
|
|
150
|
+
const y0 = Math.max(Math.floor(pass.y), oy);
|
|
151
|
+
const x1 = Math.min(
|
|
152
|
+
Math.ceil(pass.x + pass.width),
|
|
153
|
+
ox + Math.max(1, Math.round(this.rect.width)),
|
|
154
|
+
);
|
|
155
|
+
const y1 = Math.min(
|
|
156
|
+
Math.ceil(pass.y + pass.height),
|
|
157
|
+
oy + Math.max(1, Math.round(this.rect.height)),
|
|
158
|
+
);
|
|
159
|
+
if (x1 <= x0 || y1 <= y0) continue;
|
|
160
|
+
const handle = this._native.layerBeginDraw(
|
|
161
|
+
this._layer,
|
|
162
|
+
x0 - ox,
|
|
163
|
+
y0 - oy,
|
|
164
|
+
x1 - x0,
|
|
165
|
+
y1 - y0,
|
|
166
|
+
);
|
|
167
|
+
if (!handle) continue;
|
|
168
|
+
this._surface = handle;
|
|
169
|
+
this._gen++;
|
|
170
|
+
try {
|
|
171
|
+
// the surface draws in the layer's own coordinates; the painter
|
|
172
|
+
// draws in the window's
|
|
173
|
+
ctx.save();
|
|
174
|
+
try {
|
|
175
|
+
ctx.translate(-ox, -oy);
|
|
176
|
+
paint(ctx, { x: x0, y: y0, width: x1 - x0, height: y1 - y0 });
|
|
177
|
+
} finally {
|
|
178
|
+
ctx.restore();
|
|
179
|
+
}
|
|
180
|
+
drawn = true;
|
|
181
|
+
} finally {
|
|
182
|
+
this._surface = 0;
|
|
183
|
+
this._native.layerEndDraw(this._layer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (drawn) this._owe();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Move the pixels of `rect` (pane coordinates, device px) by (dx, dy) —
|
|
191
|
+
* ntk `Window.scrollRegion`'s contract: the band that survives the shift
|
|
192
|
+
* inside `rect` moves, the rest of it is left as it was, and false means
|
|
193
|
+
* nothing moved, so the caller repaints instead (issue #644).
|
|
194
|
+
*/
|
|
195
|
+
scrollRegion(rect, dx, dy) {
|
|
196
|
+
if (this.destroyed || !this._layer) return false;
|
|
197
|
+
if (!Number.isInteger(dx) || !Number.isInteger(dy)) return false;
|
|
198
|
+
const moved = this._native.layerScroll(
|
|
199
|
+
this._layer,
|
|
200
|
+
Math.round(rect.x),
|
|
201
|
+
Math.round(rect.y),
|
|
202
|
+
Math.round(rect.width),
|
|
203
|
+
Math.round(rect.height),
|
|
204
|
+
dx,
|
|
205
|
+
dy,
|
|
206
|
+
);
|
|
207
|
+
if (moved) this._owe();
|
|
208
|
+
return Boolean(moved);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** What was painted or moved, onto the screen with the window's frame. */
|
|
212
|
+
present() {
|
|
213
|
+
this._owe();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* A commit is owed. The window's frame makes one after the overlay has
|
|
218
|
+
* painted (`presentFrame`, src/win32/window.js), so a pane's pixels and
|
|
219
|
+
* the window's land together; a change made outside a frame — a pane
|
|
220
|
+
* hidden between two — is committed on a microtask instead, once.
|
|
221
|
+
*/
|
|
222
|
+
_owe() {
|
|
223
|
+
const wnd = this.parent;
|
|
224
|
+
if (!wnd || wnd.destroyed) return;
|
|
225
|
+
if (wnd._owesCommit) return;
|
|
226
|
+
wnd._owesCommit = true;
|
|
227
|
+
queueMicrotask(() => {
|
|
228
|
+
if (!wnd._owesCommit) return;
|
|
229
|
+
wnd._owesCommit = false;
|
|
230
|
+
if (!wnd.destroyed) this._native.commit();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
destroy() {
|
|
235
|
+
if (this.destroyed) return;
|
|
236
|
+
this.destroyed = true;
|
|
237
|
+
this._ctx = null;
|
|
238
|
+
if (this._layer) {
|
|
239
|
+
this._native.layerDestroy(this._layer);
|
|
240
|
+
this._layer = 0;
|
|
241
|
+
this._owe();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
package/src/win32/panewindow.js
CHANGED
|
@@ -287,6 +287,12 @@ export class Win32PaneWindow {
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
/** The frame is the ordinary paint walk, so it paints through core's
|
|
291
|
+
* paint cache (src/nodes/window/flush.js), as the window does. */
|
|
292
|
+
get usesPaintCache() {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
|
|
290
296
|
/**
|
|
291
297
|
* No scroll blit. The fast path moves a band of pixels *within the surface
|
|
292
298
|
* being drawn into*, and this one is `STALE_FRAMES` frames behind — the
|
package/src/win32/window.js
CHANGED
|
@@ -53,6 +53,11 @@ export class Win32Window {
|
|
|
53
53
|
this._pendingFrames = [];
|
|
54
54
|
this._owesFullPaint = false;
|
|
55
55
|
this._dirty = false;
|
|
56
|
+
// a pane over a `<glarea>` here changed and its commit is this frame's
|
|
57
|
+
// (src/win32/overlay.js)
|
|
58
|
+
this._owesCommit = false;
|
|
59
|
+
// pointer motion and wheel held for the next frame (`_queueInput`)
|
|
60
|
+
this._pendingInput = null;
|
|
56
61
|
|
|
57
62
|
// `overrideRedirect` is how a `<popup>` says what it is — a menu, a
|
|
58
63
|
// select's list, a tooltip. On X11 it means "the window manager does not
|
|
@@ -104,6 +109,58 @@ export class Win32Window {
|
|
|
104
109
|
for (const fn of this._handlers[name] ?? []) fn(ev);
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Hold a pointer motion or a wheel for the next frame, merged with any
|
|
114
|
+
* already held: ntk's coalescing on X11 (its lib/window.js,
|
|
115
|
+
* `_deliverEvent`), which the rest of the event path was written against
|
|
116
|
+
* — src/events.js paces motion on the strength of it.
|
|
117
|
+
*
|
|
118
|
+
* Windows merges WM_MOUSEMOVE itself, but only while a queue waits to be
|
|
119
|
+
* read, and the bridge reads its messages on a UI thread of its own and
|
|
120
|
+
* posts every one of them to the loop. So a mouse reporting a thousand
|
|
121
|
+
* times a second was a thousand events on the loop, and a handler slower
|
|
122
|
+
* than a millisecond fell behind the pointer — then replayed every
|
|
123
|
+
* position it had missed, a frame of work each, long after the pointer
|
|
124
|
+
* had stopped: a box selection over a large graph trailed the pointer by
|
|
125
|
+
* seconds. Held here, a burst is one event a frame: the newest motion,
|
|
126
|
+
* with the rest riding along in `ev.coalesced`, and the wheel's deltas
|
|
127
|
+
* added up at its last position.
|
|
128
|
+
*/
|
|
129
|
+
_queueInput(name, ev) {
|
|
130
|
+
const pending = (this._pendingInput ??= new Map());
|
|
131
|
+
const prev = pending.get(name);
|
|
132
|
+
if (!prev) {
|
|
133
|
+
ev.coalesced = [ev];
|
|
134
|
+
pending.set(name, ev);
|
|
135
|
+
} else if (name === 'wheel') {
|
|
136
|
+
// scroll distance adds up — the last delta alone would throw away all
|
|
137
|
+
// but the final step of a fast scroll
|
|
138
|
+
prev.coalesced.push(ev);
|
|
139
|
+
prev.deltaX += ev.deltaX;
|
|
140
|
+
prev.deltaY += ev.deltaY;
|
|
141
|
+
prev.x = ev.x;
|
|
142
|
+
prev.y = ev.y;
|
|
143
|
+
prev.rootx = ev.rootx;
|
|
144
|
+
prev.rooty = ev.rooty;
|
|
145
|
+
prev.smooth = prev.smooth || ev.smooth;
|
|
146
|
+
} else {
|
|
147
|
+
ev.coalesced = prev.coalesced;
|
|
148
|
+
ev.coalesced.push(ev);
|
|
149
|
+
pending.set(name, ev);
|
|
150
|
+
}
|
|
151
|
+
this.app._holdInput(this);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Deliver what `_queueInput` held: at the top of the next frame, and
|
|
155
|
+
* before any other event of this window's, so a handler sees events in
|
|
156
|
+
* the order they happened — a drag its last move, then the release. */
|
|
157
|
+
_flushInput() {
|
|
158
|
+
const pending = this._pendingInput;
|
|
159
|
+
if (pending === null) return;
|
|
160
|
+
this._pendingInput = null;
|
|
161
|
+
for (const [name, ev] of pending) this.emit(name, ev);
|
|
162
|
+
}
|
|
163
|
+
|
|
107
164
|
/**
|
|
108
165
|
* The bridge says the HWND exists. Composition is set up here and not at
|
|
109
166
|
* construction because there is no HWND to target until now — the command
|
|
@@ -168,6 +225,7 @@ export class Win32Window {
|
|
|
168
225
|
destroy() {
|
|
169
226
|
if (this.destroyed) return;
|
|
170
227
|
this.destroyed = true;
|
|
228
|
+
this._pendingInput = null;
|
|
171
229
|
this._native.destroyWindow?.(this.id);
|
|
172
230
|
this.app._unregister(this);
|
|
173
231
|
}
|
|
@@ -577,7 +635,13 @@ export class Win32Window {
|
|
|
577
635
|
this._native.endDraw(this.id);
|
|
578
636
|
}
|
|
579
637
|
}
|
|
580
|
-
|
|
638
|
+
// …and the panes over a `<glarea>` in this window, painted before this
|
|
639
|
+
// (src/win32/overlay.js), in the same commit: a frame whose only change
|
|
640
|
+
// was theirs still owes one
|
|
641
|
+
if (painted || this._owesCommit) {
|
|
642
|
+
this._owesCommit = false;
|
|
643
|
+
this._native.commit();
|
|
644
|
+
}
|
|
581
645
|
}
|
|
582
646
|
|
|
583
647
|
/**
|
|
@@ -604,6 +668,12 @@ export class Win32Window {
|
|
|
604
668
|
);
|
|
605
669
|
}
|
|
606
670
|
|
|
671
|
+
/** The frame is the ordinary paint walk, a pass per damage rect, so it
|
|
672
|
+
* paints through core's paint cache (src/nodes/window/flush.js). */
|
|
673
|
+
get usesPaintCache() {
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
|
|
607
677
|
/** DirectComposition does not hold a frame back the way an X server's fence
|
|
608
678
|
* or a WindowServer's buffer does: Commit is asynchronous and the surface
|
|
609
679
|
* retains its own pixels. So there is never a frame in flight to wait for. */
|