@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.
Files changed (80) hide show
  1. package/dist/html/controls.d.ts +12 -3
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +15 -55
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +519 -122
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/parse.d.ts +29 -2
  10. package/dist/html/css/parse.d.ts.map +1 -1
  11. package/dist/html/css/parse.js +277 -9
  12. package/dist/html/css/parse.js.map +1 -1
  13. package/dist/html/css/style.d.ts +14 -0
  14. package/dist/html/css/style.d.ts.map +1 -1
  15. package/dist/html/css/style.js +2 -3
  16. package/dist/html/css/style.js.map +1 -1
  17. package/dist/html/css/ua.d.ts +11 -1
  18. package/dist/html/css/ua.d.ts.map +1 -1
  19. package/dist/html/css/ua.js +59 -7
  20. package/dist/html/css/ua.js.map +1 -1
  21. package/dist/html/dom.d.ts.map +1 -1
  22. package/dist/html/dom.js +3 -1
  23. package/dist/html/dom.js.map +1 -1
  24. package/dist/html/fonts.d.ts +25 -1
  25. package/dist/html/fonts.d.ts.map +1 -1
  26. package/dist/html/fonts.js +99 -6
  27. package/dist/html/fonts.js.map +1 -1
  28. package/dist/html/form.d.ts +200 -0
  29. package/dist/html/form.d.ts.map +1 -0
  30. package/dist/html/form.js +801 -0
  31. package/dist/html/form.js.map +1 -0
  32. package/dist/html/index.d.ts +12 -0
  33. package/dist/html/index.d.ts.map +1 -1
  34. package/dist/html/index.js +30 -301
  35. package/dist/html/index.js.map +1 -1
  36. package/dist/html/layout/block.d.ts.map +1 -1
  37. package/dist/html/layout/block.js +5 -1
  38. package/dist/html/layout/block.js.map +1 -1
  39. package/dist/html/layout/boxes.d.ts +31 -1
  40. package/dist/html/layout/boxes.d.ts.map +1 -1
  41. package/dist/html/layout/boxes.js +52 -8
  42. package/dist/html/layout/boxes.js.map +1 -1
  43. package/dist/html/layout/flex.js +4 -1
  44. package/dist/html/layout/flex.js.map +1 -1
  45. package/dist/html/layout/inline.d.ts.map +1 -1
  46. package/dist/html/layout/inline.js +10 -0
  47. package/dist/html/layout/inline.js.map +1 -1
  48. package/dist/html/node.d.ts +87 -15
  49. package/dist/html/node.d.ts.map +1 -1
  50. package/dist/html/node.js +510 -152
  51. package/dist/html/node.js.map +1 -1
  52. package/dist/html/paint.d.ts +22 -1
  53. package/dist/html/paint.d.ts.map +1 -1
  54. package/dist/html/paint.js +290 -20
  55. package/dist/html/paint.js.map +1 -1
  56. package/dist/html/widgets.d.ts +29 -0
  57. package/dist/html/widgets.d.ts.map +1 -0
  58. package/dist/html/widgets.js +620 -0
  59. package/dist/html/widgets.js.map +1 -0
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js.map +1 -1
  63. package/package.json +2 -2
  64. package/src/html/controls.ts +29 -49
  65. package/src/html/css/cascade.ts +591 -140
  66. package/src/html/css/parse.ts +324 -9
  67. package/src/html/css/style.ts +24 -3
  68. package/src/html/css/ua.ts +62 -8
  69. package/src/html/dom.ts +3 -1
  70. package/src/html/fonts.ts +116 -6
  71. package/src/html/form.ts +962 -0
  72. package/src/html/index.ts +49 -338
  73. package/src/html/layout/block.ts +3 -0
  74. package/src/html/layout/boxes.ts +78 -11
  75. package/src/html/layout/flex.ts +4 -1
  76. package/src/html/layout/inline.ts +10 -0
  77. package/src/html/node.ts +498 -135
  78. package/src/html/paint.ts +330 -29
  79. package/src/html/widgets.ts +735 -0
  80. 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
- _invalidate(stale) {
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
- const sheets = [uaStylesheet(look)];
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 (this._stale < Stale.Boxes &&
744
- ((cascade.readsViewportWidth && this._styledWidth !== target) ||
745
- (cascade.readsViewportHeight && this._styledHeight !== viewport))) {
746
- this._stale = Stale.Boxes;
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
- collectBands(tree.root, from, to, this.abs.x, this.abs.y, out);
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: block.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
- /** The hovered chain moved: restyle where it did, or the document. */
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
- // a restyle in place names what it repainted (`_hoverInPlace`)
1209
- if (this._hoverInPlace(was, now) === false)
1210
- this._invalidate(Stale.Boxes);
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
- * A pointer move restyled where it happened, when all it changed is ink.
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
- * - Only an element whose hover state flipped and that a compound testing
1223
- * the pointer could match can change (`Cascade.hoverTouches`), with its
1224
- * subtree, and its later siblings where a sibling combinator follows.
1225
- * A move between two paragraphs under `a:hover` touches nothing.
1226
- * - Those are styled again from their parents. Where anything but ink
1227
- * differs (`PAINT_ONLY`), or a box is not the element's own — an
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
- _hoverInPlace(was, now) {
1300
+ _hoverReach(was, now) {
1239
1301
  const cascade = this._cascade;
1240
- const tree = this._tree;
1241
- const layouts = this._layouts;
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
- if (cascade.hoverTouches(el))
1256
- roots.push(el);
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
- if (!roots.length)
1270
- return 'none';
1271
- // what may restyle: the roots' subtrees, which inherit from them and a
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
- if (!collect(root))
1292
- return false;
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) && !collect(s))
1297
- return false;
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 style = cascade.styleFor(el, parentStyle, kept.inFlex);
1327
- fresh.set(el, style);
1328
- const diff = hoverChange(kept.style, style);
1329
- if (diff === false)
1330
- refused = true;
1331
- else if (diff) {
1332
- changed.set(el, style);
1333
- if (diff.reach)
1334
- reaches = true;
1335
- if (diff.order)
1336
- reorders = true;
1337
- if (diff.move)
1338
- moving.add(el);
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 'none';
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
- // the boxes: each changed element's own, and nothing that takes its
1356
- // style from one without being it
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 = [tree.root];
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 owner = box.el ?? nearestElement(box);
1366
- if (!owner)
1367
- continue;
1368
- const style = changed.get(owner);
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
- const kept = tree.styles.get(owner);
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
- // the text: each layout holding a run of a changed element's, made
1395
- // again with its new ink, where it comes out the same shape
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
- walk.push(tree.root);
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 next = reinked(text, layouts, changed, tree.styles);
1411
- if (next === false)
1634
+ const again = reinked(text, layouts, next);
1635
+ if (again === false)
1412
1636
  return false;
1413
- relaid.set(text.layout, next);
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 [box] of restyled) {
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 next = relaid.get(text.layout);
1442
- if (next)
1443
- text.layout = next;
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 'painted';
1702
+ return true;
1478
1703
  }
1479
- for (const [box] of restyled) {
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 'painted';
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. The restyle is
1546
- // still document-wide — narrowing it to the affected subtree is the
1547
- // phase-2 item the PRD records.
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 + line.y,
2449
+ y: dy + rows.y,
2124
2450
  width: right - left,
2125
- height: line.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 + line.y,
2462
+ y: dy + rows.y,
2137
2463
  width: space.width,
2138
- height: line.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
- if (key === 'custom' || sameValue(a[key], b[key]))
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
- change ??= { reach: false, order: false, move: false };
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 elements a pointer move
2542
- * restyled: from the runs it was made from, each of a changed element's
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, changed, styles) {
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 owner = spans.boxAt ? ownerAt(run.start) : null;
2557
- if (!spans.boxAt || (owner && changed.has(owner)))
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 owner = length ? ownerAt(offset) : null;
2570
- const style = owner ? changed.get(owner) : undefined;
2571
- if (owner && style) {
2572
- if (ownerAt(offset + length - 1) !== owner)
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, styles.get(owner).style);
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 next = null;
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
- next ??= { ...run };
2944
+ inked ??= { ...run };
2587
2945
  if (now[f] === undefined)
2588
- delete next[f];
2946
+ delete inked[f];
2589
2947
  else
2590
- next[f] = now[f];
2948
+ inked[f] = now[f];
2591
2949
  }
2592
- if (next) {
2950
+ if (inked) {
2593
2951
  runs ??= content.slice();
2594
- runs[i] = next;
2952
+ runs[i] = inked;
2595
2953
  }
2596
2954
  }
2597
2955
  offset += length;