@react-x11/components 0.12.0 → 0.13.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/dist/html/controls.d.ts +12 -3
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +15 -55
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +519 -122
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +29 -2
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +277 -9
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +14 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +2 -3
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +59 -7
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +25 -1
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +99 -6
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +5 -1
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +31 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +52 -8
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/flex.js +4 -1
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +10 -0
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +87 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +510 -152
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +22 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +290 -20
- package/dist/html/paint.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +620 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/html/controls.ts +29 -49
- package/src/html/css/cascade.ts +591 -140
- package/src/html/css/parse.ts +324 -9
- package/src/html/css/style.ts +24 -3
- package/src/html/css/ua.ts +62 -8
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +116 -6
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/block.ts +3 -0
- package/src/html/layout/boxes.ts +78 -11
- package/src/html/layout/flex.ts +4 -1
- package/src/html/layout/inline.ts +10 -0
- package/src/html/node.ts +498 -135
- package/src/html/paint.ts +330 -29
- package/src/html/widgets.ts +735 -0
- package/src/index.ts +1 -0
package/src/html/node.ts
CHANGED
|
@@ -50,8 +50,14 @@ import type { Element } from 'domhandler';
|
|
|
50
50
|
import { codePointAtOffset, codeUnitOffsets } from '../internal/text.js';
|
|
51
51
|
import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
|
|
52
52
|
import type { Document } from './dom.js';
|
|
53
|
-
import {
|
|
54
|
-
|
|
53
|
+
import {
|
|
54
|
+
Cascade,
|
|
55
|
+
HOVER_FOLLOWED,
|
|
56
|
+
HOVER_PSEUDO_NONE,
|
|
57
|
+
HOVER_PSEUDO_OTHER,
|
|
58
|
+
HOVER_UNTOUCHED,
|
|
59
|
+
} from './css/cascade.js';
|
|
60
|
+
import type { HoverTouch, KeptStyles, MetricFace } from './css/cascade.js';
|
|
55
61
|
import { mediaMatches, parseStylesheet } from './css/parse.js';
|
|
56
62
|
import type { Stylesheet } from './css/parse.js';
|
|
57
63
|
import { uaStylesheet } from './css/ua.js';
|
|
@@ -60,6 +66,7 @@ import {
|
|
|
60
66
|
BOX_RAISES,
|
|
61
67
|
buildBoxes,
|
|
62
68
|
CONTENT_IMAGES,
|
|
69
|
+
CLEARED_FROM,
|
|
63
70
|
CUT_BLOCKS,
|
|
64
71
|
GENERATED_FROM,
|
|
65
72
|
INLINE_OFFSETS,
|
|
@@ -104,6 +111,7 @@ import {
|
|
|
104
111
|
holdsAbsolute,
|
|
105
112
|
paintDocument,
|
|
106
113
|
queryChildIndex,
|
|
114
|
+
selectionRows,
|
|
107
115
|
stackLayers,
|
|
108
116
|
} from './paint.js';
|
|
109
117
|
import type { PaintContext } from './paint.js';
|
|
@@ -249,6 +257,8 @@ export class HtmlViewNode extends Node {
|
|
|
249
257
|
* that read it (`vw`, `vh`). */
|
|
250
258
|
private _styledWidth = -1;
|
|
251
259
|
private _styledHeight = -1;
|
|
260
|
+
/** The cascade the box tree's styles were computed by. */
|
|
261
|
+
private _styledWith: Cascade | null = null;
|
|
252
262
|
private _documentHeight = 0;
|
|
253
263
|
private _documentWidth = 0;
|
|
254
264
|
/** The viewport height the document was last laid out under, and whether
|
|
@@ -266,6 +276,20 @@ export class HtmlViewNode extends Node {
|
|
|
266
276
|
* the text carries surrogate pairs. null until checked. */
|
|
267
277
|
private _pointsAreUnits: boolean | null = null;
|
|
268
278
|
private _hovered: Element[] = [];
|
|
279
|
+
/** Where the pointer last moved to, in logical window pixels, while it
|
|
280
|
+
* is over the element (`defaultMouseMove`). */
|
|
281
|
+
private _pointerAt: { x: number; y: number } | null = null;
|
|
282
|
+
/** A hover held until the content under a still pointer stops moving:
|
|
283
|
+
* the timer, and when the content last moved (`_holdHover`). */
|
|
284
|
+
private _heldHover: unknown = null;
|
|
285
|
+
private _heldSince = 0;
|
|
286
|
+
/**
|
|
287
|
+
* The elements the build of the boxes that is due has to style again,
|
|
288
|
+
* where every change asking for it said which (`_invalidate`), and null
|
|
289
|
+
* where any element's style may have changed — a new cascade, a viewport
|
|
290
|
+
* a `vw` reads, a face that arrived.
|
|
291
|
+
*/
|
|
292
|
+
private _restyleOnly: Set<Element> | null = null;
|
|
269
293
|
private _scriptsSeen = new WeakSet<Element>();
|
|
270
294
|
private _controls: ControlRect[] = [];
|
|
271
295
|
private _reportedDomRevision = -1;
|
|
@@ -363,6 +387,7 @@ export class HtmlViewNode extends Node {
|
|
|
363
387
|
controlPadY: look.controlPadY * s,
|
|
364
388
|
controlBorder: look.controlBorder * s,
|
|
365
389
|
controlRadius: look.controlRadius * s,
|
|
390
|
+
controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
|
|
366
391
|
};
|
|
367
392
|
this._deviceLookFor = look;
|
|
368
393
|
this._deviceLookAt = s;
|
|
@@ -372,7 +397,26 @@ export class HtmlViewNode extends Node {
|
|
|
372
397
|
|
|
373
398
|
// --- the pipeline ---------------------------------------------------------
|
|
374
399
|
|
|
375
|
-
|
|
400
|
+
/**
|
|
401
|
+
* `restyle` with `Stale.Boxes`: the elements whose styles the change
|
|
402
|
+
* could have touched, where that is all of it — a pointer move's reach
|
|
403
|
+
* (`_hoverReach`). The build it asks for keeps every other element's
|
|
404
|
+
* (`_restyleOnly`).
|
|
405
|
+
*/
|
|
406
|
+
private _invalidate(
|
|
407
|
+
stale: Stale,
|
|
408
|
+
restyle: ReadonlySet<Element> | null = null,
|
|
409
|
+
): void {
|
|
410
|
+
if (stale >= Stale.Boxes) {
|
|
411
|
+
const only = stale === Stale.Boxes ? restyle : null;
|
|
412
|
+
if (this._stale < Stale.Boxes) {
|
|
413
|
+
this._restyleOnly = only ? new Set(only) : null;
|
|
414
|
+
} else if (this._restyleOnly && only) {
|
|
415
|
+
for (const el of only) this._restyleOnly.add(el);
|
|
416
|
+
} else {
|
|
417
|
+
this._restyleOnly = null;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
376
420
|
if (stale > this._stale) this._stale = stale;
|
|
377
421
|
if (stale >= Stale.Layout) this.invalidateMeasure('content');
|
|
378
422
|
this.invalidate(stale >= Stale.Layout, this, 'props');
|
|
@@ -578,7 +622,10 @@ export class HtmlViewNode extends Node {
|
|
|
578
622
|
kept.cascade.viewportHeight = this._viewportHeight();
|
|
579
623
|
faces = kept.faces;
|
|
580
624
|
} else {
|
|
581
|
-
|
|
625
|
+
// the UA sheet's `px` are CSS pixels, which the cascade scales like an
|
|
626
|
+
// author's, so it is written from the look in CSS pixels: from the
|
|
627
|
+
// device one, a control's size and a button's chrome were doubled at 2x
|
|
628
|
+
const sheets: Stylesheet[] = [uaStylesheet(props.look)];
|
|
582
629
|
const imports: ImportRead[][] = [];
|
|
583
630
|
faces = [];
|
|
584
631
|
let order = 0;
|
|
@@ -633,10 +680,17 @@ export class HtmlViewNode extends Node {
|
|
|
633
680
|
// The faces this width and scheme declare: a `@font-face` may sit in a
|
|
634
681
|
// `@media` block like any rule.
|
|
635
682
|
const cssWidth = width / this._scale;
|
|
683
|
+
const cssHeight = this._viewportHeight() / this._scale;
|
|
636
684
|
this._webFonts.setFallback(look.fontFamily);
|
|
637
685
|
this._webFonts.setFaces(
|
|
638
686
|
faces.filter((f) =>
|
|
639
|
-
mediaMatches(
|
|
687
|
+
mediaMatches(
|
|
688
|
+
f.rule.media,
|
|
689
|
+
cssWidth,
|
|
690
|
+
look.colorScheme,
|
|
691
|
+
cssHeight,
|
|
692
|
+
this._scale,
|
|
693
|
+
),
|
|
640
694
|
),
|
|
641
695
|
);
|
|
642
696
|
this._cascade.setPointer({
|
|
@@ -893,15 +947,21 @@ export class HtmlViewNode extends Node {
|
|
|
893
947
|
const wasWidth = this._documentWidth;
|
|
894
948
|
const wasHeight = this._documentHeight;
|
|
895
949
|
|
|
950
|
+
// the styles a build may keep (`_restyleOnly`), unless something
|
|
951
|
+
// found below changes them all
|
|
952
|
+
let restyleOnly = this._stale === Stale.Boxes ? this._restyleOnly : null;
|
|
953
|
+
this._restyleOnly = null;
|
|
896
954
|
if (this._stale >= Stale.Style || !this._cascade) {
|
|
897
955
|
this._restyle(target);
|
|
898
956
|
this._stale = Math.max(this._stale, Stale.Boxes) as Stale;
|
|
957
|
+
restyleOnly = null;
|
|
899
958
|
} else if (this._cascade.mediaBand(target) !== this._mediaBand) {
|
|
900
959
|
// A resize that crossed a `@media` breakpoint is the one resize that
|
|
901
960
|
// does have to restyle. Knowing which resizes those are is why the
|
|
902
961
|
// breakpoints are collected at parse time.
|
|
903
962
|
this._restyle(target);
|
|
904
963
|
this._stale = Math.max(this._stale, Stale.Boxes) as Stale;
|
|
964
|
+
restyleOnly = null;
|
|
905
965
|
}
|
|
906
966
|
|
|
907
967
|
const cascade = this._cascade;
|
|
@@ -913,18 +973,27 @@ export class HtmlViewNode extends Node {
|
|
|
913
973
|
// again, where some style reads the side that moved. A document that
|
|
914
974
|
// reads neither — most — goes on skipping the cascade on a resize.
|
|
915
975
|
if (
|
|
916
|
-
|
|
917
|
-
(
|
|
918
|
-
(cascade.readsViewportHeight && this._styledHeight !== viewport))
|
|
976
|
+
(cascade.readsViewportWidth && this._styledWidth !== target) ||
|
|
977
|
+
(cascade.readsViewportHeight && this._styledHeight !== viewport)
|
|
919
978
|
) {
|
|
920
|
-
this._stale = Stale.Boxes;
|
|
979
|
+
if (this._stale < Stale.Boxes) this._stale = Stale.Boxes;
|
|
980
|
+
restyleOnly = null;
|
|
921
981
|
}
|
|
922
982
|
|
|
923
983
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
924
984
|
const look = this._deviceLook();
|
|
985
|
+
// A build a pointer move asked for styles the elements the move
|
|
986
|
+
// reached, and takes every other element's from the tree it replaces
|
|
987
|
+
// (`Cascade.beginSharing`): the first build only, since a second is
|
|
988
|
+
// one the first found a reason for — a face, an image's size.
|
|
989
|
+
let kept: KeptStyles | null =
|
|
990
|
+
restyleOnly && this._tree && this._styledWith === cascade
|
|
991
|
+
? { styles: this._tree.styles, restyle: restyleOnly }
|
|
992
|
+
: null;
|
|
925
993
|
const build = () =>
|
|
926
994
|
buildBoxes(this._source.document, {
|
|
927
995
|
cascade,
|
|
996
|
+
kept,
|
|
928
997
|
scale: this._scale,
|
|
929
998
|
imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
|
|
930
999
|
urlSize: (url) => this._resources.imageSize(url),
|
|
@@ -932,6 +1001,8 @@ export class HtmlViewNode extends Node {
|
|
|
932
1001
|
measureControl(el, kind, style, this._fonts(), look),
|
|
933
1002
|
});
|
|
934
1003
|
this._tree = build();
|
|
1004
|
+
kept = null;
|
|
1005
|
+
this._styledWith = cascade;
|
|
935
1006
|
this._styledWidth = target;
|
|
936
1007
|
this._styledHeight = viewport;
|
|
937
1008
|
this._requestBackgrounds(this._tree);
|
|
@@ -1010,6 +1081,7 @@ export class HtmlViewNode extends Node {
|
|
|
1010
1081
|
y: r.y / s,
|
|
1011
1082
|
width: r.width / s,
|
|
1012
1083
|
height: r.height / s,
|
|
1084
|
+
fontSize: r.fontSize / s,
|
|
1013
1085
|
...(r.bare && {
|
|
1014
1086
|
bare: {
|
|
1015
1087
|
...r.bare,
|
|
@@ -1112,6 +1184,7 @@ export class HtmlViewNode extends Node {
|
|
|
1112
1184
|
}
|
|
1113
1185
|
|
|
1114
1186
|
override destroySubtree(): void {
|
|
1187
|
+
this._dropHeldHover();
|
|
1115
1188
|
this._resources.destroy();
|
|
1116
1189
|
this._webFonts.destroy();
|
|
1117
1190
|
this._source.destroy();
|
|
@@ -1209,7 +1282,8 @@ export class HtmlViewNode extends Node {
|
|
|
1209
1282
|
const to = this._toUnits(end);
|
|
1210
1283
|
if (to <= from) return [];
|
|
1211
1284
|
const out: Rect[] = [];
|
|
1212
|
-
|
|
1285
|
+
// the rows the highlight paints, over glyphs taller than their line
|
|
1286
|
+
collectBands(tree.root, from, to, this.abs.x, this.abs.y, out, true);
|
|
1213
1287
|
return out;
|
|
1214
1288
|
}
|
|
1215
1289
|
|
|
@@ -1280,11 +1354,16 @@ export class HtmlViewNode extends Node {
|
|
|
1280
1354
|
for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
|
|
1281
1355
|
const around = block.parent;
|
|
1282
1356
|
if (!around) continue;
|
|
1357
|
+
// from where clearance moved it down from, as Blink's line around
|
|
1358
|
+
// a block in an inline box starts there: the Zen Garden's 214
|
|
1359
|
+
// makes its content an inline box of floats and a footer that
|
|
1360
|
+
// clears them, and Chrome measures it from the floats' top
|
|
1361
|
+
const top = Math.min(block.y, CLEARED_FROM.get(block) ?? block.y);
|
|
1283
1362
|
bands.push({
|
|
1284
1363
|
x: around.contentX,
|
|
1285
|
-
y:
|
|
1364
|
+
y: top,
|
|
1286
1365
|
width: around.contentWidth,
|
|
1287
|
-
height: block.height,
|
|
1366
|
+
height: block.y + block.height - top,
|
|
1288
1367
|
});
|
|
1289
1368
|
}
|
|
1290
1369
|
for (const band of bands) rect = rect ? unionRect(rect, band) : band;
|
|
@@ -1381,6 +1460,7 @@ export class HtmlViewNode extends Node {
|
|
|
1381
1460
|
* contains a `:hover` rule.
|
|
1382
1461
|
*/
|
|
1383
1462
|
setHover(x: number, y: number): boolean {
|
|
1463
|
+
this._dropHeldHover();
|
|
1384
1464
|
const cascade = this._cascade;
|
|
1385
1465
|
if (!cascade || !cascade.hoverSensitive) return false;
|
|
1386
1466
|
const chain: Element[] = [];
|
|
@@ -1398,6 +1478,7 @@ export class HtmlViewNode extends Node {
|
|
|
1398
1478
|
}
|
|
1399
1479
|
|
|
1400
1480
|
clearHover(): boolean {
|
|
1481
|
+
this._dropHeldHover();
|
|
1401
1482
|
if (!this._hovered.length) return false;
|
|
1402
1483
|
const was = this._hovered;
|
|
1403
1484
|
this._hovered = [];
|
|
@@ -1406,95 +1487,145 @@ export class HtmlViewNode extends Node {
|
|
|
1406
1487
|
return true;
|
|
1407
1488
|
}
|
|
1408
1489
|
|
|
1409
|
-
/**
|
|
1490
|
+
/**
|
|
1491
|
+
* The hovered chain moved: nothing, where no rule that tests the pointer
|
|
1492
|
+
* names an element it moved over; a restyle where they are, where all it
|
|
1493
|
+
* changed is ink (`_hoverInPlace`); and otherwise the boxes built again,
|
|
1494
|
+
* with the styles of every element the move did not reach kept.
|
|
1495
|
+
*/
|
|
1410
1496
|
private _restyleHover(was: readonly Element[], now: readonly Element[]) {
|
|
1411
|
-
|
|
1412
|
-
if (
|
|
1497
|
+
const reach = this._hoverReach(was, now);
|
|
1498
|
+
if (reach !== null) {
|
|
1499
|
+
if (!reach.size) return;
|
|
1500
|
+
if (this._hoverInPlace(reach, was)) return;
|
|
1501
|
+
}
|
|
1502
|
+
this._invalidate(Stale.Boxes, reach);
|
|
1413
1503
|
}
|
|
1414
1504
|
|
|
1415
1505
|
/**
|
|
1416
|
-
*
|
|
1506
|
+
* The elements whose styles a move of the hovered chain can change, or
|
|
1507
|
+
* null where that cannot be said (`Cascade.hoverLocal`).
|
|
1417
1508
|
*
|
|
1418
|
-
*
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
* - Only an element whose hover state flipped and that a compound testing
|
|
1426
|
-
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1427
|
-
* subtree, and its later siblings where a sibling combinator follows.
|
|
1428
|
-
* A move between two paragraphs under `a:hover` touches nothing.
|
|
1429
|
-
* - Those are styled again from their parents. Where anything but ink
|
|
1430
|
-
* differs (`PAINT_ONLY`), or a box is not the element's own — an
|
|
1431
|
-
* anonymous or a pseudo-element box, a marker, a control — this is
|
|
1432
|
-
* not the move to take.
|
|
1433
|
-
* - The boxes take their new styles, and each layout of their text is
|
|
1434
|
-
* made again from the runs it was made from (`TextLayoutCache.inputsOf`)
|
|
1435
|
-
* with the new ink (`runFor`), where its geometry comes out the same.
|
|
1436
|
-
*
|
|
1437
|
-
* Everything is checked before anything is changed. 'none' where nothing
|
|
1438
|
-
* could change, 'painted' where it was restyled here, false where the
|
|
1439
|
-
* document has to be built again.
|
|
1509
|
+
* Only an element whose hover state flipped and that a compound testing
|
|
1510
|
+
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1511
|
+
* subtree, and its later siblings' where a sibling combinator is in play;
|
|
1512
|
+
* and the elements a `:has()` testing the pointer may flip, which are
|
|
1513
|
+
* around the ones that changed. A move between two paragraphs under
|
|
1514
|
+
* `a:hover` reaches nothing.
|
|
1440
1515
|
*/
|
|
1441
|
-
private
|
|
1516
|
+
private _hoverReach(
|
|
1442
1517
|
was: readonly Element[],
|
|
1443
1518
|
now: readonly Element[],
|
|
1444
|
-
):
|
|
1519
|
+
): Set<Element> | null {
|
|
1445
1520
|
const cascade = this._cascade;
|
|
1446
|
-
|
|
1447
|
-
const layouts = this._layouts;
|
|
1448
|
-
if (!cascade || !tree || !layouts || !cascade.hoverLocal) return false;
|
|
1449
|
-
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
|
|
1450
|
-
|
|
1521
|
+
if (!cascade || !cascade.hoverLocal) return null;
|
|
1451
1522
|
const before = new Set(was);
|
|
1452
1523
|
const after = new Set(now);
|
|
1453
|
-
const roots
|
|
1454
|
-
// and the elements a `:has()` testing the pointer may flip, which
|
|
1455
|
-
// are around the ones that changed; the chains are ancestor chains,
|
|
1456
|
-
// so the deepest that changed on each side is where to look from
|
|
1524
|
+
const roots = new Map<Element, HoverTouch>();
|
|
1457
1525
|
const flipped = (el: Element, other: Set<Element>): void => {
|
|
1458
1526
|
if (other.has(el)) return;
|
|
1459
|
-
|
|
1527
|
+
const touch = cascade.hoverTouches(el);
|
|
1528
|
+
if (touch > (roots.get(el) ?? HOVER_UNTOUCHED)) roots.set(el, touch);
|
|
1460
1529
|
};
|
|
1461
1530
|
for (const el of was) flipped(el, after);
|
|
1462
1531
|
for (const el of now) flipped(el, before);
|
|
1532
|
+
// the chains are ancestor chains, so the deepest that changed on each
|
|
1533
|
+
// side is where a `:has()` is looked for from
|
|
1463
1534
|
const deepest = (chain: readonly Element[], other: Set<Element>) => {
|
|
1464
1535
|
const el = chain[0];
|
|
1465
1536
|
if (el && !other.has(el)) cascade.hoverAnchors(el, roots);
|
|
1466
1537
|
};
|
|
1467
1538
|
deepest(was, after);
|
|
1468
1539
|
deepest(now, before);
|
|
1469
|
-
if (!roots.length) return 'none';
|
|
1470
1540
|
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1541
|
+
// the roots' subtrees, which inherit from them and a descendant
|
|
1542
|
+
// combinator reaches, and their later siblings' too where a sibling
|
|
1543
|
+
// combinator follows the compound that tests the pointer
|
|
1474
1544
|
const reach = new Set<Element>();
|
|
1475
|
-
const collect = (el: Element):
|
|
1545
|
+
const collect = (el: Element): void => {
|
|
1476
1546
|
const stack: Element[] = [el];
|
|
1477
1547
|
while (stack.length) {
|
|
1478
1548
|
const at = stack.pop()!;
|
|
1479
1549
|
if (reach.has(at)) continue;
|
|
1480
1550
|
reach.add(at);
|
|
1481
|
-
if (reach.size > HOVER_RESTYLE_LIMIT) return false;
|
|
1482
1551
|
for (const child of at.children)
|
|
1483
1552
|
if (isElement(child)) stack.push(child);
|
|
1484
1553
|
}
|
|
1485
|
-
return true;
|
|
1486
1554
|
};
|
|
1487
|
-
for (const root of roots) {
|
|
1488
|
-
|
|
1489
|
-
if (
|
|
1555
|
+
for (const [root, touch] of roots) {
|
|
1556
|
+
collect(root);
|
|
1557
|
+
if (touch !== HOVER_FOLLOWED) continue;
|
|
1490
1558
|
for (let s = root.nextSibling; s; s = s.nextSibling) {
|
|
1491
|
-
if (isElement(s)
|
|
1559
|
+
if (isElement(s)) collect(s);
|
|
1492
1560
|
}
|
|
1493
1561
|
}
|
|
1562
|
+
return reach;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
/**
|
|
1566
|
+
* A pointer move restyled where it happened, when all it changed is ink.
|
|
1567
|
+
*
|
|
1568
|
+
* A move to another element is a change to the cascade, and the pipeline
|
|
1569
|
+
* answers one by building every box again and laying the document out:
|
|
1570
|
+
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1571
|
+
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1572
|
+
* nearly all change a colour, an underline, a background or a border's
|
|
1573
|
+
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1574
|
+
*
|
|
1575
|
+
* - The elements the move reached (`_hoverReach`) are styled again from
|
|
1576
|
+
* their parents — the ones a rule testing the pointer answers
|
|
1577
|
+
* differently for, or whose parent's style changed; the rest, most of
|
|
1578
|
+
* a hovered card or table row, have the styles they had
|
|
1579
|
+
* (`Cascade.pointerChanged`). Where anything but ink differs
|
|
1580
|
+
* (`PAINT_ONLY`), this is not the move to take.
|
|
1581
|
+
* - The boxes take their new styles: an element's own, the text in it,
|
|
1582
|
+
* the anonymous boxes the fix-up made around what is in it, and its
|
|
1583
|
+
* `::before` and `::after`, styled again by their own rules. A box
|
|
1584
|
+
* whose style is derived some other way — a marker, a first letter, a
|
|
1585
|
+
* control — is not one to restyle here.
|
|
1586
|
+
* - Each layout of their text is made again from the runs it was made
|
|
1587
|
+
* from (`TextLayoutCache.inputsOf`) with the new ink (`runFor`), where
|
|
1588
|
+
* its geometry comes out the same.
|
|
1589
|
+
*
|
|
1590
|
+
* Everything is checked before anything is changed, and nothing is looked
|
|
1591
|
+
* at but the blocks the restyled elements are in (`_blocksOf`): a move
|
|
1592
|
+
* costs what it changed, in a document of any length. True where it was
|
|
1593
|
+
* restyled here, or had nothing to restyle; false where the boxes have to
|
|
1594
|
+
* be built again.
|
|
1595
|
+
*/
|
|
1596
|
+
private _hoverInPlace(
|
|
1597
|
+
reach: ReadonlySet<Element>,
|
|
1598
|
+
was: readonly Element[],
|
|
1599
|
+
): boolean {
|
|
1600
|
+
const cascade = this._cascade;
|
|
1601
|
+
const tree = this._tree;
|
|
1602
|
+
const layouts = this._layouts;
|
|
1603
|
+
if (!cascade || !tree || !layouts) return false;
|
|
1604
|
+
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
|
|
1605
|
+
if (reach.size > HOVER_RESTYLE_LIMIT) return false;
|
|
1606
|
+
// a first line's, a first letter's and a marker's styles are layout's
|
|
1607
|
+
const pseudoRules = cascade.hoverPseudo;
|
|
1608
|
+
if (pseudoRules === HOVER_PSEUDO_OTHER) return false;
|
|
1609
|
+
|
|
1610
|
+
// whether the rules that test the pointer answer differently for an
|
|
1611
|
+
// element than they did before the move
|
|
1612
|
+
const before = { hovered: new Set(was), active: EMPTY_SET };
|
|
1613
|
+
const flips = new Map<Element, boolean>();
|
|
1614
|
+
const flipped = (el: Element): boolean => {
|
|
1615
|
+
let flip = flips.get(el);
|
|
1616
|
+
if (flip === undefined) {
|
|
1617
|
+
flip = cascade.pointerChanged(el, before);
|
|
1618
|
+
flips.set(el, flip);
|
|
1619
|
+
}
|
|
1620
|
+
return flip;
|
|
1621
|
+
};
|
|
1494
1622
|
|
|
1495
1623
|
// styled again from their parents, a parent first
|
|
1496
1624
|
const fresh = new Map<Element, ComputedStyle>();
|
|
1497
1625
|
const changed = new Map<Element, ComputedStyle>();
|
|
1626
|
+
// the ones whose ink is what it was: only their custom properties
|
|
1627
|
+
// changed, which the elements under them read
|
|
1628
|
+
const quiet = new Set<Element>();
|
|
1498
1629
|
let refused = false;
|
|
1499
1630
|
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1500
1631
|
// reaches, the order its layer paints in, where one is
|
|
@@ -1515,51 +1646,123 @@ export class HtmlViewNode extends Node {
|
|
|
1515
1646
|
refused = true;
|
|
1516
1647
|
return null;
|
|
1517
1648
|
}
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
if (diff
|
|
1525
|
-
|
|
1526
|
-
|
|
1649
|
+
const parentWas = parent ? tree.styles.get(parent)!.style : parentStyle;
|
|
1650
|
+
let style = kept.style;
|
|
1651
|
+
if (parentStyle !== parentWas || flipped(el)) {
|
|
1652
|
+
const made = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1653
|
+
const diff = hoverChange(kept.style, made);
|
|
1654
|
+
if (diff === false) refused = true;
|
|
1655
|
+
else if (diff) {
|
|
1656
|
+
style = made;
|
|
1657
|
+
changed.set(el, made);
|
|
1658
|
+
if (!diff.ink) quiet.add(el);
|
|
1659
|
+
if (diff.reach) reaches = true;
|
|
1660
|
+
if (diff.order) reorders = true;
|
|
1661
|
+
if (diff.move) moving.add(el);
|
|
1662
|
+
}
|
|
1527
1663
|
}
|
|
1664
|
+
fresh.set(el, style);
|
|
1528
1665
|
return style;
|
|
1529
1666
|
};
|
|
1530
1667
|
for (const el of reach) {
|
|
1531
1668
|
styleOf(el);
|
|
1532
1669
|
if (refused) return false;
|
|
1533
1670
|
}
|
|
1534
|
-
if (!changed.size) return
|
|
1671
|
+
if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE) return true;
|
|
1535
1672
|
for (const el of changed.keys()) {
|
|
1536
1673
|
const tag = tagOf(el);
|
|
1537
1674
|
// their backgrounds are the canvas's
|
|
1538
1675
|
if (tag === 'html' || tag === 'body') return false;
|
|
1539
1676
|
}
|
|
1540
1677
|
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1678
|
+
// The boxes, each under its parent: what a box takes depends on what
|
|
1679
|
+
// the box it is in took.
|
|
1543
1680
|
const fonts = layouts.fonts;
|
|
1681
|
+
const blocks = this._blocksOf(
|
|
1682
|
+
tree,
|
|
1683
|
+
pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach,
|
|
1684
|
+
);
|
|
1544
1685
|
const restyled: [Box, ComputedStyle][] = [];
|
|
1686
|
+
const next = new Map<Box, ComputedStyle>();
|
|
1687
|
+
/** The restyled boxes whose ink changed, to repaint. */
|
|
1688
|
+
const inked = new Set<Box>();
|
|
1689
|
+
/** The `::before` and `::after` boxes found, by element: 1 and 2. */
|
|
1690
|
+
const generated = new Map<Element, number>();
|
|
1545
1691
|
const redecorated: [Box, Box['decoration']][] = [];
|
|
1546
|
-
const walk: Box[] =
|
|
1692
|
+
const walk: Box[] = blocks.slice();
|
|
1547
1693
|
while (walk.length) {
|
|
1548
1694
|
const box = walk.pop()!;
|
|
1549
1695
|
for (const child of box.children) walk.push(child);
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1696
|
+
const parent = box.parent;
|
|
1697
|
+
const above = parent ? next.get(parent) : undefined;
|
|
1698
|
+
let style: ComputedStyle | undefined;
|
|
1699
|
+
let ink = !!parent && inked.has(parent);
|
|
1700
|
+
if (box.kind === 'text' || box.kind === 'break') {
|
|
1701
|
+
// Text is set in the style of the box it is in — an element's, a
|
|
1702
|
+
// pseudo-element's — or of its element, where the fix-up put an
|
|
1703
|
+
// anonymous box around it.
|
|
1704
|
+
const el = box.el;
|
|
1705
|
+
if (parent && box.style === parent.style) style = above;
|
|
1706
|
+
else if (el && box.style === tree.styles.get(el)?.style) {
|
|
1707
|
+
style = changed.get(el);
|
|
1708
|
+
ink = !quiet.has(el);
|
|
1709
|
+
} else if (above || (el && changed.has(el))) return false;
|
|
1710
|
+
} else if (box.pseudo === 'before' || box.pseudo === 'after') {
|
|
1711
|
+
const from = GENERATED_FROM.get(box);
|
|
1712
|
+
if (!from) {
|
|
1713
|
+
// an inside marker, made of its item's style and its own rules
|
|
1714
|
+
if (above) return false;
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
if (!reach.has(from)) continue;
|
|
1718
|
+
generated.set(
|
|
1719
|
+
from,
|
|
1720
|
+
(generated.get(from) ?? 0) | (box.pseudo === 'before' ? 1 : 2),
|
|
1721
|
+
);
|
|
1722
|
+
const inherits = fresh.get(from);
|
|
1723
|
+
const kept = tree.styles.get(from)?.style;
|
|
1724
|
+
if (!inherits || !kept) return false;
|
|
1725
|
+
if (inherits === kept && !flipped(from)) continue;
|
|
1726
|
+
// styled by its own rules over its element's style, as the builder
|
|
1727
|
+
// styled it, in its element's box
|
|
1728
|
+
if (!parent || parent.style !== kept || box.marker) return false;
|
|
1729
|
+
const made = cascade.pseudoStyleFor(from, box.pseudo, inherits);
|
|
1730
|
+
if (!made || made.display === 'none') return false;
|
|
1731
|
+
const diff = hoverChange(box.style, made);
|
|
1732
|
+
if (diff === false) return false;
|
|
1733
|
+
if (!diff) continue;
|
|
1734
|
+
if (diff.reach || diff.order || diff.move) return false;
|
|
1735
|
+
style = made;
|
|
1736
|
+
ink = diff.ink;
|
|
1737
|
+
} else if (box.pseudo) {
|
|
1738
|
+
// a first letter, styled from the box its letter is in
|
|
1739
|
+
if (above) return false;
|
|
1740
|
+
} else if (box.el) {
|
|
1741
|
+
const el = box.el;
|
|
1742
|
+
style = changed.get(el);
|
|
1743
|
+
if (!style) continue;
|
|
1744
|
+
const kept = tree.styles.get(el)!.style;
|
|
1745
|
+
// a style derived from the element's, or drawn from it somewhere
|
|
1746
|
+
// else — a widget mounted beside the document takes its look from
|
|
1747
|
+
// it. An image, a drawing or a rule is drawn here, from its box,
|
|
1748
|
+
// like any.
|
|
1749
|
+
if (box.style !== kept) return false;
|
|
1750
|
+
if (box.marker || WIDGETS.has(box.replaced)) return false;
|
|
1751
|
+
if (moving.has(el) && !movable(box, kept, style)) return false;
|
|
1752
|
+
ink = !quiet.has(el);
|
|
1753
|
+
} else if (above) {
|
|
1754
|
+
// An anonymous box takes what its parent's style passes on
|
|
1755
|
+
// (`anonymousStyles`), so it takes that of the new one: the text of
|
|
1756
|
+
// a link made a flex row is in one, and takes the link's colour.
|
|
1757
|
+
if (GENERATED_FROM.has(box)) return false;
|
|
1758
|
+
const display = box.style.display;
|
|
1759
|
+
if (tree.anonymous(parent!.style, display) !== box.style) return false;
|
|
1760
|
+
style = tree.anonymous(above, display);
|
|
1761
|
+
}
|
|
1553
1762
|
if (!style) continue;
|
|
1554
|
-
|
|
1555
|
-
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1556
|
-
// derived from the element's, or drawn from it somewhere else — a
|
|
1557
|
-
// widget mounted beside the document takes its look from it. An
|
|
1558
|
-
// image, a drawing or a rule is drawn here, from its box, like any.
|
|
1559
|
-
if (!box.el || box.style !== kept.style) return false;
|
|
1560
|
-
if (box.marker || WIDGETS.has(box.replaced)) return false;
|
|
1561
|
-
if (moving.has(owner) && !movable(box, kept.style, style)) return false;
|
|
1763
|
+
next.set(box, style);
|
|
1562
1764
|
restyled.push([box, style]);
|
|
1765
|
+
if (ink) inked.add(box);
|
|
1563
1766
|
if (box.kind === 'inline') {
|
|
1564
1767
|
const decoration = inlineDecoration(fonts, box, style);
|
|
1565
1768
|
if ((decoration === null) !== (box.decoration === null)) {
|
|
@@ -1569,12 +1772,28 @@ export class HtmlViewNode extends Node {
|
|
|
1569
1772
|
if (decoration !== box.decoration) redecorated.push([box, decoration]);
|
|
1570
1773
|
}
|
|
1571
1774
|
}
|
|
1775
|
+
// a `::before` or an `::after` a rule that tests the pointer now gives
|
|
1776
|
+
// an element is a box to build
|
|
1777
|
+
if (pseudoRules !== HOVER_PSEUDO_NONE) {
|
|
1778
|
+
for (const el of reach) {
|
|
1779
|
+
if (!flipped(el)) continue;
|
|
1780
|
+
const style = fresh.get(el);
|
|
1781
|
+
if (!style) continue;
|
|
1782
|
+
const has = generated.get(el) ?? 0;
|
|
1783
|
+
for (const [which, bit] of GENERATED_BITS) {
|
|
1784
|
+
if (has & bit) continue;
|
|
1785
|
+
const made = cascade.pseudoStyleFor(el, which, style);
|
|
1786
|
+
if (made && made.display !== 'none') return false;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
if (!restyled.length) return true;
|
|
1572
1791
|
|
|
1573
|
-
// the text: each layout holding a run of a
|
|
1574
|
-
//
|
|
1792
|
+
// the text: each layout holding a run of a restyled box's, made again
|
|
1793
|
+
// with its new ink, where it comes out the same shape
|
|
1575
1794
|
const relaid = new Map<TextLayoutLike, TextLayoutLike | null>();
|
|
1576
1795
|
const texts: LineText[] = [];
|
|
1577
|
-
walk.push(
|
|
1796
|
+
if (inked.size) for (const block of blocks) walk.push(block);
|
|
1578
1797
|
while (walk.length) {
|
|
1579
1798
|
const box = walk.pop()!;
|
|
1580
1799
|
for (const child of box.children) walk.push(child);
|
|
@@ -1583,16 +1802,16 @@ export class HtmlViewNode extends Node {
|
|
|
1583
1802
|
for (const text of line.texts) {
|
|
1584
1803
|
texts.push(text);
|
|
1585
1804
|
if (relaid.has(text.layout)) continue;
|
|
1586
|
-
const
|
|
1587
|
-
if (
|
|
1588
|
-
relaid.set(text.layout,
|
|
1805
|
+
const again = reinked(text, layouts, next);
|
|
1806
|
+
if (again === false) return false;
|
|
1807
|
+
relaid.set(text.layout, again);
|
|
1589
1808
|
}
|
|
1590
1809
|
}
|
|
1591
1810
|
}
|
|
1592
1811
|
|
|
1593
1812
|
// what each box drew before, to be repainted with what it draws after
|
|
1594
1813
|
const inks: Rect[] = [];
|
|
1595
|
-
for (const
|
|
1814
|
+
for (const box of inked) {
|
|
1596
1815
|
const ink = inkOf(box);
|
|
1597
1816
|
if (ink) inks.push(ink);
|
|
1598
1817
|
}
|
|
@@ -1602,7 +1821,9 @@ export class HtmlViewNode extends Node {
|
|
|
1602
1821
|
for (const [box, style] of restyled) {
|
|
1603
1822
|
const was = box.style;
|
|
1604
1823
|
box.style = style;
|
|
1605
|
-
if (moving.has(box.el
|
|
1824
|
+
if (box.el && moving.has(box.el) && box.kind !== 'text') {
|
|
1825
|
+
moved.push([box, was]);
|
|
1826
|
+
}
|
|
1606
1827
|
}
|
|
1607
1828
|
for (const [box, decoration] of redecorated) {
|
|
1608
1829
|
// what is inside it keeps which boxes around it paint, and this is
|
|
@@ -1613,8 +1834,8 @@ export class HtmlViewNode extends Node {
|
|
|
1613
1834
|
box.decoration = decoration;
|
|
1614
1835
|
}
|
|
1615
1836
|
for (const text of texts) {
|
|
1616
|
-
const
|
|
1617
|
-
if (
|
|
1837
|
+
const again = relaid.get(text.layout);
|
|
1838
|
+
if (again) text.layout = again;
|
|
1618
1839
|
}
|
|
1619
1840
|
for (const [el, style] of changed) {
|
|
1620
1841
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el)!.inFlex });
|
|
@@ -1645,14 +1866,61 @@ export class HtmlViewNode extends Node {
|
|
|
1645
1866
|
if (moved.length) this._reportControls();
|
|
1646
1867
|
if (relayout) {
|
|
1647
1868
|
this._invalidate(Stale.Layout);
|
|
1648
|
-
return
|
|
1869
|
+
return true;
|
|
1649
1870
|
}
|
|
1650
|
-
for (const
|
|
1871
|
+
for (const box of inked) {
|
|
1651
1872
|
const ink = inkOf(box);
|
|
1652
1873
|
if (ink) inks.push(ink);
|
|
1653
1874
|
}
|
|
1654
1875
|
this._repaintInk(inks);
|
|
1655
|
-
return
|
|
1876
|
+
return true;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/** An element's first box in document order, for the tree it was asked
|
|
1880
|
+
* of: made on the first move over a tree, which is one walk of it. */
|
|
1881
|
+
private _firstBoxes: { tree: BoxTree; of: Map<Element, Box> } | null = null;
|
|
1882
|
+
|
|
1883
|
+
/**
|
|
1884
|
+
* The blocks a restyle of `changed` has to look in: for each element, the
|
|
1885
|
+
* box its text is set in the lines of — the nearest box around its first
|
|
1886
|
+
* that is not an inline one — which holds every box that takes its style
|
|
1887
|
+
* from it, the pieces a block in it broke it into among them
|
|
1888
|
+
* (`breakAround`). One that is inside another is left to it.
|
|
1889
|
+
*/
|
|
1890
|
+
private _blocksOf(tree: BoxTree, changed: Iterable<Element>): Box[] {
|
|
1891
|
+
let index = this._firstBoxes;
|
|
1892
|
+
if (index?.tree !== tree) {
|
|
1893
|
+
const of = new Map<Element, Box>();
|
|
1894
|
+
const stack: Box[] = [tree.root];
|
|
1895
|
+
while (stack.length) {
|
|
1896
|
+
const box = stack.pop()!;
|
|
1897
|
+
if (box.el && !of.has(box.el)) of.set(box.el, box);
|
|
1898
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1899
|
+
stack.push(box.children[i]);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
index = this._firstBoxes = { tree, of };
|
|
1903
|
+
}
|
|
1904
|
+
const blocks = new Set<Box>();
|
|
1905
|
+
for (const el of changed) {
|
|
1906
|
+
let box: Box | null = index.of.get(el) ?? null;
|
|
1907
|
+
while (
|
|
1908
|
+
box?.parent &&
|
|
1909
|
+
(box.kind === 'inline' || box.kind === 'text' || box.kind === 'break')
|
|
1910
|
+
) {
|
|
1911
|
+
box = box.parent;
|
|
1912
|
+
}
|
|
1913
|
+
if (box) blocks.add(box);
|
|
1914
|
+
}
|
|
1915
|
+
const out: Box[] = [];
|
|
1916
|
+
for (const block of blocks) {
|
|
1917
|
+
let inside = false;
|
|
1918
|
+
for (let at = block.parent; at && !inside; at = at.parent) {
|
|
1919
|
+
inside = blocks.has(at);
|
|
1920
|
+
}
|
|
1921
|
+
if (!inside) out.push(block);
|
|
1922
|
+
}
|
|
1923
|
+
return out;
|
|
1656
1924
|
}
|
|
1657
1925
|
|
|
1658
1926
|
/**
|
|
@@ -1715,20 +1983,65 @@ export class HtmlViewNode extends Node {
|
|
|
1715
1983
|
// `:hover` is wired at the element rather than through React: the cascade
|
|
1716
1984
|
// already knows whether any rule in the document tests it, `setHover`
|
|
1717
1985
|
// returns without work when none does, and a document that does use it
|
|
1718
|
-
// re-styles only when the hovered chain actually changed
|
|
1719
|
-
//
|
|
1720
|
-
//
|
|
1986
|
+
// re-styles only when the hovered chain actually changed, and then only
|
|
1987
|
+
// the elements the change reached (`_restyleHover`).
|
|
1988
|
+
//
|
|
1989
|
+
// **A hover waits for a scroll to stop.** Core asks again what is under a
|
|
1990
|
+
// pointer that stayed where it was after every frame that laid out —
|
|
1991
|
+
// hover follows content, react-x11#793 — and says so with a move to the
|
|
1992
|
+
// point the pointer is already at. While a document scrolls that is every
|
|
1993
|
+
// frame, each with something else under the pointer: a restyle a frame
|
|
1994
|
+
// at best, and where a hover moves something, the boxes built again a
|
|
1995
|
+
// frame, which took a page scrolled under a parked pointer from 52 frames
|
|
1996
|
+
// a second to 4. A browser holds its hover until the scroll is over, for
|
|
1997
|
+
// the same reason; so does this, for `HOVER_REST_MS` after the content
|
|
1998
|
+
// last moved. What was hovered stays hovered as it scrolls away, as it
|
|
1999
|
+
// does there. A move of the pointer itself is answered at once.
|
|
1721
2000
|
|
|
1722
2001
|
override defaultMouseMove(ev: X11MouseEvent): void {
|
|
1723
2002
|
super.defaultMouseMove?.(ev);
|
|
2003
|
+
const at = this._pointerAt;
|
|
2004
|
+
if (at !== null && at.x === ev.x && at.y === ev.y) {
|
|
2005
|
+
this._holdHover();
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
this._pointerAt = { x: ev.x, y: ev.y };
|
|
1724
2009
|
this.setHover(ev.x, ev.y);
|
|
1725
2010
|
}
|
|
1726
2011
|
|
|
1727
2012
|
override defaultMouseLeave(ev: X11MouseEvent): void {
|
|
1728
2013
|
super.defaultMouseLeave?.(ev);
|
|
2014
|
+
this._pointerAt = null;
|
|
1729
2015
|
this.clearHover();
|
|
1730
2016
|
}
|
|
1731
2017
|
|
|
2018
|
+
/** The content moved under the pointer: the hover is asked for once it
|
|
2019
|
+
* has been still for `HOVER_REST_MS`. One timer a wait, put off by
|
|
2020
|
+
* being armed again when it finds the content moved since. */
|
|
2021
|
+
private _holdHover(): void {
|
|
2022
|
+
if (!this._cascade?.hoverSensitive) return;
|
|
2023
|
+
this._heldSince = hoverClock.now();
|
|
2024
|
+
if (this._heldHover !== null) return;
|
|
2025
|
+
const rest = (): void => {
|
|
2026
|
+
this._heldHover = null;
|
|
2027
|
+
const at = this._pointerAt;
|
|
2028
|
+
if (!at || this.destroyed) return;
|
|
2029
|
+
const left = this._heldSince + HOVER_REST_MS - hoverClock.now();
|
|
2030
|
+
if (left > 0) {
|
|
2031
|
+
this._heldHover = hoverClock.arm(rest, left);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
this.setHover(at.x, at.y);
|
|
2035
|
+
};
|
|
2036
|
+
this._heldHover = hoverClock.arm(rest, HOVER_REST_MS);
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
private _dropHeldHover(): void {
|
|
2040
|
+
if (this._heldHover === null) return;
|
|
2041
|
+
hoverClock.disarm(this._heldHover);
|
|
2042
|
+
this._heldHover = null;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
1732
2045
|
// --- paint ----------------------------------------------------------------
|
|
1733
2046
|
|
|
1734
2047
|
override paint(ctx: Context2D): void {
|
|
@@ -2026,6 +2339,8 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
|
|
|
2026
2339
|
p.y !== q.y ||
|
|
2027
2340
|
p.width !== q.width ||
|
|
2028
2341
|
p.height !== q.height ||
|
|
2342
|
+
p.fontFamily !== q.fontFamily ||
|
|
2343
|
+
p.fontSize !== q.fontSize ||
|
|
2029
2344
|
p.opacity !== q.opacity ||
|
|
2030
2345
|
!sameBare(p.bare, q.bare)
|
|
2031
2346
|
) {
|
|
@@ -2320,6 +2635,9 @@ function lineBands(
|
|
|
2320
2635
|
/** Where the lines put the text, not where a relative offset moved it
|
|
2321
2636
|
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2322
2637
|
laidOut = false,
|
|
2638
|
+
/** The rows a highlight covers (`selectionRows`) rather than the line's:
|
|
2639
|
+
* past it where the glyphs are taller than the line. */
|
|
2640
|
+
ink = false,
|
|
2323
2641
|
): void {
|
|
2324
2642
|
// First line whose text can reach `from`, by binary search over the
|
|
2325
2643
|
// sorted text starts; stop at the first line past `to`.
|
|
@@ -2348,6 +2666,9 @@ function lineBands(
|
|
|
2348
2666
|
// a link that ends a line a space wider than Chrome's. So the text
|
|
2349
2667
|
// a line ends with is measured no further than its content.
|
|
2350
2668
|
const ends = endsLine(line, text, natural);
|
|
2669
|
+
const rows = ink
|
|
2670
|
+
? selectionRows(line, text, natural)
|
|
2671
|
+
: { y: line.y, height: line.height };
|
|
2351
2672
|
for (const band of bandsFor(
|
|
2352
2673
|
text.layout,
|
|
2353
2674
|
natural,
|
|
@@ -2363,9 +2684,9 @@ function lineBands(
|
|
|
2363
2684
|
}
|
|
2364
2685
|
out.push({
|
|
2365
2686
|
x: dx + left + text.drawX - moved,
|
|
2366
|
-
y: dy +
|
|
2687
|
+
y: dy + rows.y,
|
|
2367
2688
|
width: right - left,
|
|
2368
|
-
height:
|
|
2689
|
+
height: rows.height,
|
|
2369
2690
|
});
|
|
2370
2691
|
}
|
|
2371
2692
|
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
@@ -2375,9 +2696,9 @@ function lineBands(
|
|
|
2375
2696
|
if (at < a || at >= b) continue;
|
|
2376
2697
|
out.push({
|
|
2377
2698
|
x: dx + space.x + text.drawX - moved,
|
|
2378
|
-
y: dy +
|
|
2699
|
+
y: dy + rows.y,
|
|
2379
2700
|
width: space.width,
|
|
2380
|
-
height:
|
|
2701
|
+
height: rows.height,
|
|
2381
2702
|
});
|
|
2382
2703
|
}
|
|
2383
2704
|
}
|
|
@@ -2407,14 +2728,15 @@ function collectBands(
|
|
|
2407
2728
|
dx: number,
|
|
2408
2729
|
dy: number,
|
|
2409
2730
|
out: Rect[],
|
|
2731
|
+
ink = false,
|
|
2410
2732
|
): void {
|
|
2411
2733
|
if (box.subtreeTextEnd <= box.subtreeTextStart) return;
|
|
2412
2734
|
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to) return;
|
|
2413
|
-
if (box.lines) lineBands(box.lines, from, to, dx, dy, out);
|
|
2735
|
+
if (box.lines) lineBands(box.lines, from, to, dx, dy, out, false, ink);
|
|
2414
2736
|
// Atomics are ordinary children, reached below.
|
|
2415
2737
|
for (const child of box.children) {
|
|
2416
2738
|
if (child.kind === 'text' || child.kind === 'break') continue;
|
|
2417
|
-
collectBands(child, from, to, dx, dy, out);
|
|
2739
|
+
collectBands(child, from, to, dx, dy, out, ink);
|
|
2418
2740
|
}
|
|
2419
2741
|
}
|
|
2420
2742
|
|
|
@@ -2621,6 +2943,38 @@ function compareKeys(a: readonly number[], b: readonly number[]): number {
|
|
|
2621
2943
|
* that matches a container reaches everything in it. */
|
|
2622
2944
|
const HOVER_RESTYLE_LIMIT = 300;
|
|
2623
2945
|
|
|
2946
|
+
/** How long the content under a still pointer has to have stopped moving
|
|
2947
|
+
* before the hover is asked for again (`HtmlViewNode._holdHover`): a
|
|
2948
|
+
* tenth of a second, which is what WebKit waits after a scroll before it
|
|
2949
|
+
* sends the mouse move that updates its own. */
|
|
2950
|
+
const HOVER_REST_MS = 100;
|
|
2951
|
+
|
|
2952
|
+
const timers = globalThis as {
|
|
2953
|
+
setTimeout?(fn: () => void, ms: number): unknown;
|
|
2954
|
+
clearTimeout?(id: unknown): void;
|
|
2955
|
+
};
|
|
2956
|
+
|
|
2957
|
+
/**
|
|
2958
|
+
* The clock a held hover waits on. Through `globalThis` because `src/`
|
|
2959
|
+
* compiles with `types: []`, and unref'd where the runtime allows it: a
|
|
2960
|
+
* timer of a document's must not keep a process alive that is otherwise
|
|
2961
|
+
* done. One object, exported from this module though not from the package,
|
|
2962
|
+
* so a test can hold it and say when the rest is over (`test/held-clock.ts`).
|
|
2963
|
+
*/
|
|
2964
|
+
export const hoverClock = {
|
|
2965
|
+
now(): number {
|
|
2966
|
+
return Date.now();
|
|
2967
|
+
},
|
|
2968
|
+
arm(step: () => void, ms: number): unknown {
|
|
2969
|
+
const handle = timers.setTimeout?.(step, ms) ?? null;
|
|
2970
|
+
(handle as { unref?(): void } | null)?.unref?.();
|
|
2971
|
+
return handle;
|
|
2972
|
+
},
|
|
2973
|
+
disarm(handle: unknown): void {
|
|
2974
|
+
timers.clearTimeout?.(handle);
|
|
2975
|
+
},
|
|
2976
|
+
};
|
|
2977
|
+
|
|
2624
2978
|
/**
|
|
2625
2979
|
* The computed properties a pointer move may change in place: ink, which
|
|
2626
2980
|
* moves nothing. A background colour is drawn inside the box it colours
|
|
@@ -2676,11 +3030,21 @@ const FACE_FIELDS = [
|
|
|
2676
3030
|
/** What a restyle in place changed beyond ink: how far a box's ink
|
|
2677
3031
|
* reaches, the order its layer paints in, or where it is. */
|
|
2678
3032
|
interface HoverChange {
|
|
3033
|
+
/** Whether anything that is drawn changed: false where only the custom
|
|
3034
|
+
* properties did, which the elements under this one read. */
|
|
3035
|
+
ink: boolean;
|
|
2679
3036
|
reach: boolean;
|
|
2680
3037
|
order: boolean;
|
|
2681
3038
|
move: boolean;
|
|
2682
3039
|
}
|
|
2683
3040
|
|
|
3041
|
+
/** An element's `::before` and `::after`, each with its bit in the set
|
|
3042
|
+
* of the ones a restyle found boxes of (`_hoverInPlace`). */
|
|
3043
|
+
const GENERATED_BITS = [
|
|
3044
|
+
['before', 1],
|
|
3045
|
+
['after', 2],
|
|
3046
|
+
] as const;
|
|
3047
|
+
|
|
2684
3048
|
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
2685
3049
|
* a widget mounted beside the document, or a button whose press it reports. */
|
|
2686
3050
|
const WIDGETS = new Set<ReplacedKind>([
|
|
@@ -2717,8 +3081,17 @@ function hoverChange(
|
|
|
2717
3081
|
const b = now as unknown as Record<string, unknown>;
|
|
2718
3082
|
let change: HoverChange | null = null;
|
|
2719
3083
|
for (const key in b) {
|
|
2720
|
-
|
|
2721
|
-
|
|
3084
|
+
// the sets are shared by what made them (`Cascade._customFor`): one
|
|
3085
|
+
// object where they are the same
|
|
3086
|
+
if (key === 'custom') {
|
|
3087
|
+
if (a[key] !== b[key]) {
|
|
3088
|
+
change ??= { ink: false, reach: false, order: false, move: false };
|
|
3089
|
+
}
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
3092
|
+
if (sameValue(a[key], b[key])) continue;
|
|
3093
|
+
change ??= { ink: true, reach: false, order: false, move: false };
|
|
3094
|
+
change.ink = true;
|
|
2722
3095
|
if (PAINT_ONLY.has(key)) continue;
|
|
2723
3096
|
if (INK_REACH.has(key)) {
|
|
2724
3097
|
change.reach = true;
|
|
@@ -2802,16 +3175,9 @@ function sameValue(x: unknown, y: unknown): boolean {
|
|
|
2802
3175
|
return true;
|
|
2803
3176
|
}
|
|
2804
3177
|
|
|
2805
|
-
/** The element an anonymous box takes its style from: its nearest
|
|
2806
|
-
* ancestor's that has one. */
|
|
2807
|
-
function nearestElement(box: Box): Element | null {
|
|
2808
|
-
for (let at = box.parent; at; at = at.parent) if (at.el) return at.el;
|
|
2809
|
-
return null;
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
3178
|
/**
|
|
2813
|
-
* `text`'s layout made again with the ink of the
|
|
2814
|
-
* restyled: from the runs it was made from, each of a
|
|
3179
|
+
* `text`'s layout made again with the ink of the boxes a pointer move
|
|
3180
|
+
* restyled: from the runs it was made from, each of a restyled text box's
|
|
2815
3181
|
* given the ink its new style makes (`runFor`) and nothing else. Null where
|
|
2816
3182
|
* none of its runs is theirs; false where that cannot be told — a run this
|
|
2817
3183
|
* cannot place in the document, one some other pass inked (a first line's),
|
|
@@ -2820,19 +3186,16 @@ function nearestElement(box: Box): Element | null {
|
|
|
2820
3186
|
function reinked(
|
|
2821
3187
|
text: LineText,
|
|
2822
3188
|
layouts: TextLayoutCache,
|
|
2823
|
-
|
|
2824
|
-
styles: BoxTree['styles'],
|
|
3189
|
+
next: ReadonlyMap<Box, ComputedStyle>,
|
|
2825
3190
|
): TextLayoutLike | null | false {
|
|
2826
3191
|
const spans = text.spans;
|
|
2827
|
-
const ownerAt = (offset: number): Element | null =>
|
|
2828
|
-
spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
|
|
2829
3192
|
const inputs = layouts.inputsOf(text.layout);
|
|
2830
3193
|
if (!inputs || !spans.boxAt) {
|
|
2831
3194
|
// not one this could make again: none of its text may be theirs
|
|
2832
3195
|
for (const line of text.layout.lines) {
|
|
2833
3196
|
for (const run of line.runs) {
|
|
2834
|
-
const
|
|
2835
|
-
if (!spans.boxAt || (
|
|
3197
|
+
const box = spans.boxAt?.(run.start);
|
|
3198
|
+
if (!spans.boxAt || (box && next.has(box))) return false;
|
|
2836
3199
|
}
|
|
2837
3200
|
}
|
|
2838
3201
|
return null;
|
|
@@ -2843,25 +3206,25 @@ function reinked(
|
|
|
2843
3206
|
for (let i = 0; i < content.length; i += 1) {
|
|
2844
3207
|
const run = content[i];
|
|
2845
3208
|
const length = run.text.length;
|
|
2846
|
-
const
|
|
2847
|
-
const style =
|
|
2848
|
-
if (
|
|
2849
|
-
if (
|
|
2850
|
-
const was = runFor(run.text,
|
|
3209
|
+
const box = length ? spans.boxAt(offset) : null;
|
|
3210
|
+
const style = box ? next.get(box) : undefined;
|
|
3211
|
+
if (box && style) {
|
|
3212
|
+
if (spans.boxAt(offset + length - 1) !== box) return false;
|
|
3213
|
+
const was = runFor(run.text, box.style);
|
|
2851
3214
|
const now = runFor(run.text, style);
|
|
2852
3215
|
for (const f of FACE_FIELDS) if (!sameValue(was[f], now[f])) return false;
|
|
2853
|
-
let
|
|
3216
|
+
let inked: Record<string, unknown> | null = null;
|
|
2854
3217
|
for (const f of INK_FIELDS) {
|
|
2855
3218
|
// a run that is not what its style made — a first line's colour
|
|
2856
3219
|
if (!sameValue(run[f], was[f])) return false;
|
|
2857
3220
|
if (sameValue(was[f], now[f])) continue;
|
|
2858
|
-
|
|
2859
|
-
if (now[f] === undefined) delete
|
|
2860
|
-
else
|
|
3221
|
+
inked ??= { ...run };
|
|
3222
|
+
if (now[f] === undefined) delete inked[f];
|
|
3223
|
+
else inked[f] = now[f];
|
|
2861
3224
|
}
|
|
2862
|
-
if (
|
|
3225
|
+
if (inked) {
|
|
2863
3226
|
runs ??= content.slice();
|
|
2864
|
-
runs[i] =
|
|
3227
|
+
runs[i] = inked as unknown as TextRun;
|
|
2865
3228
|
}
|
|
2866
3229
|
}
|
|
2867
3230
|
offset += length;
|