@react-x11/components 0.11.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 (126) hide show
  1. package/dist/flow/node.d.ts +14 -6
  2. package/dist/flow/node.d.ts.map +1 -1
  3. package/dist/flow/node.js +82 -64
  4. package/dist/flow/node.js.map +1 -1
  5. package/dist/html/controls.d.ts +38 -10
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +43 -58
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts +134 -20
  10. package/dist/html/css/cascade.d.ts.map +1 -1
  11. package/dist/html/css/cascade.js +538 -128
  12. package/dist/html/css/cascade.js.map +1 -1
  13. package/dist/html/css/color.d.ts +19 -0
  14. package/dist/html/css/color.d.ts.map +1 -1
  15. package/dist/html/css/color.js +86 -0
  16. package/dist/html/css/color.js.map +1 -1
  17. package/dist/html/css/parse.d.ts +40 -2
  18. package/dist/html/css/parse.d.ts.map +1 -1
  19. package/dist/html/css/parse.js +391 -13
  20. package/dist/html/css/parse.js.map +1 -1
  21. package/dist/html/css/style.d.ts +52 -1
  22. package/dist/html/css/style.d.ts.map +1 -1
  23. package/dist/html/css/style.js +328 -11
  24. package/dist/html/css/style.js.map +1 -1
  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 -7
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +10 -1
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js +27 -2
  32. package/dist/html/css/values.js.map +1 -1
  33. package/dist/html/css/vars.d.ts +5 -0
  34. package/dist/html/css/vars.d.ts.map +1 -1
  35. package/dist/html/css/vars.js +2 -2
  36. package/dist/html/css/vars.js.map +1 -1
  37. package/dist/html/dom.d.ts.map +1 -1
  38. package/dist/html/dom.js +38 -3
  39. package/dist/html/dom.js.map +1 -1
  40. package/dist/html/fonts.d.ts +25 -1
  41. package/dist/html/fonts.d.ts.map +1 -1
  42. package/dist/html/fonts.js +99 -6
  43. package/dist/html/fonts.js.map +1 -1
  44. package/dist/html/form.d.ts +200 -0
  45. package/dist/html/form.d.ts.map +1 -0
  46. package/dist/html/form.js +801 -0
  47. package/dist/html/form.js.map +1 -0
  48. package/dist/html/index.d.ts +12 -0
  49. package/dist/html/index.d.ts.map +1 -1
  50. package/dist/html/index.js +30 -264
  51. package/dist/html/index.js.map +1 -1
  52. package/dist/html/layout/block.d.ts +11 -2
  53. package/dist/html/layout/block.d.ts.map +1 -1
  54. package/dist/html/layout/block.js +186 -19
  55. package/dist/html/layout/block.js.map +1 -1
  56. package/dist/html/layout/boxes.d.ts +65 -7
  57. package/dist/html/layout/boxes.d.ts.map +1 -1
  58. package/dist/html/layout/boxes.js +87 -22
  59. package/dist/html/layout/boxes.js.map +1 -1
  60. package/dist/html/layout/cache.d.ts +2 -2
  61. package/dist/html/layout/cache.d.ts.map +1 -1
  62. package/dist/html/layout/cache.js +8 -3
  63. package/dist/html/layout/cache.js.map +1 -1
  64. package/dist/html/layout/flex.js +4 -1
  65. package/dist/html/layout/flex.js.map +1 -1
  66. package/dist/html/layout/floats.d.ts +4 -0
  67. package/dist/html/layout/floats.d.ts.map +1 -1
  68. package/dist/html/layout/floats.js +26 -0
  69. package/dist/html/layout/floats.js.map +1 -1
  70. package/dist/html/layout/inline.d.ts +26 -2
  71. package/dist/html/layout/inline.d.ts.map +1 -1
  72. package/dist/html/layout/inline.js +1232 -115
  73. package/dist/html/layout/inline.js.map +1 -1
  74. package/dist/html/node.d.ts +120 -15
  75. package/dist/html/node.d.ts.map +1 -1
  76. package/dist/html/node.js +993 -174
  77. package/dist/html/node.js.map +1 -1
  78. package/dist/html/paint.d.ts +75 -2
  79. package/dist/html/paint.d.ts.map +1 -1
  80. package/dist/html/paint.js +836 -200
  81. package/dist/html/paint.js.map +1 -1
  82. package/dist/html/surfaces.d.ts +7 -2
  83. package/dist/html/surfaces.d.ts.map +1 -1
  84. package/dist/html/surfaces.js +16 -0
  85. package/dist/html/surfaces.js.map +1 -1
  86. package/dist/html/widgets.d.ts +29 -0
  87. package/dist/html/widgets.d.ts.map +1 -0
  88. package/dist/html/widgets.js +620 -0
  89. package/dist/html/widgets.js.map +1 -0
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/internal/window.d.ts +22 -0
  94. package/dist/internal/window.d.ts.map +1 -1
  95. package/dist/internal/window.js +35 -8
  96. package/dist/internal/window.js.map +1 -1
  97. package/dist/richtext/node.d.ts +7 -0
  98. package/dist/richtext/node.d.ts.map +1 -1
  99. package/dist/richtext/node.js.map +1 -1
  100. package/package.json +6 -4
  101. package/src/flow/node.ts +80 -64
  102. package/src/html/controls.ts +79 -58
  103. package/src/html/css/cascade.ts +610 -146
  104. package/src/html/css/color.ts +87 -0
  105. package/src/html/css/parse.ts +431 -13
  106. package/src/html/css/style.ts +367 -11
  107. package/src/html/css/ua.ts +76 -8
  108. package/src/html/css/values.ts +31 -3
  109. package/src/html/css/vars.ts +2 -2
  110. package/src/html/dom.ts +41 -3
  111. package/src/html/fonts.ts +116 -6
  112. package/src/html/form.ts +962 -0
  113. package/src/html/index.ts +49 -300
  114. package/src/html/layout/block.ts +231 -16
  115. package/src/html/layout/boxes.ts +133 -32
  116. package/src/html/layout/cache.ts +8 -3
  117. package/src/html/layout/flex.ts +4 -1
  118. package/src/html/layout/floats.ts +24 -0
  119. package/src/html/layout/inline.ts +1445 -108
  120. package/src/html/node.ts +1010 -166
  121. package/src/html/paint.ts +980 -270
  122. package/src/html/surfaces.ts +21 -1
  123. package/src/html/widgets.ts +735 -0
  124. package/src/index.ts +1 -0
  125. package/src/internal/window.ts +36 -8
  126. package/src/richtext/node.ts +7 -0
package/dist/html/node.js CHANGED
@@ -40,20 +40,20 @@ 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, LINE_BOX_RAISES, SHIFTED_LINES, } from './layout/boxes.js';
47
- import { layoutDocument } from './layout/block.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
+ import { layoutDocument, retranslate, transformed } from './layout/block.js';
48
48
  import { TextLayoutCache } from './layout/cache.js';
49
49
  import { shapingSafe } from './layout/shaping.js';
50
- import { SurfaceCache } from './surfaces.js';
50
+ import { SurfaceCache, newSurface } from './surfaces.js';
51
51
  import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
52
52
  // Through the inline module rather than a second cache: the offsets table for
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, containingBlockOf, hasRect, holds, holdsAbsolute, paintDocument, queryChildIndex, } 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';
@@ -118,6 +118,8 @@ export class HtmlViewNode extends Node {
118
118
  _sheetsRead = null;
119
119
  /** Blurred shadows, drawn once each. */
120
120
  _shadowCache = null;
121
+ /** Whether the backend was found to have no offscreen surface. */
122
+ _noSurface = false;
121
123
  _cascade = null;
122
124
  _tree = null;
123
125
  /** The faces `_warmFaces` has asked the font matcher for, and of which
@@ -133,6 +135,8 @@ export class HtmlViewNode extends Node {
133
135
  * that read it (`vw`, `vh`). */
134
136
  _styledWidth = -1;
135
137
  _styledHeight = -1;
138
+ /** The cascade the box tree's styles were computed by. */
139
+ _styledWith = null;
136
140
  _documentHeight = 0;
137
141
  _documentWidth = 0;
138
142
  /** The viewport height the document was last laid out under, and whether
@@ -150,6 +154,20 @@ export class HtmlViewNode extends Node {
150
154
  * the text carries surrogate pairs. null until checked. */
151
155
  _pointsAreUnits = null;
152
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;
153
171
  _scriptsSeen = new WeakSet();
154
172
  _controls = [];
155
173
  _reportedDomRevision = -1;
@@ -228,6 +246,7 @@ export class HtmlViewNode extends Node {
228
246
  controlPadY: look.controlPadY * s,
229
247
  controlBorder: look.controlBorder * s,
230
248
  controlRadius: look.controlRadius * s,
249
+ controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
231
250
  };
232
251
  this._deviceLookFor = look;
233
252
  this._deviceLookAt = s;
@@ -235,7 +254,26 @@ export class HtmlViewNode extends Node {
235
254
  return scaled;
236
255
  }
237
256
  // --- the pipeline ---------------------------------------------------------
238
- _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
+ }
239
277
  if (stale > this._stale)
240
278
  this._stale = stale;
241
279
  if (stale >= Stale.Layout)
@@ -315,14 +353,16 @@ export class HtmlViewNode extends Node {
315
353
  */
316
354
  _requestBackgrounds(tree) {
317
355
  for (const box of tree.backgrounds) {
318
- if (!box.el)
356
+ // a pseudo-element's are its element's
357
+ const element = box.el ?? GENERATED_FROM.get(box);
358
+ if (!element)
319
359
  continue;
320
360
  // each layer's, where there is more than one
321
361
  for (const url of box.style.backgroundImages ?? [
322
362
  box.style.backgroundImage,
323
363
  ]) {
324
364
  if (typeof url === 'string') {
325
- this._resources.request({ url, kind: 'image', element: box.el });
365
+ this._resources.request({ url, kind: 'image', element });
326
366
  }
327
367
  }
328
368
  const border = box.style.borderImage.source;
@@ -330,9 +370,15 @@ export class HtmlViewNode extends Node {
330
370
  this._resources.request({
331
371
  url: border,
332
372
  kind: 'image',
333
- element: box.el,
373
+ element,
334
374
  });
335
375
  }
376
+ // and each mask layer's, which is an image as a background's is
377
+ for (const url of box.style.mask.images) {
378
+ if (typeof url === 'string') {
379
+ this._resources.request({ url, kind: 'image', element });
380
+ }
381
+ }
336
382
  }
337
383
  // and every image generated content names, which is only known there
338
384
  // too
@@ -433,7 +479,10 @@ export class HtmlViewNode extends Node {
433
479
  faces = kept.faces;
434
480
  }
435
481
  else {
436
- 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)];
437
486
  const imports = [];
438
487
  faces = [];
439
488
  let order = 0;
@@ -480,8 +529,9 @@ export class HtmlViewNode extends Node {
480
529
  // The faces this width and scheme declare: a `@font-face` may sit in a
481
530
  // `@media` block like any rule.
482
531
  const cssWidth = width / this._scale;
532
+ const cssHeight = this._viewportHeight() / this._scale;
483
533
  this._webFonts.setFallback(look.fontFamily);
484
- 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)));
485
535
  this._cascade.setPointer({
486
536
  hovered: new Set(this._hovered),
487
537
  active: EMPTY_SET,
@@ -548,6 +598,58 @@ export class HtmlViewNode extends Node {
548
598
  }
549
599
  return true;
550
600
  }
601
+ /**
602
+ * The viewport the document is seen through, in window coordinates: the
603
+ * content box of the pane that scrolls the element, where one does —
604
+ * what `position: fixed` and a fixed background are placed against, as
605
+ * a browser places them against its page area. Null where nothing
606
+ * scrolls it, and the element is its own.
607
+ */
608
+ _viewport() {
609
+ for (let node = this.parent; node; node = node.parent) {
610
+ // `Scrollable`'s, and not every node's: asked the way core asks it
611
+ const scroller = node;
612
+ if (!scroller.isScroller?.())
613
+ continue;
614
+ const box = node.contentBox();
615
+ return box.width > 0 && box.height > 0 ? box : null;
616
+ }
617
+ return null;
618
+ }
619
+ /**
620
+ * What the document draws fixed to the viewport of the pane that scrolls
621
+ * it, for that pane's scroll blit to repaint rather than copy with the
622
+ * text (react-x11's `viewportFixedRects`): the whole viewport where a
623
+ * fixed background shows through it, which makes a scroll a repaint, and
624
+ * where each fixed box is drawn. Null for a document with neither, and
625
+ * where nothing scrolls the element.
626
+ */
627
+ viewportFixedRects() {
628
+ const tree = this._tree;
629
+ const viewport = tree && this._viewport();
630
+ if (!tree || !viewport)
631
+ return null;
632
+ if (hasFixedBackground(tree))
633
+ return [viewport];
634
+ const boxes = FIXED_BOXES.get(tree);
635
+ if (!boxes)
636
+ return null;
637
+ // laid out against the viewport at the document's top, and drawn at the
638
+ // viewport (`atViewport`)
639
+ const root = tree.root;
640
+ const out = [];
641
+ for (const box of boxes) {
642
+ if (!(box.boundsWidth > 0 && box.boundsHeight > 0))
643
+ continue;
644
+ out.push({
645
+ x: viewport.x + box.boundsX - root.x,
646
+ y: viewport.y + box.boundsY - root.y,
647
+ width: box.boundsWidth,
648
+ height: box.boundsHeight,
649
+ });
650
+ }
651
+ return out.length ? out : null;
652
+ }
551
653
  _viewportHeight() {
552
654
  // The viewport a `vh` resolves against is the one the document is seen
553
655
  // through, not the document — a document taller than it does not make
@@ -658,9 +760,14 @@ export class HtmlViewNode extends Node {
658
760
  const onlyViewport = this._stale === Stale.Nothing && this._laidOutWidth === target;
659
761
  const wasWidth = this._documentWidth;
660
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;
661
767
  if (this._stale >= Stale.Style || !this._cascade) {
662
768
  this._restyle(target);
663
769
  this._stale = Math.max(this._stale, Stale.Boxes);
770
+ restyleOnly = null;
664
771
  }
665
772
  else if (this._cascade.mediaBand(target) !== this._mediaBand) {
666
773
  // A resize that crossed a `@media` breakpoint is the one resize that
@@ -668,6 +775,7 @@ export class HtmlViewNode extends Node {
668
775
  // breakpoints are collected at parse time.
669
776
  this._restyle(target);
670
777
  this._stale = Math.max(this._stale, Stale.Boxes);
778
+ restyleOnly = null;
671
779
  }
672
780
  const cascade = this._cascade;
673
781
  if (!cascade)
@@ -678,21 +786,32 @@ export class HtmlViewNode extends Node {
678
786
  // styles computed for another viewport are wrong for this one: built
679
787
  // again, where some style reads the side that moved. A document that
680
788
  // reads neither — most — goes on skipping the cascade on a resize.
681
- if (this._stale < Stale.Boxes &&
682
- ((cascade.readsViewportWidth && this._styledWidth !== target) ||
683
- (cascade.readsViewportHeight && this._styledHeight !== viewport))) {
684
- 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;
685
794
  }
686
795
  if (this._stale >= Stale.Boxes || !this._tree) {
687
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;
688
804
  const build = () => buildBoxes(this._source.document, {
689
805
  cascade,
806
+ kept,
690
807
  scale: this._scale,
691
808
  imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
692
809
  urlSize: (url) => this._resources.imageSize(url),
693
810
  controlSize: (el, kind, style) => measureControl(el, kind, style, this._fonts(), look),
694
811
  });
695
812
  this._tree = build();
813
+ kept = null;
814
+ this._styledWith = cascade;
696
815
  this._styledWidth = target;
697
816
  this._styledHeight = viewport;
698
817
  this._requestBackgrounds(this._tree);
@@ -758,6 +877,7 @@ export class HtmlViewNode extends Node {
758
877
  y: r.y / s,
759
878
  width: r.width / s,
760
879
  height: r.height / s,
880
+ fontSize: r.fontSize / s,
761
881
  ...(r.bare && {
762
882
  bare: {
763
883
  ...r.bare,
@@ -852,6 +972,7 @@ export class HtmlViewNode extends Node {
852
972
  }
853
973
  }
854
974
  destroySubtree() {
975
+ this._dropHeldHover();
855
976
  this._resources.destroy();
856
977
  this._webFonts.destroy();
857
978
  this._source.destroy();
@@ -946,7 +1067,8 @@ export class HtmlViewNode extends Node {
946
1067
  if (to <= from)
947
1068
  return [];
948
1069
  const out = [];
949
- 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);
950
1072
  return out;
951
1073
  }
952
1074
  // --- pointer --------------------------------------------------------------
@@ -1002,29 +1124,57 @@ export class HtmlViewNode extends Node {
1002
1124
  }
1003
1125
  else {
1004
1126
  const bands = [];
1005
- // an inline box's fragments are on the lines of the block it is laid
1006
- // out in, and no text of a float or a positioned box inside it — on
1007
- // lines of their own — is one of them (CSSOM View 6.1): a list item
1008
- // made inline around an absolute link measured as the link
1009
- let block = box.parent;
1010
- while (block && !block.lines)
1011
- block = block.parent;
1012
- if (box.subtreeTextEnd > box.subtreeTextStart) {
1013
- if (box.kind === 'inline' && block?.lines) {
1014
- lineBands(block.lines, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
1015
- }
1016
- else {
1017
- collectBands(tree.root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
1018
- }
1127
+ const fonts = this._layouts?.fonts ?? null;
1128
+ // an inline box broken around the blocks in it is its pieces, a box
1129
+ // of the element's each (CSS 2.1 9.2.1.1), and its fragments are
1130
+ // theirs; and on the lines between them it has a fragment around each
1131
+ // block, across the box the block is in and as tall as its border
1132
+ // box, as a browser's has — Blink's block-in-inline, Gecko's split
1133
+ // inline's anonymous block. A list item made inline around a block
1134
+ // link measured as the empty edge after it
1135
+ for (const piece of piecesOf(tree.root, box)) {
1136
+ inlineBands(tree.root, piece, fonts, bands);
1019
1137
  }
1020
- if (box.kind === 'inline') {
1021
- fragmentReach(box, bands);
1022
- const fonts = this._layouts?.fonts;
1023
- if (fonts)
1024
- fragmentHeights(box, bands, fonts);
1138
+ for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
1139
+ const around = block.parent;
1140
+ if (!around)
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);
1147
+ bands.push({
1148
+ x: around.contentX,
1149
+ y: top,
1150
+ width: around.contentWidth,
1151
+ height: block.y + block.height - top,
1152
+ });
1025
1153
  }
1026
1154
  for (const band of bands)
1027
1155
  rect = rect ? unionRect(rect, band) : band;
1156
+ if (!rect) {
1157
+ // on no line, where its block has none: text set at no size takes
1158
+ // no room and gets no box, and CSS puts it on lines of no height
1159
+ // at the block's top (9.4.2), as a browser measures it — nothing,
1160
+ // where the block's content starts. Where the text after it
1161
+ // starts was the next text that took room, elsewhere in the
1162
+ // document: the Zen Garden's 170 hides a heading at `font-size:
1163
+ // 0`, and an `<abbr>` in it measured as the end of the paragraph
1164
+ // above it
1165
+ let block = box.parent;
1166
+ while (block && block.kind === 'inline')
1167
+ block = block.parent;
1168
+ if (block && !block.lines?.length) {
1169
+ const rtl = block.style.direction === 'rtl';
1170
+ rect = {
1171
+ x: rtl ? block.contentX + block.contentWidth : block.contentX,
1172
+ y: block.contentY,
1173
+ width: 0,
1174
+ height: 0,
1175
+ };
1176
+ }
1177
+ }
1028
1178
  if (!rect) {
1029
1179
  // an empty subtree's range is `[0, 0)`, wherever it stands: where
1030
1180
  // its text would be is where the text after it starts
@@ -1048,7 +1198,20 @@ export class HtmlViewNode extends Node {
1048
1198
  if (!tree)
1049
1199
  return null;
1050
1200
  const local = this._toDocument(x, y);
1051
- return deepestAt(tree, local.x, local.y);
1201
+ return deepestAt(tree, local.x, local.y, undefined, this._fixedShift());
1202
+ }
1203
+ /** How far a box fixed to the viewport is drawn from where it was laid
1204
+ * out, in the document's pixels: the scroll of the pane the element is
1205
+ * in (`atViewport`). Null where nothing scrolls it. */
1206
+ _fixedShift() {
1207
+ const tree = this._tree;
1208
+ const viewport = tree && FIXED_BOXES.has(tree) && this._viewport();
1209
+ if (!tree || !viewport)
1210
+ return null;
1211
+ return {
1212
+ x: viewport.x - this.abs.x - tree.root.x,
1213
+ y: viewport.y - this.abs.y - tree.root.y,
1214
+ };
1052
1215
  }
1053
1216
  /**
1054
1217
  * The cursor for a point of this element's, in device pixels: what core
@@ -1065,7 +1228,7 @@ export class HtmlViewNode extends Node {
1065
1228
  if (!tree)
1066
1229
  return null;
1067
1230
  const hit = { text: false };
1068
- const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit);
1231
+ const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit, this._fixedShift());
1069
1232
  const cursor = el ? tree.styles.get(el)?.style.cursor : null;
1070
1233
  // a keyword; a `url()` this cannot load falls back, as its list would
1071
1234
  if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor))
@@ -1079,6 +1242,7 @@ export class HtmlViewNode extends Node {
1079
1242
  * contains a `:hover` rule.
1080
1243
  */
1081
1244
  setHover(x, y) {
1245
+ this._dropHeldHover();
1082
1246
  const cascade = this._cascade;
1083
1247
  if (!cascade || !cascade.hoverSensitive)
1084
1248
  return false;
@@ -1097,6 +1261,7 @@ export class HtmlViewNode extends Node {
1097
1261
  return true;
1098
1262
  }
1099
1263
  clearHover() {
1264
+ this._dropHeldHover();
1100
1265
  if (!this._hovered.length)
1101
1266
  return false;
1102
1267
  const was = this._hovered;
@@ -1105,64 +1270,53 @@ export class HtmlViewNode extends Node {
1105
1270
  this._restyleHover(was, []);
1106
1271
  return true;
1107
1272
  }
1108
- /** 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
+ */
1109
1279
  _restyleHover(was, now) {
1110
- const done = this._hoverInPlace(was, now);
1111
- if (done === 'painted')
1112
- this.invalidate(false, this, 'props');
1113
- else if (done === false)
1114
- 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);
1115
1288
  }
1116
1289
  /**
1117
- * A pointer move restyled where it happened, when all it changed is ink.
1118
- *
1119
- * A move to another element is a change to the cascade, and the pipeline
1120
- * answers one by building every box again and laying the document out:
1121
- * 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
1122
- * link the pointer crossed. But the rules a page writes for `:hover`
1123
- * nearly all change a colour, an underline, a background or a border's
1124
- * colour — 77 of that article's 79 — and those move nothing. So:
1125
- *
1126
- * - Only an element whose hover state flipped and that a compound testing
1127
- * the pointer could match can change (`Cascade.hoverTouches`), with its
1128
- * subtree, and its later siblings where a sibling combinator follows.
1129
- * A move between two paragraphs under `a:hover` touches nothing.
1130
- * - Those are styled again from their parents. Where anything but ink
1131
- * differs (`PAINT_ONLY`), or a box is not the element's own — an
1132
- * anonymous or a pseudo-element box, a marker, a control — this is
1133
- * not the move to take.
1134
- * - The boxes take their new styles, and each layout of their text is
1135
- * made again from the runs it was made from (`TextLayoutCache.inputsOf`)
1136
- * with the new ink (`runFor`), where its geometry comes out the same.
1290
+ * The elements whose styles a move of the hovered chain can change, or
1291
+ * null where that cannot be said (`Cascade.hoverLocal`).
1137
1292
  *
1138
- * Everything is checked before anything is changed. 'none' where nothing
1139
- * could change, 'painted' where it was restyled here, false where the
1140
- * 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.
1141
1299
  */
1142
- _hoverInPlace(was, now) {
1300
+ _hoverReach(was, now) {
1143
1301
  const cascade = this._cascade;
1144
- const tree = this._tree;
1145
- const layouts = this._layouts;
1146
- if (!cascade || !tree || !layouts || !cascade.hoverLocal)
1147
- return false;
1148
- if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
1149
- return false;
1302
+ if (!cascade || !cascade.hoverLocal)
1303
+ return null;
1150
1304
  const before = new Set(was);
1151
1305
  const after = new Set(now);
1152
- const roots = [];
1153
- // and the elements a `:has()` testing the pointer may flip, which
1154
- // are around the ones that changed; the chains are ancestor chains,
1155
- // so the deepest that changed on each side is where to look from
1306
+ const roots = new Map();
1156
1307
  const flipped = (el, other) => {
1157
1308
  if (other.has(el))
1158
1309
  return;
1159
- if (cascade.hoverTouches(el))
1160
- roots.push(el);
1310
+ const touch = cascade.hoverTouches(el);
1311
+ if (touch > (roots.get(el) ?? HOVER_UNTOUCHED))
1312
+ roots.set(el, touch);
1161
1313
  };
1162
1314
  for (const el of was)
1163
1315
  flipped(el, after);
1164
1316
  for (const el of now)
1165
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
1166
1320
  const deepest = (chain, other) => {
1167
1321
  const el = chain[0];
1168
1322
  if (el && !other.has(el))
@@ -1170,11 +1324,9 @@ export class HtmlViewNode extends Node {
1170
1324
  };
1171
1325
  deepest(was, after);
1172
1326
  deepest(now, before);
1173
- if (!roots.length)
1174
- return 'none';
1175
- // what may restyle: the roots' subtrees, which inherit from them and a
1176
- // descendant combinator reaches, and their later siblings' too where a
1177
- // 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
1178
1330
  const reach = new Set();
1179
1331
  const collect = (el) => {
1180
1332
  const stack = [el];
@@ -1183,28 +1335,91 @@ export class HtmlViewNode extends Node {
1183
1335
  if (reach.has(at))
1184
1336
  continue;
1185
1337
  reach.add(at);
1186
- if (reach.size > HOVER_RESTYLE_LIMIT)
1187
- return false;
1188
1338
  for (const child of at.children)
1189
1339
  if (isElement(child))
1190
1340
  stack.push(child);
1191
1341
  }
1192
- return true;
1193
1342
  };
1194
- for (const root of roots) {
1195
- if (!collect(root))
1196
- return false;
1197
- if (!cascade.hoverSiblings)
1343
+ for (const [root, touch] of roots) {
1344
+ collect(root);
1345
+ if (touch !== HOVER_FOLLOWED)
1198
1346
  continue;
1199
1347
  for (let s = root.nextSibling; s; s = s.nextSibling) {
1200
- if (isElement(s) && !collect(s))
1201
- return false;
1348
+ if (isElement(s))
1349
+ collect(s);
1202
1350
  }
1203
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
+ };
1204
1411
  // styled again from their parents, a parent first
1205
1412
  const fresh = new Map();
1206
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();
1207
1417
  let refused = false;
1418
+ // what changed beyond ink (`hoverChange`): how far some box's ink
1419
+ // reaches, the order its layer paints in, where one is
1420
+ let reaches = false;
1421
+ let reorders = false;
1422
+ const moving = new Set();
1208
1423
  const styleOf = (el) => {
1209
1424
  const kept = tree.styles.get(el);
1210
1425
  if (!kept)
@@ -1222,13 +1437,27 @@ export class HtmlViewNode extends Node {
1222
1437
  refused = true;
1223
1438
  return null;
1224
1439
  }
1225
- const style = cascade.styleFor(el, parentStyle, kept.inFlex);
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
+ }
1459
+ }
1226
1460
  fresh.set(el, style);
1227
- const diff = inkOnly(kept.style, style);
1228
- if (diff === false)
1229
- refused = true;
1230
- else if (diff)
1231
- changed.set(el, style);
1232
1461
  return style;
1233
1462
  };
1234
1463
  for (const el of reach) {
@@ -1236,38 +1465,122 @@ export class HtmlViewNode extends Node {
1236
1465
  if (refused)
1237
1466
  return false;
1238
1467
  }
1239
- if (!changed.size)
1240
- return 'none';
1468
+ if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE)
1469
+ return true;
1241
1470
  for (const el of changed.keys()) {
1242
1471
  const tag = tagOf(el);
1243
1472
  // their backgrounds are the canvas's
1244
1473
  if (tag === 'html' || tag === 'body')
1245
1474
  return false;
1246
1475
  }
1247
- // the boxes: each changed element's own, and nothing that takes its
1248
- // 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.
1249
1478
  const fonts = layouts.fonts;
1479
+ const blocks = this._blocksOf(tree, pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach);
1250
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();
1251
1486
  const redecorated = [];
1252
- const walk = [tree.root];
1487
+ const walk = blocks.slice();
1253
1488
  while (walk.length) {
1254
1489
  const box = walk.pop();
1255
1490
  for (const child of box.children)
1256
1491
  walk.push(child);
1257
- const owner = box.el ?? nearestElement(box);
1258
- if (!owner)
1259
- continue;
1260
- 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
+ }
1261
1578
  if (!style)
1262
1579
  continue;
1263
- const kept = tree.styles.get(owner);
1264
- // an anonymous box, a pseudo-element, a marker, a control: a style
1265
- // derived from the element's, or drawn from it somewhere else
1266
- if (!box.el || box.style !== kept.style)
1267
- return false;
1268
- if (box.marker || box.replaced !== 'none')
1269
- return false;
1580
+ next.set(box, style);
1270
1581
  restyled.push([box, style]);
1582
+ if (ink)
1583
+ inked.add(box);
1271
1584
  if (box.kind === 'inline') {
1272
1585
  const decoration = inlineDecoration(fonts, box, style);
1273
1586
  if ((decoration === null) !== (box.decoration === null)) {
@@ -1279,11 +1592,34 @@ export class HtmlViewNode extends Node {
1279
1592
  redecorated.push([box, decoration]);
1280
1593
  }
1281
1594
  }
1282
- // the text: each layout holding a run of a changed element's, made
1283
- // 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
1284
1618
  const relaid = new Map();
1285
1619
  const texts = [];
1286
- walk.push(tree.root);
1620
+ if (inked.size)
1621
+ for (const block of blocks)
1622
+ walk.push(block);
1287
1623
  while (walk.length) {
1288
1624
  const box = walk.pop();
1289
1625
  for (const child of box.children)
@@ -1295,27 +1631,156 @@ export class HtmlViewNode extends Node {
1295
1631
  texts.push(text);
1296
1632
  if (relaid.has(text.layout))
1297
1633
  continue;
1298
- const next = reinked(text, layouts, changed, tree.styles);
1299
- if (next === false)
1634
+ const again = reinked(text, layouts, next);
1635
+ if (again === false)
1300
1636
  return false;
1301
- relaid.set(text.layout, next);
1637
+ relaid.set(text.layout, again);
1302
1638
  }
1303
1639
  }
1304
1640
  }
1641
+ // what each box drew before, to be repainted with what it draws after
1642
+ const inks = [];
1643
+ for (const box of inked) {
1644
+ const ink = inkOf(box);
1645
+ if (ink)
1646
+ inks.push(ink);
1647
+ }
1305
1648
  // and only now, all of it
1306
- for (const [box, style] of restyled)
1649
+ const moved = [];
1650
+ for (const [box, style] of restyled) {
1651
+ const was = box.style;
1307
1652
  box.style = style;
1308
- for (const [box, decoration] of redecorated)
1653
+ if (box.el && moving.has(box.el) && box.kind !== 'text') {
1654
+ moved.push([box, was]);
1655
+ }
1656
+ }
1657
+ for (const [box, decoration] of redecorated) {
1658
+ // what is inside it keeps which boxes around it paint, and this is
1659
+ // one more or one fewer of them
1660
+ if ((decoration === null) !== (box.decoration === null)) {
1661
+ forgetDecoratedAncestors(box);
1662
+ }
1309
1663
  box.decoration = decoration;
1664
+ }
1310
1665
  for (const text of texts) {
1311
- const next = relaid.get(text.layout);
1312
- if (next)
1313
- text.layout = next;
1666
+ const again = relaid.get(text.layout);
1667
+ if (again)
1668
+ text.layout = again;
1314
1669
  }
1315
1670
  for (const [el, style] of changed) {
1316
1671
  tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
1317
1672
  }
1318
- return 'painted';
1673
+ // A box that moved moves what is in it, as layout moved it the first
1674
+ // time (`applyRelativeOffsets`), and a later layout moves it the same
1675
+ // way; where it reached the document's end, before or after, the
1676
+ // document's height may have changed with it, which only a layout says.
1677
+ let relayout = false;
1678
+ for (const [box, was] of moved) {
1679
+ const before = box.boundsY + box.boundsHeight;
1680
+ retranslate(box, was);
1681
+ tree.relative = true;
1682
+ if (before >= this._documentHeight)
1683
+ relayout = true;
1684
+ }
1685
+ // how far the boxes' ink reaches, and the order their layers paint in,
1686
+ // as layout leaves them (`layoutDocument`)
1687
+ if (reaches || reorders || moved.length) {
1688
+ computePaintBounds(tree.root, tree.movedInline);
1689
+ if (tree.negative)
1690
+ hoistNegative(tree.root);
1691
+ stackLayers(tree.root);
1692
+ }
1693
+ for (const [box] of moved) {
1694
+ if (box.boundsY + box.boundsHeight >= this._documentHeight) {
1695
+ relayout = true;
1696
+ }
1697
+ }
1698
+ if (moved.length)
1699
+ this._reportControls();
1700
+ if (relayout) {
1701
+ this._invalidate(Stale.Layout);
1702
+ return true;
1703
+ }
1704
+ for (const box of inked) {
1705
+ const ink = inkOf(box);
1706
+ if (ink)
1707
+ inks.push(ink);
1708
+ }
1709
+ this._repaintInk(inks);
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;
1757
+ }
1758
+ /**
1759
+ * Repaint the document where `inks` are, in its own coordinates: the ink
1760
+ * of the boxes a hover restyled, before and after. Past a handful of rects
1761
+ * the damage is one around them all, which is what core would make of
1762
+ * them anyway.
1763
+ */
1764
+ _repaintInk(inks) {
1765
+ if (!inks.length)
1766
+ return;
1767
+ const x = this.abs.x;
1768
+ const y = this.abs.y;
1769
+ const place = (r) => ({
1770
+ x: Math.floor(x + r.x),
1771
+ y: Math.floor(y + r.y),
1772
+ width: Math.ceil(r.width) + 1,
1773
+ height: Math.ceil(r.height) + 1,
1774
+ });
1775
+ if (inks.length > 6) {
1776
+ let all = inks[0];
1777
+ for (const r of inks)
1778
+ all = unionRect(all, r);
1779
+ this.invalidate(false, place(all), 'props');
1780
+ return;
1781
+ }
1782
+ for (const r of inks)
1783
+ this.invalidate(false, place(r), 'props');
1319
1784
  }
1320
1785
  /** The document, for an application that wants to read or change it. */
1321
1786
  get document() {
@@ -1348,17 +1813,64 @@ export class HtmlViewNode extends Node {
1348
1813
  // `:hover` is wired at the element rather than through React: the cascade
1349
1814
  // already knows whether any rule in the document tests it, `setHover`
1350
1815
  // returns without work when none does, and a document that does use it
1351
- // re-styles only when the hovered chain actually changed. The restyle is
1352
- // still document-wide — narrowing it to the affected subtree is the
1353
- // 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.
1354
1830
  defaultMouseMove(ev) {
1355
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 };
1356
1838
  this.setHover(ev.x, ev.y);
1357
1839
  }
1358
1840
  defaultMouseLeave(ev) {
1359
1841
  super.defaultMouseLeave?.(ev);
1842
+ this._pointerAt = null;
1360
1843
  this.clearHover();
1361
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
+ }
1362
1874
  // --- paint ----------------------------------------------------------------
1363
1875
  paint(ctx) {
1364
1876
  super.paint(ctx); // background, border, clip to `abs`
@@ -1388,6 +1900,16 @@ export class HtmlViewNode extends Node {
1388
1900
  this._fail(error, this._laidOutWidth);
1389
1901
  }
1390
1902
  }
1903
+ /** A surface for the painter to draw a masked element on, asked of the
1904
+ * backend until it is found to have none. */
1905
+ _surface(width, height) {
1906
+ if (this._noSurface)
1907
+ return null;
1908
+ const surface = newSurface(this.app, width, height);
1909
+ if (!surface)
1910
+ this._noSurface = true;
1911
+ return surface;
1912
+ }
1391
1913
  _paint(ctx, tree, range, damage) {
1392
1914
  paintDocument(ctx, tree, {
1393
1915
  originX: this.abs.x,
@@ -1395,6 +1917,7 @@ export class HtmlViewNode extends Node {
1395
1917
  // the root's background covers the whole element, not only the
1396
1918
  // document: an element grown past its content is canvas too
1397
1919
  canvas: this.abs,
1920
+ viewport: this._viewport() ?? undefined,
1398
1921
  scale: this._scale,
1399
1922
  damage,
1400
1923
  selection: range
@@ -1410,11 +1933,34 @@ export class HtmlViewNode extends Node {
1410
1933
  const size = image ? this._resources.imageSize(url) : null;
1411
1934
  return image && size ? { image, ...size } : null;
1412
1935
  },
1936
+ surface: (width, height) => this._surface(width, height),
1413
1937
  cached: (key, width, height, draw) => (this._shadowCache ??= new SurfaceCache(this.app)).get(key, width, height, draw),
1414
1938
  });
1415
1939
  }
1416
1940
  }
1417
1941
  const EMPTY_SET = new Set();
1942
+ /** Whether a tree has a background fixed to the viewport: an image a box
1943
+ * names (`tree.backgrounds`), or the canvas's image or gradient — the
1944
+ * root's, or the body's it paints, which the root box stands in for where
1945
+ * the markup has neither element. */
1946
+ function hasFixedBackground(tree) {
1947
+ for (const box of tree.backgrounds) {
1948
+ if (box.style.backgroundAttachment === 'fixed')
1949
+ return true;
1950
+ }
1951
+ const root = tree.root;
1952
+ const body = root.children.find((child) => child.el?.name === 'body');
1953
+ for (const box of body ? [root, body] : [root]) {
1954
+ const style = box.style;
1955
+ if (style.backgroundAttachment === 'fixed' &&
1956
+ (style.backgroundImage ||
1957
+ style.backgroundImages ||
1958
+ style.backgroundGradient)) {
1959
+ return true;
1960
+ }
1961
+ }
1962
+ return false;
1963
+ }
1418
1964
  /** The first box an element made, depth first. */
1419
1965
  function boxFor(root, element) {
1420
1966
  const stack = [root];
@@ -1428,6 +1974,83 @@ function boxFor(root, element) {
1428
1974
  }
1429
1975
  return null;
1430
1976
  }
1977
+ /**
1978
+ * An inline element's boxes: the one it has, or where a block in it broke
1979
+ * it (`breakAround`), each piece, in document order.
1980
+ */
1981
+ function piecesOf(root, first) {
1982
+ // the first piece is cut at its end where there are more
1983
+ if (first.kind !== 'inline' || !(first.cut & 2))
1984
+ return [first];
1985
+ const pieces = [];
1986
+ const stack = [root];
1987
+ while (stack.length) {
1988
+ const box = stack.pop();
1989
+ if (box.el === first.el && box.kind === 'inline')
1990
+ pieces.push(box);
1991
+ for (let i = box.children.length - 1; i >= 0; i -= 1) {
1992
+ stack.push(box.children[i]);
1993
+ }
1994
+ }
1995
+ return pieces;
1996
+ }
1997
+ /**
1998
+ * The bands of one inline box's fragments, or a text's: on the lines of
1999
+ * the block it is laid out in, and no text of a float or a positioned box
2000
+ * inside it — on lines of their own — is one of them (CSSOM View 6.1): a
2001
+ * list item made inline around an absolute link measured as the link.
2002
+ * Each across its text, the inline-blocks and inline boxes' edges it holds
2003
+ * and its own padding and border where it starts or ends there, and as
2004
+ * tall as its border box.
2005
+ */
2006
+ function inlineBands(root, box, fonts, out) {
2007
+ // its own, apart from any other piece's: `fragmentHeights` makes each of
2008
+ // the bands it is handed as tall as this box on that band's line
2009
+ const bands = [];
2010
+ let block = box.parent;
2011
+ while (block && !block.lines)
2012
+ block = block.parent;
2013
+ if (box.subtreeTextEnd > box.subtreeTextStart) {
2014
+ if (box.kind === 'inline' && block?.lines) {
2015
+ lineBands(block.lines, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands, true);
2016
+ }
2017
+ else {
2018
+ collectBands(root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
2019
+ }
2020
+ }
2021
+ if (box.kind === 'inline') {
2022
+ fragmentReach(box, bands);
2023
+ if (fonts)
2024
+ fragmentHeights(box, bands, fonts);
2025
+ // where `position: relative` on it or an inline box around it moved
2026
+ // it, all of it (CSS 2.1 9.4.3), as `getBoundingClientRect` has it
2027
+ // (CSSOM View 6.1): the bands are where the lines put it, and a link
2028
+ // moved 120px down measured on its line, as wide as from where it was
2029
+ // laid out to where it was drawn
2030
+ const { x, y } = relativeOffsetOf(box);
2031
+ if (x || y) {
2032
+ for (const band of bands) {
2033
+ band.x += x;
2034
+ band.y += y;
2035
+ }
2036
+ }
2037
+ }
2038
+ out.push(...bands);
2039
+ }
2040
+ /** How far an inline box and the inline boxes around it moved it by their
2041
+ * relative offsets (`INLINE_OFFSETS`). */
2042
+ function relativeOffsetOf(box) {
2043
+ let x = 0;
2044
+ let y = 0;
2045
+ for (let at = box; at?.kind === 'inline'; at = at.parent) {
2046
+ const offset = INLINE_OFFSETS.get(at);
2047
+ if (offset) {
2048
+ x += offset.x;
2049
+ y += offset.y;
2050
+ }
2051
+ }
2052
+ return { x, y };
2053
+ }
1431
2054
  /** Where the document's text goes on after a box with none: the start of
1432
2055
  * the next text in document order, or the end of the last before it. */
1433
2056
  function textAfter(root, target) {
@@ -1488,6 +2111,9 @@ function sameRects(a, b) {
1488
2111
  p.y !== q.y ||
1489
2112
  p.width !== q.width ||
1490
2113
  p.height !== q.height ||
2114
+ p.fontFamily !== q.fontFamily ||
2115
+ p.fontSize !== q.fontSize ||
2116
+ p.opacity !== q.opacity ||
1491
2117
  !sameBare(p.bare, q.bare)) {
1492
2118
  return false;
1493
2119
  }
@@ -1503,7 +2129,8 @@ function sameBare(a, b) {
1503
2129
  a.height === b.height &&
1504
2130
  a.color === b.color &&
1505
2131
  a.fontFamily === b.fontFamily &&
1506
- a.fontSize === b.fontSize);
2132
+ a.fontSize === b.fontSize &&
2133
+ a.chevron === b.chevron);
1507
2134
  }
1508
2135
  // --- walks over the laid-out tree -------------------------------------------
1509
2136
  /** Distance from a coordinate to an interval, zero inside it. */
@@ -1768,7 +2395,13 @@ function fragmentHeights(box, bands, fonts) {
1768
2395
  }
1769
2396
  }
1770
2397
  /** The bands of a text range on one block's own lines. */
1771
- function lineBands(lines, from, to, dx, dy, out) {
2398
+ function lineBands(lines, from, to, dx, dy, out,
2399
+ /** Where the lines put the text, not where a relative offset moved it
2400
+ * (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
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) {
1772
2405
  // First line whose text can reach `from`, by binary search over the
1773
2406
  // sorted text starts; stop at the first line past `to`.
1774
2407
  let lo = 0;
@@ -1794,30 +2427,71 @@ function lineBands(lines, from, to, dx, dy, out) {
1794
2427
  if (b <= a)
1795
2428
  continue;
1796
2429
  const offsets = layoutOffsetsOf(text.layout);
2430
+ const moved = laidOut ? (TEXT_SHIFTS.get(text)?.x ?? 0) : 0;
2431
+ // The spaces a line ends on are removed (CSS Text 3, 4.1.2), and
2432
+ // take no room on it: the engine sets them past its line's content,
2433
+ // where a caret after them goes, and a band that took them in made
2434
+ // a link that ends a line a space wider than Chrome's. So the text
2435
+ // a line ends with is measured no further than its content.
2436
+ const ends = endsLine(line, text, natural);
2437
+ const rows = ink
2438
+ ? selectionRows(line, text, natural)
2439
+ : { y: line.y, height: line.height };
1797
2440
  for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
2441
+ let left = band.x;
2442
+ let right = band.x + band.width;
2443
+ if (ends) {
2444
+ left = Math.max(left, natural.x);
2445
+ right = Math.max(left, Math.min(right, natural.x + natural.width));
2446
+ }
2447
+ out.push({
2448
+ x: dx + left + text.drawX - moved,
2449
+ y: dy + rows.y,
2450
+ width: right - left,
2451
+ height: rows.height,
2452
+ });
2453
+ }
2454
+ // the spaces `pre-wrap` keeps that the line ends on, which have no
2455
+ // run: the engine hung them past the line (`LineText.hung`)
2456
+ for (const space of text.hung ?? []) {
2457
+ const at = documentOffsetOf(text, space.at);
2458
+ if (at < a || at >= b)
2459
+ continue;
1798
2460
  out.push({
1799
- x: dx + band.x + text.drawX,
1800
- y: dy + line.y,
1801
- width: band.width,
1802
- height: line.height,
2461
+ x: dx + space.x + text.drawX - moved,
2462
+ y: dy + rows.y,
2463
+ width: space.width,
2464
+ height: rows.height,
1803
2465
  });
1804
2466
  }
1805
2467
  }
1806
2468
  }
1807
2469
  }
1808
2470
  }
1809
- function collectBands(box, from, to, dx, dy, out) {
2471
+ /** Whether a line's text is where the line ends: its last, with nothing
2472
+ * placed after it — an inline-block or an image past its content. */
2473
+ function endsLine(line, text, natural) {
2474
+ if (line.texts[line.texts.length - 1] !== text)
2475
+ return false;
2476
+ const right = text.drawX + natural.x + natural.width;
2477
+ for (const placed of line.atomics) {
2478
+ if (placed.x >= right - 0.01)
2479
+ return false;
2480
+ }
2481
+ return true;
2482
+ }
2483
+ function collectBands(box, from, to, dx, dy, out, ink = false) {
1810
2484
  if (box.subtreeTextEnd <= box.subtreeTextStart)
1811
2485
  return;
1812
2486
  if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
1813
2487
  return;
1814
2488
  if (box.lines)
1815
- lineBands(box.lines, from, to, dx, dy, out);
2489
+ lineBands(box.lines, from, to, dx, dy, out, false, ink);
1816
2490
  // Atomics are ordinary children, reached below.
1817
2491
  for (const child of box.children) {
1818
2492
  if (child.kind === 'text' || child.kind === 'break')
1819
2493
  continue;
1820
- collectBands(child, from, to, dx, dy, out);
2494
+ collectBands(child, from, to, dx, dy, out, ink);
1821
2495
  }
1822
2496
  }
1823
2497
  /** The layers of a hit test, in the order CSS paints them within a
@@ -1831,7 +2505,11 @@ const HIT_POSITIONED = 3;
1831
2505
  function deepestAt(tree, x, y,
1832
2506
  /** Told whether what was found was found under text: a run's, rather
1833
2507
  * than a box's. */
1834
- hit) {
2508
+ hit,
2509
+ /** How far a box fixed to the viewport is drawn from where it was laid
2510
+ * out (`HtmlViewNode._fixedShift`): the point is taken back by it in
2511
+ * there, as paint moved the box by it (`atViewport`). */
2512
+ fixedShift = null) {
1835
2513
  const box = tree.root;
1836
2514
  let found = box.el;
1837
2515
  let foundKey = [];
@@ -1872,7 +2550,25 @@ hit) {
1872
2550
  // Zen Garden's archive links are absolute `<li>`s in an `overflow:
1873
2551
  // hidden` list with no height of its own, and not one of them could be
1874
2552
  // hovered or pressed.
2553
+ let atViewport = false;
1875
2554
  const enter = (child, context, clipped) => {
2555
+ if (!fixedShift || atViewport || !fixedToViewport(child)) {
2556
+ enterAt(child, context, clipped);
2557
+ return;
2558
+ }
2559
+ x -= fixedShift.x;
2560
+ y -= fixedShift.y;
2561
+ atViewport = true;
2562
+ try {
2563
+ enterAt(child, context, clipped);
2564
+ }
2565
+ finally {
2566
+ x += fixedShift.x;
2567
+ y += fixedShift.y;
2568
+ atViewport = false;
2569
+ }
2570
+ };
2571
+ const enterAt = (child, context, clipped) => {
1876
2572
  const style = child.style;
1877
2573
  if (style.position !== 'static' ||
1878
2574
  (child.parent?.kind === 'flex' && typeof style.zIndex === 'number')) {
@@ -1984,6 +2680,32 @@ function compareKeys(a, b) {
1984
2680
  * is built again instead: a link's subtree is a handful, and a compound
1985
2681
  * that matches a container reaches everything in it. */
1986
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
+ };
1987
2709
  /**
1988
2710
  * The computed properties a pointer move may change in place: ink, which
1989
2711
  * moves nothing. A background colour is drawn inside the box it colours
@@ -2030,21 +2752,127 @@ const FACE_FIELDS = [
2030
2752
  'letterSpacing',
2031
2753
  'features',
2032
2754
  ];
2033
- /** Whether two styles differ in ink alone: true where they do, null where
2034
- * they do not differ, false where something else does. Custom properties
2035
- * are read through the properties that use them. */
2036
- function inkOnly(was, now) {
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
+ ];
2761
+ /** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
2762
+ * a widget mounted beside the document, or a button whose press it reports. */
2763
+ const WIDGETS = new Set([
2764
+ 'input',
2765
+ 'textarea',
2766
+ 'select',
2767
+ 'button',
2768
+ 'checkbox',
2769
+ 'radio',
2770
+ ]);
2771
+ /** What only moves how far a box's ink reaches: a shadow, and an outline's
2772
+ * size, which take no room (CSS Backgrounds 3, 7.1; CSS UI 4, 3). */
2773
+ const INK_REACH = new Set([
2774
+ 'boxShadow',
2775
+ 'outlineStyle',
2776
+ 'outlineWidth',
2777
+ 'outlineOffset',
2778
+ ]);
2779
+ /**
2780
+ * How two styles of one element differ, for a restyle in place: null where
2781
+ * they do not, a `HoverChange` where every difference is ink, how far ink
2782
+ * reaches, a `z-index` that stays a stacking context's, or a translation —
2783
+ * none of which moves anything but the box and what it holds (CSS
2784
+ * Transforms 1: a transform does not affect layout) — and false where
2785
+ * anything else differs.
2786
+ */
2787
+ function hoverChange(was, now) {
2037
2788
  const a = was;
2038
2789
  const b = now;
2039
- let ink = false;
2790
+ let change = null;
2040
2791
  for (const key in b) {
2041
- 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
+ }
2798
+ continue;
2799
+ }
2800
+ if (sameValue(a[key], b[key]))
2801
+ continue;
2802
+ change ??= { ink: true, reach: false, order: false, move: false };
2803
+ change.ink = true;
2804
+ if (PAINT_ONLY.has(key))
2805
+ continue;
2806
+ if (INK_REACH.has(key)) {
2807
+ change.reach = true;
2808
+ continue;
2809
+ }
2810
+ if (key === 'zIndex') {
2811
+ // one stacking context either way, with nothing hoisted below the
2812
+ // flow (`hoistNegative`): only its place among its layers changes
2813
+ const z0 = a[key];
2814
+ const z1 = b[key];
2815
+ if (typeof z0 !== 'number' || typeof z1 !== 'number')
2816
+ return false;
2817
+ if (z0 < 0 || z1 < 0)
2818
+ return false;
2819
+ change.order = true;
2820
+ continue;
2821
+ }
2822
+ if (key === 'transformTranslate' || key === 'translate') {
2823
+ change.move = true;
2042
2824
  continue;
2043
- if (!PAINT_ONLY.has(key))
2825
+ }
2826
+ return false;
2827
+ }
2828
+ return change;
2829
+ }
2830
+ /**
2831
+ * Whether a box can take a new translation where it is: a box of its own
2832
+ * rather than an inline one, whose text is what moves; and the same
2833
+ * containing block and stacking context for what is in it, which a
2834
+ * transform makes a box (CSS Transforms 1, 2) — so either transformed
2835
+ * before and after, or already positioned with a `z-index`, and holding
2836
+ * nothing fixed, which only a transform takes in.
2837
+ */
2838
+ function movable(box, was, now) {
2839
+ if (box.kind === 'inline')
2840
+ return false;
2841
+ if (transformed(was) === transformed(now))
2842
+ return true;
2843
+ if (was.position === 'static')
2844
+ return false;
2845
+ if (typeof was.zIndex !== 'number' || typeof now.zIndex !== 'number') {
2846
+ return false;
2847
+ }
2848
+ const stack = [...box.children];
2849
+ while (stack.length) {
2850
+ const at = stack.pop();
2851
+ if (at.style.position === 'fixed')
2044
2852
  return false;
2045
- ink = true;
2853
+ for (const child of at.children)
2854
+ stack.push(child);
2046
2855
  }
2047
- return ink ? true : null;
2856
+ return true;
2857
+ }
2858
+ /** Where a box draws, in document coordinates: its ink bounds, or for an
2859
+ * inline box or a run of text, which are drawn on their block's lines and
2860
+ * have none of their own, the block's. */
2861
+ function inkOf(box) {
2862
+ let at = box;
2863
+ while (at && (at.kind === 'text' || at.kind === 'break' || !hasRect(at))) {
2864
+ at = at.parent;
2865
+ }
2866
+ if (!at || at.boundsY === Infinity)
2867
+ return null;
2868
+ if (!(at.boundsWidth > 0 && at.boundsHeight > 0))
2869
+ return null;
2870
+ return {
2871
+ x: at.boundsX,
2872
+ y: at.boundsY,
2873
+ width: at.boundsWidth,
2874
+ height: at.boundsHeight,
2875
+ };
2048
2876
  }
2049
2877
  /** Structural equality for a computed value: a number, a string, or the
2050
2878
  * plain objects and arrays a length or a shadow list is. */
@@ -2068,32 +2896,23 @@ function sameValue(x, y) {
2068
2896
  return false;
2069
2897
  return true;
2070
2898
  }
2071
- /** The element an anonymous box takes its style from: its nearest
2072
- * ancestor's that has one. */
2073
- function nearestElement(box) {
2074
- for (let at = box.parent; at; at = at.parent)
2075
- if (at.el)
2076
- return at.el;
2077
- return null;
2078
- }
2079
2899
  /**
2080
- * `text`'s layout made again with the ink of the elements a pointer move
2081
- * 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
2082
2902
  * given the ink its new style makes (`runFor`) and nothing else. Null where
2083
2903
  * none of its runs is theirs; false where that cannot be told — a run this
2084
2904
  * cannot place in the document, one some other pass inked (a first line's),
2085
2905
  * or a layout that comes out another shape.
2086
2906
  */
2087
- function reinked(text, layouts, changed, styles) {
2907
+ function reinked(text, layouts, next) {
2088
2908
  const spans = text.spans;
2089
- const ownerAt = (offset) => spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
2090
2909
  const inputs = layouts.inputsOf(text.layout);
2091
2910
  if (!inputs || !spans.boxAt) {
2092
2911
  // not one this could make again: none of its text may be theirs
2093
2912
  for (const line of text.layout.lines) {
2094
2913
  for (const run of line.runs) {
2095
- const owner = spans.boxAt ? ownerAt(run.start) : null;
2096
- if (!spans.boxAt || (owner && changed.has(owner)))
2914
+ const box = spans.boxAt?.(run.start);
2915
+ if (!spans.boxAt || (box && next.has(box)))
2097
2916
  return false;
2098
2917
  }
2099
2918
  }
@@ -2105,32 +2924,32 @@ function reinked(text, layouts, changed, styles) {
2105
2924
  for (let i = 0; i < content.length; i += 1) {
2106
2925
  const run = content[i];
2107
2926
  const length = run.text.length;
2108
- const owner = length ? ownerAt(offset) : null;
2109
- const style = owner ? changed.get(owner) : undefined;
2110
- if (owner && style) {
2111
- 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)
2112
2931
  return false;
2113
- const was = runFor(run.text, styles.get(owner).style);
2932
+ const was = runFor(run.text, box.style);
2114
2933
  const now = runFor(run.text, style);
2115
2934
  for (const f of FACE_FIELDS)
2116
2935
  if (!sameValue(was[f], now[f]))
2117
2936
  return false;
2118
- let next = null;
2937
+ let inked = null;
2119
2938
  for (const f of INK_FIELDS) {
2120
2939
  // a run that is not what its style made — a first line's colour
2121
2940
  if (!sameValue(run[f], was[f]))
2122
2941
  return false;
2123
2942
  if (sameValue(was[f], now[f]))
2124
2943
  continue;
2125
- next ??= { ...run };
2944
+ inked ??= { ...run };
2126
2945
  if (now[f] === undefined)
2127
- delete next[f];
2946
+ delete inked[f];
2128
2947
  else
2129
- next[f] = now[f];
2948
+ inked[f] = now[f];
2130
2949
  }
2131
- if (next) {
2950
+ if (inked) {
2132
2951
  runs ??= content.slice();
2133
- runs[i] = next;
2952
+ runs[i] = inked;
2134
2953
  }
2135
2954
  }
2136
2955
  offset += length;