@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/dist/html/node.js
CHANGED
|
@@ -40,10 +40,10 @@ import { registerElement, registeredElements } from 'react-x11/host';
|
|
|
40
40
|
import { Node } from 'react-x11/node';
|
|
41
41
|
import { codePointAtOffset, codeUnitOffsets } from '../internal/text.js';
|
|
42
42
|
import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
|
|
43
|
-
import { Cascade } from './css/cascade.js';
|
|
43
|
+
import { Cascade, HOVER_FOLLOWED, HOVER_PSEUDO_NONE, HOVER_PSEUDO_OTHER, HOVER_UNTOUCHED, } from './css/cascade.js';
|
|
44
44
|
import { mediaMatches, parseStylesheet } from './css/parse.js';
|
|
45
45
|
import { uaStylesheet } from './css/ua.js';
|
|
46
|
-
import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
46
|
+
import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, CLEARED_FROM, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
47
47
|
import { layoutDocument, retranslate, transformed } from './layout/block.js';
|
|
48
48
|
import { TextLayoutCache } from './layout/cache.js';
|
|
49
49
|
import { shapingSafe } from './layout/shaping.js';
|
|
@@ -53,7 +53,7 @@ import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
|
53
53
|
// a layout is built once, on the first selection that needs it.
|
|
54
54
|
import { documentOffsetOf, layoutOffsetOf, layoutOffsets as layoutOffsetsOf, } from './layout/inline.js';
|
|
55
55
|
import { lineBands as bandsFor } from '../richtext/runs.js';
|
|
56
|
-
import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, paintDocument, queryChildIndex, stackLayers, } from './paint.js';
|
|
56
|
+
import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, paintDocument, queryChildIndex, selectionRows, stackLayers, } from './paint.js';
|
|
57
57
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
58
58
|
import { ResourceStore } from './resources.js';
|
|
59
59
|
import { WebFonts } from './fonts.js';
|
|
@@ -135,6 +135,8 @@ export class HtmlViewNode extends Node {
|
|
|
135
135
|
* that read it (`vw`, `vh`). */
|
|
136
136
|
_styledWidth = -1;
|
|
137
137
|
_styledHeight = -1;
|
|
138
|
+
/** The cascade the box tree's styles were computed by. */
|
|
139
|
+
_styledWith = null;
|
|
138
140
|
_documentHeight = 0;
|
|
139
141
|
_documentWidth = 0;
|
|
140
142
|
/** The viewport height the document was last laid out under, and whether
|
|
@@ -152,6 +154,20 @@ export class HtmlViewNode extends Node {
|
|
|
152
154
|
* the text carries surrogate pairs. null until checked. */
|
|
153
155
|
_pointsAreUnits = null;
|
|
154
156
|
_hovered = [];
|
|
157
|
+
/** Where the pointer last moved to, in logical window pixels, while it
|
|
158
|
+
* is over the element (`defaultMouseMove`). */
|
|
159
|
+
_pointerAt = null;
|
|
160
|
+
/** A hover held until the content under a still pointer stops moving:
|
|
161
|
+
* the timer, and when the content last moved (`_holdHover`). */
|
|
162
|
+
_heldHover = null;
|
|
163
|
+
_heldSince = 0;
|
|
164
|
+
/**
|
|
165
|
+
* The elements the build of the boxes that is due has to style again,
|
|
166
|
+
* where every change asking for it said which (`_invalidate`), and null
|
|
167
|
+
* where any element's style may have changed — a new cascade, a viewport
|
|
168
|
+
* a `vw` reads, a face that arrived.
|
|
169
|
+
*/
|
|
170
|
+
_restyleOnly = null;
|
|
155
171
|
_scriptsSeen = new WeakSet();
|
|
156
172
|
_controls = [];
|
|
157
173
|
_reportedDomRevision = -1;
|
|
@@ -230,6 +246,7 @@ export class HtmlViewNode extends Node {
|
|
|
230
246
|
controlPadY: look.controlPadY * s,
|
|
231
247
|
controlBorder: look.controlBorder * s,
|
|
232
248
|
controlRadius: look.controlRadius * s,
|
|
249
|
+
controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
|
|
233
250
|
};
|
|
234
251
|
this._deviceLookFor = look;
|
|
235
252
|
this._deviceLookAt = s;
|
|
@@ -237,7 +254,26 @@ export class HtmlViewNode extends Node {
|
|
|
237
254
|
return scaled;
|
|
238
255
|
}
|
|
239
256
|
// --- the pipeline ---------------------------------------------------------
|
|
240
|
-
|
|
257
|
+
/**
|
|
258
|
+
* `restyle` with `Stale.Boxes`: the elements whose styles the change
|
|
259
|
+
* could have touched, where that is all of it — a pointer move's reach
|
|
260
|
+
* (`_hoverReach`). The build it asks for keeps every other element's
|
|
261
|
+
* (`_restyleOnly`).
|
|
262
|
+
*/
|
|
263
|
+
_invalidate(stale, restyle = null) {
|
|
264
|
+
if (stale >= Stale.Boxes) {
|
|
265
|
+
const only = stale === Stale.Boxes ? restyle : null;
|
|
266
|
+
if (this._stale < Stale.Boxes) {
|
|
267
|
+
this._restyleOnly = only ? new Set(only) : null;
|
|
268
|
+
}
|
|
269
|
+
else if (this._restyleOnly && only) {
|
|
270
|
+
for (const el of only)
|
|
271
|
+
this._restyleOnly.add(el);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
this._restyleOnly = null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
241
277
|
if (stale > this._stale)
|
|
242
278
|
this._stale = stale;
|
|
243
279
|
if (stale >= Stale.Layout)
|
|
@@ -443,7 +479,10 @@ export class HtmlViewNode extends Node {
|
|
|
443
479
|
faces = kept.faces;
|
|
444
480
|
}
|
|
445
481
|
else {
|
|
446
|
-
|
|
482
|
+
// the UA sheet's `px` are CSS pixels, which the cascade scales like an
|
|
483
|
+
// author's, so it is written from the look in CSS pixels: from the
|
|
484
|
+
// device one, a control's size and a button's chrome were doubled at 2x
|
|
485
|
+
const sheets = [uaStylesheet(props.look)];
|
|
447
486
|
const imports = [];
|
|
448
487
|
faces = [];
|
|
449
488
|
let order = 0;
|
|
@@ -490,8 +529,9 @@ export class HtmlViewNode extends Node {
|
|
|
490
529
|
// The faces this width and scheme declare: a `@font-face` may sit in a
|
|
491
530
|
// `@media` block like any rule.
|
|
492
531
|
const cssWidth = width / this._scale;
|
|
532
|
+
const cssHeight = this._viewportHeight() / this._scale;
|
|
493
533
|
this._webFonts.setFallback(look.fontFamily);
|
|
494
|
-
this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme)));
|
|
534
|
+
this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme, cssHeight, this._scale)));
|
|
495
535
|
this._cascade.setPointer({
|
|
496
536
|
hovered: new Set(this._hovered),
|
|
497
537
|
active: EMPTY_SET,
|
|
@@ -720,9 +760,14 @@ export class HtmlViewNode extends Node {
|
|
|
720
760
|
const onlyViewport = this._stale === Stale.Nothing && this._laidOutWidth === target;
|
|
721
761
|
const wasWidth = this._documentWidth;
|
|
722
762
|
const wasHeight = this._documentHeight;
|
|
763
|
+
// the styles a build may keep (`_restyleOnly`), unless something
|
|
764
|
+
// found below changes them all
|
|
765
|
+
let restyleOnly = this._stale === Stale.Boxes ? this._restyleOnly : null;
|
|
766
|
+
this._restyleOnly = null;
|
|
723
767
|
if (this._stale >= Stale.Style || !this._cascade) {
|
|
724
768
|
this._restyle(target);
|
|
725
769
|
this._stale = Math.max(this._stale, Stale.Boxes);
|
|
770
|
+
restyleOnly = null;
|
|
726
771
|
}
|
|
727
772
|
else if (this._cascade.mediaBand(target) !== this._mediaBand) {
|
|
728
773
|
// A resize that crossed a `@media` breakpoint is the one resize that
|
|
@@ -730,6 +775,7 @@ export class HtmlViewNode extends Node {
|
|
|
730
775
|
// breakpoints are collected at parse time.
|
|
731
776
|
this._restyle(target);
|
|
732
777
|
this._stale = Math.max(this._stale, Stale.Boxes);
|
|
778
|
+
restyleOnly = null;
|
|
733
779
|
}
|
|
734
780
|
const cascade = this._cascade;
|
|
735
781
|
if (!cascade)
|
|
@@ -740,21 +786,32 @@ export class HtmlViewNode extends Node {
|
|
|
740
786
|
// styles computed for another viewport are wrong for this one: built
|
|
741
787
|
// again, where some style reads the side that moved. A document that
|
|
742
788
|
// reads neither — most — goes on skipping the cascade on a resize.
|
|
743
|
-
if (
|
|
744
|
-
(
|
|
745
|
-
|
|
746
|
-
|
|
789
|
+
if ((cascade.readsViewportWidth && this._styledWidth !== target) ||
|
|
790
|
+
(cascade.readsViewportHeight && this._styledHeight !== viewport)) {
|
|
791
|
+
if (this._stale < Stale.Boxes)
|
|
792
|
+
this._stale = Stale.Boxes;
|
|
793
|
+
restyleOnly = null;
|
|
747
794
|
}
|
|
748
795
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
749
796
|
const look = this._deviceLook();
|
|
797
|
+
// A build a pointer move asked for styles the elements the move
|
|
798
|
+
// reached, and takes every other element's from the tree it replaces
|
|
799
|
+
// (`Cascade.beginSharing`): the first build only, since a second is
|
|
800
|
+
// one the first found a reason for — a face, an image's size.
|
|
801
|
+
let kept = restyleOnly && this._tree && this._styledWith === cascade
|
|
802
|
+
? { styles: this._tree.styles, restyle: restyleOnly }
|
|
803
|
+
: null;
|
|
750
804
|
const build = () => buildBoxes(this._source.document, {
|
|
751
805
|
cascade,
|
|
806
|
+
kept,
|
|
752
807
|
scale: this._scale,
|
|
753
808
|
imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
|
|
754
809
|
urlSize: (url) => this._resources.imageSize(url),
|
|
755
810
|
controlSize: (el, kind, style) => measureControl(el, kind, style, this._fonts(), look),
|
|
756
811
|
});
|
|
757
812
|
this._tree = build();
|
|
813
|
+
kept = null;
|
|
814
|
+
this._styledWith = cascade;
|
|
758
815
|
this._styledWidth = target;
|
|
759
816
|
this._styledHeight = viewport;
|
|
760
817
|
this._requestBackgrounds(this._tree);
|
|
@@ -820,6 +877,7 @@ export class HtmlViewNode extends Node {
|
|
|
820
877
|
y: r.y / s,
|
|
821
878
|
width: r.width / s,
|
|
822
879
|
height: r.height / s,
|
|
880
|
+
fontSize: r.fontSize / s,
|
|
823
881
|
...(r.bare && {
|
|
824
882
|
bare: {
|
|
825
883
|
...r.bare,
|
|
@@ -914,6 +972,7 @@ export class HtmlViewNode extends Node {
|
|
|
914
972
|
}
|
|
915
973
|
}
|
|
916
974
|
destroySubtree() {
|
|
975
|
+
this._dropHeldHover();
|
|
917
976
|
this._resources.destroy();
|
|
918
977
|
this._webFonts.destroy();
|
|
919
978
|
this._source.destroy();
|
|
@@ -1008,7 +1067,8 @@ export class HtmlViewNode extends Node {
|
|
|
1008
1067
|
if (to <= from)
|
|
1009
1068
|
return [];
|
|
1010
1069
|
const out = [];
|
|
1011
|
-
|
|
1070
|
+
// the rows the highlight paints, over glyphs taller than their line
|
|
1071
|
+
collectBands(tree.root, from, to, this.abs.x, this.abs.y, out, true);
|
|
1012
1072
|
return out;
|
|
1013
1073
|
}
|
|
1014
1074
|
// --- pointer --------------------------------------------------------------
|
|
@@ -1079,11 +1139,16 @@ export class HtmlViewNode extends Node {
|
|
|
1079
1139
|
const around = block.parent;
|
|
1080
1140
|
if (!around)
|
|
1081
1141
|
continue;
|
|
1142
|
+
// from where clearance moved it down from, as Blink's line around
|
|
1143
|
+
// a block in an inline box starts there: the Zen Garden's 214
|
|
1144
|
+
// makes its content an inline box of floats and a footer that
|
|
1145
|
+
// clears them, and Chrome measures it from the floats' top
|
|
1146
|
+
const top = Math.min(block.y, CLEARED_FROM.get(block) ?? block.y);
|
|
1082
1147
|
bands.push({
|
|
1083
1148
|
x: around.contentX,
|
|
1084
|
-
y:
|
|
1149
|
+
y: top,
|
|
1085
1150
|
width: around.contentWidth,
|
|
1086
|
-
height: block.height,
|
|
1151
|
+
height: block.y + block.height - top,
|
|
1087
1152
|
});
|
|
1088
1153
|
}
|
|
1089
1154
|
for (const band of bands)
|
|
@@ -1177,6 +1242,7 @@ export class HtmlViewNode extends Node {
|
|
|
1177
1242
|
* contains a `:hover` rule.
|
|
1178
1243
|
*/
|
|
1179
1244
|
setHover(x, y) {
|
|
1245
|
+
this._dropHeldHover();
|
|
1180
1246
|
const cascade = this._cascade;
|
|
1181
1247
|
if (!cascade || !cascade.hoverSensitive)
|
|
1182
1248
|
return false;
|
|
@@ -1195,6 +1261,7 @@ export class HtmlViewNode extends Node {
|
|
|
1195
1261
|
return true;
|
|
1196
1262
|
}
|
|
1197
1263
|
clearHover() {
|
|
1264
|
+
this._dropHeldHover();
|
|
1198
1265
|
if (!this._hovered.length)
|
|
1199
1266
|
return false;
|
|
1200
1267
|
const was = this._hovered;
|
|
@@ -1203,62 +1270,53 @@ export class HtmlViewNode extends Node {
|
|
|
1203
1270
|
this._restyleHover(was, []);
|
|
1204
1271
|
return true;
|
|
1205
1272
|
}
|
|
1206
|
-
/**
|
|
1273
|
+
/**
|
|
1274
|
+
* The hovered chain moved: nothing, where no rule that tests the pointer
|
|
1275
|
+
* names an element it moved over; a restyle where they are, where all it
|
|
1276
|
+
* changed is ink (`_hoverInPlace`); and otherwise the boxes built again,
|
|
1277
|
+
* with the styles of every element the move did not reach kept.
|
|
1278
|
+
*/
|
|
1207
1279
|
_restyleHover(was, now) {
|
|
1208
|
-
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1280
|
+
const reach = this._hoverReach(was, now);
|
|
1281
|
+
if (reach !== null) {
|
|
1282
|
+
if (!reach.size)
|
|
1283
|
+
return;
|
|
1284
|
+
if (this._hoverInPlace(reach, was))
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
this._invalidate(Stale.Boxes, reach);
|
|
1211
1288
|
}
|
|
1212
1289
|
/**
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
* A move to another element is a change to the cascade, and the pipeline
|
|
1216
|
-
* answers one by building every box again and laying the document out:
|
|
1217
|
-
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1218
|
-
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1219
|
-
* nearly all change a colour, an underline, a background or a border's
|
|
1220
|
-
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1290
|
+
* The elements whose styles a move of the hovered chain can change, or
|
|
1291
|
+
* null where that cannot be said (`Cascade.hoverLocal`).
|
|
1221
1292
|
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
* anonymous or a pseudo-element box, a marker, a control — this is
|
|
1229
|
-
* not the move to take.
|
|
1230
|
-
* - The boxes take their new styles, and each layout of their text is
|
|
1231
|
-
* made again from the runs it was made from (`TextLayoutCache.inputsOf`)
|
|
1232
|
-
* with the new ink (`runFor`), where its geometry comes out the same.
|
|
1233
|
-
*
|
|
1234
|
-
* Everything is checked before anything is changed. 'none' where nothing
|
|
1235
|
-
* could change, 'painted' where it was restyled here, false where the
|
|
1236
|
-
* document has to be built again.
|
|
1293
|
+
* Only an element whose hover state flipped and that a compound testing
|
|
1294
|
+
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1295
|
+
* subtree, and its later siblings' where a sibling combinator is in play;
|
|
1296
|
+
* and the elements a `:has()` testing the pointer may flip, which are
|
|
1297
|
+
* around the ones that changed. A move between two paragraphs under
|
|
1298
|
+
* `a:hover` reaches nothing.
|
|
1237
1299
|
*/
|
|
1238
|
-
|
|
1300
|
+
_hoverReach(was, now) {
|
|
1239
1301
|
const cascade = this._cascade;
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
if (!cascade || !tree || !layouts || !cascade.hoverLocal)
|
|
1243
|
-
return false;
|
|
1244
|
-
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
|
|
1245
|
-
return false;
|
|
1302
|
+
if (!cascade || !cascade.hoverLocal)
|
|
1303
|
+
return null;
|
|
1246
1304
|
const before = new Set(was);
|
|
1247
1305
|
const after = new Set(now);
|
|
1248
|
-
const roots =
|
|
1249
|
-
// and the elements a `:has()` testing the pointer may flip, which
|
|
1250
|
-
// are around the ones that changed; the chains are ancestor chains,
|
|
1251
|
-
// so the deepest that changed on each side is where to look from
|
|
1306
|
+
const roots = new Map();
|
|
1252
1307
|
const flipped = (el, other) => {
|
|
1253
1308
|
if (other.has(el))
|
|
1254
1309
|
return;
|
|
1255
|
-
|
|
1256
|
-
|
|
1310
|
+
const touch = cascade.hoverTouches(el);
|
|
1311
|
+
if (touch > (roots.get(el) ?? HOVER_UNTOUCHED))
|
|
1312
|
+
roots.set(el, touch);
|
|
1257
1313
|
};
|
|
1258
1314
|
for (const el of was)
|
|
1259
1315
|
flipped(el, after);
|
|
1260
1316
|
for (const el of now)
|
|
1261
1317
|
flipped(el, before);
|
|
1318
|
+
// the chains are ancestor chains, so the deepest that changed on each
|
|
1319
|
+
// side is where a `:has()` is looked for from
|
|
1262
1320
|
const deepest = (chain, other) => {
|
|
1263
1321
|
const el = chain[0];
|
|
1264
1322
|
if (el && !other.has(el))
|
|
@@ -1266,11 +1324,9 @@ export class HtmlViewNode extends Node {
|
|
|
1266
1324
|
};
|
|
1267
1325
|
deepest(was, after);
|
|
1268
1326
|
deepest(now, before);
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
//
|
|
1272
|
-
// descendant combinator reaches, and their later siblings' too where a
|
|
1273
|
-
// sibling combinator follows a compound that tests the pointer
|
|
1327
|
+
// the roots' subtrees, which inherit from them and a descendant
|
|
1328
|
+
// combinator reaches, and their later siblings' too where a sibling
|
|
1329
|
+
// combinator follows the compound that tests the pointer
|
|
1274
1330
|
const reach = new Set();
|
|
1275
1331
|
const collect = (el) => {
|
|
1276
1332
|
const stack = [el];
|
|
@@ -1279,27 +1335,85 @@ export class HtmlViewNode extends Node {
|
|
|
1279
1335
|
if (reach.has(at))
|
|
1280
1336
|
continue;
|
|
1281
1337
|
reach.add(at);
|
|
1282
|
-
if (reach.size > HOVER_RESTYLE_LIMIT)
|
|
1283
|
-
return false;
|
|
1284
1338
|
for (const child of at.children)
|
|
1285
1339
|
if (isElement(child))
|
|
1286
1340
|
stack.push(child);
|
|
1287
1341
|
}
|
|
1288
|
-
return true;
|
|
1289
1342
|
};
|
|
1290
|
-
for (const root of roots) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
if (!cascade.hoverSiblings)
|
|
1343
|
+
for (const [root, touch] of roots) {
|
|
1344
|
+
collect(root);
|
|
1345
|
+
if (touch !== HOVER_FOLLOWED)
|
|
1294
1346
|
continue;
|
|
1295
1347
|
for (let s = root.nextSibling; s; s = s.nextSibling) {
|
|
1296
|
-
if (isElement(s)
|
|
1297
|
-
|
|
1348
|
+
if (isElement(s))
|
|
1349
|
+
collect(s);
|
|
1298
1350
|
}
|
|
1299
1351
|
}
|
|
1352
|
+
return reach;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* A pointer move restyled where it happened, when all it changed is ink.
|
|
1356
|
+
*
|
|
1357
|
+
* A move to another element is a change to the cascade, and the pipeline
|
|
1358
|
+
* answers one by building every box again and laying the document out:
|
|
1359
|
+
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1360
|
+
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1361
|
+
* nearly all change a colour, an underline, a background or a border's
|
|
1362
|
+
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1363
|
+
*
|
|
1364
|
+
* - The elements the move reached (`_hoverReach`) are styled again from
|
|
1365
|
+
* their parents — the ones a rule testing the pointer answers
|
|
1366
|
+
* differently for, or whose parent's style changed; the rest, most of
|
|
1367
|
+
* a hovered card or table row, have the styles they had
|
|
1368
|
+
* (`Cascade.pointerChanged`). Where anything but ink differs
|
|
1369
|
+
* (`PAINT_ONLY`), this is not the move to take.
|
|
1370
|
+
* - The boxes take their new styles: an element's own, the text in it,
|
|
1371
|
+
* the anonymous boxes the fix-up made around what is in it, and its
|
|
1372
|
+
* `::before` and `::after`, styled again by their own rules. A box
|
|
1373
|
+
* whose style is derived some other way — a marker, a first letter, a
|
|
1374
|
+
* control — is not one to restyle here.
|
|
1375
|
+
* - Each layout of their text is made again from the runs it was made
|
|
1376
|
+
* from (`TextLayoutCache.inputsOf`) with the new ink (`runFor`), where
|
|
1377
|
+
* its geometry comes out the same.
|
|
1378
|
+
*
|
|
1379
|
+
* Everything is checked before anything is changed, and nothing is looked
|
|
1380
|
+
* at but the blocks the restyled elements are in (`_blocksOf`): a move
|
|
1381
|
+
* costs what it changed, in a document of any length. True where it was
|
|
1382
|
+
* restyled here, or had nothing to restyle; false where the boxes have to
|
|
1383
|
+
* be built again.
|
|
1384
|
+
*/
|
|
1385
|
+
_hoverInPlace(reach, was) {
|
|
1386
|
+
const cascade = this._cascade;
|
|
1387
|
+
const tree = this._tree;
|
|
1388
|
+
const layouts = this._layouts;
|
|
1389
|
+
if (!cascade || !tree || !layouts)
|
|
1390
|
+
return false;
|
|
1391
|
+
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
|
|
1392
|
+
return false;
|
|
1393
|
+
if (reach.size > HOVER_RESTYLE_LIMIT)
|
|
1394
|
+
return false;
|
|
1395
|
+
// a first line's, a first letter's and a marker's styles are layout's
|
|
1396
|
+
const pseudoRules = cascade.hoverPseudo;
|
|
1397
|
+
if (pseudoRules === HOVER_PSEUDO_OTHER)
|
|
1398
|
+
return false;
|
|
1399
|
+
// whether the rules that test the pointer answer differently for an
|
|
1400
|
+
// element than they did before the move
|
|
1401
|
+
const before = { hovered: new Set(was), active: EMPTY_SET };
|
|
1402
|
+
const flips = new Map();
|
|
1403
|
+
const flipped = (el) => {
|
|
1404
|
+
let flip = flips.get(el);
|
|
1405
|
+
if (flip === undefined) {
|
|
1406
|
+
flip = cascade.pointerChanged(el, before);
|
|
1407
|
+
flips.set(el, flip);
|
|
1408
|
+
}
|
|
1409
|
+
return flip;
|
|
1410
|
+
};
|
|
1300
1411
|
// styled again from their parents, a parent first
|
|
1301
1412
|
const fresh = new Map();
|
|
1302
1413
|
const changed = new Map();
|
|
1414
|
+
// the ones whose ink is what it was: only their custom properties
|
|
1415
|
+
// changed, which the elements under them read
|
|
1416
|
+
const quiet = new Set();
|
|
1303
1417
|
let refused = false;
|
|
1304
1418
|
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1305
1419
|
// reaches, the order its layer paints in, where one is
|
|
@@ -1323,20 +1437,27 @@ export class HtmlViewNode extends Node {
|
|
|
1323
1437
|
refused = true;
|
|
1324
1438
|
return null;
|
|
1325
1439
|
}
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
if (diff
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1440
|
+
const parentWas = parent ? tree.styles.get(parent).style : parentStyle;
|
|
1441
|
+
let style = kept.style;
|
|
1442
|
+
if (parentStyle !== parentWas || flipped(el)) {
|
|
1443
|
+
const made = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1444
|
+
const diff = hoverChange(kept.style, made);
|
|
1445
|
+
if (diff === false)
|
|
1446
|
+
refused = true;
|
|
1447
|
+
else if (diff) {
|
|
1448
|
+
style = made;
|
|
1449
|
+
changed.set(el, made);
|
|
1450
|
+
if (!diff.ink)
|
|
1451
|
+
quiet.add(el);
|
|
1452
|
+
if (diff.reach)
|
|
1453
|
+
reaches = true;
|
|
1454
|
+
if (diff.order)
|
|
1455
|
+
reorders = true;
|
|
1456
|
+
if (diff.move)
|
|
1457
|
+
moving.add(el);
|
|
1458
|
+
}
|
|
1339
1459
|
}
|
|
1460
|
+
fresh.set(el, style);
|
|
1340
1461
|
return style;
|
|
1341
1462
|
};
|
|
1342
1463
|
for (const el of reach) {
|
|
@@ -1344,42 +1465,122 @@ export class HtmlViewNode extends Node {
|
|
|
1344
1465
|
if (refused)
|
|
1345
1466
|
return false;
|
|
1346
1467
|
}
|
|
1347
|
-
if (!changed.size)
|
|
1348
|
-
return
|
|
1468
|
+
if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE)
|
|
1469
|
+
return true;
|
|
1349
1470
|
for (const el of changed.keys()) {
|
|
1350
1471
|
const tag = tagOf(el);
|
|
1351
1472
|
// their backgrounds are the canvas's
|
|
1352
1473
|
if (tag === 'html' || tag === 'body')
|
|
1353
1474
|
return false;
|
|
1354
1475
|
}
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1476
|
+
// The boxes, each under its parent: what a box takes depends on what
|
|
1477
|
+
// the box it is in took.
|
|
1357
1478
|
const fonts = layouts.fonts;
|
|
1479
|
+
const blocks = this._blocksOf(tree, pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach);
|
|
1358
1480
|
const restyled = [];
|
|
1481
|
+
const next = new Map();
|
|
1482
|
+
/** The restyled boxes whose ink changed, to repaint. */
|
|
1483
|
+
const inked = new Set();
|
|
1484
|
+
/** The `::before` and `::after` boxes found, by element: 1 and 2. */
|
|
1485
|
+
const generated = new Map();
|
|
1359
1486
|
const redecorated = [];
|
|
1360
|
-
const walk =
|
|
1487
|
+
const walk = blocks.slice();
|
|
1361
1488
|
while (walk.length) {
|
|
1362
1489
|
const box = walk.pop();
|
|
1363
1490
|
for (const child of box.children)
|
|
1364
1491
|
walk.push(child);
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1492
|
+
const parent = box.parent;
|
|
1493
|
+
const above = parent ? next.get(parent) : undefined;
|
|
1494
|
+
let style;
|
|
1495
|
+
let ink = !!parent && inked.has(parent);
|
|
1496
|
+
if (box.kind === 'text' || box.kind === 'break') {
|
|
1497
|
+
// Text is set in the style of the box it is in — an element's, a
|
|
1498
|
+
// pseudo-element's — or of its element, where the fix-up put an
|
|
1499
|
+
// anonymous box around it.
|
|
1500
|
+
const el = box.el;
|
|
1501
|
+
if (parent && box.style === parent.style)
|
|
1502
|
+
style = above;
|
|
1503
|
+
else if (el && box.style === tree.styles.get(el)?.style) {
|
|
1504
|
+
style = changed.get(el);
|
|
1505
|
+
ink = !quiet.has(el);
|
|
1506
|
+
}
|
|
1507
|
+
else if (above || (el && changed.has(el)))
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1510
|
+
else if (box.pseudo === 'before' || box.pseudo === 'after') {
|
|
1511
|
+
const from = GENERATED_FROM.get(box);
|
|
1512
|
+
if (!from) {
|
|
1513
|
+
// an inside marker, made of its item's style and its own rules
|
|
1514
|
+
if (above)
|
|
1515
|
+
return false;
|
|
1516
|
+
continue;
|
|
1517
|
+
}
|
|
1518
|
+
if (!reach.has(from))
|
|
1519
|
+
continue;
|
|
1520
|
+
generated.set(from, (generated.get(from) ?? 0) | (box.pseudo === 'before' ? 1 : 2));
|
|
1521
|
+
const inherits = fresh.get(from);
|
|
1522
|
+
const kept = tree.styles.get(from)?.style;
|
|
1523
|
+
if (!inherits || !kept)
|
|
1524
|
+
return false;
|
|
1525
|
+
if (inherits === kept && !flipped(from))
|
|
1526
|
+
continue;
|
|
1527
|
+
// styled by its own rules over its element's style, as the builder
|
|
1528
|
+
// styled it, in its element's box
|
|
1529
|
+
if (!parent || parent.style !== kept || box.marker)
|
|
1530
|
+
return false;
|
|
1531
|
+
const made = cascade.pseudoStyleFor(from, box.pseudo, inherits);
|
|
1532
|
+
if (!made || made.display === 'none')
|
|
1533
|
+
return false;
|
|
1534
|
+
const diff = hoverChange(box.style, made);
|
|
1535
|
+
if (diff === false)
|
|
1536
|
+
return false;
|
|
1537
|
+
if (!diff)
|
|
1538
|
+
continue;
|
|
1539
|
+
if (diff.reach || diff.order || diff.move)
|
|
1540
|
+
return false;
|
|
1541
|
+
style = made;
|
|
1542
|
+
ink = diff.ink;
|
|
1543
|
+
}
|
|
1544
|
+
else if (box.pseudo) {
|
|
1545
|
+
// a first letter, styled from the box its letter is in
|
|
1546
|
+
if (above)
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
else if (box.el) {
|
|
1550
|
+
const el = box.el;
|
|
1551
|
+
style = changed.get(el);
|
|
1552
|
+
if (!style)
|
|
1553
|
+
continue;
|
|
1554
|
+
const kept = tree.styles.get(el).style;
|
|
1555
|
+
// a style derived from the element's, or drawn from it somewhere
|
|
1556
|
+
// else — a widget mounted beside the document takes its look from
|
|
1557
|
+
// it. An image, a drawing or a rule is drawn here, from its box,
|
|
1558
|
+
// like any.
|
|
1559
|
+
if (box.style !== kept)
|
|
1560
|
+
return false;
|
|
1561
|
+
if (box.marker || WIDGETS.has(box.replaced))
|
|
1562
|
+
return false;
|
|
1563
|
+
if (moving.has(el) && !movable(box, kept, style))
|
|
1564
|
+
return false;
|
|
1565
|
+
ink = !quiet.has(el);
|
|
1566
|
+
}
|
|
1567
|
+
else if (above) {
|
|
1568
|
+
// An anonymous box takes what its parent's style passes on
|
|
1569
|
+
// (`anonymousStyles`), so it takes that of the new one: the text of
|
|
1570
|
+
// a link made a flex row is in one, and takes the link's colour.
|
|
1571
|
+
if (GENERATED_FROM.has(box))
|
|
1572
|
+
return false;
|
|
1573
|
+
const display = box.style.display;
|
|
1574
|
+
if (tree.anonymous(parent.style, display) !== box.style)
|
|
1575
|
+
return false;
|
|
1576
|
+
style = tree.anonymous(above, display);
|
|
1577
|
+
}
|
|
1369
1578
|
if (!style)
|
|
1370
1579
|
continue;
|
|
1371
|
-
|
|
1372
|
-
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
1373
|
-
// derived from the element's, or drawn from it somewhere else — a
|
|
1374
|
-
// widget mounted beside the document takes its look from it. An
|
|
1375
|
-
// image, a drawing or a rule is drawn here, from its box, like any.
|
|
1376
|
-
if (!box.el || box.style !== kept.style)
|
|
1377
|
-
return false;
|
|
1378
|
-
if (box.marker || WIDGETS.has(box.replaced))
|
|
1379
|
-
return false;
|
|
1380
|
-
if (moving.has(owner) && !movable(box, kept.style, style))
|
|
1381
|
-
return false;
|
|
1580
|
+
next.set(box, style);
|
|
1382
1581
|
restyled.push([box, style]);
|
|
1582
|
+
if (ink)
|
|
1583
|
+
inked.add(box);
|
|
1383
1584
|
if (box.kind === 'inline') {
|
|
1384
1585
|
const decoration = inlineDecoration(fonts, box, style);
|
|
1385
1586
|
if ((decoration === null) !== (box.decoration === null)) {
|
|
@@ -1391,11 +1592,34 @@ export class HtmlViewNode extends Node {
|
|
|
1391
1592
|
redecorated.push([box, decoration]);
|
|
1392
1593
|
}
|
|
1393
1594
|
}
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1595
|
+
// a `::before` or an `::after` a rule that tests the pointer now gives
|
|
1596
|
+
// an element is a box to build
|
|
1597
|
+
if (pseudoRules !== HOVER_PSEUDO_NONE) {
|
|
1598
|
+
for (const el of reach) {
|
|
1599
|
+
if (!flipped(el))
|
|
1600
|
+
continue;
|
|
1601
|
+
const style = fresh.get(el);
|
|
1602
|
+
if (!style)
|
|
1603
|
+
continue;
|
|
1604
|
+
const has = generated.get(el) ?? 0;
|
|
1605
|
+
for (const [which, bit] of GENERATED_BITS) {
|
|
1606
|
+
if (has & bit)
|
|
1607
|
+
continue;
|
|
1608
|
+
const made = cascade.pseudoStyleFor(el, which, style);
|
|
1609
|
+
if (made && made.display !== 'none')
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (!restyled.length)
|
|
1615
|
+
return true;
|
|
1616
|
+
// the text: each layout holding a run of a restyled box's, made again
|
|
1617
|
+
// with its new ink, where it comes out the same shape
|
|
1396
1618
|
const relaid = new Map();
|
|
1397
1619
|
const texts = [];
|
|
1398
|
-
|
|
1620
|
+
if (inked.size)
|
|
1621
|
+
for (const block of blocks)
|
|
1622
|
+
walk.push(block);
|
|
1399
1623
|
while (walk.length) {
|
|
1400
1624
|
const box = walk.pop();
|
|
1401
1625
|
for (const child of box.children)
|
|
@@ -1407,16 +1631,16 @@ export class HtmlViewNode extends Node {
|
|
|
1407
1631
|
texts.push(text);
|
|
1408
1632
|
if (relaid.has(text.layout))
|
|
1409
1633
|
continue;
|
|
1410
|
-
const
|
|
1411
|
-
if (
|
|
1634
|
+
const again = reinked(text, layouts, next);
|
|
1635
|
+
if (again === false)
|
|
1412
1636
|
return false;
|
|
1413
|
-
relaid.set(text.layout,
|
|
1637
|
+
relaid.set(text.layout, again);
|
|
1414
1638
|
}
|
|
1415
1639
|
}
|
|
1416
1640
|
}
|
|
1417
1641
|
// what each box drew before, to be repainted with what it draws after
|
|
1418
1642
|
const inks = [];
|
|
1419
|
-
for (const
|
|
1643
|
+
for (const box of inked) {
|
|
1420
1644
|
const ink = inkOf(box);
|
|
1421
1645
|
if (ink)
|
|
1422
1646
|
inks.push(ink);
|
|
@@ -1426,8 +1650,9 @@ export class HtmlViewNode extends Node {
|
|
|
1426
1650
|
for (const [box, style] of restyled) {
|
|
1427
1651
|
const was = box.style;
|
|
1428
1652
|
box.style = style;
|
|
1429
|
-
if (moving.has(box.el))
|
|
1653
|
+
if (box.el && moving.has(box.el) && box.kind !== 'text') {
|
|
1430
1654
|
moved.push([box, was]);
|
|
1655
|
+
}
|
|
1431
1656
|
}
|
|
1432
1657
|
for (const [box, decoration] of redecorated) {
|
|
1433
1658
|
// what is inside it keeps which boxes around it paint, and this is
|
|
@@ -1438,9 +1663,9 @@ export class HtmlViewNode extends Node {
|
|
|
1438
1663
|
box.decoration = decoration;
|
|
1439
1664
|
}
|
|
1440
1665
|
for (const text of texts) {
|
|
1441
|
-
const
|
|
1442
|
-
if (
|
|
1443
|
-
text.layout =
|
|
1666
|
+
const again = relaid.get(text.layout);
|
|
1667
|
+
if (again)
|
|
1668
|
+
text.layout = again;
|
|
1444
1669
|
}
|
|
1445
1670
|
for (const [el, style] of changed) {
|
|
1446
1671
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
|
|
@@ -1474,15 +1699,61 @@ export class HtmlViewNode extends Node {
|
|
|
1474
1699
|
this._reportControls();
|
|
1475
1700
|
if (relayout) {
|
|
1476
1701
|
this._invalidate(Stale.Layout);
|
|
1477
|
-
return
|
|
1702
|
+
return true;
|
|
1478
1703
|
}
|
|
1479
|
-
for (const
|
|
1704
|
+
for (const box of inked) {
|
|
1480
1705
|
const ink = inkOf(box);
|
|
1481
1706
|
if (ink)
|
|
1482
1707
|
inks.push(ink);
|
|
1483
1708
|
}
|
|
1484
1709
|
this._repaintInk(inks);
|
|
1485
|
-
return
|
|
1710
|
+
return true;
|
|
1711
|
+
}
|
|
1712
|
+
/** An element's first box in document order, for the tree it was asked
|
|
1713
|
+
* of: made on the first move over a tree, which is one walk of it. */
|
|
1714
|
+
_firstBoxes = null;
|
|
1715
|
+
/**
|
|
1716
|
+
* The blocks a restyle of `changed` has to look in: for each element, the
|
|
1717
|
+
* box its text is set in the lines of — the nearest box around its first
|
|
1718
|
+
* that is not an inline one — which holds every box that takes its style
|
|
1719
|
+
* from it, the pieces a block in it broke it into among them
|
|
1720
|
+
* (`breakAround`). One that is inside another is left to it.
|
|
1721
|
+
*/
|
|
1722
|
+
_blocksOf(tree, changed) {
|
|
1723
|
+
let index = this._firstBoxes;
|
|
1724
|
+
if (index?.tree !== tree) {
|
|
1725
|
+
const of = new Map();
|
|
1726
|
+
const stack = [tree.root];
|
|
1727
|
+
while (stack.length) {
|
|
1728
|
+
const box = stack.pop();
|
|
1729
|
+
if (box.el && !of.has(box.el))
|
|
1730
|
+
of.set(box.el, box);
|
|
1731
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1732
|
+
stack.push(box.children[i]);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
index = this._firstBoxes = { tree, of };
|
|
1736
|
+
}
|
|
1737
|
+
const blocks = new Set();
|
|
1738
|
+
for (const el of changed) {
|
|
1739
|
+
let box = index.of.get(el) ?? null;
|
|
1740
|
+
while (box?.parent &&
|
|
1741
|
+
(box.kind === 'inline' || box.kind === 'text' || box.kind === 'break')) {
|
|
1742
|
+
box = box.parent;
|
|
1743
|
+
}
|
|
1744
|
+
if (box)
|
|
1745
|
+
blocks.add(box);
|
|
1746
|
+
}
|
|
1747
|
+
const out = [];
|
|
1748
|
+
for (const block of blocks) {
|
|
1749
|
+
let inside = false;
|
|
1750
|
+
for (let at = block.parent; at && !inside; at = at.parent) {
|
|
1751
|
+
inside = blocks.has(at);
|
|
1752
|
+
}
|
|
1753
|
+
if (!inside)
|
|
1754
|
+
out.push(block);
|
|
1755
|
+
}
|
|
1756
|
+
return out;
|
|
1486
1757
|
}
|
|
1487
1758
|
/**
|
|
1488
1759
|
* Repaint the document where `inks` are, in its own coordinates: the ink
|
|
@@ -1542,17 +1813,64 @@ export class HtmlViewNode extends Node {
|
|
|
1542
1813
|
// `:hover` is wired at the element rather than through React: the cascade
|
|
1543
1814
|
// already knows whether any rule in the document tests it, `setHover`
|
|
1544
1815
|
// returns without work when none does, and a document that does use it
|
|
1545
|
-
// re-styles only when the hovered chain actually changed
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1816
|
+
// re-styles only when the hovered chain actually changed, and then only
|
|
1817
|
+
// the elements the change reached (`_restyleHover`).
|
|
1818
|
+
//
|
|
1819
|
+
// **A hover waits for a scroll to stop.** Core asks again what is under a
|
|
1820
|
+
// pointer that stayed where it was after every frame that laid out —
|
|
1821
|
+
// hover follows content, react-x11#793 — and says so with a move to the
|
|
1822
|
+
// point the pointer is already at. While a document scrolls that is every
|
|
1823
|
+
// frame, each with something else under the pointer: a restyle a frame
|
|
1824
|
+
// at best, and where a hover moves something, the boxes built again a
|
|
1825
|
+
// frame, which took a page scrolled under a parked pointer from 52 frames
|
|
1826
|
+
// a second to 4. A browser holds its hover until the scroll is over, for
|
|
1827
|
+
// the same reason; so does this, for `HOVER_REST_MS` after the content
|
|
1828
|
+
// last moved. What was hovered stays hovered as it scrolls away, as it
|
|
1829
|
+
// does there. A move of the pointer itself is answered at once.
|
|
1548
1830
|
defaultMouseMove(ev) {
|
|
1549
1831
|
super.defaultMouseMove?.(ev);
|
|
1832
|
+
const at = this._pointerAt;
|
|
1833
|
+
if (at !== null && at.x === ev.x && at.y === ev.y) {
|
|
1834
|
+
this._holdHover();
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
this._pointerAt = { x: ev.x, y: ev.y };
|
|
1550
1838
|
this.setHover(ev.x, ev.y);
|
|
1551
1839
|
}
|
|
1552
1840
|
defaultMouseLeave(ev) {
|
|
1553
1841
|
super.defaultMouseLeave?.(ev);
|
|
1842
|
+
this._pointerAt = null;
|
|
1554
1843
|
this.clearHover();
|
|
1555
1844
|
}
|
|
1845
|
+
/** The content moved under the pointer: the hover is asked for once it
|
|
1846
|
+
* has been still for `HOVER_REST_MS`. One timer a wait, put off by
|
|
1847
|
+
* being armed again when it finds the content moved since. */
|
|
1848
|
+
_holdHover() {
|
|
1849
|
+
if (!this._cascade?.hoverSensitive)
|
|
1850
|
+
return;
|
|
1851
|
+
this._heldSince = hoverClock.now();
|
|
1852
|
+
if (this._heldHover !== null)
|
|
1853
|
+
return;
|
|
1854
|
+
const rest = () => {
|
|
1855
|
+
this._heldHover = null;
|
|
1856
|
+
const at = this._pointerAt;
|
|
1857
|
+
if (!at || this.destroyed)
|
|
1858
|
+
return;
|
|
1859
|
+
const left = this._heldSince + HOVER_REST_MS - hoverClock.now();
|
|
1860
|
+
if (left > 0) {
|
|
1861
|
+
this._heldHover = hoverClock.arm(rest, left);
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
this.setHover(at.x, at.y);
|
|
1865
|
+
};
|
|
1866
|
+
this._heldHover = hoverClock.arm(rest, HOVER_REST_MS);
|
|
1867
|
+
}
|
|
1868
|
+
_dropHeldHover() {
|
|
1869
|
+
if (this._heldHover === null)
|
|
1870
|
+
return;
|
|
1871
|
+
hoverClock.disarm(this._heldHover);
|
|
1872
|
+
this._heldHover = null;
|
|
1873
|
+
}
|
|
1556
1874
|
// --- paint ----------------------------------------------------------------
|
|
1557
1875
|
paint(ctx) {
|
|
1558
1876
|
super.paint(ctx); // background, border, clip to `abs`
|
|
@@ -1793,6 +2111,8 @@ function sameRects(a, b) {
|
|
|
1793
2111
|
p.y !== q.y ||
|
|
1794
2112
|
p.width !== q.width ||
|
|
1795
2113
|
p.height !== q.height ||
|
|
2114
|
+
p.fontFamily !== q.fontFamily ||
|
|
2115
|
+
p.fontSize !== q.fontSize ||
|
|
1796
2116
|
p.opacity !== q.opacity ||
|
|
1797
2117
|
!sameBare(p.bare, q.bare)) {
|
|
1798
2118
|
return false;
|
|
@@ -2078,7 +2398,10 @@ function fragmentHeights(box, bands, fonts) {
|
|
|
2078
2398
|
function lineBands(lines, from, to, dx, dy, out,
|
|
2079
2399
|
/** Where the lines put the text, not where a relative offset moved it
|
|
2080
2400
|
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2081
|
-
laidOut = false
|
|
2401
|
+
laidOut = false,
|
|
2402
|
+
/** The rows a highlight covers (`selectionRows`) rather than the line's:
|
|
2403
|
+
* past it where the glyphs are taller than the line. */
|
|
2404
|
+
ink = false) {
|
|
2082
2405
|
// First line whose text can reach `from`, by binary search over the
|
|
2083
2406
|
// sorted text starts; stop at the first line past `to`.
|
|
2084
2407
|
let lo = 0;
|
|
@@ -2111,6 +2434,9 @@ laidOut = false) {
|
|
|
2111
2434
|
// a link that ends a line a space wider than Chrome's. So the text
|
|
2112
2435
|
// a line ends with is measured no further than its content.
|
|
2113
2436
|
const ends = endsLine(line, text, natural);
|
|
2437
|
+
const rows = ink
|
|
2438
|
+
? selectionRows(line, text, natural)
|
|
2439
|
+
: { y: line.y, height: line.height };
|
|
2114
2440
|
for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
|
|
2115
2441
|
let left = band.x;
|
|
2116
2442
|
let right = band.x + band.width;
|
|
@@ -2120,9 +2446,9 @@ laidOut = false) {
|
|
|
2120
2446
|
}
|
|
2121
2447
|
out.push({
|
|
2122
2448
|
x: dx + left + text.drawX - moved,
|
|
2123
|
-
y: dy +
|
|
2449
|
+
y: dy + rows.y,
|
|
2124
2450
|
width: right - left,
|
|
2125
|
-
height:
|
|
2451
|
+
height: rows.height,
|
|
2126
2452
|
});
|
|
2127
2453
|
}
|
|
2128
2454
|
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
@@ -2133,9 +2459,9 @@ laidOut = false) {
|
|
|
2133
2459
|
continue;
|
|
2134
2460
|
out.push({
|
|
2135
2461
|
x: dx + space.x + text.drawX - moved,
|
|
2136
|
-
y: dy +
|
|
2462
|
+
y: dy + rows.y,
|
|
2137
2463
|
width: space.width,
|
|
2138
|
-
height:
|
|
2464
|
+
height: rows.height,
|
|
2139
2465
|
});
|
|
2140
2466
|
}
|
|
2141
2467
|
}
|
|
@@ -2154,18 +2480,18 @@ function endsLine(line, text, natural) {
|
|
|
2154
2480
|
}
|
|
2155
2481
|
return true;
|
|
2156
2482
|
}
|
|
2157
|
-
function collectBands(box, from, to, dx, dy, out) {
|
|
2483
|
+
function collectBands(box, from, to, dx, dy, out, ink = false) {
|
|
2158
2484
|
if (box.subtreeTextEnd <= box.subtreeTextStart)
|
|
2159
2485
|
return;
|
|
2160
2486
|
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
|
|
2161
2487
|
return;
|
|
2162
2488
|
if (box.lines)
|
|
2163
|
-
lineBands(box.lines, from, to, dx, dy, out);
|
|
2489
|
+
lineBands(box.lines, from, to, dx, dy, out, false, ink);
|
|
2164
2490
|
// Atomics are ordinary children, reached below.
|
|
2165
2491
|
for (const child of box.children) {
|
|
2166
2492
|
if (child.kind === 'text' || child.kind === 'break')
|
|
2167
2493
|
continue;
|
|
2168
|
-
collectBands(child, from, to, dx, dy, out);
|
|
2494
|
+
collectBands(child, from, to, dx, dy, out, ink);
|
|
2169
2495
|
}
|
|
2170
2496
|
}
|
|
2171
2497
|
/** The layers of a hit test, in the order CSS paints them within a
|
|
@@ -2354,6 +2680,32 @@ function compareKeys(a, b) {
|
|
|
2354
2680
|
* is built again instead: a link's subtree is a handful, and a compound
|
|
2355
2681
|
* that matches a container reaches everything in it. */
|
|
2356
2682
|
const HOVER_RESTYLE_LIMIT = 300;
|
|
2683
|
+
/** How long the content under a still pointer has to have stopped moving
|
|
2684
|
+
* before the hover is asked for again (`HtmlViewNode._holdHover`): a
|
|
2685
|
+
* tenth of a second, which is what WebKit waits after a scroll before it
|
|
2686
|
+
* sends the mouse move that updates its own. */
|
|
2687
|
+
const HOVER_REST_MS = 100;
|
|
2688
|
+
const timers = globalThis;
|
|
2689
|
+
/**
|
|
2690
|
+
* The clock a held hover waits on. Through `globalThis` because `src/`
|
|
2691
|
+
* compiles with `types: []`, and unref'd where the runtime allows it: a
|
|
2692
|
+
* timer of a document's must not keep a process alive that is otherwise
|
|
2693
|
+
* done. One object, exported from this module though not from the package,
|
|
2694
|
+
* so a test can hold it and say when the rest is over (`test/held-clock.ts`).
|
|
2695
|
+
*/
|
|
2696
|
+
export const hoverClock = {
|
|
2697
|
+
now() {
|
|
2698
|
+
return Date.now();
|
|
2699
|
+
},
|
|
2700
|
+
arm(step, ms) {
|
|
2701
|
+
const handle = timers.setTimeout?.(step, ms) ?? null;
|
|
2702
|
+
handle?.unref?.();
|
|
2703
|
+
return handle;
|
|
2704
|
+
},
|
|
2705
|
+
disarm(handle) {
|
|
2706
|
+
timers.clearTimeout?.(handle);
|
|
2707
|
+
},
|
|
2708
|
+
};
|
|
2357
2709
|
/**
|
|
2358
2710
|
* The computed properties a pointer move may change in place: ink, which
|
|
2359
2711
|
* moves nothing. A background colour is drawn inside the box it colours
|
|
@@ -2400,6 +2752,12 @@ const FACE_FIELDS = [
|
|
|
2400
2752
|
'letterSpacing',
|
|
2401
2753
|
'features',
|
|
2402
2754
|
];
|
|
2755
|
+
/** An element's `::before` and `::after`, each with its bit in the set
|
|
2756
|
+
* of the ones a restyle found boxes of (`_hoverInPlace`). */
|
|
2757
|
+
const GENERATED_BITS = [
|
|
2758
|
+
['before', 1],
|
|
2759
|
+
['after', 2],
|
|
2760
|
+
];
|
|
2403
2761
|
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
2404
2762
|
* a widget mounted beside the document, or a button whose press it reports. */
|
|
2405
2763
|
const WIDGETS = new Set([
|
|
@@ -2431,9 +2789,18 @@ function hoverChange(was, now) {
|
|
|
2431
2789
|
const b = now;
|
|
2432
2790
|
let change = null;
|
|
2433
2791
|
for (const key in b) {
|
|
2434
|
-
|
|
2792
|
+
// the sets are shared by what made them (`Cascade._customFor`): one
|
|
2793
|
+
// object where they are the same
|
|
2794
|
+
if (key === 'custom') {
|
|
2795
|
+
if (a[key] !== b[key]) {
|
|
2796
|
+
change ??= { ink: false, reach: false, order: false, move: false };
|
|
2797
|
+
}
|
|
2435
2798
|
continue;
|
|
2436
|
-
|
|
2799
|
+
}
|
|
2800
|
+
if (sameValue(a[key], b[key]))
|
|
2801
|
+
continue;
|
|
2802
|
+
change ??= { ink: true, reach: false, order: false, move: false };
|
|
2803
|
+
change.ink = true;
|
|
2437
2804
|
if (PAINT_ONLY.has(key))
|
|
2438
2805
|
continue;
|
|
2439
2806
|
if (INK_REACH.has(key)) {
|
|
@@ -2529,32 +2896,23 @@ function sameValue(x, y) {
|
|
|
2529
2896
|
return false;
|
|
2530
2897
|
return true;
|
|
2531
2898
|
}
|
|
2532
|
-
/** The element an anonymous box takes its style from: its nearest
|
|
2533
|
-
* ancestor's that has one. */
|
|
2534
|
-
function nearestElement(box) {
|
|
2535
|
-
for (let at = box.parent; at; at = at.parent)
|
|
2536
|
-
if (at.el)
|
|
2537
|
-
return at.el;
|
|
2538
|
-
return null;
|
|
2539
|
-
}
|
|
2540
2899
|
/**
|
|
2541
|
-
* `text`'s layout made again with the ink of the
|
|
2542
|
-
* restyled: from the runs it was made from, each of a
|
|
2900
|
+
* `text`'s layout made again with the ink of the boxes a pointer move
|
|
2901
|
+
* restyled: from the runs it was made from, each of a restyled text box's
|
|
2543
2902
|
* given the ink its new style makes (`runFor`) and nothing else. Null where
|
|
2544
2903
|
* none of its runs is theirs; false where that cannot be told — a run this
|
|
2545
2904
|
* cannot place in the document, one some other pass inked (a first line's),
|
|
2546
2905
|
* or a layout that comes out another shape.
|
|
2547
2906
|
*/
|
|
2548
|
-
function reinked(text, layouts,
|
|
2907
|
+
function reinked(text, layouts, next) {
|
|
2549
2908
|
const spans = text.spans;
|
|
2550
|
-
const ownerAt = (offset) => spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
|
|
2551
2909
|
const inputs = layouts.inputsOf(text.layout);
|
|
2552
2910
|
if (!inputs || !spans.boxAt) {
|
|
2553
2911
|
// not one this could make again: none of its text may be theirs
|
|
2554
2912
|
for (const line of text.layout.lines) {
|
|
2555
2913
|
for (const run of line.runs) {
|
|
2556
|
-
const
|
|
2557
|
-
if (!spans.boxAt || (
|
|
2914
|
+
const box = spans.boxAt?.(run.start);
|
|
2915
|
+
if (!spans.boxAt || (box && next.has(box)))
|
|
2558
2916
|
return false;
|
|
2559
2917
|
}
|
|
2560
2918
|
}
|
|
@@ -2566,32 +2924,32 @@ function reinked(text, layouts, changed, styles) {
|
|
|
2566
2924
|
for (let i = 0; i < content.length; i += 1) {
|
|
2567
2925
|
const run = content[i];
|
|
2568
2926
|
const length = run.text.length;
|
|
2569
|
-
const
|
|
2570
|
-
const style =
|
|
2571
|
-
if (
|
|
2572
|
-
if (
|
|
2927
|
+
const box = length ? spans.boxAt(offset) : null;
|
|
2928
|
+
const style = box ? next.get(box) : undefined;
|
|
2929
|
+
if (box && style) {
|
|
2930
|
+
if (spans.boxAt(offset + length - 1) !== box)
|
|
2573
2931
|
return false;
|
|
2574
|
-
const was = runFor(run.text,
|
|
2932
|
+
const was = runFor(run.text, box.style);
|
|
2575
2933
|
const now = runFor(run.text, style);
|
|
2576
2934
|
for (const f of FACE_FIELDS)
|
|
2577
2935
|
if (!sameValue(was[f], now[f]))
|
|
2578
2936
|
return false;
|
|
2579
|
-
let
|
|
2937
|
+
let inked = null;
|
|
2580
2938
|
for (const f of INK_FIELDS) {
|
|
2581
2939
|
// a run that is not what its style made — a first line's colour
|
|
2582
2940
|
if (!sameValue(run[f], was[f]))
|
|
2583
2941
|
return false;
|
|
2584
2942
|
if (sameValue(was[f], now[f]))
|
|
2585
2943
|
continue;
|
|
2586
|
-
|
|
2944
|
+
inked ??= { ...run };
|
|
2587
2945
|
if (now[f] === undefined)
|
|
2588
|
-
delete
|
|
2946
|
+
delete inked[f];
|
|
2589
2947
|
else
|
|
2590
|
-
|
|
2948
|
+
inked[f] = now[f];
|
|
2591
2949
|
}
|
|
2592
|
-
if (
|
|
2950
|
+
if (inked) {
|
|
2593
2951
|
runs ??= content.slice();
|
|
2594
|
-
runs[i] =
|
|
2952
|
+
runs[i] = inked;
|
|
2595
2953
|
}
|
|
2596
2954
|
}
|
|
2597
2955
|
offset += length;
|