react-x11 2.6.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cocoa/app.js +41 -19
- package/src/cocoa/context2d.js +271 -25
- package/src/screens.js +39 -4
package/package.json
CHANGED
package/src/cocoa/app.js
CHANGED
|
@@ -723,6 +723,46 @@ export class CocoaApp {
|
|
|
723
723
|
}
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
+
/**
|
|
727
|
+
* `listScreens()` turned into the screen layout `src/screens.js` publishes.
|
|
728
|
+
*
|
|
729
|
+
* **One scale for every screen, and it is the app's.** macOS lays all the
|
|
730
|
+
* displays out in a single global point space, and `app.scale` is this
|
|
731
|
+
* app's points-to-device-pixels factor for the whole of it — window
|
|
732
|
+
* origins, event coordinates, these rects. Converting a 1x external
|
|
733
|
+
* display by *its own* 1 while windows on it still report `points * 2`
|
|
734
|
+
* would put the monitor somewhere no window ever is, and `monitorAt()`
|
|
735
|
+
* would answer with the wrong head. (What backing scale a window on a
|
|
736
|
+
* mixed-DPI desk should raster at is a real and separate question; the
|
|
737
|
+
* layout is not where it is answered.)
|
|
738
|
+
*
|
|
739
|
+
* **A usable rect per monitor.** `NSScreen.visibleFrame` is per screen —
|
|
740
|
+
* that display's own menu bar and Dock taken off — so each monitor carries
|
|
741
|
+
* its `visible` and `usable()` takes it as a rect. Publishing only the
|
|
742
|
+
* primary's, the way `_NET_WORKAREA` forces on X11, applied the primary's
|
|
743
|
+
* *width* as a bound to every other head: a second display wider than the
|
|
744
|
+
* built-in had its right edge pulled in by the difference, and every
|
|
745
|
+
* anchored popup that reached past it was clamped back (issue #453).
|
|
746
|
+
*/
|
|
747
|
+
export function screenLayout(screens, scale) {
|
|
748
|
+
const rect = (r) => ({
|
|
749
|
+
x: Math.round(r.x * scale),
|
|
750
|
+
y: Math.round(r.y * scale),
|
|
751
|
+
width: Math.round(r.width * scale),
|
|
752
|
+
height: Math.round(r.height * scale),
|
|
753
|
+
});
|
|
754
|
+
const primary = screens?.[0];
|
|
755
|
+
return {
|
|
756
|
+
monitors: (screens ?? []).map((screen) => ({
|
|
757
|
+
...rect(screen),
|
|
758
|
+
...(screen.visible ? { visible: rect(screen.visible) } : null),
|
|
759
|
+
})),
|
|
760
|
+
// Still published for `useScreens().workArea`, which is one rect for
|
|
761
|
+
// the desktop by definition; the primary's is the closest macOS has.
|
|
762
|
+
workArea: primary?.visible ? rect(primary.visible) : null,
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
726
766
|
/**
|
|
727
767
|
* Build the app and seed the platform stores the way the mock seeds them —
|
|
728
768
|
* `beginScale`/`beginScreens`/`beginCompositing` find a session already
|
|
@@ -733,25 +773,7 @@ export async function createCocoaApp(options = {}) {
|
|
|
733
773
|
const app = new CocoaApp(native, options);
|
|
734
774
|
|
|
735
775
|
setScaleForTests(app, app.scale, 'cocoa');
|
|
736
|
-
|
|
737
|
-
const monitors = app._screens.map((screen) => ({
|
|
738
|
-
x: Math.round(screen.x * s),
|
|
739
|
-
y: Math.round(screen.y * s),
|
|
740
|
-
width: Math.round(screen.width * s),
|
|
741
|
-
height: Math.round(screen.height * s),
|
|
742
|
-
}));
|
|
743
|
-
const primary = app._screens[0];
|
|
744
|
-
setScreensForTests(app, {
|
|
745
|
-
monitors,
|
|
746
|
-
workArea: primary
|
|
747
|
-
? {
|
|
748
|
-
x: Math.round(primary.visible.x * s),
|
|
749
|
-
y: Math.round(primary.visible.y * s),
|
|
750
|
-
width: Math.round(primary.visible.width * s),
|
|
751
|
-
height: Math.round(primary.visible.height * s),
|
|
752
|
-
}
|
|
753
|
-
: null,
|
|
754
|
-
});
|
|
776
|
+
setScreensForTests(app, screenLayout(app._screens, app.scale));
|
|
755
777
|
setCompositingForTests(app, true);
|
|
756
778
|
|
|
757
779
|
app.start(options.cocoa ?? {});
|
package/src/cocoa/context2d.js
CHANGED
|
@@ -92,6 +92,61 @@ const clamp01 = (v) => Math.min(1, Math.max(0, Number(v) || 0));
|
|
|
92
92
|
const PICT_OP = Object.freeze({ Src: 1, Over: 3 });
|
|
93
93
|
const RENDER = Object.freeze({ PictOp: PICT_OP });
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The path, recorded alongside the native one, so `stroke` can re-issue it
|
|
97
|
+
* in pieces — `CGContextStrokePath` is QUADRATIC in the number of subpaths
|
|
98
|
+
* in the path it is given (issue #456). Measured here, 13-vertex closed
|
|
99
|
+
* rings scattered over a 1024x1024 surface at a 2px line, one stroke call:
|
|
100
|
+
*
|
|
101
|
+
* 500 rings 20ms | 1000 rings 59ms | 2000 rings 208ms | 4000 rings 986ms
|
|
102
|
+
*
|
|
103
|
+
* Splitting the same geometry into strokes of a few hundred subpaths is
|
|
104
|
+
* linear in it: 14ms, 29ms, 56ms, 113ms. The driver is the subpath count,
|
|
105
|
+
* not the vertex count — one 26,000-vertex subpath strokes in 4ms where
|
|
106
|
+
* two thousand 13-vertex ones take 208. The full table, the two shapes
|
|
107
|
+
* left whole and why, are in docs/macos.md
|
|
108
|
+
* §"Stroking a path with many subpaths".
|
|
109
|
+
*
|
|
110
|
+
* Note the X11 context wants the opposite — there a stroke is an a8
|
|
111
|
+
* coverage mask over the path's bounding box uploaded with one PutImage,
|
|
112
|
+
* so a bigger path is fewer uploads over the same pixels. That is why this
|
|
113
|
+
* lives in the backend: a caller that batches for one backend pessimizes
|
|
114
|
+
* the other, and it cannot know which it is drawing on.
|
|
115
|
+
*
|
|
116
|
+
* The commands are a flat number array — `[op, ...args, op, ...args]` —
|
|
117
|
+
* reused across paths, so a path build is three pushes into a packed
|
|
118
|
+
* double array per point next to the napi call it already makes.
|
|
119
|
+
*/
|
|
120
|
+
const P_MOVE = 0;
|
|
121
|
+
const P_LINE = 1;
|
|
122
|
+
const P_CURVE = 2;
|
|
123
|
+
const P_QUAD = 3;
|
|
124
|
+
const P_CLOSE = 4;
|
|
125
|
+
const P_RECT = 5;
|
|
126
|
+
const P_ROUND = 6;
|
|
127
|
+
const P_ARC = 7;
|
|
128
|
+
const P_ELLIPSE = 8;
|
|
129
|
+
/** how many numbers each op carries, and what it costs a chunk's budget */
|
|
130
|
+
const P_ARGS = [2, 2, 6, 4, 0, 4, 8, 6, 4];
|
|
131
|
+
const P_POINTS = [1, 1, 3, 2, 0, 4, 8, 8, 4];
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A chunk closes at the first subpath boundary past either budget. Swept
|
|
135
|
+
* over the shapes above: 512 points is within 5% of the best chunk for
|
|
136
|
+
* every one of them, and the subpath cap catches the degenerate shape the
|
|
137
|
+
* point budget misses — thousands of 3- and 4-point subpaths, where 512
|
|
138
|
+
* points is already 128 strokes' worth of setup.
|
|
139
|
+
*/
|
|
140
|
+
const STROKE_CHUNK_POINTS = 512;
|
|
141
|
+
const STROKE_CHUNK_SUBPATHS = 128;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The off switch, for a process that cannot reach the context — the same
|
|
145
|
+
* line `ctx.strokeChunking = false` draws, and what a bench comparing the
|
|
146
|
+
* two sets. Read once.
|
|
147
|
+
*/
|
|
148
|
+
const NO_STROKE_CHUNKING = process.env.REACT_X11_NO_STROKE_CHUNKING === '1';
|
|
149
|
+
|
|
95
150
|
/**
|
|
96
151
|
* A solid ink for `drawGlyphs` — ntk's `createSolidPicture` answers an
|
|
97
152
|
* XRender picture; here it is the colour itself. Premultiplied 0..1 in, as
|
|
@@ -136,6 +191,11 @@ export class CocoaContext2D {
|
|
|
136
191
|
ctm: [1, 0, 0, 1, 0, 0],
|
|
137
192
|
};
|
|
138
193
|
this._onDirty = null;
|
|
194
|
+
// the recorded path, and whether the native one still matches it (a
|
|
195
|
+
// chunked stroke leaves only its last chunk behind)
|
|
196
|
+
this._cmds = [];
|
|
197
|
+
this._pathStale = false;
|
|
198
|
+
this._strokeChunking = !NO_STROKE_CHUNKING;
|
|
139
199
|
}
|
|
140
200
|
|
|
141
201
|
_s() {
|
|
@@ -152,6 +212,10 @@ export class CocoaContext2D {
|
|
|
152
212
|
n.ctxSetGlobalAlpha(surface, st.globalAlpha);
|
|
153
213
|
n.ctxSetLineDash(surface, st.dash, st.dashOffset);
|
|
154
214
|
this._stack.length = 0;
|
|
215
|
+
// the path went with the surface it was built on; nothing may
|
|
216
|
+
// replay it onto the new one
|
|
217
|
+
this._cmds.length = 0;
|
|
218
|
+
this._pathStale = false;
|
|
155
219
|
}
|
|
156
220
|
return surface;
|
|
157
221
|
}
|
|
@@ -218,6 +282,26 @@ export class CocoaContext2D {
|
|
|
218
282
|
}
|
|
219
283
|
}
|
|
220
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Whether a stroke of a path with many subpaths may go out as several
|
|
287
|
+
* `CGContextStrokePath` calls — on by default, because the alternative
|
|
288
|
+
* is quadratic (see P_MOVE above) and every path small enough for the
|
|
289
|
+
* difference to be invisible is below the threshold anyway.
|
|
290
|
+
*
|
|
291
|
+
* Set it false for a path whose subpaths OVERLAP and whose seams have to
|
|
292
|
+
* composite exactly: chunked, a pixel the strokes of two subpaths each
|
|
293
|
+
* half cover is inked twice at half coverage rather than once at full,
|
|
294
|
+
* and reads a little lighter. `REACT_X11_NO_STROKE_CHUNKING=1` is the
|
|
295
|
+
* same switch for a whole process.
|
|
296
|
+
*/
|
|
297
|
+
get strokeChunking() {
|
|
298
|
+
return this._strokeChunking;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
set strokeChunking(value) {
|
|
302
|
+
this._strokeChunking = !!value;
|
|
303
|
+
}
|
|
304
|
+
|
|
221
305
|
get font() {
|
|
222
306
|
return this._state.font;
|
|
223
307
|
}
|
|
@@ -389,20 +473,96 @@ export class CocoaContext2D {
|
|
|
389
473
|
|
|
390
474
|
// --- paths ---------------------------------------------------------------
|
|
391
475
|
|
|
476
|
+
/**
|
|
477
|
+
* The surface to build or paint the current path on, with the native
|
|
478
|
+
* path restored first if a chunked stroke consumed it. Lazy on purpose:
|
|
479
|
+
* a caller that strokes and then starts a new path — which is every
|
|
480
|
+
* caller in a paint loop — never pays for the rebuild.
|
|
481
|
+
*/
|
|
482
|
+
_path() {
|
|
483
|
+
const surface = this._s();
|
|
484
|
+
if (this._pathStale) {
|
|
485
|
+
this._pathStale = false;
|
|
486
|
+
this._native.ctxBeginPath(surface);
|
|
487
|
+
this._emit(surface, 0, this._cmds.length);
|
|
488
|
+
}
|
|
489
|
+
return surface;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** replay recorded commands `[from, to)` into the native path */
|
|
493
|
+
_emit(surface, from, to) {
|
|
494
|
+
const c = this._cmds;
|
|
495
|
+
const n = this._native;
|
|
496
|
+
for (let i = from; i < to;) {
|
|
497
|
+
const op = c[i];
|
|
498
|
+
const a = i + 1;
|
|
499
|
+
if (op === P_MOVE) n.ctxMoveTo(surface, c[a], c[a + 1]);
|
|
500
|
+
else if (op === P_LINE) n.ctxLineTo(surface, c[a], c[a + 1]);
|
|
501
|
+
else if (op === P_CURVE)
|
|
502
|
+
n.ctxCurveTo(
|
|
503
|
+
surface,
|
|
504
|
+
c[a],
|
|
505
|
+
c[a + 1],
|
|
506
|
+
c[a + 2],
|
|
507
|
+
c[a + 3],
|
|
508
|
+
c[a + 4],
|
|
509
|
+
c[a + 5],
|
|
510
|
+
);
|
|
511
|
+
else if (op === P_QUAD)
|
|
512
|
+
n.ctxQuadTo(surface, c[a], c[a + 1], c[a + 2], c[a + 3]);
|
|
513
|
+
else if (op === P_CLOSE) n.ctxClosePath(surface);
|
|
514
|
+
else if (op === P_RECT)
|
|
515
|
+
n.ctxRect(surface, c[a], c[a + 1], c[a + 2], c[a + 3]);
|
|
516
|
+
else if (op === P_ROUND)
|
|
517
|
+
n.ctxRoundRect(
|
|
518
|
+
surface,
|
|
519
|
+
c[a],
|
|
520
|
+
c[a + 1],
|
|
521
|
+
c[a + 2],
|
|
522
|
+
c[a + 3],
|
|
523
|
+
c[a + 4],
|
|
524
|
+
c[a + 5],
|
|
525
|
+
c[a + 6],
|
|
526
|
+
c[a + 7],
|
|
527
|
+
);
|
|
528
|
+
else if (op === P_ARC)
|
|
529
|
+
n.ctxArc(
|
|
530
|
+
surface,
|
|
531
|
+
c[a],
|
|
532
|
+
c[a + 1],
|
|
533
|
+
c[a + 2],
|
|
534
|
+
c[a + 3],
|
|
535
|
+
c[a + 4],
|
|
536
|
+
!!c[a + 5],
|
|
537
|
+
);
|
|
538
|
+
else if (op === P_ELLIPSE)
|
|
539
|
+
n.ctxEllipse(surface, c[a], c[a + 1], c[a + 2], c[a + 3]);
|
|
540
|
+
i += 1 + P_ARGS[op];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
392
544
|
beginPath() {
|
|
545
|
+
this._cmds.length = 0;
|
|
546
|
+
this._pathStale = false;
|
|
393
547
|
this._native.ctxBeginPath(this._s());
|
|
394
548
|
}
|
|
395
549
|
|
|
396
550
|
moveTo(x, y) {
|
|
397
|
-
this.
|
|
551
|
+
const surface = this._path();
|
|
552
|
+
this._cmds.push(P_MOVE, x, y);
|
|
553
|
+
this._native.ctxMoveTo(surface, x, y);
|
|
398
554
|
}
|
|
399
555
|
|
|
400
556
|
lineTo(x, y) {
|
|
401
|
-
this.
|
|
557
|
+
const surface = this._path();
|
|
558
|
+
this._cmds.push(P_LINE, x, y);
|
|
559
|
+
this._native.ctxLineTo(surface, x, y);
|
|
402
560
|
}
|
|
403
561
|
|
|
404
562
|
rect(x, y, w, h) {
|
|
405
|
-
this.
|
|
563
|
+
const surface = this._path();
|
|
564
|
+
this._cmds.push(P_RECT, x, y, w, h);
|
|
565
|
+
this._native.ctxRect(surface, x, y, w, h);
|
|
406
566
|
}
|
|
407
567
|
|
|
408
568
|
roundRect(x, y, w, h, radii) {
|
|
@@ -413,37 +573,45 @@ export class CocoaContext2D {
|
|
|
413
573
|
else if (r.length === 3) r = [r[0], r[1], r[2], r[1]];
|
|
414
574
|
const cap = Math.min(Math.abs(w) / 2, Math.abs(h) / 2);
|
|
415
575
|
const clamp = (v) => Math.max(0, Math.min(Number(v) || 0, cap));
|
|
416
|
-
this.
|
|
417
|
-
|
|
418
|
-
x,
|
|
419
|
-
y,
|
|
420
|
-
w,
|
|
421
|
-
h,
|
|
576
|
+
const surface = this._path();
|
|
577
|
+
const [r0, r1, r2, r3] = [
|
|
422
578
|
clamp(r[0]),
|
|
423
579
|
clamp(r[1]),
|
|
424
580
|
clamp(r[2]),
|
|
425
581
|
clamp(r[3]),
|
|
426
|
-
|
|
582
|
+
];
|
|
583
|
+
this._cmds.push(P_ROUND, x, y, w, h, r0, r1, r2, r3);
|
|
584
|
+
this._native.ctxRoundRect(surface, x, y, w, h, r0, r1, r2, r3);
|
|
427
585
|
}
|
|
428
586
|
|
|
429
587
|
arc(x, y, radius, start, end, anticlockwise = false) {
|
|
430
|
-
this.
|
|
588
|
+
const surface = this._path();
|
|
589
|
+
this._cmds.push(P_ARC, x, y, radius, start, end, anticlockwise ? 1 : 0);
|
|
590
|
+
this._native.ctxArc(surface, x, y, radius, start, end, anticlockwise);
|
|
431
591
|
}
|
|
432
592
|
|
|
433
593
|
ellipse(x, y, rx, ry) {
|
|
434
|
-
this.
|
|
594
|
+
const surface = this._path();
|
|
595
|
+
this._cmds.push(P_ELLIPSE, x, y, rx, ry);
|
|
596
|
+
this._native.ctxEllipse(surface, x, y, rx, ry);
|
|
435
597
|
}
|
|
436
598
|
|
|
437
599
|
bezierCurveTo(c1x, c1y, c2x, c2y, x, y) {
|
|
438
|
-
this.
|
|
600
|
+
const surface = this._path();
|
|
601
|
+
this._cmds.push(P_CURVE, c1x, c1y, c2x, c2y, x, y);
|
|
602
|
+
this._native.ctxCurveTo(surface, c1x, c1y, c2x, c2y, x, y);
|
|
439
603
|
}
|
|
440
604
|
|
|
441
605
|
quadraticCurveTo(cx, cy, x, y) {
|
|
442
|
-
this.
|
|
606
|
+
const surface = this._path();
|
|
607
|
+
this._cmds.push(P_QUAD, cx, cy, x, y);
|
|
608
|
+
this._native.ctxQuadTo(surface, cx, cy, x, y);
|
|
443
609
|
}
|
|
444
610
|
|
|
445
611
|
closePath() {
|
|
446
|
-
this.
|
|
612
|
+
const surface = this._path();
|
|
613
|
+
this._cmds.push(P_CLOSE);
|
|
614
|
+
this._native.ctxClosePath(surface);
|
|
447
615
|
}
|
|
448
616
|
|
|
449
617
|
// --- painting ------------------------------------------------------------
|
|
@@ -477,24 +645,95 @@ export class CocoaContext2D {
|
|
|
477
645
|
_replayPath(path) {
|
|
478
646
|
const cmds = path?._cmds;
|
|
479
647
|
if (!Array.isArray(cmds)) return false;
|
|
480
|
-
|
|
481
|
-
const s = this._s();
|
|
482
|
-
n.ctxBeginPath(s);
|
|
648
|
+
this.beginPath();
|
|
483
649
|
for (const c of cmds) {
|
|
484
|
-
if (c.type === 'M')
|
|
485
|
-
else if (c.type === 'L')
|
|
650
|
+
if (c.type === 'M') this.moveTo(c.x, c.y);
|
|
651
|
+
else if (c.type === 'L') this.lineTo(c.x, c.y);
|
|
486
652
|
else if (c.type === 'C')
|
|
487
|
-
|
|
488
|
-
else if (c.type === 'Q')
|
|
489
|
-
else if (c.type === 'Z')
|
|
653
|
+
this.bezierCurveTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y);
|
|
654
|
+
else if (c.type === 'Q') this.quadraticCurveTo(c.x1, c.y1, c.x, c.y);
|
|
655
|
+
else if (c.type === 'Z') this.closePath();
|
|
490
656
|
}
|
|
491
657
|
return true;
|
|
492
658
|
}
|
|
493
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Whether a stroke of the current path may be split into several
|
|
662
|
+
* `CGContextStrokePath` calls. Two things say no:
|
|
663
|
+
*
|
|
664
|
+
* - A hairline. At or below a device-space width of 1 CoreGraphics
|
|
665
|
+
* strokes through a path that is already linear in the subpath count
|
|
666
|
+
* — 2,000 rings cost 7ms whole — and splitting it is a 2x LOSS, the
|
|
667
|
+
* per-call setup with nothing to win back.
|
|
668
|
+
* - Anything that composites. Each chunk paints separately, so where two
|
|
669
|
+
* subpaths' strokes overlap, a translucent ink, a globalAlpha or a
|
|
670
|
+
* shadow blends twice and reads darker where one call blends the union
|
|
671
|
+
* once. An opaque ink is exact everywhere the coverage is full, which
|
|
672
|
+
* is what the geometry that gets big looks like; what is left is the
|
|
673
|
+
* antialiased fringe at those same overlaps, half-covered twice
|
|
674
|
+
* instead of covered once, which reads a little lighter — the one
|
|
675
|
+
* difference `strokeChunking` exists to turn off.
|
|
676
|
+
*/
|
|
677
|
+
_chunkableStroke() {
|
|
678
|
+
if (!this._strokeChunking) return false;
|
|
679
|
+
const st = this._state;
|
|
680
|
+
const [a, b, c, d] = st.ctm;
|
|
681
|
+
const scale = Math.sqrt(Math.abs(a * d - b * c));
|
|
682
|
+
if (!(st.lineWidth * scale > 1)) return false;
|
|
683
|
+
if (st.globalAlpha < 1) return false;
|
|
684
|
+
if (parseColor(st.strokeStyle)[3] < 1) return false;
|
|
685
|
+
if (st.shadowBlur > 0 && parseColor(st.shadowColor)[3] > 0) return false;
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Stroke the recorded path as a series of chunks, cut at subpath
|
|
691
|
+
* boundaries so every chunk carries the `moveTo` its segments start
|
|
692
|
+
* from. Answers false when there was nothing to split, leaving the
|
|
693
|
+
* native path untouched for the caller's single stroke.
|
|
694
|
+
*/
|
|
695
|
+
_strokeChunks(surface) {
|
|
696
|
+
if (!this._chunkableStroke()) return false;
|
|
697
|
+
const cmds = this._cmds;
|
|
698
|
+
const n = this._native;
|
|
699
|
+
let start = 0;
|
|
700
|
+
let points = 0;
|
|
701
|
+
let subpaths = 0;
|
|
702
|
+
let split = false;
|
|
703
|
+
for (let i = 0; i < cmds.length;) {
|
|
704
|
+
const op = cmds[i];
|
|
705
|
+
if (
|
|
706
|
+
op === P_MOVE &&
|
|
707
|
+
i > start &&
|
|
708
|
+
(points >= STROKE_CHUNK_POINTS || subpaths >= STROKE_CHUNK_SUBPATHS)
|
|
709
|
+
) {
|
|
710
|
+
n.ctxBeginPath(surface);
|
|
711
|
+
this._emit(surface, start, i);
|
|
712
|
+
n.ctxStroke(surface);
|
|
713
|
+
split = true;
|
|
714
|
+
start = i;
|
|
715
|
+
points = 0;
|
|
716
|
+
subpaths = 0;
|
|
717
|
+
}
|
|
718
|
+
if (op === P_MOVE) subpaths++;
|
|
719
|
+
points += P_POINTS[op];
|
|
720
|
+
i += 1 + P_ARGS[op];
|
|
721
|
+
}
|
|
722
|
+
if (!split) return false;
|
|
723
|
+
n.ctxBeginPath(surface);
|
|
724
|
+
this._emit(surface, start, cmds.length);
|
|
725
|
+
n.ctxStroke(surface);
|
|
726
|
+
// the native path is the last chunk now; _path() puts the whole one
|
|
727
|
+
// back if anything asks for it
|
|
728
|
+
this._pathStale = true;
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
|
|
494
732
|
fill(pathOrRule, maybeRule) {
|
|
495
733
|
const hasPath = pathOrRule != null && typeof pathOrRule === 'object';
|
|
496
734
|
const rule = hasPath ? maybeRule : pathOrRule;
|
|
497
735
|
if (hasPath && !this._replayPath(pathOrRule)) return;
|
|
736
|
+
this._path();
|
|
498
737
|
const style = this._state.fillStyle;
|
|
499
738
|
if (style instanceof LinearGradient) {
|
|
500
739
|
const { coords, flat } = style._normalized();
|
|
@@ -518,7 +757,14 @@ export class CocoaContext2D {
|
|
|
518
757
|
return;
|
|
519
758
|
}
|
|
520
759
|
this._applyStroke();
|
|
521
|
-
|
|
760
|
+
// one call per chunk where the path has enough subpaths to be worth it
|
|
761
|
+
// — see P_MOVE and _chunkableStroke above — and one for everything
|
|
762
|
+
// else. The chunks are issued from the record, so a stroke that splits
|
|
763
|
+
// never pays for the restore a previous split owed: `_path()` is asked
|
|
764
|
+
// for the surface only on the whole-path route.
|
|
765
|
+
if (!this._strokeChunks(this._s())) {
|
|
766
|
+
this._native.ctxStroke(this._path());
|
|
767
|
+
}
|
|
522
768
|
this._dirty();
|
|
523
769
|
}
|
|
524
770
|
|
|
@@ -530,7 +776,7 @@ export class CocoaContext2D {
|
|
|
530
776
|
) {
|
|
531
777
|
return;
|
|
532
778
|
}
|
|
533
|
-
this._native.ctxClip(this.
|
|
779
|
+
this._native.ctxClip(this._path());
|
|
534
780
|
}
|
|
535
781
|
|
|
536
782
|
fillRect(x, y, w, h) {
|
package/src/screens.js
CHANGED
|
@@ -72,12 +72,17 @@
|
|
|
72
72
|
* Letting the WM have the last word costs a clamped-to-the-edge window one
|
|
73
73
|
* correction it would have made anyway.
|
|
74
74
|
*
|
|
75
|
-
* **A per-monitor work area.** `_NET_WORKAREA` is one rect for the
|
|
76
|
-
* virtual desktop. Deriving a real per-monitor one means reading
|
|
75
|
+
* **A per-monitor work area, on X11.** `_NET_WORKAREA` is one rect for the
|
|
76
|
+
* whole virtual desktop. Deriving a real per-monitor one means reading
|
|
77
77
|
* `_NET_WM_STRUT_PARTIAL` off every window on the screen and intersecting
|
|
78
78
|
* the reservations that fall on each head — a full window-tree walk, redone
|
|
79
79
|
* whenever any panel changes. `available` below is the per-axis
|
|
80
80
|
* approximation instead, and says so.
|
|
81
|
+
*
|
|
82
|
+
* A backend that *does* know each monitor's usable rect says so directly
|
|
83
|
+
* instead: a `visible` rect on the monitor record, which `usable()` prefers
|
|
84
|
+
* over the whole-desktop compromise. Cocoa's `NSScreen.visibleFrame` is
|
|
85
|
+
* exactly that, so the macOS backend never goes through the approximation.
|
|
81
86
|
*/
|
|
82
87
|
|
|
83
88
|
import { requireExtension } from './extensions.js';
|
|
@@ -206,9 +211,36 @@ function monitorAt(monitors, point) {
|
|
|
206
211
|
return best;
|
|
207
212
|
}
|
|
208
213
|
|
|
214
|
+
/** The overlap of two rects, or `null` where they do not touch. */
|
|
215
|
+
function intersect(a, b) {
|
|
216
|
+
const x0 = Math.max(a.x, b.x);
|
|
217
|
+
const y0 = Math.max(a.y, b.y);
|
|
218
|
+
const x1 = Math.min(a.x + a.width, b.x + b.width);
|
|
219
|
+
const y1 = Math.min(a.y + a.height, b.y + b.height);
|
|
220
|
+
if (x1 <= x0 || y1 <= y0) return null;
|
|
221
|
+
return { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
|
|
222
|
+
}
|
|
223
|
+
|
|
209
224
|
/**
|
|
210
|
-
* The
|
|
211
|
-
*
|
|
225
|
+
* The usable part of one monitor.
|
|
226
|
+
*
|
|
227
|
+
* Two ways to get there, and the first one is the real answer:
|
|
228
|
+
*
|
|
229
|
+
* - A monitor record may carry its **own** usable rect as `visible`, in the
|
|
230
|
+
* same screen coordinates as the monitor itself. That is what Cocoa's
|
|
231
|
+
* `NSScreen.visibleFrame` is — per screen, minus that screen's menu bar
|
|
232
|
+
* and Dock — and it is taken as a rect, intersected with the monitor in
|
|
233
|
+
* case a backend reports one that overhangs.
|
|
234
|
+
* - Otherwise the monitor rect clamped **per axis** by `_NET_WORKAREA`,
|
|
235
|
+
* which is one rect for the whole virtual desktop and so cannot be
|
|
236
|
+
* positioned against a single head — see the note at the top of the file.
|
|
237
|
+
*
|
|
238
|
+
* The distinction matters the moment the monitors differ in size. A global
|
|
239
|
+
* work area no wider than the primary, applied as a width bound to a wider
|
|
240
|
+
* second display, moves that display's right edge inward by the difference
|
|
241
|
+
* and every anchored popup with it. On X11 the property spans the virtual
|
|
242
|
+
* desktop and the clamp is a no-op on the width, which is why the
|
|
243
|
+
* approximation held there and only there.
|
|
212
244
|
*
|
|
213
245
|
* Always **only** a rect. A monitor record carries a name, a primary flag and
|
|
214
246
|
* physical sizes as well, and spreading it here put all of that inside
|
|
@@ -221,6 +253,7 @@ function usable(monitor, work) {
|
|
|
221
253
|
width: monitor.width,
|
|
222
254
|
height: monitor.height,
|
|
223
255
|
};
|
|
256
|
+
if (monitor.visible) return intersect(rect, monitor.visible) ?? rect;
|
|
224
257
|
if (!work) return rect;
|
|
225
258
|
rect.width = Math.min(rect.width, work.width);
|
|
226
259
|
rect.height = Math.min(rect.height, work.height);
|
|
@@ -756,6 +789,8 @@ export function endScreens(app) {
|
|
|
756
789
|
* `monitors` entries may carry the RandR fields (`name`, `primary`,
|
|
757
790
|
* `widthMM`, `heightMM`, `refreshRate`, `rotation`) as well as the rect, so
|
|
758
791
|
* a test can state a named two-head desktop without a server that has RandR.
|
|
792
|
+
* An entry may also carry its own `visible` rect — the monitor's usable
|
|
793
|
+
* area, which takes precedence over the whole-desktop `workArea`.
|
|
759
794
|
*/
|
|
760
795
|
export function setScreensForTests(app, { monitors = null, workArea = null }) {
|
|
761
796
|
let session = sessions.get(app);
|