@react-x11/components 0.12.0 → 0.14.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 (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  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 +545 -126
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
package/dist/html/node.js CHANGED
@@ -40,12 +40,14 @@ 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';
47
- import { layoutDocument, retranslate, transformed } from './layout/block.js';
46
+ import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, CLEARED_FROM, COLUMN_LINES, COLUMN_PIECES, COLUMN_ROWS, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, columned, } from './layout/boxes.js';
47
+ import { layoutDocument, placedMatrix, retranslate } from './layout/block.js';
48
+ import { invert, mapPoint, mapRect, transformed } from './css/transform.js';
48
49
  import { TextLayoutCache } from './layout/cache.js';
50
+ import { weightAxes } from './layout/axes.js';
49
51
  import { shapingSafe } from './layout/shaping.js';
50
52
  import { SurfaceCache, newSurface } from './surfaces.js';
51
53
  import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
@@ -53,7 +55,7 @@ import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
53
55
  // a layout is built once, on the first selection that needs it.
54
56
  import { documentOffsetOf, layoutOffsetOf, layoutOffsets as layoutOffsetsOf, } from './layout/inline.js';
55
57
  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';
58
+ import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, inClip, inClipPath, onLine, ownBounds, paintDocument, pathClips, queryChildIndex, selectionRows, stackLayers, stacksLayers, } from './paint.js';
57
59
  import { controlRectsOf, measureControl } from './controls.js';
58
60
  import { ResourceStore } from './resources.js';
59
61
  import { WebFonts } from './fonts.js';
@@ -135,6 +137,8 @@ export class HtmlViewNode extends Node {
135
137
  * that read it (`vw`, `vh`). */
136
138
  _styledWidth = -1;
137
139
  _styledHeight = -1;
140
+ /** The cascade the box tree's styles were computed by. */
141
+ _styledWith = null;
138
142
  _documentHeight = 0;
139
143
  _documentWidth = 0;
140
144
  /** The viewport height the document was last laid out under, and whether
@@ -152,6 +156,20 @@ export class HtmlViewNode extends Node {
152
156
  * the text carries surrogate pairs. null until checked. */
153
157
  _pointsAreUnits = null;
154
158
  _hovered = [];
159
+ /** Where the pointer last moved to, in logical window pixels, while it
160
+ * is over the element (`defaultMouseMove`). */
161
+ _pointerAt = null;
162
+ /** A hover held until the content under a still pointer stops moving:
163
+ * the timer, and when the content last moved (`_holdHover`). */
164
+ _heldHover = null;
165
+ _heldSince = 0;
166
+ /**
167
+ * The elements the build of the boxes that is due has to style again,
168
+ * where every change asking for it said which (`_invalidate`), and null
169
+ * where any element's style may have changed — a new cascade, a viewport
170
+ * a `vw` reads, a face that arrived.
171
+ */
172
+ _restyleOnly = null;
155
173
  _scriptsSeen = new WeakSet();
156
174
  _controls = [];
157
175
  _reportedDomRevision = -1;
@@ -230,6 +248,7 @@ export class HtmlViewNode extends Node {
230
248
  controlPadY: look.controlPadY * s,
231
249
  controlBorder: look.controlBorder * s,
232
250
  controlRadius: look.controlRadius * s,
251
+ controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
233
252
  };
234
253
  this._deviceLookFor = look;
235
254
  this._deviceLookAt = s;
@@ -237,7 +256,26 @@ export class HtmlViewNode extends Node {
237
256
  return scaled;
238
257
  }
239
258
  // --- the pipeline ---------------------------------------------------------
240
- _invalidate(stale) {
259
+ /**
260
+ * `restyle` with `Stale.Boxes`: the elements whose styles the change
261
+ * could have touched, where that is all of it — a pointer move's reach
262
+ * (`_hoverReach`). The build it asks for keeps every other element's
263
+ * (`_restyleOnly`).
264
+ */
265
+ _invalidate(stale, restyle = null) {
266
+ if (stale >= Stale.Boxes) {
267
+ const only = stale === Stale.Boxes ? restyle : null;
268
+ if (this._stale < Stale.Boxes) {
269
+ this._restyleOnly = only ? new Set(only) : null;
270
+ }
271
+ else if (this._restyleOnly && only) {
272
+ for (const el of only)
273
+ this._restyleOnly.add(el);
274
+ }
275
+ else {
276
+ this._restyleOnly = null;
277
+ }
278
+ }
241
279
  if (stale > this._stale)
242
280
  this._stale = stale;
243
281
  if (stale >= Stale.Layout)
@@ -443,7 +481,10 @@ export class HtmlViewNode extends Node {
443
481
  faces = kept.faces;
444
482
  }
445
483
  else {
446
- const sheets = [uaStylesheet(look)];
484
+ // the UA sheet's `px` are CSS pixels, which the cascade scales like an
485
+ // author's, so it is written from the look in CSS pixels: from the
486
+ // device one, a control's size and a button's chrome were doubled at 2x
487
+ const sheets = [uaStylesheet(props.look)];
447
488
  const imports = [];
448
489
  faces = [];
449
490
  let order = 0;
@@ -490,8 +531,9 @@ export class HtmlViewNode extends Node {
490
531
  // The faces this width and scheme declare: a `@font-face` may sit in a
491
532
  // `@media` block like any rule.
492
533
  const cssWidth = width / this._scale;
534
+ const cssHeight = this._viewportHeight() / this._scale;
493
535
  this._webFonts.setFallback(look.fontFamily);
494
- this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme)));
536
+ this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme, cssHeight, this._scale)));
495
537
  this._cascade.setPointer({
496
538
  hovered: new Set(this._hovered),
497
539
  active: EMPTY_SET,
@@ -505,8 +547,16 @@ export class HtmlViewNode extends Node {
505
547
  * next restyle whether the sheets read the same (`_sameSheets`). A sheet
506
548
  * that imports itself, or one of the sheets importing it, is read once.
507
549
  */
508
- _placeImports(sheet, encoding, element, layers, seen, place, depth = 0, chain = new Set()) {
509
- for (const url of sheet.imports) {
550
+ _placeImports(sheet, encoding, element, layers, seen, place, depth = 0, chain = new Set(),
551
+ /** The media queries `sheet` is itself under, where it was imported
552
+ * under some. */
553
+ under = null) {
554
+ for (let i = 0; i < sheet.imports.length; i += 1) {
555
+ const url = sheet.imports[i];
556
+ // what it imports is under its import's media queries, and under
557
+ // the ones the sheet importing it is under
558
+ const conditions = sheet.importConditions[i];
559
+ const media = conditions ? [...(under ?? []), conditions] : under;
510
560
  this._resources.request({ url, kind: 'stylesheet', element });
511
561
  const fetched = this._resources.stylesheet(url, [encoding]);
512
562
  const base = this._resources.sheetBase(url);
@@ -514,9 +564,9 @@ export class HtmlViewNode extends Node {
514
564
  const key = base ?? url;
515
565
  if (!fetched || depth >= MAX_IMPORT_DEPTH || chain.has(key))
516
566
  continue;
517
- const imported = parseStylesheet(fetched.text, 0, layers, base);
567
+ const imported = parseStylesheet(fetched.text, 0, layers, base, media);
518
568
  chain.add(key);
519
- this._placeImports(imported, fetched.encoding, element, layers, seen, place, depth + 1, chain);
569
+ this._placeImports(imported, fetched.encoding, element, layers, seen, place, depth + 1, chain, media);
520
570
  chain.delete(key);
521
571
  place(imported, element);
522
572
  }
@@ -631,11 +681,22 @@ export class HtmlViewNode extends Node {
631
681
  const height = root?.abs?.height;
632
682
  return height && height > 0 ? height : 600;
633
683
  }
684
+ /** The engine this document lays text out with, and the one it is over:
685
+ * one object while the engine is, since caches are kept by it. */
686
+ _engine = null;
634
687
  _fonts() {
635
688
  const fonts = this.app?.fonts;
689
+ if (!fonts)
690
+ return null;
636
691
  // a face the engine cannot shape from costs its characters, not the
637
692
  // document (`shaping.ts`)
638
- return fonts ? shapingSafe(fonts) : null;
693
+ const safe = shapingSafe(fonts);
694
+ if (this._engine?.over !== safe) {
695
+ // and a variable face of the document's is set at the weight its
696
+ // rule has for a style's (`axes.ts`)
697
+ this._engine = { over: safe, fonts: weightAxes(safe, this._webFonts) };
698
+ }
699
+ return this._engine.fonts;
639
700
  }
640
701
  /** The text layouts the last pass made, for this one to reuse. */
641
702
  _layouts = null;
@@ -720,9 +781,14 @@ export class HtmlViewNode extends Node {
720
781
  const onlyViewport = this._stale === Stale.Nothing && this._laidOutWidth === target;
721
782
  const wasWidth = this._documentWidth;
722
783
  const wasHeight = this._documentHeight;
784
+ // the styles a build may keep (`_restyleOnly`), unless something
785
+ // found below changes them all
786
+ let restyleOnly = this._stale === Stale.Boxes ? this._restyleOnly : null;
787
+ this._restyleOnly = null;
723
788
  if (this._stale >= Stale.Style || !this._cascade) {
724
789
  this._restyle(target);
725
790
  this._stale = Math.max(this._stale, Stale.Boxes);
791
+ restyleOnly = null;
726
792
  }
727
793
  else if (this._cascade.mediaBand(target) !== this._mediaBand) {
728
794
  // A resize that crossed a `@media` breakpoint is the one resize that
@@ -730,6 +796,7 @@ export class HtmlViewNode extends Node {
730
796
  // breakpoints are collected at parse time.
731
797
  this._restyle(target);
732
798
  this._stale = Math.max(this._stale, Stale.Boxes);
799
+ restyleOnly = null;
733
800
  }
734
801
  const cascade = this._cascade;
735
802
  if (!cascade)
@@ -740,21 +807,32 @@ export class HtmlViewNode extends Node {
740
807
  // styles computed for another viewport are wrong for this one: built
741
808
  // again, where some style reads the side that moved. A document that
742
809
  // 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;
810
+ if ((cascade.readsViewportWidth && this._styledWidth !== target) ||
811
+ (cascade.readsViewportHeight && this._styledHeight !== viewport)) {
812
+ if (this._stale < Stale.Boxes)
813
+ this._stale = Stale.Boxes;
814
+ restyleOnly = null;
747
815
  }
748
816
  if (this._stale >= Stale.Boxes || !this._tree) {
749
817
  const look = this._deviceLook();
818
+ // A build a pointer move asked for styles the elements the move
819
+ // reached, and takes every other element's from the tree it replaces
820
+ // (`Cascade.beginSharing`): the first build only, since a second is
821
+ // one the first found a reason for — a face, an image's size.
822
+ let kept = restyleOnly && this._tree && this._styledWith === cascade
823
+ ? { styles: this._tree.styles, restyle: restyleOnly }
824
+ : null;
750
825
  const build = () => buildBoxes(this._source.document, {
751
826
  cascade,
827
+ kept,
752
828
  scale: this._scale,
753
829
  imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
754
830
  urlSize: (url) => this._resources.imageSize(url),
755
831
  controlSize: (el, kind, style) => measureControl(el, kind, style, this._fonts(), look),
756
832
  });
757
833
  this._tree = build();
834
+ kept = null;
835
+ this._styledWith = cascade;
758
836
  this._styledWidth = target;
759
837
  this._styledHeight = viewport;
760
838
  this._requestBackgrounds(this._tree);
@@ -812,7 +890,7 @@ export class HtmlViewNode extends Node {
812
890
  // The boxes are device pixels; each rect becomes the style of a widget
813
891
  // mounted beside this element, and a style is logical.
814
892
  const s = this._scale;
815
- const rects = controlRectsOf(tree).map((r) => s === 1
893
+ const rects = controlRectsOf(tree, s).map((r) => s === 1
816
894
  ? r
817
895
  : {
818
896
  ...r,
@@ -820,6 +898,15 @@ export class HtmlViewNode extends Node {
820
898
  y: r.y / s,
821
899
  width: r.width / s,
822
900
  height: r.height / s,
901
+ fontSize: r.fontSize / s,
902
+ ...(r.clip && {
903
+ clip: {
904
+ x: r.clip.x / s,
905
+ y: r.clip.y / s,
906
+ width: r.clip.width / s,
907
+ height: r.clip.height / s,
908
+ },
909
+ }),
823
910
  ...(r.bare && {
824
911
  bare: {
825
912
  ...r.bare,
@@ -914,6 +1001,7 @@ export class HtmlViewNode extends Node {
914
1001
  }
915
1002
  }
916
1003
  destroySubtree() {
1004
+ this._dropHeldHover();
917
1005
  this._resources.destroy();
918
1006
  this._webFonts.destroy();
919
1007
  this._source.destroy();
@@ -1008,7 +1096,8 @@ export class HtmlViewNode extends Node {
1008
1096
  if (to <= from)
1009
1097
  return [];
1010
1098
  const out = [];
1011
- collectBands(tree.root, from, to, this.abs.x, this.abs.y, out);
1099
+ // the rows the highlight paints, over glyphs taller than their line
1100
+ collectBands(tree.root, from, to, this.abs.x, this.abs.y, out, true);
1012
1101
  return out;
1013
1102
  }
1014
1103
  // --- pointer --------------------------------------------------------------
@@ -1079,11 +1168,16 @@ export class HtmlViewNode extends Node {
1079
1168
  const around = block.parent;
1080
1169
  if (!around)
1081
1170
  continue;
1171
+ // from where clearance moved it down from, as Blink's line around
1172
+ // a block in an inline box starts there: the Zen Garden's 214
1173
+ // makes its content an inline box of floats and a footer that
1174
+ // clears them, and Chrome measures it from the floats' top
1175
+ const top = Math.min(block.y, CLEARED_FROM.get(block) ?? block.y);
1082
1176
  bands.push({
1083
1177
  x: around.contentX,
1084
- y: block.y,
1178
+ y: top,
1085
1179
  width: around.contentWidth,
1086
- height: block.height,
1180
+ height: block.y + block.height - top,
1087
1181
  });
1088
1182
  }
1089
1183
  for (const band of bands)
@@ -1119,6 +1213,9 @@ export class HtmlViewNode extends Node {
1119
1213
  rect = { x: caret.x, y: caret.y, width: 0, height: caret.height };
1120
1214
  }
1121
1215
  }
1216
+ // where it is drawn: the rectangle around it, turned and scaled as the
1217
+ // boxes it is in are, and itself (`getBoundingClientRect`, CSSOM View)
1218
+ rect = throughTransforms(rect, box);
1122
1219
  const s = this._scale;
1123
1220
  return {
1124
1221
  x: rect.x / s,
@@ -1177,6 +1274,7 @@ export class HtmlViewNode extends Node {
1177
1274
  * contains a `:hover` rule.
1178
1275
  */
1179
1276
  setHover(x, y) {
1277
+ this._dropHeldHover();
1180
1278
  const cascade = this._cascade;
1181
1279
  if (!cascade || !cascade.hoverSensitive)
1182
1280
  return false;
@@ -1195,6 +1293,7 @@ export class HtmlViewNode extends Node {
1195
1293
  return true;
1196
1294
  }
1197
1295
  clearHover() {
1296
+ this._dropHeldHover();
1198
1297
  if (!this._hovered.length)
1199
1298
  return false;
1200
1299
  const was = this._hovered;
@@ -1203,62 +1302,53 @@ export class HtmlViewNode extends Node {
1203
1302
  this._restyleHover(was, []);
1204
1303
  return true;
1205
1304
  }
1206
- /** The hovered chain moved: restyle where it did, or the document. */
1305
+ /**
1306
+ * The hovered chain moved: nothing, where no rule that tests the pointer
1307
+ * names an element it moved over; a restyle where they are, where all it
1308
+ * changed is ink (`_hoverInPlace`); and otherwise the boxes built again,
1309
+ * with the styles of every element the move did not reach kept.
1310
+ */
1207
1311
  _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);
1312
+ const reach = this._hoverReach(was, now);
1313
+ if (reach !== null) {
1314
+ if (!reach.size)
1315
+ return;
1316
+ if (this._hoverInPlace(reach, was))
1317
+ return;
1318
+ }
1319
+ this._invalidate(Stale.Boxes, reach);
1211
1320
  }
1212
1321
  /**
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:
1322
+ * The elements whose styles a move of the hovered chain can change, or
1323
+ * null where that cannot be said (`Cascade.hoverLocal`).
1221
1324
  *
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.
1325
+ * Only an element whose hover state flipped and that a compound testing
1326
+ * the pointer could match can change (`Cascade.hoverTouches`), with its
1327
+ * subtree, and its later siblings' where a sibling combinator is in play;
1328
+ * and the elements a `:has()` testing the pointer may flip, which are
1329
+ * around the ones that changed. A move between two paragraphs under
1330
+ * `a:hover` reaches nothing.
1237
1331
  */
1238
- _hoverInPlace(was, now) {
1332
+ _hoverReach(was, now) {
1239
1333
  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;
1334
+ if (!cascade || !cascade.hoverLocal)
1335
+ return null;
1246
1336
  const before = new Set(was);
1247
1337
  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
1338
+ const roots = new Map();
1252
1339
  const flipped = (el, other) => {
1253
1340
  if (other.has(el))
1254
1341
  return;
1255
- if (cascade.hoverTouches(el))
1256
- roots.push(el);
1342
+ const touch = cascade.hoverTouches(el);
1343
+ if (touch > (roots.get(el) ?? HOVER_UNTOUCHED))
1344
+ roots.set(el, touch);
1257
1345
  };
1258
1346
  for (const el of was)
1259
1347
  flipped(el, after);
1260
1348
  for (const el of now)
1261
1349
  flipped(el, before);
1350
+ // the chains are ancestor chains, so the deepest that changed on each
1351
+ // side is where a `:has()` is looked for from
1262
1352
  const deepest = (chain, other) => {
1263
1353
  const el = chain[0];
1264
1354
  if (el && !other.has(el))
@@ -1266,11 +1356,9 @@ export class HtmlViewNode extends Node {
1266
1356
  };
1267
1357
  deepest(was, after);
1268
1358
  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
1359
+ // the roots' subtrees, which inherit from them and a descendant
1360
+ // combinator reaches, and their later siblings' too where a sibling
1361
+ // combinator follows the compound that tests the pointer
1274
1362
  const reach = new Set();
1275
1363
  const collect = (el) => {
1276
1364
  const stack = [el];
@@ -1279,27 +1367,85 @@ export class HtmlViewNode extends Node {
1279
1367
  if (reach.has(at))
1280
1368
  continue;
1281
1369
  reach.add(at);
1282
- if (reach.size > HOVER_RESTYLE_LIMIT)
1283
- return false;
1284
1370
  for (const child of at.children)
1285
1371
  if (isElement(child))
1286
1372
  stack.push(child);
1287
1373
  }
1288
- return true;
1289
1374
  };
1290
- for (const root of roots) {
1291
- if (!collect(root))
1292
- return false;
1293
- if (!cascade.hoverSiblings)
1375
+ for (const [root, touch] of roots) {
1376
+ collect(root);
1377
+ if (touch !== HOVER_FOLLOWED)
1294
1378
  continue;
1295
1379
  for (let s = root.nextSibling; s; s = s.nextSibling) {
1296
- if (isElement(s) && !collect(s))
1297
- return false;
1380
+ if (isElement(s))
1381
+ collect(s);
1298
1382
  }
1299
1383
  }
1384
+ return reach;
1385
+ }
1386
+ /**
1387
+ * A pointer move restyled where it happened, when all it changed is ink.
1388
+ *
1389
+ * A move to another element is a change to the cascade, and the pipeline
1390
+ * answers one by building every box again and laying the document out:
1391
+ * 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
1392
+ * link the pointer crossed. But the rules a page writes for `:hover`
1393
+ * nearly all change a colour, an underline, a background or a border's
1394
+ * colour — 77 of that article's 79 — and those move nothing. So:
1395
+ *
1396
+ * - The elements the move reached (`_hoverReach`) are styled again from
1397
+ * their parents — the ones a rule testing the pointer answers
1398
+ * differently for, or whose parent's style changed; the rest, most of
1399
+ * a hovered card or table row, have the styles they had
1400
+ * (`Cascade.pointerChanged`). Where anything but ink differs
1401
+ * (`PAINT_ONLY`), this is not the move to take.
1402
+ * - The boxes take their new styles: an element's own, the text in it,
1403
+ * the anonymous boxes the fix-up made around what is in it, and its
1404
+ * `::before` and `::after`, styled again by their own rules. A box
1405
+ * whose style is derived some other way — a marker, a first letter, a
1406
+ * control — is not one to restyle here.
1407
+ * - Each layout of their text is made again from the runs it was made
1408
+ * from (`TextLayoutCache.inputsOf`) with the new ink (`runFor`), where
1409
+ * its geometry comes out the same.
1410
+ *
1411
+ * Everything is checked before anything is changed, and nothing is looked
1412
+ * at but the blocks the restyled elements are in (`_blocksOf`): a move
1413
+ * costs what it changed, in a document of any length. True where it was
1414
+ * restyled here, or had nothing to restyle; false where the boxes have to
1415
+ * be built again.
1416
+ */
1417
+ _hoverInPlace(reach, was) {
1418
+ const cascade = this._cascade;
1419
+ const tree = this._tree;
1420
+ const layouts = this._layouts;
1421
+ if (!cascade || !tree || !layouts)
1422
+ return false;
1423
+ if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
1424
+ return false;
1425
+ if (reach.size > HOVER_RESTYLE_LIMIT)
1426
+ return false;
1427
+ // a first line's, a first letter's and a marker's styles are layout's
1428
+ const pseudoRules = cascade.hoverPseudo;
1429
+ if (pseudoRules === HOVER_PSEUDO_OTHER)
1430
+ return false;
1431
+ // whether the rules that test the pointer answer differently for an
1432
+ // element than they did before the move
1433
+ const before = { hovered: new Set(was), active: EMPTY_SET };
1434
+ const flips = new Map();
1435
+ const flipped = (el) => {
1436
+ let flip = flips.get(el);
1437
+ if (flip === undefined) {
1438
+ flip = cascade.pointerChanged(el, before);
1439
+ flips.set(el, flip);
1440
+ }
1441
+ return flip;
1442
+ };
1300
1443
  // styled again from their parents, a parent first
1301
1444
  const fresh = new Map();
1302
1445
  const changed = new Map();
1446
+ // the ones whose ink is what it was: only their custom properties
1447
+ // changed, which the elements under them read
1448
+ const quiet = new Set();
1303
1449
  let refused = false;
1304
1450
  // what changed beyond ink (`hoverChange`): how far some box's ink
1305
1451
  // reaches, the order its layer paints in, where one is
@@ -1323,20 +1469,27 @@ export class HtmlViewNode extends Node {
1323
1469
  refused = true;
1324
1470
  return null;
1325
1471
  }
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);
1472
+ const parentWas = parent ? tree.styles.get(parent).style : parentStyle;
1473
+ let style = kept.style;
1474
+ if (parentStyle !== parentWas || flipped(el)) {
1475
+ const made = cascade.styleFor(el, parentStyle, kept.inFlex);
1476
+ const diff = hoverChange(kept.style, made);
1477
+ if (diff === false)
1478
+ refused = true;
1479
+ else if (diff) {
1480
+ style = made;
1481
+ changed.set(el, made);
1482
+ if (!diff.ink)
1483
+ quiet.add(el);
1484
+ if (diff.reach)
1485
+ reaches = true;
1486
+ if (diff.order)
1487
+ reorders = true;
1488
+ if (diff.move)
1489
+ moving.add(el);
1490
+ }
1339
1491
  }
1492
+ fresh.set(el, style);
1340
1493
  return style;
1341
1494
  };
1342
1495
  for (const el of reach) {
@@ -1344,42 +1497,122 @@ export class HtmlViewNode extends Node {
1344
1497
  if (refused)
1345
1498
  return false;
1346
1499
  }
1347
- if (!changed.size)
1348
- return 'none';
1500
+ if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE)
1501
+ return true;
1349
1502
  for (const el of changed.keys()) {
1350
1503
  const tag = tagOf(el);
1351
1504
  // their backgrounds are the canvas's
1352
1505
  if (tag === 'html' || tag === 'body')
1353
1506
  return false;
1354
1507
  }
1355
- // the boxes: each changed element's own, and nothing that takes its
1356
- // style from one without being it
1508
+ // The boxes, each under its parent: what a box takes depends on what
1509
+ // the box it is in took.
1357
1510
  const fonts = layouts.fonts;
1511
+ const blocks = this._blocksOf(tree, pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach);
1358
1512
  const restyled = [];
1513
+ const next = new Map();
1514
+ /** The restyled boxes whose ink changed, to repaint. */
1515
+ const inked = new Set();
1516
+ /** The `::before` and `::after` boxes found, by element: 1 and 2. */
1517
+ const generated = new Map();
1359
1518
  const redecorated = [];
1360
- const walk = [tree.root];
1519
+ const walk = blocks.slice();
1361
1520
  while (walk.length) {
1362
1521
  const box = walk.pop();
1363
1522
  for (const child of box.children)
1364
1523
  walk.push(child);
1365
- const owner = box.el ?? nearestElement(box);
1366
- if (!owner)
1367
- continue;
1368
- const style = changed.get(owner);
1524
+ const parent = box.parent;
1525
+ const above = parent ? next.get(parent) : undefined;
1526
+ let style;
1527
+ let ink = !!parent && inked.has(parent);
1528
+ if (box.kind === 'text' || box.kind === 'break') {
1529
+ // Text is set in the style of the box it is in — an element's, a
1530
+ // pseudo-element's — or of its element, where the fix-up put an
1531
+ // anonymous box around it.
1532
+ const el = box.el;
1533
+ if (parent && box.style === parent.style)
1534
+ style = above;
1535
+ else if (el && box.style === tree.styles.get(el)?.style) {
1536
+ style = changed.get(el);
1537
+ ink = !quiet.has(el);
1538
+ }
1539
+ else if (above || (el && changed.has(el)))
1540
+ return false;
1541
+ }
1542
+ else if (box.pseudo === 'before' || box.pseudo === 'after') {
1543
+ const from = GENERATED_FROM.get(box);
1544
+ if (!from) {
1545
+ // an inside marker, made of its item's style and its own rules
1546
+ if (above)
1547
+ return false;
1548
+ continue;
1549
+ }
1550
+ if (!reach.has(from))
1551
+ continue;
1552
+ generated.set(from, (generated.get(from) ?? 0) | (box.pseudo === 'before' ? 1 : 2));
1553
+ const inherits = fresh.get(from);
1554
+ const kept = tree.styles.get(from)?.style;
1555
+ if (!inherits || !kept)
1556
+ return false;
1557
+ if (inherits === kept && !flipped(from))
1558
+ continue;
1559
+ // styled by its own rules over its element's style, as the builder
1560
+ // styled it, in its element's box
1561
+ if (!parent || parent.style !== kept || box.marker)
1562
+ return false;
1563
+ const made = cascade.pseudoStyleFor(from, box.pseudo, inherits);
1564
+ if (!made || made.display === 'none')
1565
+ return false;
1566
+ const diff = hoverChange(box.style, made);
1567
+ if (diff === false)
1568
+ return false;
1569
+ if (!diff)
1570
+ continue;
1571
+ if (diff.reach || diff.order || diff.move)
1572
+ return false;
1573
+ style = made;
1574
+ ink = diff.ink;
1575
+ }
1576
+ else if (box.pseudo) {
1577
+ // a first letter, styled from the box its letter is in
1578
+ if (above)
1579
+ return false;
1580
+ }
1581
+ else if (box.el) {
1582
+ const el = box.el;
1583
+ style = changed.get(el);
1584
+ if (!style)
1585
+ continue;
1586
+ const kept = tree.styles.get(el).style;
1587
+ // a style derived from the element's, or drawn from it somewhere
1588
+ // else — a widget mounted beside the document takes its look from
1589
+ // it. An image, a drawing or a rule is drawn here, from its box,
1590
+ // like any.
1591
+ if (box.style !== kept)
1592
+ return false;
1593
+ if (box.marker || WIDGETS.has(box.replaced))
1594
+ return false;
1595
+ if (moving.has(el) && !movable(box, kept, style))
1596
+ return false;
1597
+ ink = !quiet.has(el);
1598
+ }
1599
+ else if (above) {
1600
+ // An anonymous box takes what its parent's style passes on
1601
+ // (`anonymousStyles`), so it takes that of the new one: the text of
1602
+ // a link made a flex row is in one, and takes the link's colour.
1603
+ if (GENERATED_FROM.has(box))
1604
+ return false;
1605
+ const display = box.style.display;
1606
+ if (tree.anonymous(parent.style, display) !== box.style)
1607
+ return false;
1608
+ style = tree.anonymous(above, display);
1609
+ }
1369
1610
  if (!style)
1370
1611
  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;
1612
+ next.set(box, style);
1382
1613
  restyled.push([box, style]);
1614
+ if (ink)
1615
+ inked.add(box);
1383
1616
  if (box.kind === 'inline') {
1384
1617
  const decoration = inlineDecoration(fonts, box, style);
1385
1618
  if ((decoration === null) !== (box.decoration === null)) {
@@ -1391,11 +1624,34 @@ export class HtmlViewNode extends Node {
1391
1624
  redecorated.push([box, decoration]);
1392
1625
  }
1393
1626
  }
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
1627
+ // a `::before` or an `::after` a rule that tests the pointer now gives
1628
+ // an element is a box to build
1629
+ if (pseudoRules !== HOVER_PSEUDO_NONE) {
1630
+ for (const el of reach) {
1631
+ if (!flipped(el))
1632
+ continue;
1633
+ const style = fresh.get(el);
1634
+ if (!style)
1635
+ continue;
1636
+ const has = generated.get(el) ?? 0;
1637
+ for (const [which, bit] of GENERATED_BITS) {
1638
+ if (has & bit)
1639
+ continue;
1640
+ const made = cascade.pseudoStyleFor(el, which, style);
1641
+ if (made && made.display !== 'none')
1642
+ return false;
1643
+ }
1644
+ }
1645
+ }
1646
+ if (!restyled.length)
1647
+ return true;
1648
+ // the text: each layout holding a run of a restyled box's, made again
1649
+ // with its new ink, where it comes out the same shape
1396
1650
  const relaid = new Map();
1397
1651
  const texts = [];
1398
- walk.push(tree.root);
1652
+ if (inked.size)
1653
+ for (const block of blocks)
1654
+ walk.push(block);
1399
1655
  while (walk.length) {
1400
1656
  const box = walk.pop();
1401
1657
  for (const child of box.children)
@@ -1407,16 +1663,16 @@ export class HtmlViewNode extends Node {
1407
1663
  texts.push(text);
1408
1664
  if (relaid.has(text.layout))
1409
1665
  continue;
1410
- const next = reinked(text, layouts, changed, tree.styles);
1411
- if (next === false)
1666
+ const again = reinked(text, layouts, next);
1667
+ if (again === false)
1412
1668
  return false;
1413
- relaid.set(text.layout, next);
1669
+ relaid.set(text.layout, again);
1414
1670
  }
1415
1671
  }
1416
1672
  }
1417
1673
  // what each box drew before, to be repainted with what it draws after
1418
1674
  const inks = [];
1419
- for (const [box] of restyled) {
1675
+ for (const box of inked) {
1420
1676
  const ink = inkOf(box);
1421
1677
  if (ink)
1422
1678
  inks.push(ink);
@@ -1426,8 +1682,9 @@ export class HtmlViewNode extends Node {
1426
1682
  for (const [box, style] of restyled) {
1427
1683
  const was = box.style;
1428
1684
  box.style = style;
1429
- if (moving.has(box.el))
1685
+ if (box.el && moving.has(box.el) && box.kind !== 'text') {
1430
1686
  moved.push([box, was]);
1687
+ }
1431
1688
  }
1432
1689
  for (const [box, decoration] of redecorated) {
1433
1690
  // what is inside it keeps which boxes around it paint, and this is
@@ -1438,9 +1695,9 @@ export class HtmlViewNode extends Node {
1438
1695
  box.decoration = decoration;
1439
1696
  }
1440
1697
  for (const text of texts) {
1441
- const next = relaid.get(text.layout);
1442
- if (next)
1443
- text.layout = next;
1698
+ const again = relaid.get(text.layout);
1699
+ if (again)
1700
+ text.layout = again;
1444
1701
  }
1445
1702
  for (const [el, style] of changed) {
1446
1703
  tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
@@ -1474,15 +1731,61 @@ export class HtmlViewNode extends Node {
1474
1731
  this._reportControls();
1475
1732
  if (relayout) {
1476
1733
  this._invalidate(Stale.Layout);
1477
- return 'painted';
1734
+ return true;
1478
1735
  }
1479
- for (const [box] of restyled) {
1736
+ for (const box of inked) {
1480
1737
  const ink = inkOf(box);
1481
1738
  if (ink)
1482
1739
  inks.push(ink);
1483
1740
  }
1484
1741
  this._repaintInk(inks);
1485
- return 'painted';
1742
+ return true;
1743
+ }
1744
+ /** An element's first box in document order, for the tree it was asked
1745
+ * of: made on the first move over a tree, which is one walk of it. */
1746
+ _firstBoxes = null;
1747
+ /**
1748
+ * The blocks a restyle of `changed` has to look in: for each element, the
1749
+ * box its text is set in the lines of — the nearest box around its first
1750
+ * that is not an inline one — which holds every box that takes its style
1751
+ * from it, the pieces a block in it broke it into among them
1752
+ * (`breakAround`). One that is inside another is left to it.
1753
+ */
1754
+ _blocksOf(tree, changed) {
1755
+ let index = this._firstBoxes;
1756
+ if (index?.tree !== tree) {
1757
+ const of = new Map();
1758
+ const stack = [tree.root];
1759
+ while (stack.length) {
1760
+ const box = stack.pop();
1761
+ if (box.el && !of.has(box.el))
1762
+ of.set(box.el, box);
1763
+ for (let i = box.children.length - 1; i >= 0; i -= 1) {
1764
+ stack.push(box.children[i]);
1765
+ }
1766
+ }
1767
+ index = this._firstBoxes = { tree, of };
1768
+ }
1769
+ const blocks = new Set();
1770
+ for (const el of changed) {
1771
+ let box = index.of.get(el) ?? null;
1772
+ while (box?.parent &&
1773
+ (box.kind === 'inline' || box.kind === 'text' || box.kind === 'break')) {
1774
+ box = box.parent;
1775
+ }
1776
+ if (box)
1777
+ blocks.add(box);
1778
+ }
1779
+ const out = [];
1780
+ for (const block of blocks) {
1781
+ let inside = false;
1782
+ for (let at = block.parent; at && !inside; at = at.parent) {
1783
+ inside = blocks.has(at);
1784
+ }
1785
+ if (!inside)
1786
+ out.push(block);
1787
+ }
1788
+ return out;
1486
1789
  }
1487
1790
  /**
1488
1791
  * Repaint the document where `inks` are, in its own coordinates: the ink
@@ -1542,17 +1845,64 @@ export class HtmlViewNode extends Node {
1542
1845
  // `:hover` is wired at the element rather than through React: the cascade
1543
1846
  // already knows whether any rule in the document tests it, `setHover`
1544
1847
  // 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.
1848
+ // re-styles only when the hovered chain actually changed, and then only
1849
+ // the elements the change reached (`_restyleHover`).
1850
+ //
1851
+ // **A hover waits for a scroll to stop.** Core asks again what is under a
1852
+ // pointer that stayed where it was after every frame that laid out —
1853
+ // hover follows content, react-x11#793 — and says so with a move to the
1854
+ // point the pointer is already at. While a document scrolls that is every
1855
+ // frame, each with something else under the pointer: a restyle a frame
1856
+ // at best, and where a hover moves something, the boxes built again a
1857
+ // frame, which took a page scrolled under a parked pointer from 52 frames
1858
+ // a second to 4. A browser holds its hover until the scroll is over, for
1859
+ // the same reason; so does this, for `HOVER_REST_MS` after the content
1860
+ // last moved. What was hovered stays hovered as it scrolls away, as it
1861
+ // does there. A move of the pointer itself is answered at once.
1548
1862
  defaultMouseMove(ev) {
1549
1863
  super.defaultMouseMove?.(ev);
1864
+ const at = this._pointerAt;
1865
+ if (at !== null && at.x === ev.x && at.y === ev.y) {
1866
+ this._holdHover();
1867
+ return;
1868
+ }
1869
+ this._pointerAt = { x: ev.x, y: ev.y };
1550
1870
  this.setHover(ev.x, ev.y);
1551
1871
  }
1552
1872
  defaultMouseLeave(ev) {
1553
1873
  super.defaultMouseLeave?.(ev);
1874
+ this._pointerAt = null;
1554
1875
  this.clearHover();
1555
1876
  }
1877
+ /** The content moved under the pointer: the hover is asked for once it
1878
+ * has been still for `HOVER_REST_MS`. One timer a wait, put off by
1879
+ * being armed again when it finds the content moved since. */
1880
+ _holdHover() {
1881
+ if (!this._cascade?.hoverSensitive)
1882
+ return;
1883
+ this._heldSince = hoverClock.now();
1884
+ if (this._heldHover !== null)
1885
+ return;
1886
+ const rest = () => {
1887
+ this._heldHover = null;
1888
+ const at = this._pointerAt;
1889
+ if (!at || this.destroyed)
1890
+ return;
1891
+ const left = this._heldSince + HOVER_REST_MS - hoverClock.now();
1892
+ if (left > 0) {
1893
+ this._heldHover = hoverClock.arm(rest, left);
1894
+ return;
1895
+ }
1896
+ this.setHover(at.x, at.y);
1897
+ };
1898
+ this._heldHover = hoverClock.arm(rest, HOVER_REST_MS);
1899
+ }
1900
+ _dropHeldHover() {
1901
+ if (this._heldHover === null)
1902
+ return;
1903
+ hoverClock.disarm(this._heldHover);
1904
+ this._heldHover = null;
1905
+ }
1556
1906
  // --- paint ----------------------------------------------------------------
1557
1907
  paint(ctx) {
1558
1908
  super.paint(ctx); // background, border, clip to `abs`
@@ -1793,13 +2143,21 @@ function sameRects(a, b) {
1793
2143
  p.y !== q.y ||
1794
2144
  p.width !== q.width ||
1795
2145
  p.height !== q.height ||
2146
+ p.fontFamily !== q.fontFamily ||
2147
+ p.fontSize !== q.fontSize ||
1796
2148
  p.opacity !== q.opacity ||
2149
+ !sameClip(p.clip, q.clip) ||
1797
2150
  !sameBare(p.bare, q.bare)) {
1798
2151
  return false;
1799
2152
  }
1800
2153
  }
1801
2154
  return true;
1802
2155
  }
2156
+ function sameClip(a, b) {
2157
+ if (!a || !b)
2158
+ return a === b;
2159
+ return (a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height);
2160
+ }
1803
2161
  function sameBare(a, b) {
1804
2162
  if (!a || !b)
1805
2163
  return a === b;
@@ -1846,7 +2204,13 @@ function nearestText(box, x, y) {
1846
2204
  if (distance >= bestDistance)
1847
2205
  continue;
1848
2206
  bestDistance = distance;
1849
- const local = text.layout.indexAt(x - text.drawX, y - text.drawY);
2207
+ // in the row that is this line's, where the layout's other lines are
2208
+ // in another column and a point past this one is not over them
2209
+ let row = y - text.drawY;
2210
+ if (columned.any && COLUMN_ROWS.has(text)) {
2211
+ row = Math.max(natural.y, Math.min(row, natural.y + natural.height - 0.01));
2212
+ }
2213
+ const local = text.layout.indexAt(x - text.drawX, row);
1850
2214
  const offsets = layoutOffsetsOf(text.layout);
1851
2215
  const units = offsets.length
1852
2216
  ? offsets[Math.max(0, Math.min(local, offsets.length - 1))]
@@ -1859,13 +2223,38 @@ function nearestText(box, x, y) {
1859
2223
  axisDistance(x, node.boundsX, node.boundsWidth);
1860
2224
  if (boundsDistance >= bestDistance)
1861
2225
  return;
2226
+ // in a box painted through a matrix, the point taken back through it
2227
+ // (`deepestAt`): the text is where it was laid out
2228
+ const matrix = placedMatrix(node);
2229
+ if (!matrix) {
2230
+ visitIn(node);
2231
+ return;
2232
+ }
2233
+ const back = invert(matrix);
2234
+ if (!back)
2235
+ return;
2236
+ const px = x;
2237
+ const py = y;
2238
+ [x, y] = mapPoint(back, px, py);
2239
+ try {
2240
+ visitIn(node);
2241
+ }
2242
+ finally {
2243
+ x = px;
2244
+ y = py;
2245
+ }
2246
+ };
2247
+ const visitIn = (node) => {
1862
2248
  const lines = node.lines;
1863
2249
  if (lines?.length) {
1864
2250
  // The line nearest in y, by binary search; then outward both ways
1865
2251
  // while vertical distance alone could still beat the best.
1866
2252
  let lo = 0;
1867
2253
  let hi = lines.length;
1868
- while (lo < hi) {
2254
+ // the lines columns took apart are in no order down the page: each
2255
+ // is tried
2256
+ const apart = columned.any && COLUMN_LINES.has(lines);
2257
+ while (!apart && lo < hi) {
1869
2258
  const mid = (lo + hi) >> 1;
1870
2259
  if (lines[mid].y > y)
1871
2260
  hi = mid;
@@ -1878,8 +2267,11 @@ function nearestText(box, x, y) {
1878
2267
  tryLine(lines[i]);
1879
2268
  }
1880
2269
  for (let i = lo; i < lines.length; i += 1) {
1881
- if (axisDistance(y, lines[i].y, lines[i].height) * 4 >= bestDistance)
2270
+ if (axisDistance(y, lines[i].y, lines[i].height) * 4 >= bestDistance) {
2271
+ if (apart)
2272
+ continue;
1882
2273
  break;
2274
+ }
1883
2275
  tryLine(lines[i]);
1884
2276
  }
1885
2277
  }
@@ -1967,7 +2359,13 @@ function caretAt(box, units) {
1967
2359
  const offsets = layoutOffsetsOf(text.layout);
1968
2360
  const layoutUnits = layoutOffsetOf(text, units);
1969
2361
  const caret = text.layout.caretPosition(codePointAtOffset(offsets, layoutUnits));
1970
- found = { x: text.drawX + caret.x, y: line.y, height: line.height };
2362
+ const at = throughTransforms({
2363
+ x: text.drawX + caret.x,
2364
+ y: line.y,
2365
+ width: 0,
2366
+ height: line.height,
2367
+ }, node);
2368
+ found = { x: at.x, y: at.y, height: at.height };
1971
2369
  return;
1972
2370
  }
1973
2371
  }
@@ -1983,6 +2381,21 @@ function caretAt(box, units) {
1983
2381
  visit(box);
1984
2382
  return found;
1985
2383
  }
2384
+ /**
2385
+ * A rectangle in a box's own coordinates — the ones it and what it holds
2386
+ * were laid out in — as the rectangle around where it is drawn: through
2387
+ * the matrix of each box from `from` up that is painted through one
2388
+ * (`placedMatrix`). The rectangle itself, in a document that transforms
2389
+ * nothing but by moving it.
2390
+ */
2391
+ function throughTransforms(rect, from) {
2392
+ for (let at = from; at; at = at.parent) {
2393
+ const matrix = placedMatrix(at);
2394
+ if (matrix)
2395
+ rect = mapRect(matrix, rect.x, rect.y, rect.width, rect.height);
2396
+ }
2397
+ return rect;
2398
+ }
1986
2399
  /** Every band a document range covers, in window coordinates. The subtree
1987
2400
  * text ranges prune the walk to the boxes the range actually crosses, so a
1988
2401
  * drag's small range costs its own paragraphs; only Ctrl+A pays for all. */
@@ -2078,7 +2491,10 @@ function fragmentHeights(box, bands, fonts) {
2078
2491
  function lineBands(lines, from, to, dx, dy, out,
2079
2492
  /** Where the lines put the text, not where a relative offset moved it
2080
2493
  * (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
2081
- laidOut = false) {
2494
+ laidOut = false,
2495
+ /** The rows a highlight covers (`selectionRows`) rather than the line's:
2496
+ * past it where the glyphs are taller than the line. */
2497
+ ink = false) {
2082
2498
  // First line whose text can reach `from`, by binary search over the
2083
2499
  // sorted text starts; stop at the first line past `to`.
2084
2500
  let lo = 0;
@@ -2111,6 +2527,9 @@ laidOut = false) {
2111
2527
  // a link that ends a line a space wider than Chrome's. So the text
2112
2528
  // a line ends with is measured no further than its content.
2113
2529
  const ends = endsLine(line, text, natural);
2530
+ const rows = ink
2531
+ ? selectionRows(line, text, natural)
2532
+ : { y: line.y, height: line.height };
2114
2533
  for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
2115
2534
  let left = band.x;
2116
2535
  let right = band.x + band.width;
@@ -2120,9 +2539,22 @@ laidOut = false) {
2120
2539
  }
2121
2540
  out.push({
2122
2541
  x: dx + left + text.drawX - moved,
2123
- y: dy + line.y,
2542
+ y: dy + rows.y,
2124
2543
  width: right - left,
2125
- height: line.height,
2544
+ height: rows.height,
2545
+ });
2546
+ }
2547
+ // The spaces a piece of a line ends on, where the line goes on
2548
+ // after them: the engine stripped them from the piece it laid out
2549
+ // as a line, and they are the text's all the same, in the elements
2550
+ // that hold its end — a link whose text ends in a space, before
2551
+ // the next link, is that space wider than its letters
2552
+ if (text.trail && b >= text.textEnd && !rtlLine(natural)) {
2553
+ out.push({
2554
+ x: dx + natural.x + natural.width + text.drawX - moved,
2555
+ y: dy + rows.y,
2556
+ width: text.trail,
2557
+ height: rows.height,
2126
2558
  });
2127
2559
  }
2128
2560
  // the spaces `pre-wrap` keeps that the line ends on, which have no
@@ -2133,15 +2565,20 @@ laidOut = false) {
2133
2565
  continue;
2134
2566
  out.push({
2135
2567
  x: dx + space.x + text.drawX - moved,
2136
- y: dy + line.y,
2568
+ y: dy + rows.y,
2137
2569
  width: space.width,
2138
- height: line.height,
2570
+ height: rows.height,
2139
2571
  });
2140
2572
  }
2141
2573
  }
2142
2574
  }
2143
2575
  }
2144
2576
  }
2577
+ /** Whether a laid-out line's last run reads right to left, its end at its
2578
+ * left. */
2579
+ function rtlLine(natural) {
2580
+ return natural.runs[natural.runs.length - 1]?.run?.direction === 'rtl';
2581
+ }
2145
2582
  /** Whether a line's text is where the line ends: its last, with nothing
2146
2583
  * placed after it — an inline-block or an image past its content. */
2147
2584
  function endsLine(line, text, natural) {
@@ -2154,18 +2591,28 @@ function endsLine(line, text, natural) {
2154
2591
  }
2155
2592
  return true;
2156
2593
  }
2157
- function collectBands(box, from, to, dx, dy, out) {
2594
+ function collectBands(box, from, to, dx, dy, out, ink = false) {
2158
2595
  if (box.subtreeTextEnd <= box.subtreeTextStart)
2159
2596
  return;
2160
2597
  if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
2161
2598
  return;
2599
+ const first = out.length;
2162
2600
  if (box.lines)
2163
- lineBands(box.lines, from, to, dx, dy, out);
2601
+ lineBands(box.lines, from, to, dx, dy, out, false, ink);
2164
2602
  // Atomics are ordinary children, reached below.
2165
2603
  for (const child of box.children) {
2166
2604
  if (child.kind === 'text' || child.kind === 'break')
2167
2605
  continue;
2168
- collectBands(child, from, to, dx, dy, out);
2606
+ collectBands(child, from, to, dx, dy, out, ink);
2607
+ }
2608
+ // where a box painted through a matrix draws them
2609
+ const matrix = out.length > first ? placedMatrix(box) : null;
2610
+ if (!matrix)
2611
+ return;
2612
+ for (let i = first; i < out.length; i += 1) {
2613
+ const band = out[i];
2614
+ const to = mapRect(matrix, band.x - dx, band.y - dy, band.width, band.height);
2615
+ out[i] = { ...band, ...to, x: to.x + dx, y: to.y + dy };
2169
2616
  }
2170
2617
  }
2171
2618
  /** The layers of a hit test, in the order CSS paints them within a
@@ -2200,7 +2647,17 @@ fixedShift = null) {
2200
2647
  // are painted in `z-index` order and then the document's (`byZIndex`),
2201
2648
  // the negative ones under the flow: the Zen Garden's `›` has
2202
2649
  // `z-index: 3` over the bar the "View All Designs" link fills after it.
2203
- const take = (el, key, text) => {
2650
+ // Nor is a box the pointer passes through under it: one that is not
2651
+ // visible, which is no target for the pointer in any browser (CSS 2.1
2652
+ // 11.2 has it invisible, and a hit test finds what is seen), or one with
2653
+ // `pointer-events: none` (CSS UI 4, 5.2). Both are inherited, and what is
2654
+ // in such a box may say otherwise, so the walk goes on through it. A
2655
+ // site's closed menu is laid out over its page, hidden: GitHub's covered
2656
+ // its repository's tabs, and the pointer over a tab was over the menu.
2657
+ const take = (el, style, key, text) => {
2658
+ if (style.visibility !== 'visible' || style.pointerEvents === 'none') {
2659
+ return;
2660
+ }
2204
2661
  if (compareKeys(key, foundKey) < 0)
2205
2662
  return;
2206
2663
  found = el;
@@ -2242,14 +2699,79 @@ fixedShift = null) {
2242
2699
  atViewport = false;
2243
2700
  }
2244
2701
  };
2702
+ // A box painted through a matrix (`placedMatrix`) is under the point
2703
+ // where the matrix puts it: inside the rectangle around what it draws
2704
+ // there, the point is taken back through the matrix, into the
2705
+ // coordinates the box and all it holds were laid out in, and the walk
2706
+ // goes on with that. An icon turned a quarter is pressed where it is
2707
+ // drawn; a box flattened to nothing is nowhere.
2245
2708
  const enterAt = (child, context, clipped) => {
2709
+ const matrix = placedMatrix(child);
2710
+ if (!matrix) {
2711
+ enterIn(child, context, clipped, null);
2712
+ return;
2713
+ }
2714
+ if (x < child.boundsX ||
2715
+ x >= child.boundsX + child.boundsWidth ||
2716
+ y < child.boundsY ||
2717
+ y >= child.boundsY + child.boundsHeight) {
2718
+ return;
2719
+ }
2720
+ const back = invert(matrix);
2721
+ if (!back)
2722
+ return;
2723
+ const px = x;
2724
+ const py = y;
2725
+ [x, y] = mapPoint(back, px, py);
2726
+ try {
2727
+ enterIn(child, context, clipped, ownBounds(child));
2728
+ }
2729
+ finally {
2730
+ x = px;
2731
+ y = py;
2732
+ }
2733
+ };
2734
+ const enterIn = (child, context, clipped,
2735
+ /** What the box draws in its own coordinates, where those are not the
2736
+ * ones its bounds are in: a box painted through a matrix. */
2737
+ bounds) => {
2246
2738
  const style = child.style;
2739
+ // What a `clip-path` cuts away of a box it cuts away of all the box
2740
+ // holds, whatever that is positioned from (CSS Masking 1, 5.1), and
2741
+ // nothing not drawn is under a pointer: a label a page hides for a
2742
+ // screen reader alone under `clip-path: inset(50%)` took the hover and
2743
+ // the press of what was drawn where it lay.
2744
+ if (pathClips(child) && !inClipPath(child, x, y))
2745
+ return;
2746
+ // `clip` is not that rule as paint has it. It shows the part of an
2747
+ // absolute box it names (CSS 2.1 11.1.2), and past that part nothing is
2748
+ // drawn of the box, nor of what is painted with it. A stacking context
2749
+ // paints all it holds, inside its clip. A box that is none leaves its
2750
+ // positioned boxes to the context around it, which cuts each to the
2751
+ // clips its containing block is under (`clipsFor`): this one for all
2752
+ // but a fixed box, which is under none and is drawn past it. So past
2753
+ // its clip the first is left, and the second is walked as a box that
2754
+ // clips its overflow is past its edge — whether or not the point is in
2755
+ // its own rectangle, which a clip may show only a part of. Both were
2756
+ // walked as if they showed whole: a label hidden for a screen reader
2757
+ // alone under `clip: rect(0, 0, 0, 0)` took the hover and the press of
2758
+ // the link drawn where it lay.
2759
+ let cut = !inClip(child, x, y);
2760
+ if (cut && stacksLayers(child))
2761
+ return;
2247
2762
  if (style.position !== 'static' ||
2248
2763
  (child.parent?.kind === 'flex' && typeof style.zIndex === 'number')) {
2249
2764
  // a flex item with a `z-index` is layered unpositioned (`layered`)
2250
2765
  const z = style.zIndex === 'auto' ? 0 : style.zIndex;
2251
2766
  context = [...context, z < 0 ? HIT_NEGATIVE : HIT_POSITIONED, z];
2252
2767
  }
2768
+ else if (transformed(style) &&
2769
+ child.kind !== 'inline' &&
2770
+ child.parent &&
2771
+ !onLine(child.parent, child)) {
2772
+ // a transformed box is painted with the positioned ones (`layered`)
2773
+ context = [...context, HIT_POSITIONED, 0];
2774
+ }
2253
2775
  else if (style.float !== 'none')
2254
2776
  context = [...context, HIT_FLOAT];
2255
2777
  if (clipped.length !== 0 && child.outOfFlow) {
@@ -2258,33 +2780,45 @@ fixedShift = null) {
2258
2780
  ? clipped.filter((clip) => holds(clip, containing))
2259
2781
  : [];
2260
2782
  }
2261
- const inside = x >= child.x &&
2262
- x < child.x + child.width &&
2263
- y >= child.y &&
2264
- y < child.y + child.height;
2783
+ // in one of its pieces, where columns broke it: its rect takes in the
2784
+ // columns between them, which are other boxes'
2785
+ const pieces = columned.any ? COLUMN_PIECES.get(child) : undefined;
2786
+ const inside = pieces
2787
+ ? pieces.some((piece) => x >= piece.x &&
2788
+ x < piece.x + piece.width &&
2789
+ y >= piece.y &&
2790
+ y < piece.y + piece.height)
2791
+ : x >= child.x &&
2792
+ x < child.x + child.width &&
2793
+ y >= child.y &&
2794
+ y < child.y + child.height;
2265
2795
  const own = inside && hasRect(child);
2266
2796
  if (!own) {
2267
- const reach = child.boundsY;
2268
- const known = Number.isFinite(reach) &&
2269
- (child.boundsWidth > 0 || child.boundsHeight > 0);
2797
+ const left = bounds ? bounds.x : child.boundsX;
2798
+ const reach = bounds ? bounds.y : child.boundsY;
2799
+ const across = bounds ? bounds.width : child.boundsWidth;
2800
+ const down = bounds ? bounds.height : child.boundsHeight;
2801
+ const known = Number.isFinite(reach) && (across > 0 || down > 0);
2270
2802
  if (!known) {
2271
2803
  if (!inside)
2272
2804
  return;
2273
2805
  }
2274
- else if (x < child.boundsX ||
2275
- x >= child.boundsX + child.boundsWidth ||
2806
+ else if (x < left ||
2807
+ x >= left + across ||
2276
2808
  y < reach ||
2277
- y >= reach + child.boundsHeight) {
2809
+ y >= reach + down) {
2278
2810
  return;
2279
2811
  }
2280
- if (clipsOverflow(child)) {
2281
- if (!holdsAbsolute(child))
2282
- return;
2283
- clipped = [...clipped, child];
2284
- }
2812
+ if (clipsOverflow(child))
2813
+ cut = true;
2814
+ }
2815
+ if (cut) {
2816
+ if (!holdsAbsolute(child))
2817
+ return;
2818
+ clipped = [...clipped, child];
2285
2819
  }
2286
2820
  if (own && child.el && clipped.length === 0) {
2287
- take(child.el, [...context, HIT_BLOCK], false);
2821
+ take(child.el, style, [...context, HIT_BLOCK], false);
2288
2822
  }
2289
2823
  visit(child, context, clipped);
2290
2824
  };
@@ -2326,8 +2860,9 @@ fixedShift = null) {
2326
2860
  const left = text.drawX + natural.x + run.x;
2327
2861
  if (x >= left && x < left + run.width) {
2328
2862
  const owner = ownerOf(tree.textBoxes, text.spans.documentAt(run.start));
2329
- if (owner)
2330
- take(owner, [...context, HIT_INLINE], true);
2863
+ const style = owner && tree.styles.get(owner)?.style;
2864
+ if (style)
2865
+ take(owner, style, [...context, HIT_INLINE], true);
2331
2866
  }
2332
2867
  }
2333
2868
  }
@@ -2354,6 +2889,32 @@ function compareKeys(a, b) {
2354
2889
  * is built again instead: a link's subtree is a handful, and a compound
2355
2890
  * that matches a container reaches everything in it. */
2356
2891
  const HOVER_RESTYLE_LIMIT = 300;
2892
+ /** How long the content under a still pointer has to have stopped moving
2893
+ * before the hover is asked for again (`HtmlViewNode._holdHover`): a
2894
+ * tenth of a second, which is what WebKit waits after a scroll before it
2895
+ * sends the mouse move that updates its own. */
2896
+ const HOVER_REST_MS = 100;
2897
+ const timers = globalThis;
2898
+ /**
2899
+ * The clock a held hover waits on. Through `globalThis` because `src/`
2900
+ * compiles with `types: []`, and unref'd where the runtime allows it: a
2901
+ * timer of a document's must not keep a process alive that is otherwise
2902
+ * done. One object, exported from this module though not from the package,
2903
+ * so a test can hold it and say when the rest is over (`test/held-clock.ts`).
2904
+ */
2905
+ export const hoverClock = {
2906
+ now() {
2907
+ return Date.now();
2908
+ },
2909
+ arm(step, ms) {
2910
+ const handle = timers.setTimeout?.(step, ms) ?? null;
2911
+ handle?.unref?.();
2912
+ return handle;
2913
+ },
2914
+ disarm(handle) {
2915
+ timers.clearTimeout?.(handle);
2916
+ },
2917
+ };
2357
2918
  /**
2358
2919
  * The computed properties a pointer move may change in place: ink, which
2359
2920
  * moves nothing. A background colour is drawn inside the box it colours
@@ -2365,6 +2926,9 @@ const PAINT_ONLY = new Set([
2365
2926
  'color',
2366
2927
  'textFillColor',
2367
2928
  'cursor',
2929
+ 'pointerEvents',
2930
+ 'fill',
2931
+ 'stroke',
2368
2932
  'backgroundColor',
2369
2933
  'borderTopColor',
2370
2934
  'borderRightColor',
@@ -2381,6 +2945,8 @@ const PAINT_ONLY = new Set([
2381
2945
  'underlineThickness',
2382
2946
  'underlineOffset',
2383
2947
  'lineThrough',
2948
+ 'lineThroughStyle',
2949
+ 'lineThroughThickness',
2384
2950
  ]);
2385
2951
  /** The fields of a run its ink is: what `runFor` takes from `PAINT_ONLY`. */
2386
2952
  const INK_FIELDS = [
@@ -2390,6 +2956,8 @@ const INK_FIELDS = [
2390
2956
  'underlineOffset',
2391
2957
  'underlineThickness',
2392
2958
  'strike',
2959
+ 'strikeStyle',
2960
+ 'strikeThickness',
2393
2961
  ];
2394
2962
  /** The fields of a run its shape is, which ink never changes. */
2395
2963
  const FACE_FIELDS = [
@@ -2400,6 +2968,12 @@ const FACE_FIELDS = [
2400
2968
  'letterSpacing',
2401
2969
  'features',
2402
2970
  ];
2971
+ /** An element's `::before` and `::after`, each with its bit in the set
2972
+ * of the ones a restyle found boxes of (`_hoverInPlace`). */
2973
+ const GENERATED_BITS = [
2974
+ ['before', 1],
2975
+ ['after', 2],
2976
+ ];
2403
2977
  /** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
2404
2978
  * a widget mounted beside the document, or a button whose press it reports. */
2405
2979
  const WIDGETS = new Set([
@@ -2418,10 +2992,19 @@ const INK_REACH = new Set([
2418
2992
  'outlineWidth',
2419
2993
  'outlineOffset',
2420
2994
  ]);
2995
+ /** What transforms a box: it moves, turns or scales where it is, and
2996
+ * nothing around it does (CSS Transforms 1, 3). */
2997
+ const TRANSFORM_FIELDS = new Set([
2998
+ 'translate',
2999
+ 'rotate',
3000
+ 'scale',
3001
+ 'transform',
3002
+ 'transformOrigin',
3003
+ ]);
2421
3004
  /**
2422
3005
  * How two styles of one element differ, for a restyle in place: null where
2423
3006
  * they do not, a `HoverChange` where every difference is ink, how far ink
2424
- * reaches, a `z-index` that stays a stacking context's, or a translation —
3007
+ * reaches, a `z-index` that stays a stacking context's, or a transform —
2425
3008
  * none of which moves anything but the box and what it holds (CSS
2426
3009
  * Transforms 1: a transform does not affect layout) — and false where
2427
3010
  * anything else differs.
@@ -2431,9 +3014,18 @@ function hoverChange(was, now) {
2431
3014
  const b = now;
2432
3015
  let change = null;
2433
3016
  for (const key in b) {
2434
- if (key === 'custom' || sameValue(a[key], b[key]))
3017
+ // the sets are shared by what made them (`Cascade._customFor`): one
3018
+ // object where they are the same
3019
+ if (key === 'custom') {
3020
+ if (a[key] !== b[key]) {
3021
+ change ??= { ink: false, reach: false, order: false, move: false };
3022
+ }
2435
3023
  continue;
2436
- change ??= { reach: false, order: false, move: false };
3024
+ }
3025
+ if (sameValue(a[key], b[key]))
3026
+ continue;
3027
+ change ??= { ink: true, reach: false, order: false, move: false };
3028
+ change.ink = true;
2437
3029
  if (PAINT_ONLY.has(key))
2438
3030
  continue;
2439
3031
  if (INK_REACH.has(key)) {
@@ -2452,7 +3044,7 @@ function hoverChange(was, now) {
2452
3044
  change.order = true;
2453
3045
  continue;
2454
3046
  }
2455
- if (key === 'transformTranslate' || key === 'translate') {
3047
+ if (TRANSFORM_FIELDS.has(key)) {
2456
3048
  change.move = true;
2457
3049
  continue;
2458
3050
  }
@@ -2500,12 +3092,14 @@ function inkOf(box) {
2500
3092
  return null;
2501
3093
  if (!(at.boundsWidth > 0 && at.boundsHeight > 0))
2502
3094
  return null;
2503
- return {
3095
+ // its bounds are in the coordinates of the box it is in, which a box
3096
+ // around it painted through a matrix draws somewhere else
3097
+ return throughTransforms({
2504
3098
  x: at.boundsX,
2505
3099
  y: at.boundsY,
2506
3100
  width: at.boundsWidth,
2507
3101
  height: at.boundsHeight,
2508
- };
3102
+ }, at.parent);
2509
3103
  }
2510
3104
  /** Structural equality for a computed value: a number, a string, or the
2511
3105
  * plain objects and arrays a length or a shadow list is. */
@@ -2529,32 +3123,23 @@ function sameValue(x, y) {
2529
3123
  return false;
2530
3124
  return true;
2531
3125
  }
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
3126
  /**
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
3127
+ * `text`'s layout made again with the ink of the boxes a pointer move
3128
+ * restyled: from the runs it was made from, each of a restyled text box's
2543
3129
  * given the ink its new style makes (`runFor`) and nothing else. Null where
2544
3130
  * none of its runs is theirs; false where that cannot be told — a run this
2545
3131
  * cannot place in the document, one some other pass inked (a first line's),
2546
3132
  * or a layout that comes out another shape.
2547
3133
  */
2548
- function reinked(text, layouts, changed, styles) {
3134
+ function reinked(text, layouts, next) {
2549
3135
  const spans = text.spans;
2550
- const ownerAt = (offset) => spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
2551
3136
  const inputs = layouts.inputsOf(text.layout);
2552
3137
  if (!inputs || !spans.boxAt) {
2553
3138
  // not one this could make again: none of its text may be theirs
2554
3139
  for (const line of text.layout.lines) {
2555
3140
  for (const run of line.runs) {
2556
- const owner = spans.boxAt ? ownerAt(run.start) : null;
2557
- if (!spans.boxAt || (owner && changed.has(owner)))
3141
+ const box = spans.boxAt?.(run.start);
3142
+ if (!spans.boxAt || (box && next.has(box)))
2558
3143
  return false;
2559
3144
  }
2560
3145
  }
@@ -2566,32 +3151,32 @@ function reinked(text, layouts, changed, styles) {
2566
3151
  for (let i = 0; i < content.length; i += 1) {
2567
3152
  const run = content[i];
2568
3153
  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)
3154
+ const box = length ? spans.boxAt(offset) : null;
3155
+ const style = box ? next.get(box) : undefined;
3156
+ if (box && style) {
3157
+ if (spans.boxAt(offset + length - 1) !== box)
2573
3158
  return false;
2574
- const was = runFor(run.text, styles.get(owner).style);
3159
+ const was = runFor(run.text, box.style);
2575
3160
  const now = runFor(run.text, style);
2576
3161
  for (const f of FACE_FIELDS)
2577
3162
  if (!sameValue(was[f], now[f]))
2578
3163
  return false;
2579
- let next = null;
3164
+ let inked = null;
2580
3165
  for (const f of INK_FIELDS) {
2581
3166
  // a run that is not what its style made — a first line's colour
2582
3167
  if (!sameValue(run[f], was[f]))
2583
3168
  return false;
2584
3169
  if (sameValue(was[f], now[f]))
2585
3170
  continue;
2586
- next ??= { ...run };
3171
+ inked ??= { ...run };
2587
3172
  if (now[f] === undefined)
2588
- delete next[f];
3173
+ delete inked[f];
2589
3174
  else
2590
- next[f] = now[f];
3175
+ inked[f] = now[f];
2591
3176
  }
2592
- if (next) {
3177
+ if (inked) {
2593
3178
  runs ??= content.slice();
2594
- runs[i] = next;
3179
+ runs[i] = inked;
2595
3180
  }
2596
3181
  }
2597
3182
  offset += length;