react-x11 2.24.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/backend/context2d.js +368 -20
- package/src/backend/shadowtiles.js +84 -0
- package/src/compositing.js +21 -1
- package/src/events.js +42 -0
- package/src/node.d.ts +20 -1
- package/src/nodes/boxpaint.js +32 -0
- package/src/nodes/invalidate.js +23 -0
- package/src/nodes/node.js +6 -1
- package/src/nodes/paint.js +38 -13
- package/src/nodes/scrollable.js +16 -6
- package/src/nodes/scrollblit.js +38 -1
- package/src/nodes/text.js +80 -3
- package/src/nodes/window/floors.js +1 -1
- package/src/nodes/window/flush.js +13 -1
- package/src/nodes/window/size.js +56 -32
- package/src/nodes/window/spine.js +143 -10
- package/src/nodes/window/window.js +6 -0
- package/src/xsettings.js +29 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.25.0",
|
|
4
4
|
"description": "react renderer with X11 as a target",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -103,15 +103,15 @@
|
|
|
103
103
|
"dependencies": {
|
|
104
104
|
"ical.js": "^2.2.1",
|
|
105
105
|
"linebreak": "^1.1.0",
|
|
106
|
-
"ntk": "^8.
|
|
106
|
+
"ntk": "^8.16.1",
|
|
107
107
|
"react-reconciler": "^0.34.0",
|
|
108
108
|
"yoga-layout": "^3.2.1"
|
|
109
109
|
},
|
|
110
110
|
"optionalDependencies": {
|
|
111
|
-
"@windowkit/appkit": "^0.
|
|
111
|
+
"@windowkit/appkit": "^0.17.0",
|
|
112
112
|
"@windowkit/wayland": "^3.1.1",
|
|
113
|
-
"@windowkit/win32": "^0.0.
|
|
114
|
-
"dbus-native": "^0.
|
|
113
|
+
"@windowkit/win32": "^0.0.6",
|
|
114
|
+
"dbus-native": "^0.16.0",
|
|
115
115
|
"x11-dri": "^0.9.0"
|
|
116
116
|
},
|
|
117
117
|
"peerDependencies": {
|
package/src/backend/context2d.js
CHANGED
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
// resize — `_gen` is that generation.
|
|
26
26
|
import { cssColorStraight } from 'ntk/color';
|
|
27
27
|
|
|
28
|
+
import { normalizeRadii, planShadowTiles } from 'ntk/shadow-tiles';
|
|
29
|
+
|
|
30
|
+
import { shadowTilesFor } from './shadowtiles.js';
|
|
31
|
+
|
|
28
32
|
const BLACK = [0, 0, 0, 1];
|
|
29
33
|
|
|
30
34
|
// A colour string is parsed once: a frame over a large tree sets the same
|
|
@@ -244,9 +248,56 @@ const P_RECT = 5;
|
|
|
244
248
|
const P_ROUND = 6;
|
|
245
249
|
const P_ARC = 7;
|
|
246
250
|
const P_ELLIPSE = 8;
|
|
251
|
+
/** A rounded rect whose corners are elliptical: x y w h, then each corner's
|
|
252
|
+
* x and y radius, top left first. A bridge answering `ctxRoundRectXY`
|
|
253
|
+
* reads it in `ctxPath` too; one that does not is sent the curves. */
|
|
254
|
+
const P_ROUND_XY = 9;
|
|
247
255
|
/** how many numbers each op carries, and what it costs a chunk's budget */
|
|
248
|
-
const P_ARGS = [2, 2, 6, 4, 0, 4, 8, 6, 4];
|
|
249
|
-
const P_POINTS = [1, 1, 3, 2, 0, 4, 8, 8, 4];
|
|
256
|
+
const P_ARGS = [2, 2, 6, 4, 0, 4, 8, 6, 4, 12];
|
|
257
|
+
const P_POINTS = [1, 1, 3, 2, 0, 4, 8, 8, 4, 8];
|
|
258
|
+
|
|
259
|
+
/** A quarter ellipse as one cubic: how far along each tangent its handles
|
|
260
|
+
* reach, as a fraction of the radius. */
|
|
261
|
+
const KAPPA = 0.5522847498307936;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* An elliptical-cornered rounded rect as lines and curves, through `to` —
|
|
265
|
+
* a bridge with no `ctxRoundRectXY`. `c[a]…` is the op's twelve numbers.
|
|
266
|
+
*/
|
|
267
|
+
function roundRectCurves(to, c, a) {
|
|
268
|
+
const x = c[a];
|
|
269
|
+
const y = c[a + 1];
|
|
270
|
+
const r = x + c[a + 2];
|
|
271
|
+
const b = y + c[a + 3];
|
|
272
|
+
const [tlx, tly, trx, tRy, brx, bry, blx, bly] = [
|
|
273
|
+
c[a + 4],
|
|
274
|
+
c[a + 5],
|
|
275
|
+
c[a + 6],
|
|
276
|
+
c[a + 7],
|
|
277
|
+
c[a + 8],
|
|
278
|
+
c[a + 9],
|
|
279
|
+
c[a + 10],
|
|
280
|
+
c[a + 11],
|
|
281
|
+
];
|
|
282
|
+
to.move(x + tlx, y);
|
|
283
|
+
to.line(r - trx, y);
|
|
284
|
+
if (trx > 0 && tRy > 0) {
|
|
285
|
+
to.curve(r - trx * (1 - KAPPA), y, r, y + tRy * (1 - KAPPA), r, y + tRy);
|
|
286
|
+
}
|
|
287
|
+
to.line(r, b - bry);
|
|
288
|
+
if (brx > 0 && bry > 0) {
|
|
289
|
+
to.curve(r, b - bry * (1 - KAPPA), r - brx * (1 - KAPPA), b, r - brx, b);
|
|
290
|
+
}
|
|
291
|
+
to.line(x + blx, b);
|
|
292
|
+
if (blx > 0 && bly > 0) {
|
|
293
|
+
to.curve(x + blx * (1 - KAPPA), b, x, b - bly * (1 - KAPPA), x, b - bly);
|
|
294
|
+
}
|
|
295
|
+
to.line(x, y + tly);
|
|
296
|
+
if (tlx > 0 && tly > 0) {
|
|
297
|
+
to.curve(x, y + tly * (1 - KAPPA), x + tlx * (1 - KAPPA), y, x + tlx, y);
|
|
298
|
+
}
|
|
299
|
+
to.close();
|
|
300
|
+
}
|
|
250
301
|
|
|
251
302
|
/**
|
|
252
303
|
* A chunk closes at the first subpath boundary past either budget. Swept
|
|
@@ -265,6 +316,77 @@ const STROKE_CHUNK_SUBPATHS = 128;
|
|
|
265
316
|
*/
|
|
266
317
|
const NO_STROKE_CHUNKING = process.env.REACT_X11_NO_STROKE_CHUNKING === '1';
|
|
267
318
|
|
|
319
|
+
const SQUARE = Object.freeze([
|
|
320
|
+
{ x: 0, y: 0 },
|
|
321
|
+
{ x: 0, y: 0 },
|
|
322
|
+
{ x: 0, y: 0 },
|
|
323
|
+
{ x: 0, y: 0 },
|
|
324
|
+
]);
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The shape the recorded op at `i` draws, where it is one a shadow tile
|
|
328
|
+
* knows — a `rect`, or a `roundRect` with circular or elliptical corners —
|
|
329
|
+
* as `{ x, y, w, h, corners, end }`, `end` the index of the op after it;
|
|
330
|
+
* null for any other op.
|
|
331
|
+
*/
|
|
332
|
+
function shapeAt(c, i) {
|
|
333
|
+
const op = c[i];
|
|
334
|
+
const end = i + 1 + P_ARGS[op];
|
|
335
|
+
if (op === P_RECT) {
|
|
336
|
+
let [x, y, w, h] = [c[i + 1], c[i + 2], c[i + 3], c[i + 4]];
|
|
337
|
+
if (w < 0) ((x += w), (w = -w));
|
|
338
|
+
if (h < 0) ((y += h), (h = -h));
|
|
339
|
+
return { x, y, w, h, corners: SQUARE, end };
|
|
340
|
+
}
|
|
341
|
+
if (op === P_ROUND) {
|
|
342
|
+
const r = (v) => ({ x: v, y: v });
|
|
343
|
+
return {
|
|
344
|
+
x: c[i + 1],
|
|
345
|
+
y: c[i + 2],
|
|
346
|
+
w: c[i + 3],
|
|
347
|
+
h: c[i + 4],
|
|
348
|
+
corners: [r(c[i + 5]), r(c[i + 6]), r(c[i + 7]), r(c[i + 8])],
|
|
349
|
+
end,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (op === P_ROUND_XY) {
|
|
353
|
+
const r = (j) => ({ x: c[i + j], y: c[i + j + 1] });
|
|
354
|
+
return {
|
|
355
|
+
x: c[i + 1],
|
|
356
|
+
y: c[i + 2],
|
|
357
|
+
w: c[i + 3],
|
|
358
|
+
h: c[i + 4],
|
|
359
|
+
corners: [r(5), r(7), r(9), r(11)],
|
|
360
|
+
end,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Whether shape `b` lies inside shape `a`'s box. */
|
|
367
|
+
function contains(a, b) {
|
|
368
|
+
return (
|
|
369
|
+
b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Whether two `{ x, y, width, height }` rects share any area. */
|
|
374
|
+
function meets(a, b) {
|
|
375
|
+
return (
|
|
376
|
+
a.x < b.x + b.width &&
|
|
377
|
+
b.x < a.x + a.width &&
|
|
378
|
+
a.y < b.y + b.height &&
|
|
379
|
+
b.y < a.y + a.height
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The off switch for shadow tiles (src/backend/shadowtiles.js): every
|
|
385
|
+
* shadow drawn by the bridge, as before them. For measuring the tiles
|
|
386
|
+
* against the live blur, and as first aid. Read once.
|
|
387
|
+
*/
|
|
388
|
+
const NO_SHADOW_TILES = process.env.REACT_X11_NO_SHADOW_TILES === '1';
|
|
389
|
+
|
|
268
390
|
/**
|
|
269
391
|
* A solid ink for `drawGlyphs` — ntk's `createSolidPicture` answers an
|
|
270
392
|
* XRender picture; here it is the colour itself. Premultiplied 0..1 in, as
|
|
@@ -340,6 +462,13 @@ export class BackendContext2D {
|
|
|
340
462
|
'ctxPath' in native && typeof native.ctxPath === 'function';
|
|
341
463
|
this._pathBuf = null;
|
|
342
464
|
this._strokeChunking = !NO_STROKE_CHUNKING;
|
|
465
|
+
// Elliptical corners go over whole where the bridge takes them; the
|
|
466
|
+
// count of them in the recorded path is what tells `_emit` whether a
|
|
467
|
+
// whole path handed to `ctxPath` needs them spelled as curves first.
|
|
468
|
+
this._roundXY =
|
|
469
|
+
'ctxRoundRectXY' in native && typeof native.ctxRoundRectXY === 'function';
|
|
470
|
+
this._xyOps = 0;
|
|
471
|
+
this._shadowTiles = !NO_SHADOW_TILES;
|
|
343
472
|
}
|
|
344
473
|
|
|
345
474
|
/**
|
|
@@ -388,6 +517,7 @@ export class BackendContext2D {
|
|
|
388
517
|
// the path went with the surface it was built on; nothing may
|
|
389
518
|
// replay it onto the new one
|
|
390
519
|
this._cmds.length = 0;
|
|
520
|
+
this._xyOps = 0;
|
|
391
521
|
this._pathStale = false;
|
|
392
522
|
}
|
|
393
523
|
return surface;
|
|
@@ -745,6 +875,11 @@ export class BackendContext2D {
|
|
|
745
875
|
const c = this._cmds;
|
|
746
876
|
const n = this._native;
|
|
747
877
|
if (this._bulkPaths) {
|
|
878
|
+
if (this._xyOps > 0 && !this._roundXY) {
|
|
879
|
+
// a bridge that does not read op 9 would stop at it
|
|
880
|
+
n.ctxPath(surface, this._spelledPath(from, to));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
748
883
|
// one call, through a buffer kept between paths — the record is the
|
|
749
884
|
// same stream the bridge parses, arguments and all
|
|
750
885
|
const count = to - from;
|
|
@@ -803,12 +938,36 @@ export class BackendContext2D {
|
|
|
803
938
|
);
|
|
804
939
|
else if (op === P_ELLIPSE)
|
|
805
940
|
n.ctxEllipse(surface, c[a], c[a + 1], c[a + 2], c[a + 3]);
|
|
941
|
+
else if (op === P_ROUND_XY) this._emitRoundXY(surface, c, a);
|
|
806
942
|
i += 1 + P_ARGS[op];
|
|
807
943
|
}
|
|
808
944
|
}
|
|
809
945
|
|
|
946
|
+
/** The record `[from, to)` as a stream for a bridge that does not know
|
|
947
|
+
* op 9: each elliptical rounded rect spelled as the lines and curves
|
|
948
|
+
* it is, everything else as it was. */
|
|
949
|
+
_spelledPath(from, to) {
|
|
950
|
+
const c = this._cmds;
|
|
951
|
+
const out = [];
|
|
952
|
+
const spell = {
|
|
953
|
+
move: (x, y) => out.push(P_MOVE, x, y),
|
|
954
|
+
line: (x, y) => out.push(P_LINE, x, y),
|
|
955
|
+
curve: (ax, ay, bx, by, x, y) => out.push(P_CURVE, ax, ay, bx, by, x, y),
|
|
956
|
+
close: () => out.push(P_CLOSE),
|
|
957
|
+
};
|
|
958
|
+
for (let i = from; i < to;) {
|
|
959
|
+
const op = c[i];
|
|
960
|
+
const end = i + 1 + P_ARGS[op];
|
|
961
|
+
if (op === P_ROUND_XY) roundRectCurves(spell, c, i + 1);
|
|
962
|
+
else for (let j = i; j < end; j++) out.push(c[j]);
|
|
963
|
+
i = end;
|
|
964
|
+
}
|
|
965
|
+
return new Float64Array(out);
|
|
966
|
+
}
|
|
967
|
+
|
|
810
968
|
beginPath() {
|
|
811
969
|
this._cmds.length = 0;
|
|
970
|
+
this._xyOps = 0;
|
|
812
971
|
if (this._bulkPaths) {
|
|
813
972
|
this._s();
|
|
814
973
|
// uploaded, empty or not, when something paints it
|
|
@@ -849,24 +1008,93 @@ export class BackendContext2D {
|
|
|
849
1008
|
if (surface !== null) this._native.ctxRect(surface, x, y, w, h);
|
|
850
1009
|
}
|
|
851
1010
|
|
|
1011
|
+
/**
|
|
1012
|
+
* Canvas's `roundRect`: `radii` a number, a `{ x, y }` point for an
|
|
1013
|
+
* elliptical corner, or a list of one to four of either, scaled down
|
|
1014
|
+
* together where two corners on a side would overlap (HTML, "roundRect").
|
|
1015
|
+
* Circular corners go to the bridge as they always did; elliptical ones
|
|
1016
|
+
* as `ctxRoundRectXY` where the bridge has it, and as curves where not.
|
|
1017
|
+
*/
|
|
852
1018
|
roundRect(x, y, w, h, radii) {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1019
|
+
if (![x, y, w, h].every(Number.isFinite)) return;
|
|
1020
|
+
// an ellipse with no extent on one axis is no curve at all: the corner
|
|
1021
|
+
// is square, as the spec's arc of nothing leaves it
|
|
1022
|
+
let corners = normalizeRadii(Math.abs(w), Math.abs(h), radii).map((r) =>
|
|
1023
|
+
r.x > 0 && r.y > 0 ? r : { x: 0, y: 0 },
|
|
1024
|
+
);
|
|
1025
|
+
// a negative extent is the same box drawn from its other side: the
|
|
1026
|
+
// corners swap across the axis it runs back along
|
|
1027
|
+
if (w < 0) {
|
|
1028
|
+
x += w;
|
|
1029
|
+
w = -w;
|
|
1030
|
+
corners = [corners[1], corners[0], corners[3], corners[2]];
|
|
1031
|
+
}
|
|
1032
|
+
if (h < 0) {
|
|
1033
|
+
y += h;
|
|
1034
|
+
h = -h;
|
|
1035
|
+
corners = [corners[3], corners[2], corners[1], corners[0]];
|
|
1036
|
+
}
|
|
1037
|
+
const [tl, tr, br, bl] = corners;
|
|
860
1038
|
const surface = this._pathFor();
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
this._cmds.
|
|
868
|
-
|
|
869
|
-
|
|
1039
|
+
if (tl.x === tl.y && tr.x === tr.y && br.x === br.y && bl.x === bl.y) {
|
|
1040
|
+
this._cmds.push(P_ROUND, x, y, w, h, tl.x, tr.x, br.x, bl.x);
|
|
1041
|
+
if (surface !== null)
|
|
1042
|
+
this._native.ctxRoundRect(surface, x, y, w, h, tl.x, tr.x, br.x, bl.x);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const at = this._cmds.length + 1;
|
|
1046
|
+
this._cmds.push(
|
|
1047
|
+
P_ROUND_XY,
|
|
1048
|
+
x,
|
|
1049
|
+
y,
|
|
1050
|
+
w,
|
|
1051
|
+
h,
|
|
1052
|
+
tl.x,
|
|
1053
|
+
tl.y,
|
|
1054
|
+
tr.x,
|
|
1055
|
+
tr.y,
|
|
1056
|
+
br.x,
|
|
1057
|
+
br.y,
|
|
1058
|
+
bl.x,
|
|
1059
|
+
bl.y,
|
|
1060
|
+
);
|
|
1061
|
+
this._xyOps++;
|
|
1062
|
+
if (surface !== null) this._emitRoundXY(surface, this._cmds, at);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** One elliptical-cornered rounded rect, from the record at `a`, onto a
|
|
1066
|
+
* bridge's path: whole where it takes one, as curves where not. */
|
|
1067
|
+
_emitRoundXY(surface, c, a) {
|
|
1068
|
+
const n = this._native;
|
|
1069
|
+
if (this._roundXY) {
|
|
1070
|
+
n.ctxRoundRectXY(
|
|
1071
|
+
surface,
|
|
1072
|
+
c[a],
|
|
1073
|
+
c[a + 1],
|
|
1074
|
+
c[a + 2],
|
|
1075
|
+
c[a + 3],
|
|
1076
|
+
c[a + 4],
|
|
1077
|
+
c[a + 5],
|
|
1078
|
+
c[a + 6],
|
|
1079
|
+
c[a + 7],
|
|
1080
|
+
c[a + 8],
|
|
1081
|
+
c[a + 9],
|
|
1082
|
+
c[a + 10],
|
|
1083
|
+
c[a + 11],
|
|
1084
|
+
);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
roundRectCurves(
|
|
1088
|
+
{
|
|
1089
|
+
move: (px, py) => n.ctxMoveTo(surface, px, py),
|
|
1090
|
+
line: (px, py) => n.ctxLineTo(surface, px, py),
|
|
1091
|
+
curve: (ax, ay, bx, by, px, py) =>
|
|
1092
|
+
n.ctxCurveTo(surface, ax, ay, bx, by, px, py),
|
|
1093
|
+
close: () => n.ctxClosePath(surface),
|
|
1094
|
+
},
|
|
1095
|
+
c,
|
|
1096
|
+
a,
|
|
1097
|
+
);
|
|
870
1098
|
}
|
|
871
1099
|
|
|
872
1100
|
arc(x, y, radius, start, end, anticlockwise = false) {
|
|
@@ -1020,6 +1248,13 @@ export class BackendContext2D {
|
|
|
1020
1248
|
const hasPath = pathOrRule != null && typeof pathOrRule === 'object';
|
|
1021
1249
|
const rule = hasPath ? maybeRule : pathOrRule;
|
|
1022
1250
|
if (hasPath && !this._replayPath(pathOrRule)) return;
|
|
1251
|
+
if (this._castsShadow() && this._tiledShadow(rule === 'evenodd')) {
|
|
1252
|
+
this._unshadowed(() => this._fillPath(rule));
|
|
1253
|
+
} else this._fillPath(rule);
|
|
1254
|
+
this._dirty();
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
_fillPath(rule) {
|
|
1023
1258
|
this._path();
|
|
1024
1259
|
const style = this._state.fillStyle;
|
|
1025
1260
|
if (style instanceof LinearGradient) {
|
|
@@ -1036,7 +1271,111 @@ export class BackendContext2D {
|
|
|
1036
1271
|
this._applyFill();
|
|
1037
1272
|
this._native.ctxFill(this._s(), rule === 'evenodd');
|
|
1038
1273
|
}
|
|
1039
|
-
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// --- shadows from tiles (src/backend/shadowtiles.js) -----------------------
|
|
1277
|
+
|
|
1278
|
+
/** Whether what is filled now casts a blurred shadow a tile could draw:
|
|
1279
|
+
* one there, in a colour, composited the ordinary way. */
|
|
1280
|
+
_castsShadow() {
|
|
1281
|
+
if (!this._shadowTiles) return false;
|
|
1282
|
+
const st = this._state;
|
|
1283
|
+
return (
|
|
1284
|
+
st.shadowBlur > 0 &&
|
|
1285
|
+
st.gco === 'source-over' &&
|
|
1286
|
+
parseColor(st.shadowColor)[3] > 0
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* The current path's shadow, from a tile, where the path is a shape a
|
|
1292
|
+
* tile draws: one `rect` or `roundRect`, or — filled `evenodd` — two of
|
|
1293
|
+
* them one inside the other, the frame an inset box shadow is cast
|
|
1294
|
+
* around. False for anything else, and the bridge draws it.
|
|
1295
|
+
*/
|
|
1296
|
+
_tiledShadow(evenodd) {
|
|
1297
|
+
const c = this._cmds;
|
|
1298
|
+
const first = shapeAt(c, 0);
|
|
1299
|
+
if (!first) return false;
|
|
1300
|
+
if (first.end === c.length) return this._drawTiledShadow(first, null);
|
|
1301
|
+
if (!evenodd) return false;
|
|
1302
|
+
const second = shapeAt(c, first.end);
|
|
1303
|
+
if (!second || second.end !== c.length) return false;
|
|
1304
|
+
if (contains(first, second)) return this._drawTiledShadow(first, second);
|
|
1305
|
+
if (contains(second, first)) return this._drawTiledShadow(second, first);
|
|
1306
|
+
return false;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/**
|
|
1310
|
+
* Draw the shadow of `outer` — less `inner`, where there is one — from a
|
|
1311
|
+
* tile: in device pixels, moved by the shadow's offset, which, like its
|
|
1312
|
+
* blur, the transform does not scale (HTML, "shadows"). A transform
|
|
1313
|
+
* that rotates, skews or mirrors is not one a tile can follow.
|
|
1314
|
+
*/
|
|
1315
|
+
_drawTiledShadow(outer, inner) {
|
|
1316
|
+
const st = this._state;
|
|
1317
|
+
const [a, b, c, d, e, f] = st.ctm;
|
|
1318
|
+
if (b !== 0 || c !== 0 || !(a > 0) || !(d > 0)) return false;
|
|
1319
|
+
const device = (r) => {
|
|
1320
|
+
const x0 = Math.round(a * r.x + e + st.shadowOffsetX);
|
|
1321
|
+
const y0 = Math.round(d * r.y + f + st.shadowOffsetY);
|
|
1322
|
+
return {
|
|
1323
|
+
x0,
|
|
1324
|
+
y0,
|
|
1325
|
+
x1: Math.round(a * (r.x + r.w) + e + st.shadowOffsetX),
|
|
1326
|
+
y1: Math.round(d * (r.y + r.h) + f + st.shadowOffsetY),
|
|
1327
|
+
corners: r.corners.map((k) => ({ x: k.x * a, y: k.y * d })),
|
|
1328
|
+
};
|
|
1329
|
+
};
|
|
1330
|
+
const o = device(outer);
|
|
1331
|
+
if (![o.x0, o.y0, o.x1, o.y1].every(Number.isFinite)) return false;
|
|
1332
|
+
// a shape with nothing in it casts nothing
|
|
1333
|
+
if (!(o.x1 > o.x0 && o.y1 > o.y0)) return true;
|
|
1334
|
+
let hole = inner && device(inner);
|
|
1335
|
+
if (hole && !(hole.x1 > hole.x0 && hole.y1 > hole.y0)) hole = null;
|
|
1336
|
+
const plan = planShadowTiles(o, hole, st.shadowBlur);
|
|
1337
|
+
if (!plan) return false;
|
|
1338
|
+
const clip = st.clip === NON_RECT ? null : st.clip;
|
|
1339
|
+
if (clip && !meets(plan.bounds, clip)) return true;
|
|
1340
|
+
const tile = shadowTilesFor(this._native).get(
|
|
1341
|
+
plan,
|
|
1342
|
+
parseColor(st.shadowColor),
|
|
1343
|
+
);
|
|
1344
|
+
if (tile == null) return false;
|
|
1345
|
+
const s = this._s();
|
|
1346
|
+
const n = this._native;
|
|
1347
|
+
n.ctxSave(s);
|
|
1348
|
+
n.ctxSetShadow(s, 0, 0, 0, 0, 0, 0, 0);
|
|
1349
|
+
for (const [sx, sy, sw, sh, dx, dy, dw, dh] of plan.pieces) {
|
|
1350
|
+
if (clip && !meets({ x: dx, y: dy, width: dw, height: dh }, clip))
|
|
1351
|
+
continue;
|
|
1352
|
+
n.ctxDrawSurface(
|
|
1353
|
+
s,
|
|
1354
|
+
tile,
|
|
1355
|
+
sx,
|
|
1356
|
+
sy,
|
|
1357
|
+
sw,
|
|
1358
|
+
sh,
|
|
1359
|
+
(dx - e) / a,
|
|
1360
|
+
(dy - f) / d,
|
|
1361
|
+
dw / a,
|
|
1362
|
+
dh / d,
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
n.ctxRestore(s);
|
|
1366
|
+
// the shape is filled next, from the record: nothing above may have
|
|
1367
|
+
// left the bridge's own copy of the path as it was
|
|
1368
|
+
this._pathStale = true;
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
/** `fn`'s drawing with no shadow: the tile has drawn it already. */
|
|
1373
|
+
_unshadowed(fn) {
|
|
1374
|
+
const s = this._s();
|
|
1375
|
+
this._native.ctxSave(s);
|
|
1376
|
+
this._native.ctxSetShadow(s, 0, 0, 0, 0, 0, 0, 0);
|
|
1377
|
+
fn();
|
|
1378
|
+
this._native.ctxRestore(s);
|
|
1040
1379
|
}
|
|
1041
1380
|
|
|
1042
1381
|
stroke(path) {
|
|
@@ -1129,6 +1468,16 @@ export class BackendContext2D {
|
|
|
1129
1468
|
|
|
1130
1469
|
fillRect(x, y, w, h) {
|
|
1131
1470
|
if (!(w > 0) || !(h > 0)) return;
|
|
1471
|
+
if (
|
|
1472
|
+
this._castsShadow() &&
|
|
1473
|
+
this._drawTiledShadow({ x, y, w, h, corners: SQUARE }, null)
|
|
1474
|
+
) {
|
|
1475
|
+
this._unshadowed(() => this._fillRectNow(x, y, w, h));
|
|
1476
|
+
} else this._fillRectNow(x, y, w, h);
|
|
1477
|
+
this._dirty();
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
_fillRectNow(x, y, w, h) {
|
|
1132
1481
|
const style = this._state.fillStyle;
|
|
1133
1482
|
if (style instanceof LinearGradient) {
|
|
1134
1483
|
const { coords, flat } = style._normalized();
|
|
@@ -1148,7 +1497,6 @@ export class BackendContext2D {
|
|
|
1148
1497
|
this._applyFill();
|
|
1149
1498
|
this._native.ctxFillRect(this._s(), x, y, w, h);
|
|
1150
1499
|
}
|
|
1151
|
-
this._dirty();
|
|
1152
1500
|
}
|
|
1153
1501
|
|
|
1154
1502
|
fillRects(rects) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The tiles blurred shadows are drawn from, on a bridge's own surfaces.
|
|
2
|
+
//
|
|
3
|
+
// A gaussian over a shape costs its area times its width, and CoreGraphics
|
|
4
|
+
// pays it on every `fill` that has a shadow set — a header's
|
|
5
|
+
// `box-shadow: inset 0 0 100px` was 130 to 380 ms of each scroll frame on a
|
|
6
|
+
// 2x display, and a card's 200px hover shadow 110 ms, on every strip a
|
|
7
|
+
// scroll exposed across it. Direct2D draws no live shadow at all (the win32
|
|
8
|
+
// bridge records one and draws nothing). ntk's `ntk/shadow-tiles` plans
|
|
9
|
+
// the shadow of a rect, a rounded rect or a rect with a rounded hole as a
|
|
10
|
+
// small tile and nine pieces of it — the corners as they are, a straight
|
|
11
|
+
// pixel stretched between them — and makes the tile's pixels; this keeps
|
|
12
|
+
// the tiles, as surfaces of the bridge's, and `BackendContext2D` draws the
|
|
13
|
+
// pieces (src/backend/context2d.js, `_drawTiledShadow`).
|
|
14
|
+
//
|
|
15
|
+
// A surface here has no coverage format to composite through a colour, as
|
|
16
|
+
// X's `a8` does, so a tile is made in its colour and the colour is part of
|
|
17
|
+
// its name: one per shadow colour in use, rather than one per shadow.
|
|
18
|
+
|
|
19
|
+
import { rasterShadowTile } from 'ntk/shadow-tiles';
|
|
20
|
+
|
|
21
|
+
/** What the tiles of one bridge may hold, least recently drawn let go
|
|
22
|
+
* first. A card's tile is tens of kilobytes. */
|
|
23
|
+
export const TILE_BUDGET = 8 << 20;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The tiles made for one bridge, least recently drawn let go first once
|
|
27
|
+
* they hold more than `budget` bytes. There is one bridge in a process, so
|
|
28
|
+
* every window of an app shares them.
|
|
29
|
+
*/
|
|
30
|
+
export class ShadowTileCache {
|
|
31
|
+
constructor(native, budget = TILE_BUDGET) {
|
|
32
|
+
this._native = native;
|
|
33
|
+
this._budget = budget;
|
|
34
|
+
this._tiles = new Map();
|
|
35
|
+
this._bytes = 0;
|
|
36
|
+
this.stats = { made: 0, hits: 0, released: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The tile for `plan` in `rgba` (0..1 each), made now if it is not
|
|
40
|
+
* held; null where the bridge made no surface. */
|
|
41
|
+
get(plan, rgba) {
|
|
42
|
+
const key = `${plan.key}|${rgba.join(',')}`;
|
|
43
|
+
const held = this._tiles.get(key);
|
|
44
|
+
if (held) {
|
|
45
|
+
this._tiles.delete(key);
|
|
46
|
+
this._tiles.set(key, held);
|
|
47
|
+
this.stats.hits++;
|
|
48
|
+
return held.handle;
|
|
49
|
+
}
|
|
50
|
+
const n = this._native;
|
|
51
|
+
const handle = n.createSurface(plan.width, plan.height, 1);
|
|
52
|
+
if (handle == null) return null;
|
|
53
|
+
const pixels = rasterShadowTile(plan, rgba);
|
|
54
|
+
n.ctxPutImageData(
|
|
55
|
+
handle,
|
|
56
|
+
Buffer.from(pixels.buffer, pixels.byteOffset, pixels.byteLength),
|
|
57
|
+
plan.width,
|
|
58
|
+
plan.height,
|
|
59
|
+
0,
|
|
60
|
+
0,
|
|
61
|
+
);
|
|
62
|
+
const bytes = plan.width * plan.height * 4;
|
|
63
|
+
this._tiles.set(key, { handle, bytes });
|
|
64
|
+
this._bytes += bytes;
|
|
65
|
+
this.stats.made++;
|
|
66
|
+
for (const [oldest, tile] of this._tiles) {
|
|
67
|
+
if (this._bytes <= this._budget || tile.handle === handle) break;
|
|
68
|
+
this._tiles.delete(oldest);
|
|
69
|
+
this._bytes -= tile.bytes;
|
|
70
|
+
this.stats.released++;
|
|
71
|
+
n.releaseSurface?.(tile.handle);
|
|
72
|
+
}
|
|
73
|
+
return handle;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const caches = new WeakMap();
|
|
78
|
+
|
|
79
|
+
/** The one tile cache for a bridge. */
|
|
80
|
+
export function shadowTilesFor(native) {
|
|
81
|
+
let cache = caches.get(native);
|
|
82
|
+
if (!cache) caches.set(native, (cache = new ShadowTileCache(native)));
|
|
83
|
+
return cache;
|
|
84
|
+
}
|
package/src/compositing.js
CHANGED
|
@@ -40,6 +40,9 @@ class CompositingSession {
|
|
|
40
40
|
this.stopped = false;
|
|
41
41
|
this._window = null;
|
|
42
42
|
this._selection = null;
|
|
43
|
+
/** What `stop` takes off the connection: the listener and the XFixes
|
|
44
|
+
* address window `watchSelection` put on it. */
|
|
45
|
+
this._teardown = [];
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
_set(supported) {
|
|
@@ -64,6 +67,16 @@ class CompositingSession {
|
|
|
64
67
|
stop() {
|
|
65
68
|
this.stopped = true;
|
|
66
69
|
this.listeners.clear();
|
|
70
|
+
// Taken off, not just ignored — the same as XSETTINGS's session: an app
|
|
71
|
+
// that mounts one root after another on one connection gained a
|
|
72
|
+
// listener and a window a root.
|
|
73
|
+
for (const undo of this._teardown.splice(0)) {
|
|
74
|
+
try {
|
|
75
|
+
undo();
|
|
76
|
+
} catch {
|
|
77
|
+
// a connection already gone takes all of it with it
|
|
78
|
+
}
|
|
79
|
+
}
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
82
|
|
|
@@ -144,13 +157,20 @@ async function watchSelection(app, session, atom) {
|
|
|
144
157
|
fixes.SelectionEventMask.SetSelectionOwner |
|
|
145
158
|
fixes.SelectionEventMask.SelectionWindowDestroy |
|
|
146
159
|
fixes.SelectionEventMask.SelectionClientClose;
|
|
147
|
-
|
|
160
|
+
const onOwner = (ev) => {
|
|
148
161
|
if (session.stopped) return;
|
|
149
162
|
// XFixes events carry a server-assigned type above the core range, so
|
|
150
163
|
// this cannot be confused with core SelectionNotify (31)
|
|
151
164
|
if (ev.type !== fixes.firstEvent) return;
|
|
152
165
|
if (ev.selection !== atom) return;
|
|
153
166
|
session._set(Boolean(ev.owner));
|
|
167
|
+
};
|
|
168
|
+
X.on('event', onOwner);
|
|
169
|
+
session._teardown.push(() => {
|
|
170
|
+
X.removeListener('event', onOwner);
|
|
171
|
+
// the selection input goes with the window it was addressed to
|
|
172
|
+
X.DestroyWindow(id);
|
|
173
|
+
session._window = null;
|
|
154
174
|
});
|
|
155
175
|
fixes.SelectSelectionInput(id, atom, mask);
|
|
156
176
|
}
|