react-x11 2.18.4 → 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/backend/context2d.js +65 -18
- package/src/gloverlay.js +47 -24
- 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/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/package.json
CHANGED
package/src/backend/context2d.js
CHANGED
|
@@ -324,6 +324,17 @@ export class BackendContext2D {
|
|
|
324
324
|
// chunked stroke leaves only its last chunk behind)
|
|
325
325
|
this._cmds = [];
|
|
326
326
|
this._pathStale = false;
|
|
327
|
+
// A bridge that takes a whole path in one call (`ctxPath`, the Windows
|
|
328
|
+
// bridge's) is handed it that way when something paints it: the verbs
|
|
329
|
+
// below only record. A graph's edges are tens of thousands of `lineTo`s
|
|
330
|
+
// a frame, and a call across the boundary for each was milliseconds of
|
|
331
|
+
// every frame spent on the crossing. A bridge without it is handed the
|
|
332
|
+
// path a point at a time, as it is built, exactly as before.
|
|
333
|
+
// Asked with `in` as well: a test's bridge that answers every name with
|
|
334
|
+
// a no-op must not look like one that takes paths whole.
|
|
335
|
+
this._bulkPaths =
|
|
336
|
+
'ctxPath' in native && typeof native.ctxPath === 'function';
|
|
337
|
+
this._pathBuf = null;
|
|
327
338
|
this._strokeChunking = !NO_STROKE_CHUNKING;
|
|
328
339
|
}
|
|
329
340
|
|
|
@@ -670,6 +681,21 @@ export class BackendContext2D {
|
|
|
670
681
|
_emit(surface, from, to) {
|
|
671
682
|
const c = this._cmds;
|
|
672
683
|
const n = this._native;
|
|
684
|
+
if (this._bulkPaths) {
|
|
685
|
+
// one call, through a buffer kept between paths — the record is the
|
|
686
|
+
// same stream the bridge parses, arguments and all
|
|
687
|
+
const count = to - from;
|
|
688
|
+
if (count <= 0) return;
|
|
689
|
+
let buf = this._pathBuf;
|
|
690
|
+
if (!buf || buf.length < count) {
|
|
691
|
+
buf = this._pathBuf = new Float64Array(
|
|
692
|
+
Math.max(count, (buf?.length ?? 512) * 2),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
for (let i = 0; i < count; i++) buf[i] = c[from + i];
|
|
696
|
+
n.ctxPath(surface, buf.subarray(0, count));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
673
699
|
for (let i = from; i < to;) {
|
|
674
700
|
const op = c[i];
|
|
675
701
|
const a = i + 1;
|
|
@@ -720,26 +746,44 @@ export class BackendContext2D {
|
|
|
720
746
|
|
|
721
747
|
beginPath() {
|
|
722
748
|
this._cmds.length = 0;
|
|
749
|
+
if (this._bulkPaths) {
|
|
750
|
+
this._s();
|
|
751
|
+
// uploaded, empty or not, when something paints it
|
|
752
|
+
this._pathStale = true;
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
723
755
|
this._pathStale = false;
|
|
724
756
|
this._native.ctxBeginPath(this._s());
|
|
725
757
|
}
|
|
726
758
|
|
|
759
|
+
/**
|
|
760
|
+
* The surface to add a path command to now, or null where the path is
|
|
761
|
+
* only recorded and handed over whole when painted (`_bulkPaths`). Either
|
|
762
|
+
* way a fresh surface drops the path first, as `_s` has always done.
|
|
763
|
+
*/
|
|
764
|
+
_pathFor() {
|
|
765
|
+
if (!this._bulkPaths) return this._path();
|
|
766
|
+
this._s();
|
|
767
|
+
this._pathStale = true;
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
727
771
|
moveTo(x, y) {
|
|
728
|
-
const surface = this.
|
|
772
|
+
const surface = this._pathFor();
|
|
729
773
|
this._cmds.push(P_MOVE, x, y);
|
|
730
|
-
this._native.ctxMoveTo(surface, x, y);
|
|
774
|
+
if (surface !== null) this._native.ctxMoveTo(surface, x, y);
|
|
731
775
|
}
|
|
732
776
|
|
|
733
777
|
lineTo(x, y) {
|
|
734
|
-
const surface = this.
|
|
778
|
+
const surface = this._pathFor();
|
|
735
779
|
this._cmds.push(P_LINE, x, y);
|
|
736
|
-
this._native.ctxLineTo(surface, x, y);
|
|
780
|
+
if (surface !== null) this._native.ctxLineTo(surface, x, y);
|
|
737
781
|
}
|
|
738
782
|
|
|
739
783
|
rect(x, y, w, h) {
|
|
740
|
-
const surface = this.
|
|
784
|
+
const surface = this._pathFor();
|
|
741
785
|
this._cmds.push(P_RECT, x, y, w, h);
|
|
742
|
-
this._native.ctxRect(surface, x, y, w, h);
|
|
786
|
+
if (surface !== null) this._native.ctxRect(surface, x, y, w, h);
|
|
743
787
|
}
|
|
744
788
|
|
|
745
789
|
roundRect(x, y, w, h, radii) {
|
|
@@ -750,7 +794,7 @@ export class BackendContext2D {
|
|
|
750
794
|
else if (r.length === 3) r = [r[0], r[1], r[2], r[1]];
|
|
751
795
|
const cap = Math.min(Math.abs(w) / 2, Math.abs(h) / 2);
|
|
752
796
|
const clamp = (v) => Math.max(0, Math.min(Number(v) || 0, cap));
|
|
753
|
-
const surface = this.
|
|
797
|
+
const surface = this._pathFor();
|
|
754
798
|
const [r0, r1, r2, r3] = [
|
|
755
799
|
clamp(r[0]),
|
|
756
800
|
clamp(r[1]),
|
|
@@ -758,37 +802,40 @@ export class BackendContext2D {
|
|
|
758
802
|
clamp(r[3]),
|
|
759
803
|
];
|
|
760
804
|
this._cmds.push(P_ROUND, x, y, w, h, r0, r1, r2, r3);
|
|
761
|
-
|
|
805
|
+
if (surface !== null)
|
|
806
|
+
this._native.ctxRoundRect(surface, x, y, w, h, r0, r1, r2, r3);
|
|
762
807
|
}
|
|
763
808
|
|
|
764
809
|
arc(x, y, radius, start, end, anticlockwise = false) {
|
|
765
|
-
const surface = this.
|
|
810
|
+
const surface = this._pathFor();
|
|
766
811
|
this._cmds.push(P_ARC, x, y, radius, start, end, anticlockwise ? 1 : 0);
|
|
767
|
-
|
|
812
|
+
if (surface !== null)
|
|
813
|
+
this._native.ctxArc(surface, x, y, radius, start, end, anticlockwise);
|
|
768
814
|
}
|
|
769
815
|
|
|
770
816
|
ellipse(x, y, rx, ry) {
|
|
771
|
-
const surface = this.
|
|
817
|
+
const surface = this._pathFor();
|
|
772
818
|
this._cmds.push(P_ELLIPSE, x, y, rx, ry);
|
|
773
|
-
this._native.ctxEllipse(surface, x, y, rx, ry);
|
|
819
|
+
if (surface !== null) this._native.ctxEllipse(surface, x, y, rx, ry);
|
|
774
820
|
}
|
|
775
821
|
|
|
776
822
|
bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
|
|
777
|
-
const surface = this.
|
|
823
|
+
const surface = this._pathFor();
|
|
778
824
|
this._cmds.push(P_CURVE, c1x, c1y, c2x, c2y, x, y);
|
|
779
|
-
|
|
825
|
+
if (surface !== null)
|
|
826
|
+
this._native.ctxCurveTo(surface, c1x, c1y, c2x, c2y, x, y);
|
|
780
827
|
}
|
|
781
828
|
|
|
782
829
|
quadraticCurveTo(cx, cy, x, y) {
|
|
783
|
-
const surface = this.
|
|
830
|
+
const surface = this._pathFor();
|
|
784
831
|
this._cmds.push(P_QUAD, cx, cy, x, y);
|
|
785
|
-
this._native.ctxQuadTo(surface, cx, cy, x, y);
|
|
832
|
+
if (surface !== null) this._native.ctxQuadTo(surface, cx, cy, x, y);
|
|
786
833
|
}
|
|
787
834
|
|
|
788
835
|
closePath() {
|
|
789
|
-
const surface = this.
|
|
836
|
+
const surface = this._pathFor();
|
|
790
837
|
this._cmds.push(P_CLOSE);
|
|
791
|
-
this._native.ctxClosePath(surface);
|
|
838
|
+
if (surface !== null) this._native.ctxClosePath(surface);
|
|
792
839
|
}
|
|
793
840
|
|
|
794
841
|
// --- painting ------------------------------------------------------------
|
package/src/gloverlay.js
CHANGED
|
@@ -114,6 +114,18 @@ export function overlayRefusal(app) {
|
|
|
114
114
|
return (app && refusals.get(app)) ?? null;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* A backend saying nothing can be drawn over a GL surface on this app, and
|
|
119
|
+
* why: one whose child windows are not X windows, and that has no
|
|
120
|
+
* composited pane either — on Windows a window with a parent is a GL
|
|
121
|
+
* surface of its own (src/win32/glarea.js), and a bridge without layers has
|
|
122
|
+
* nothing else to offer. Before the first render, like `beginGlOverlay`, so
|
|
123
|
+
* `useSupports('glOverlay')` never changes its answer.
|
|
124
|
+
*/
|
|
125
|
+
export function refuseOverlay(app, reason) {
|
|
126
|
+
if (app) refusals.set(app, reason);
|
|
127
|
+
}
|
|
128
|
+
|
|
117
129
|
/**
|
|
118
130
|
* Is this an X server where nothing can be drawn over a GL surface? Asked
|
|
119
131
|
* in `createRoot`, with the other startup probes and before the first
|
|
@@ -654,35 +666,46 @@ export class GlOverlay {
|
|
|
654
666
|
_paintPane(pane, passes) {
|
|
655
667
|
const area = this.area;
|
|
656
668
|
const root = area.root;
|
|
657
|
-
|
|
658
|
-
if (!ctx || !root) return;
|
|
669
|
+
if (!root) return;
|
|
659
670
|
// an opaque pane is filled with what the surface starts its frames from;
|
|
660
671
|
// a composited one is cleared, and shows the frame itself
|
|
661
672
|
const ground = pane.transparent ? null : groundOf(area.props);
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
ctx.
|
|
665
|
-
|
|
666
|
-
ctx.
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
ctx.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
} else {
|
|
675
|
-
ctx.clearRect(pass.x, pass.y, pass.width, pass.height);
|
|
676
|
-
}
|
|
677
|
-
root._paintDamage = pass;
|
|
678
|
-
area._paintChildren(ctx);
|
|
679
|
-
} finally {
|
|
680
|
-
root._paintDamage = null;
|
|
681
|
-
ctx.restore();
|
|
673
|
+
// one pass, drawn by a context in window coordinates
|
|
674
|
+
const paintPass = (ctx, pass) => {
|
|
675
|
+
ctx.save();
|
|
676
|
+
try {
|
|
677
|
+
ctx.beginPath();
|
|
678
|
+
ctx.rect(pass.x, pass.y, pass.width, pass.height);
|
|
679
|
+
ctx.clip();
|
|
680
|
+
if (ground) {
|
|
681
|
+
ctx.fillStyle = ground;
|
|
682
|
+
ctx.fillRect(pass.x, pass.y, pass.width, pass.height);
|
|
683
|
+
} else {
|
|
684
|
+
ctx.clearRect(pass.x, pass.y, pass.width, pass.height);
|
|
682
685
|
}
|
|
686
|
+
root._paintDamage = pass;
|
|
687
|
+
area._paintChildren(ctx);
|
|
688
|
+
} finally {
|
|
689
|
+
root._paintDamage = null;
|
|
690
|
+
ctx.restore();
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
if (typeof pane.wnd.paintPasses === 'function') {
|
|
694
|
+
// A pane drawn a rect at a time — a DirectComposition surface hands
|
|
695
|
+
// out a context per `BeginDraw`, valid until `EndDraw` — opens each
|
|
696
|
+
// pass itself (src/win32/overlay.js), the way such a window takes its
|
|
697
|
+
// frame through `presentFrame`.
|
|
698
|
+
pane.wnd.paintPasses(passes, paintPass);
|
|
699
|
+
} else {
|
|
700
|
+
const ctx = pane.context();
|
|
701
|
+
if (!ctx) return;
|
|
702
|
+
ctx.save();
|
|
703
|
+
try {
|
|
704
|
+
ctx.translate(-pane.rect.x, -pane.rect.y);
|
|
705
|
+
for (const pass of passes) paintPass(ctx, pass);
|
|
706
|
+
} finally {
|
|
707
|
+
ctx.restore();
|
|
683
708
|
}
|
|
684
|
-
} finally {
|
|
685
|
-
ctx.restore();
|
|
686
709
|
}
|
|
687
710
|
// A layer's contents are a copy the bitmap is pushed to; an X window's
|
|
688
711
|
// backing store is blitted by ntk on its own, from the paint above
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -314,8 +314,16 @@ export class WindowInvalidate {
|
|
|
314
314
|
// the whole reason this is not just `needsLayout`: a scroll moves an
|
|
315
315
|
// offset applied during `absolutize` and leaves every yoga node exactly
|
|
316
316
|
// as it was, at input rate, on the biggest trees in any app.
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
//
|
|
318
|
+
// Nor does a box that only moved (`'position'`, from `applyProps`):
|
|
319
|
+
// absolutely positioned and sized by its own width and height, it is
|
|
320
|
+
// out of every ancestor's flow and sizes nothing inside it by its
|
|
321
|
+
// insets. A card dragged across a graph — or a thousand of them
|
|
322
|
+
// panned — was a full measurement of the content floors a step.
|
|
323
|
+
if (reason !== 'scroll' && reason !== 'position') {
|
|
324
|
+
this._floorsDirty = true;
|
|
325
|
+
}
|
|
326
|
+
if (reason !== 'scroll' && reason !== 'resize' && reason !== 'position') {
|
|
319
327
|
this._floorsContentDirty = true;
|
|
320
328
|
}
|
|
321
329
|
}
|
package/src/nodes/node.js
CHANGED
|
@@ -605,6 +605,18 @@ export class Node {
|
|
|
605
605
|
// which is what keeps a commit from widening the region to every node
|
|
606
606
|
// it touched.
|
|
607
607
|
if (layoutChanged) {
|
|
608
|
+
const moved = onlyInsetsChanged(style, prevStyle);
|
|
609
|
+
// An absolutely positioned box of a set size that moved and changed
|
|
610
|
+
// nothing else is out of its ancestors' flow and sized by its width
|
|
611
|
+
// and height rather than its insets, so no content floor anywhere
|
|
612
|
+
// moved with it (src/nodes/invalidate.js, `'position'`).
|
|
613
|
+
const reason =
|
|
614
|
+
moved &&
|
|
615
|
+
style.position === 'absolute' &&
|
|
616
|
+
typeof style.width === 'number' &&
|
|
617
|
+
typeof style.height === 'number'
|
|
618
|
+
? 'position'
|
|
619
|
+
: 'props';
|
|
608
620
|
// A child of a `<glarea>` that only moved (issue #644): its pixels are
|
|
609
621
|
// on a pane, one shift away, and the layout pass reports the move to
|
|
610
622
|
// the overlay rather than claiming it (src/glnodes.js). A claim of the
|
|
@@ -613,12 +625,12 @@ export class Node {
|
|
|
613
625
|
// whenever the overlay cannot move the pixels.
|
|
614
626
|
if (
|
|
615
627
|
this.parent?.isGlArea &&
|
|
616
|
-
|
|
628
|
+
moved &&
|
|
617
629
|
!this.paintChanged(newProps, prev)
|
|
618
630
|
) {
|
|
619
|
-
this.invalidate(true, NO_DAMAGE,
|
|
631
|
+
this.invalidate(true, NO_DAMAGE, reason);
|
|
620
632
|
} else {
|
|
621
|
-
this._invalidateLayout(
|
|
633
|
+
this._invalidateLayout(reason);
|
|
622
634
|
}
|
|
623
635
|
} else {
|
|
624
636
|
// The style half is asked here rather than inside `paintChanged`, and
|
package/src/nodes/scrollblit.js
CHANGED
|
@@ -970,12 +970,22 @@ export class WindowScrollBlit {
|
|
|
970
970
|
if (ancestors.has(child)) {
|
|
971
971
|
// on the path down: its solid background under the viewport is
|
|
972
972
|
// translation-invariant, its border ring and corners are not.
|
|
973
|
-
// The widest side is conservative for a non-uniform border
|
|
973
|
+
// The widest side is conservative for a non-uniform border.
|
|
974
|
+
//
|
|
975
|
+
// Rounded, because that is where the ring ends for what is
|
|
976
|
+
// inside it: at a fractional scale a 1px border is 1.25 or 1.5
|
|
977
|
+
// device pixels, and layout puts the children at the rounded
|
|
978
|
+
// inner edge — so the column the ring reaches a quarter of the
|
|
979
|
+
// way into is a child's, painted over whole. Tested exactly, that
|
|
980
|
+
// column refused every blit under a bordered box at 125%, the
|
|
981
|
+
// scale most Windows laptops run at.
|
|
974
982
|
const bw = resolveBorderWidths(
|
|
975
983
|
child.style ?? EMPTY_STYLE,
|
|
976
984
|
child.direction,
|
|
977
985
|
);
|
|
978
|
-
const ring = Math.
|
|
986
|
+
const ring = Math.round(
|
|
987
|
+
Math.max(bw.top, bw.right, bw.bottom, bw.left),
|
|
988
|
+
);
|
|
979
989
|
if (ring > 0 && !rectContains(insetRect(child.abs, ring), vp)) {
|
|
980
990
|
return false;
|
|
981
991
|
}
|
|
@@ -326,9 +326,26 @@ export class WindowFlush {
|
|
|
326
326
|
// list was still taken (its bookkeeping is what keeps the two paths one
|
|
327
327
|
// code) and is simply not consumed; the presenter diffs at the layer.
|
|
328
328
|
if (typeof this.window.presentFrame === 'function') {
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
329
|
+
// A presenter whose frame is the ordinary paint walk — a pass per
|
|
330
|
+
// damage rect into a context, the Windows window's — paints through
|
|
331
|
+
// the paint cache as an ntk window does, and says so
|
|
332
|
+
// (`usesPaintCache`). Without it every `<canvas cacheKey>` drew
|
|
333
|
+
// afresh in every pass that reached it, and a shadow, which is only
|
|
334
|
+
// ever baked into the cache (issue #413), was not drawn at all. The
|
|
335
|
+
// retained layer presenter paints nodes onto layers of their own and
|
|
336
|
+
// keeps to itself.
|
|
337
|
+
const cache = this.window.usesPaintCache
|
|
338
|
+
? (this._paintCache ??= paintCacheFor(this.app))
|
|
339
|
+
: null;
|
|
340
|
+
cache?.beginFrame();
|
|
341
|
+
try {
|
|
342
|
+
// the panes are no presenter's: they paint from their damage either
|
|
343
|
+
// way
|
|
344
|
+
if (this._overlaid.size !== 0) this._paintOverlays(paneDamage);
|
|
345
|
+
this.window.presentFrame(this, damage);
|
|
346
|
+
} finally {
|
|
347
|
+
cache?.endFrame();
|
|
348
|
+
}
|
|
332
349
|
this.app._reactX11Startup?.painted();
|
|
333
350
|
return true;
|
|
334
351
|
}
|
package/src/win32/a11y.js
CHANGED
|
@@ -36,6 +36,22 @@ import { onApp } from '../trace-registry.js';
|
|
|
36
36
|
|
|
37
37
|
const TRACE = process.env.REACT_X11_TRACE_A11Y === '1';
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* How often commits may push the mirror, in ms. A commit that follows a
|
|
41
|
+
* quiet spell pushes at once; a stream of them — a pan, a drag, a value
|
|
42
|
+
* ticking — pushes once per interval, the last of them catching up.
|
|
43
|
+
*
|
|
44
|
+
* UIA's clients are almost always listening on a desktop Windows (the
|
|
45
|
+
* touch keyboard, the text services and others ask for the tree), so the
|
|
46
|
+
* push is not the rare case the listening gate makes it elsewhere: a walk
|
|
47
|
+
* of the window and a marshalled diff, on every commit. For a graph pane
|
|
48
|
+
* whose every item moves on every step of a pan that was 1.6 ms of each
|
|
49
|
+
* frame. A screen reader reads positions on demand and does not need
|
|
50
|
+
* sixty of them a second; a focus change, which it is waiting on, still
|
|
51
|
+
* pushes at once (`hooks.focus`), as does an announcement.
|
|
52
|
+
*/
|
|
53
|
+
const COMMIT_PUSH_MS = 100;
|
|
54
|
+
|
|
39
55
|
// --------------------------------------------------------------------------
|
|
40
56
|
// The one translation that is Windows' own
|
|
41
57
|
// --------------------------------------------------------------------------
|
|
@@ -300,6 +316,9 @@ export class Win32Accessibility {
|
|
|
300
316
|
this._pushed = new Set();
|
|
301
317
|
this._unsubscribes = [];
|
|
302
318
|
this.dead = false;
|
|
319
|
+
/** When a commit last pushed, and the push a stream of commits owes. */
|
|
320
|
+
this._lastCommitPush = -Infinity;
|
|
321
|
+
this._trailing = null;
|
|
303
322
|
}
|
|
304
323
|
|
|
305
324
|
_idOf(node) {
|
|
@@ -388,15 +407,29 @@ export class Win32Accessibility {
|
|
|
388
407
|
* commit and paying nothing — the same shape as the AT-SPI bridge's "no
|
|
389
408
|
* bus, no work".
|
|
390
409
|
*
|
|
410
|
+
* *Nobody* is asked per window where the bridge can say (`uiaActive`):
|
|
411
|
+
* whether a client has read this window's tree in the last few seconds.
|
|
412
|
+
* UIA's own answer, `UiaClientsAreListening`, is whether any client on
|
|
413
|
+
* the desktop is subscribed to anything, and on a Windows running the
|
|
414
|
+
* touch keyboard or the text services that is always yes — so every
|
|
415
|
+
* commit walked and diffed the window for a mirror nobody read. A client
|
|
416
|
+
* that comes back after a quiet spell is noticed on its first read and
|
|
417
|
+
* asks for a fresh push (`wanted`). `global` keeps the desktop-wide answer
|
|
418
|
+
* for a focus change and an announcement: a client subscribed to focus
|
|
419
|
+
* events reads the focused element the moment it hears of it, before
|
|
420
|
+
* any read of ours could say it was there.
|
|
421
|
+
*
|
|
391
422
|
* With one exception, which is what makes the gate safe: **every window is
|
|
392
423
|
* pushed once regardless.** A client's first `WM_GETOBJECT` has to find a
|
|
393
424
|
* tree, and it arrives before anything in this process knows a client
|
|
394
425
|
* exists. After that first push the mirror is live, and a client attaching
|
|
395
426
|
* later asks for a fresh one (`uia-wanted`).
|
|
396
427
|
*/
|
|
397
|
-
flush({ force = false } = {}) {
|
|
428
|
+
flush({ force = false, global = false } = {}) {
|
|
398
429
|
if (this.dead || this._dirty.size === 0) return;
|
|
399
|
-
const
|
|
430
|
+
const perWindow =
|
|
431
|
+
!force && !global && typeof this._native.uiaActive === 'function';
|
|
432
|
+
const listening = force || (!perWindow && this._native.uiaListening());
|
|
400
433
|
const windows = [...this._dirty];
|
|
401
434
|
this._dirty.clear();
|
|
402
435
|
for (const win of windows) {
|
|
@@ -409,12 +442,37 @@ export class Win32Accessibility {
|
|
|
409
442
|
this._dirty.add(win);
|
|
410
443
|
continue;
|
|
411
444
|
}
|
|
412
|
-
if (
|
|
445
|
+
if (
|
|
446
|
+
!listening &&
|
|
447
|
+
this._pushed.has(wnd.id) &&
|
|
448
|
+
!(perWindow && this._native.uiaActive(wnd.id))
|
|
449
|
+
) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
413
452
|
this._pushed.add(wnd.id);
|
|
414
453
|
this._push(win);
|
|
415
454
|
}
|
|
416
455
|
}
|
|
417
456
|
|
|
457
|
+
/** A commit's push, at most once per `COMMIT_PUSH_MS`. */
|
|
458
|
+
_commitFlush() {
|
|
459
|
+
if (this.dead || this._dirty.size === 0) return;
|
|
460
|
+
const now = performance.now();
|
|
461
|
+
const wait = this._lastCommitPush + COMMIT_PUSH_MS - now;
|
|
462
|
+
if (wait <= 0) {
|
|
463
|
+
this._lastCommitPush = now;
|
|
464
|
+
this.flush();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (this._trailing) return;
|
|
468
|
+
this._trailing = setTimeout(() => {
|
|
469
|
+
this._trailing = null;
|
|
470
|
+
this._lastCommitPush = performance.now();
|
|
471
|
+
this.flush();
|
|
472
|
+
}, wait);
|
|
473
|
+
this._trailing.unref?.();
|
|
474
|
+
}
|
|
475
|
+
|
|
418
476
|
/** A window's HWND exists now, so what is owed for it can be pushed. */
|
|
419
477
|
windowReady(wnd) {
|
|
420
478
|
const win = this.toplevels.find((w) => w.window === wnd);
|
|
@@ -546,12 +604,12 @@ export class Win32Accessibility {
|
|
|
546
604
|
// Pushed now rather than at the next commit: a focus change is what a
|
|
547
605
|
// screen reader is waiting for, and the event has to follow an update
|
|
548
606
|
// that already carries the new state or the client reads the old one.
|
|
549
|
-
this.flush();
|
|
607
|
+
this.flush({ global: true });
|
|
550
608
|
const wnd = this._windowOf(win);
|
|
551
609
|
if (wnd && next) this._native.uiaFocusChanged(wnd.id, this._idOf(next));
|
|
552
610
|
};
|
|
553
611
|
hooks.windowFocus = (win) => this._dirty.add(win);
|
|
554
|
-
hooks.commit = () => this.
|
|
612
|
+
hooks.commit = () => this._commitFlush();
|
|
555
613
|
hooks.announce = (text, opts) => {
|
|
556
614
|
const win =
|
|
557
615
|
this.toplevels.find((w) => w.events?.windowFocused) ??
|
|
@@ -562,7 +620,7 @@ export class Win32Accessibility {
|
|
|
562
620
|
// The window must be in the mirror before anything can be announced
|
|
563
621
|
// from it, which on the first announcement of a session it may not be.
|
|
564
622
|
this._dirty.add(win);
|
|
565
|
-
this.flush();
|
|
623
|
+
this.flush({ global: true });
|
|
566
624
|
return Boolean(
|
|
567
625
|
this._native.uiaAnnounce(wnd.id, String(text), !opts?.assertive),
|
|
568
626
|
);
|
|
@@ -579,6 +637,8 @@ export class Win32Accessibility {
|
|
|
579
637
|
bury() {
|
|
580
638
|
if (this.dead) return;
|
|
581
639
|
this.dead = true;
|
|
640
|
+
if (this._trailing) clearTimeout(this._trailing);
|
|
641
|
+
this._trailing = null;
|
|
582
642
|
for (const key of Object.keys(hooks)) hooks[key] = null;
|
|
583
643
|
for (const unsubscribe of this._unsubscribes) unsubscribe();
|
|
584
644
|
this._unsubscribes = [];
|
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. */
|