react-x11 2.24.1 → 2.26.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/components/Select.js +22 -2
- 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/scrollblit.js +38 -1
- package/src/nodes/window/flush.js +13 -1
- package/src/nodes/window/window.js +6 -0
- package/src/types/components.d.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-x11",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.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/components/Select.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { useEffect, useRef, useState } from 'react';
|
|
6
6
|
import { useAppOrNull } from '../appcontext.js';
|
|
7
|
+
import { flattenStyle } from '../styles.js';
|
|
7
8
|
import { capBand, capTrim, rowRadius, useTheme } from './theme.js';
|
|
8
9
|
import { Icon } from './Icon.js';
|
|
9
10
|
import {
|
|
@@ -282,6 +283,14 @@ function Option({
|
|
|
282
283
|
* the active option with wrapping; Home/End jump to the ends; Enter/Space
|
|
283
284
|
* pick the active option (or open the menu when closed); Escape closes.
|
|
284
285
|
* The active option is scrolled into view.
|
|
286
|
+
*
|
|
287
|
+
* `labelStyle` and `chevronStyle` are style slots over the caption and the
|
|
288
|
+
* chevron, each merged over the part's own default, as `<Slider>`'s are over
|
|
289
|
+
* its parts — so a select that has to wear someone else's look, a toolbar's
|
|
290
|
+
* or a web page's, is the widget restyled, with `style` on the trigger. The
|
|
291
|
+
* caption's `fontSize` is read back: the chevron is as tall as the capitals
|
|
292
|
+
* beside it. Any slot also chooses the drawn trigger over the platform's
|
|
293
|
+
* popup bezel, whose pixels no style reaches.
|
|
285
294
|
*/
|
|
286
295
|
export function Select({
|
|
287
296
|
value,
|
|
@@ -291,11 +300,18 @@ export function Select({
|
|
|
291
300
|
placeholder = 'Select…',
|
|
292
301
|
native,
|
|
293
302
|
style,
|
|
303
|
+
labelStyle,
|
|
304
|
+
chevronStyle,
|
|
294
305
|
...boxProps
|
|
295
306
|
}) {
|
|
296
307
|
const theme = useTheme();
|
|
297
308
|
const app = useAppOrNull();
|
|
298
|
-
const
|
|
309
|
+
const styled = Boolean(labelStyle || chevronStyle);
|
|
310
|
+
const nativeControls = useNativeControls(styled ? false : native);
|
|
311
|
+
// The size the caption is set in, where its slot names one: the chevron is
|
|
312
|
+
// measured against those capitals, and an `<Icon>`'s size does not inherit.
|
|
313
|
+
const ownSize = flattenStyle(labelStyle).fontSize;
|
|
314
|
+
const captionSize = typeof ownSize === 'number' ? ownSize : theme.fontSize;
|
|
299
315
|
const [open, setOpen] = useState(false);
|
|
300
316
|
const [anchor, setAnchor] = useState(null);
|
|
301
317
|
const [focused, setFocused] = useState(false);
|
|
@@ -572,6 +588,7 @@ export function Select({
|
|
|
572
588
|
ONE_LINE,
|
|
573
589
|
nativeControls && nativeTitleStyle('regular'),
|
|
574
590
|
{ color: current ? theme.text : theme.textMuted },
|
|
591
|
+
labelStyle,
|
|
575
592
|
],
|
|
576
593
|
},
|
|
577
594
|
current ? current.label : placeholder,
|
|
@@ -593,8 +610,11 @@ export function Select({
|
|
|
593
610
|
})
|
|
594
611
|
: h(Icon, {
|
|
595
612
|
name: 'chevronDown',
|
|
596
|
-
size: capBand(
|
|
613
|
+
size: capBand(captionSize),
|
|
597
614
|
color: theme.textMuted,
|
|
615
|
+
// `display: 'none'` leaves it out, for a trigger whose arrow is
|
|
616
|
+
// drawn by something else
|
|
617
|
+
style: chevronStyle,
|
|
598
618
|
}),
|
|
599
619
|
open &&
|
|
600
620
|
anchor &&
|
package/src/events.js
CHANGED
|
@@ -431,6 +431,10 @@ export class EventManager {
|
|
|
431
431
|
this._wheelOwed = { x: 0, y: 0 };
|
|
432
432
|
// the window's DragSession while a press has armed it (src/dnd.js)
|
|
433
433
|
this._dragArmed = null;
|
|
434
|
+
// where the pointer last was over this window — the motion or the wheel
|
|
435
|
+
// that said so — for asking again what is under it once what is under
|
|
436
|
+
// it has moved (`refreshHover`). Null while the pointer is elsewhere.
|
|
437
|
+
this._pointer = null;
|
|
434
438
|
// on a *top-level* window's manager: the chords bound anywhere in its
|
|
435
439
|
// tree, newest last (`registerAccelerator`). Null until one is bound,
|
|
436
440
|
// which is the common case and costs one null check per key.
|
|
@@ -792,6 +796,7 @@ export class EventManager {
|
|
|
792
796
|
* render it started for the notch before.
|
|
793
797
|
*/
|
|
794
798
|
_onWheel(native) {
|
|
799
|
+
this._pointer = native;
|
|
795
800
|
// The first wheel is what says this window wants smooth scrolling. It was
|
|
796
801
|
// created on core events — an XI2 selection costs four times as many
|
|
797
802
|
// bytes per *pointer move*, which a window that is never scrolled would
|
|
@@ -1004,6 +1009,7 @@ export class EventManager {
|
|
|
1004
1009
|
}
|
|
1005
1010
|
|
|
1006
1011
|
_onMouseMove(native) {
|
|
1012
|
+
this._pointer = native;
|
|
1007
1013
|
if (inspectHandler) {
|
|
1008
1014
|
inspectHandler('move', this._hit(native), native);
|
|
1009
1015
|
return;
|
|
@@ -1090,6 +1096,7 @@ export class EventManager {
|
|
|
1090
1096
|
// ancestor was told the pointer had gone and then that it was back, and
|
|
1091
1097
|
// `onMouseOut` that it had left a window it was still in.
|
|
1092
1098
|
if (this._leftIntoSurface(native)) return;
|
|
1099
|
+
this._pointer = null;
|
|
1093
1100
|
runWithPriority(ContinuousEventPriority, () => {
|
|
1094
1101
|
this._updateHover([], native);
|
|
1095
1102
|
// the pointer is somewhere else entirely: whatever it was heading for
|
|
@@ -1186,6 +1193,41 @@ export class EventManager {
|
|
|
1186
1193
|
this.pressPath = path;
|
|
1187
1194
|
}
|
|
1188
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* Hover follows content. The pointer stayed where it was and what is
|
|
1198
|
+
* under it moved — a scroll, or a layout that put something else there —
|
|
1199
|
+
* so the question a motion asks is asked again at the point, as a browser
|
|
1200
|
+
* asks it at the frame after a scroll: `:hover`, enter and leave change
|
|
1201
|
+
* as a motion would change them, and the node under the pointer hears
|
|
1202
|
+
* `defaultMouseMove` there, since an element that paints its own hover
|
|
1203
|
+
* (a document, a graph) has content that moved under the point even where
|
|
1204
|
+
* the node did not. The application's `onMouseMove` is not called: the
|
|
1205
|
+
* pointer did not move. Nothing is asked while a press or a capture holds
|
|
1206
|
+
* the pointer, where hover is frozen for the gesture.
|
|
1207
|
+
*
|
|
1208
|
+
* Called by the window after a frame that laid out (nodes/window/flush.js),
|
|
1209
|
+
* which is every frame a scroll moves content in.
|
|
1210
|
+
*/
|
|
1211
|
+
refreshHover() {
|
|
1212
|
+
const at = this._pointer;
|
|
1213
|
+
if (!at || inspectHandler || this.node.destroyed) return;
|
|
1214
|
+
if (this._captured() || this.downNode || this._dragArmed) return;
|
|
1215
|
+
runWithPriority(ContinuousEventPriority, () => {
|
|
1216
|
+
const target = this._hit(at);
|
|
1217
|
+
const path = this._path(target);
|
|
1218
|
+
const old = this.hoverPath;
|
|
1219
|
+
if (
|
|
1220
|
+
path.length !== old.length ||
|
|
1221
|
+
path.some((node, i) => node !== old[i])
|
|
1222
|
+
) {
|
|
1223
|
+
this._updateHover(path, at);
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof target.defaultMouseMove === 'function') {
|
|
1226
|
+
target.defaultMouseMove(this._makeEvent('mouseMove', at, target));
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1189
1231
|
/** enter/leave do not propagate: each node on the diff gets its own call. */
|
|
1190
1232
|
_updateHover(newPath, native) {
|
|
1191
1233
|
const oldPath = this.hoverPath;
|
package/src/node.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export interface ImageDataLike {
|
|
|
48
48
|
* Anything ntk documents beyond this is reachable at runtime and is
|
|
49
49
|
* ntk's to change.
|
|
50
50
|
*/
|
|
51
|
+
/** One corner's radius for `roundRect`: circular, or elliptical as a
|
|
52
|
+
* `DOMPointInit`. */
|
|
53
|
+
export type RoundRectRadius = number | { x?: number; y?: number };
|
|
54
|
+
|
|
51
55
|
export interface Context2D {
|
|
52
56
|
fillStyle: string | CanvasGradientLike;
|
|
53
57
|
strokeStyle: string | CanvasGradientLike;
|
|
@@ -125,12 +129,16 @@ export interface Context2D {
|
|
|
125
129
|
anticlockwise?: boolean,
|
|
126
130
|
): void;
|
|
127
131
|
rect(x: number, y: number, width: number, height: number): void;
|
|
132
|
+
/** Canvas's `roundRect`: a radius for every corner, a `{ x, y }` point
|
|
133
|
+
* for an elliptical one, or a list of one to four of either, clockwise
|
|
134
|
+
* from the top left. A shadowed `fill` of one — or of a `rect` less one,
|
|
135
|
+
* filled `'evenodd'` — is drawn from a shadow tile made once. */
|
|
128
136
|
roundRect(
|
|
129
137
|
x: number,
|
|
130
138
|
y: number,
|
|
131
139
|
width: number,
|
|
132
140
|
height: number,
|
|
133
|
-
radii:
|
|
141
|
+
radii: RoundRectRadius | RoundRectRadius[],
|
|
134
142
|
): void;
|
|
135
143
|
fill(fillRule?: 'nonzero' | 'evenodd'): void;
|
|
136
144
|
stroke(): void;
|
|
@@ -515,6 +523,17 @@ export declare class Node {
|
|
|
515
523
|
* paint, whatever the props. A translucent element answers `null`.
|
|
516
524
|
*/
|
|
517
525
|
opaqueRect(): Rect | null;
|
|
526
|
+
/**
|
|
527
|
+
* What this element draws that stays put in the viewport of the scroll
|
|
528
|
+
* pane it is in while the pane scrolls — a document's fixed background, a
|
|
529
|
+
* `position: fixed` header — as rects in window coordinates, device
|
|
530
|
+
* pixels, or null, the default, for drawing that moves with the content.
|
|
531
|
+
* A scroll of that pane that blits repaints them where they are and where
|
|
532
|
+
* the copy dragged their image; one that covers most of the viewport
|
|
533
|
+
* makes the scroll a repaint. Only the nearest scroll pane asks. Read on
|
|
534
|
+
* each scroll the pane blits, after layout.
|
|
535
|
+
*/
|
|
536
|
+
viewportFixedRects(): Rect[] | null;
|
|
518
537
|
/**
|
|
519
538
|
* How far this element's own drawing reaches past its box, in device
|
|
520
539
|
* pixels on every side — 0, the default, for an element that stays inside
|
package/src/nodes/boxpaint.js
CHANGED
|
@@ -267,10 +267,42 @@ export class NodeBoxPaint {
|
|
|
267
267
|
ctx.fillShadow(rect, r, shadow.blur, color);
|
|
268
268
|
continue;
|
|
269
269
|
}
|
|
270
|
+
// No X server to bake a blur on — CoreGraphics, Direct2D: the
|
|
271
|
+
// context's own shadow, which draws a rounded rect's from a tile it
|
|
272
|
+
// keeps (src/backend/shadowtiles.js). The coverage route below needs
|
|
273
|
+
// RENDER and drew nothing there.
|
|
274
|
+
if (!this.app?.display?.Render) {
|
|
275
|
+
this._paintShadowOnContext(ctx, rect, r, shadow.blur, color);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
270
278
|
this._paintBlurredShadow(ctx, rect, r, shadow.blur, color);
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
|
|
282
|
+
/**
|
|
283
|
+
* One blurred shadow through the context's `shadowBlur`: the rounded rect
|
|
284
|
+
* filled clear of the surface to its left, and its shadow thrown back by
|
|
285
|
+
* as much, so that only the shadow lands. Offsets are device pixels and
|
|
286
|
+
* the shape moves in the context's own units, hence the scale between.
|
|
287
|
+
*/
|
|
288
|
+
_paintShadowOnContext(ctx, rect, radius, blur, color) {
|
|
289
|
+
if (!('shadowBlur' in ctx) || typeof ctx.roundRect !== 'function') return;
|
|
290
|
+
const m = ctx.getTransform?.() ?? { a: 1, e: 0 };
|
|
291
|
+
const reach = Math.ceil(blur * 1.5) + 1;
|
|
292
|
+
const away =
|
|
293
|
+
Math.ceil(Math.abs(rect.x) + rect.width + Math.abs(m.e) + reach) + 1;
|
|
294
|
+
ctx.save();
|
|
295
|
+
ctx.shadowColor = color;
|
|
296
|
+
ctx.shadowBlur = blur;
|
|
297
|
+
ctx.shadowOffsetX = away * m.a;
|
|
298
|
+
ctx.shadowOffsetY = 0;
|
|
299
|
+
ctx.fillStyle = '#000000';
|
|
300
|
+
ctx.beginPath();
|
|
301
|
+
ctx.roundRect(rect.x - away, rect.y, rect.width, rect.height, radius);
|
|
302
|
+
ctx.fill();
|
|
303
|
+
ctx.restore();
|
|
304
|
+
}
|
|
305
|
+
|
|
274
306
|
/**
|
|
275
307
|
* One blurred shadow, through the paint cache when there is one.
|
|
276
308
|
*
|
package/src/nodes/invalidate.js
CHANGED
|
@@ -114,6 +114,29 @@ export class NodeInvalidate {
|
|
|
114
114
|
return null;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* What this element draws that stays put in the viewport of the scroll
|
|
119
|
+
* pane it is in while the pane scrolls, as rects in window coordinates,
|
|
120
|
+
* device pixels, like `abs` — or null, the default, for an element
|
|
121
|
+
* whose drawing moves with the content. A document's fixed background or
|
|
122
|
+
* `position: fixed` header is drawn where the pane's viewport is, not
|
|
123
|
+
* where the content under it is, and a scroll's blit copies the pane's
|
|
124
|
+
* pixels with the content: it dragged the header along, and left the
|
|
125
|
+
* background scrolling with the text.
|
|
126
|
+
*
|
|
127
|
+
* A pane scroll that blits repaints what these rects answer where they
|
|
128
|
+
* are and where the copy dragged their image, as it does the corners of
|
|
129
|
+
* a rounded box around the pane; one that covers most of the viewport
|
|
130
|
+
* makes the scroll a repaint. Only the nearest scroll pane's scroll is
|
|
131
|
+
* asked about: a pane further out moves the inner one, and what is fixed
|
|
132
|
+
* to it, with its content. Read on each scroll the pane blits, after
|
|
133
|
+
* layout, so it may follow `abs` and the pane's `contentBox()`; the
|
|
134
|
+
* element draws those rects in its `paint` from the same geometry.
|
|
135
|
+
*/
|
|
136
|
+
viewportFixedRects() {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
/**
|
|
118
141
|
* How far this element's own drawing reaches past its box — device
|
|
119
142
|
* pixels, on every side — or 0, the default, for one that stays inside
|
package/src/nodes/node.js
CHANGED
|
@@ -607,6 +607,7 @@ export class Node {
|
|
|
607
607
|
// nothing else ever asks for a frame
|
|
608
608
|
this.root?._animating.delete(this);
|
|
609
609
|
this.root?._opaqueNodes?.delete(this);
|
|
610
|
+
this.root?._viewportFixedNodes?.delete(this);
|
|
610
611
|
// a surface that goes away takes its selection with it, and the app-wide
|
|
611
612
|
// claim on being the one showing one goes with it too
|
|
612
613
|
this._textSelection?.destroy();
|
|
@@ -625,11 +626,15 @@ export class Node {
|
|
|
625
626
|
this.root?._layoutHosts?.delete(this);
|
|
626
627
|
root?._layoutHosts?.add(this);
|
|
627
628
|
}
|
|
628
|
-
// an element answering `opaqueRect()` is one the window asks per pass
|
|
629
|
+
// an element answering `opaqueRect()` is one the window asks per pass,
|
|
630
|
+
// and one answering `viewportFixedRects()` one it asks per scroll blit
|
|
629
631
|
const opaque = this.opaqueRect !== Node.prototype.opaqueRect;
|
|
632
|
+
const fixed = this.viewportFixedRects !== Node.prototype.viewportFixedRects;
|
|
630
633
|
if (opaque) this.root?._opaqueNodes?.delete(this);
|
|
634
|
+
if (fixed) this.root?._viewportFixedNodes?.delete(this);
|
|
631
635
|
this.root = root;
|
|
632
636
|
if (opaque) root?._opaqueNodes?.add(this);
|
|
637
|
+
if (fixed) root?._viewportFixedNodes?.add(this);
|
|
633
638
|
// styled before it had a window, so this is where a placed node is
|
|
634
639
|
// first registered (WindowNode._placeNodes)
|
|
635
640
|
if (this.style && isPlaced(this.style)) root?._placedNodes?.add(this);
|
package/src/nodes/scrollblit.js
CHANGED
|
@@ -218,6 +218,13 @@ function unionIsBox(a, b) {
|
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/** Two lists of pins as one, either of which may be null. */
|
|
222
|
+
function joinPins(a, b) {
|
|
223
|
+
if (!a) return b;
|
|
224
|
+
if (!b) return a;
|
|
225
|
+
return [...a, ...b];
|
|
226
|
+
}
|
|
227
|
+
|
|
221
228
|
/** Is `rect` inside one of `pins`, the furniture an element blit pinned —
|
|
222
229
|
* repaired whole after the copy, whatever changed in it (issue #682)? */
|
|
223
230
|
function insidePinned(rect, pins) {
|
|
@@ -862,7 +869,13 @@ export class WindowScrollBlit {
|
|
|
862
869
|
// (issue #691) and repaired below, where they are and where the copy
|
|
863
870
|
// dragged their image. Refused instead, a list or a document inside a
|
|
864
871
|
// rounded frame repainted its whole viewport on every step of a scroll.
|
|
865
|
-
|
|
872
|
+
// …and so is what an element in the pane draws fixed to its viewport
|
|
873
|
+
// (`viewportFixedRects()`): a document's fixed background or header,
|
|
874
|
+
// which the copy would drag along with the text under it
|
|
875
|
+
const corners = joinPins(
|
|
876
|
+
this._cornerPins(node, vp, target.top),
|
|
877
|
+
this._viewportFixedPins(node, vp),
|
|
878
|
+
);
|
|
866
879
|
if (!this._scrollBlitSafe(node, vp, target.top, null, corners)) return;
|
|
867
880
|
// The band the scrolled bar's thumb travels in is repaired on its own.
|
|
868
881
|
// The thumb's rects are thin and run along the viewport's edge, and a
|
|
@@ -1189,6 +1202,30 @@ export class WindowScrollBlit {
|
|
|
1189
1202
|
* Ancestors below `upTo`, the node the blit's surface belongs to
|
|
1190
1203
|
* (`_blitTarget`): a pane is a rectangle no corner above it clips.
|
|
1191
1204
|
*/
|
|
1205
|
+
/**
|
|
1206
|
+
* What the elements in scroll pane `node` draw fixed to its viewport
|
|
1207
|
+
* (`viewportFixedRects()`), inside `vp` and out to whole pixels: pinned
|
|
1208
|
+
* like a rounded ancestor's corners, repaired where they are and where the
|
|
1209
|
+
* blit dragged their image. Asked only of an element whose nearest scroll
|
|
1210
|
+
* pane is `node` — a pane further in moves with the content, and whatever
|
|
1211
|
+
* is fixed to it with it — and not of one hidden or gone.
|
|
1212
|
+
*/
|
|
1213
|
+
_viewportFixedPins(node, vp) {
|
|
1214
|
+
let out = null;
|
|
1215
|
+
for (const fixed of this._viewportFixedNodes ?? []) {
|
|
1216
|
+
if (fixed.destroyed || fixed.hidden || fixed.root !== this) continue;
|
|
1217
|
+
let pane = fixed.parent;
|
|
1218
|
+
while (pane && !pane.isWindow && !pane.isScroller?.()) pane = pane.parent;
|
|
1219
|
+
if (pane !== node) continue;
|
|
1220
|
+
for (const rect of fixed.viewportFixedRects() ?? []) {
|
|
1221
|
+
if (!rectsOverlap(rect, vp)) continue;
|
|
1222
|
+
const inside = intersectRects(outerPixels(rect), vp);
|
|
1223
|
+
if (inside) (out ??= []).push(inside);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return out;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1192
1229
|
_cornerPins(node, vp, upTo = this) {
|
|
1193
1230
|
let out = null;
|
|
1194
1231
|
for (let n = node.parent; n && n !== upTo; n = n.parent) {
|
|
@@ -138,6 +138,14 @@ export class WindowFlush {
|
|
|
138
138
|
if (!this.destroyed) this._scheduleFrame();
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
// A frame that laid out may have moved what is under a pointer that did
|
|
142
|
+
// not move — a scroll does, every frame of it — and hover follows the
|
|
143
|
+
// content (`EventManager.refreshHover`). After the frame: what hover
|
|
144
|
+
// changes is the next frame's to paint.
|
|
145
|
+
if (this._hoverRecheck) {
|
|
146
|
+
this._hoverRecheck = false;
|
|
147
|
+
if (!this.destroyed) this.events?.refreshHover();
|
|
148
|
+
}
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
/** The frame itself. True when it painted or presented something. */
|
|
@@ -289,7 +297,11 @@ export class WindowFlush {
|
|
|
289
297
|
// like `presentFrame`; an ntk window has no such half.
|
|
290
298
|
this.window.prepareFrame?.(this, layoutRan);
|
|
291
299
|
// any node this pass laid out may be what an open popup is anchored to
|
|
292
|
-
|
|
300
|
+
// — or what a pointer that has not moved is over now (`flush`)
|
|
301
|
+
if (layoutRan) {
|
|
302
|
+
this._notifyAnchorChange();
|
|
303
|
+
this._hoverRecheck = true;
|
|
304
|
+
}
|
|
293
305
|
// A subtree that only moved and covers its box opaquely is copied where
|
|
294
306
|
// it went, and the frame paints around the copy — or its move is
|
|
295
307
|
// claimed, where it was and where it is (nodes/moveblit.js). Once every
|
|
@@ -185,11 +185,17 @@ export class WindowNode extends Scrollable(Node) {
|
|
|
185
185
|
// over and its cost is known (`flush`)
|
|
186
186
|
this._inFlush = false;
|
|
187
187
|
this._claimAfterFlush = false;
|
|
188
|
+
// a frame laid out, so what is under the pointer may have moved
|
|
189
|
+
// (`flush`, `EventManager.refreshHover`)
|
|
190
|
+
this._hoverRecheck = false;
|
|
188
191
|
// the nodes answering `opaqueRect()`, and — during a paint pass one of
|
|
189
192
|
// them covers — that node with its ancestors, whose fills are skipped
|
|
190
193
|
// (`_coverFor`, `Node._paintBackground`)
|
|
191
194
|
this._opaqueNodes = new Set();
|
|
192
195
|
this._coverChain = null;
|
|
196
|
+
// the nodes answering `viewportFixedRects()`, whose rects a scroll pane
|
|
197
|
+
// they are in repairs when it blits (`_viewportFixedPins`)
|
|
198
|
+
this._viewportFixedNodes = new Set();
|
|
193
199
|
// Nodes that want the `attention` event (ntk#37) — an
|
|
194
200
|
// `unstable_onAttention` prop,
|
|
195
201
|
// an `:attention` block, or both. Built before the EventManager so the
|
|
@@ -511,6 +511,16 @@ export interface SelectProps<T = unknown>
|
|
|
511
511
|
onChange?: (ev: WidgetChangeEvent<T>) => void;
|
|
512
512
|
placeholder?: string;
|
|
513
513
|
style?: StyleProp;
|
|
514
|
+
/**
|
|
515
|
+
* The caption's style — the value, or the placeholder — over the palette's
|
|
516
|
+
* `text` (`textMuted` for the placeholder). Its `fontSize`, as a number, is
|
|
517
|
+
* the one the chevron is sized against. Any slot chooses the drawn trigger
|
|
518
|
+
* over the platform's popup bezel.
|
|
519
|
+
*/
|
|
520
|
+
labelStyle?: StyleProp;
|
|
521
|
+
/** The chevron's style, over `textMuted` at the caption's cap height.
|
|
522
|
+
* `display: 'none'` leaves it out. */
|
|
523
|
+
chevronStyle?: StyleProp;
|
|
514
524
|
}
|
|
515
525
|
export function Select<T = unknown>(props: SelectProps<T>): ReactNode;
|
|
516
526
|
|