@react-x11/components 0.11.0 → 0.12.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 (100) 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 +26 -7
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +28 -3
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts.map +1 -1
  10. package/dist/html/css/cascade.js +19 -6
  11. package/dist/html/css/cascade.js.map +1 -1
  12. package/dist/html/css/color.d.ts +19 -0
  13. package/dist/html/css/color.d.ts.map +1 -1
  14. package/dist/html/css/color.js +86 -0
  15. package/dist/html/css/color.js.map +1 -1
  16. package/dist/html/css/parse.d.ts +11 -0
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +114 -4
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/style.d.ts +38 -1
  21. package/dist/html/css/style.d.ts.map +1 -1
  22. package/dist/html/css/style.js +326 -8
  23. package/dist/html/css/style.js.map +1 -1
  24. package/dist/html/css/ua.js +14 -0
  25. package/dist/html/css/ua.js.map +1 -1
  26. package/dist/html/css/values.d.ts +10 -1
  27. package/dist/html/css/values.d.ts.map +1 -1
  28. package/dist/html/css/values.js +27 -2
  29. package/dist/html/css/values.js.map +1 -1
  30. package/dist/html/css/vars.d.ts +5 -0
  31. package/dist/html/css/vars.d.ts.map +1 -1
  32. package/dist/html/css/vars.js +2 -2
  33. package/dist/html/css/vars.js.map +1 -1
  34. package/dist/html/dom.d.ts.map +1 -1
  35. package/dist/html/dom.js +35 -2
  36. package/dist/html/dom.js.map +1 -1
  37. package/dist/html/index.d.ts.map +1 -1
  38. package/dist/html/index.js +38 -1
  39. package/dist/html/index.js.map +1 -1
  40. package/dist/html/layout/block.d.ts +11 -2
  41. package/dist/html/layout/block.d.ts.map +1 -1
  42. package/dist/html/layout/block.js +181 -18
  43. package/dist/html/layout/block.js.map +1 -1
  44. package/dist/html/layout/boxes.d.ts +34 -6
  45. package/dist/html/layout/boxes.d.ts.map +1 -1
  46. package/dist/html/layout/boxes.js +35 -14
  47. package/dist/html/layout/boxes.js.map +1 -1
  48. package/dist/html/layout/cache.d.ts +2 -2
  49. package/dist/html/layout/cache.d.ts.map +1 -1
  50. package/dist/html/layout/cache.js +8 -3
  51. package/dist/html/layout/cache.js.map +1 -1
  52. package/dist/html/layout/floats.d.ts +4 -0
  53. package/dist/html/layout/floats.d.ts.map +1 -1
  54. package/dist/html/layout/floats.js +26 -0
  55. package/dist/html/layout/floats.js.map +1 -1
  56. package/dist/html/layout/inline.d.ts +26 -2
  57. package/dist/html/layout/inline.d.ts.map +1 -1
  58. package/dist/html/layout/inline.js +1222 -115
  59. package/dist/html/layout/inline.js.map +1 -1
  60. package/dist/html/node.d.ts +33 -0
  61. package/dist/html/node.d.ts.map +1 -1
  62. package/dist/html/node.js +512 -51
  63. package/dist/html/node.js.map +1 -1
  64. package/dist/html/paint.d.ts +53 -1
  65. package/dist/html/paint.d.ts.map +1 -1
  66. package/dist/html/paint.js +546 -180
  67. package/dist/html/paint.js.map +1 -1
  68. package/dist/html/surfaces.d.ts +7 -2
  69. package/dist/html/surfaces.d.ts.map +1 -1
  70. package/dist/html/surfaces.js +16 -0
  71. package/dist/html/surfaces.js.map +1 -1
  72. package/dist/internal/window.d.ts +22 -0
  73. package/dist/internal/window.d.ts.map +1 -1
  74. package/dist/internal/window.js +35 -8
  75. package/dist/internal/window.js.map +1 -1
  76. package/dist/richtext/node.d.ts +7 -0
  77. package/dist/richtext/node.d.ts.map +1 -1
  78. package/dist/richtext/node.js.map +1 -1
  79. package/package.json +5 -3
  80. package/src/flow/node.ts +80 -64
  81. package/src/html/controls.ts +50 -9
  82. package/src/html/css/cascade.ts +19 -6
  83. package/src/html/css/color.ts +87 -0
  84. package/src/html/css/parse.ts +107 -4
  85. package/src/html/css/style.ts +343 -8
  86. package/src/html/css/ua.ts +14 -0
  87. package/src/html/css/values.ts +31 -3
  88. package/src/html/css/vars.ts +2 -2
  89. package/src/html/dom.ts +38 -2
  90. package/src/html/index.ts +39 -1
  91. package/src/html/layout/block.ts +228 -16
  92. package/src/html/layout/boxes.ts +55 -21
  93. package/src/html/layout/cache.ts +8 -3
  94. package/src/html/layout/floats.ts +24 -0
  95. package/src/html/layout/inline.ts +1435 -108
  96. package/src/html/node.ts +535 -54
  97. package/src/html/paint.ts +650 -241
  98. package/src/html/surfaces.ts +21 -1
  99. package/src/internal/window.ts +36 -8
  100. package/src/richtext/node.ts +7 -0
@@ -22,9 +22,9 @@
22
22
  import { canFill, lineBands, paintRunBackgrounds, paintRunRules, } from '../richtext/runs.js';
23
23
  import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
24
24
  import { blend, borderShades } from './css/color.js';
25
- import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, scrolls, } from './css/style.js';
25
+ import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, masked, scrolls, } from './css/style.js';
26
26
  import { contained } from './layout/block.js';
27
- import { BOX_RAISES, LINE_BOX_RAISES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
27
+ import { BOX_RAISES, LINE_BOX_RAISES, PADDED_FACES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
28
28
  import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
29
29
  import { tableGrid } from './layout/grid.js';
30
30
  import { SvgDrawing, inlineDrawing } from './svg.js';
@@ -36,6 +36,13 @@ import { SvgDrawing, inlineDrawing } from './svg.js';
36
36
  * document's height — handed up rather than kept on every box, where
37
37
  * writing it and reading it back cost this walk a fifth of its time.
38
38
  */
39
+ /**
40
+ * How far down an out-of-flow box's scrollable overflow reaches: its border
41
+ * box, and what it holds where it does not clip it (`computePaintBounds`),
42
+ * which the box it is in does not take in and the document does
43
+ * (`layoutDocument`).
44
+ */
45
+ export const OUT_OF_FLOW_REACH = new WeakMap();
39
46
  export function computePaintBounds(box, moved = false) {
40
47
  // A box with no rectangle of its own gives only what it holds: nothing,
41
48
  // until a child with bounds is met.
@@ -47,8 +54,12 @@ export function computePaintBounds(box, moved = false) {
47
54
  // How far down the content reaches, for the document's height: the
48
55
  // border box, and every box and line under it, but not past a box that
49
56
  // clips what it holds — where the scrollable overflow ends. Out-of-flow
50
- // boxes are the layout's to count.
51
- let bottom = y2;
57
+ // boxes are the layout's to count. A border box of no area reaches
58
+ // nowhere, as a browser has it (Blink's `ScrollableOverflowCalculator`
59
+ // adds no empty rect): the empty blocks a design leaves at its end,
60
+ // below the last one's bottom margin, made the page that margin taller
61
+ const empty = own && !(box.width > 0 && box.height > 0);
62
+ let bottom = empty ? -Infinity : y2;
52
63
  // A shadow is ink past the box, and no overflow: a repaint of the strip
53
64
  // under a card has to reach the card, and the document is no taller.
54
65
  const shadows = own ? box.style.boxShadow : null;
@@ -71,6 +82,25 @@ export function computePaintBounds(box, moved = false) {
71
82
  x2 = Math.max(x2, box.x + box.width + reach);
72
83
  y2 = Math.max(y2, box.y + box.height + reach);
73
84
  }
85
+ // An inline box's fragments are boxes on the lines they are on, and its
86
+ // padding and border reach above and below those lines: they count in
87
+ // the scrollable overflow of the block it is in (CSS Overflow 3, 2.2) —
88
+ // Blink adds each inline box fragment's border box
89
+ // (`ScrollableOverflowCalculator::AddItemsInternal`). Design 150's footer
90
+ // links, 50px of padding under their text, made Chrome's page 19px
91
+ // taller than the box they end. Only a box with padding or a border
92
+ // above or below its text can reach past its lines.
93
+ if (box.kind === 'inline' &&
94
+ box.padTop + box.padBottom + box.borderTop + box.borderBottom > 0) {
95
+ const reach = inlineFragmentsReach(box);
96
+ if (reach) {
97
+ x1 = Math.min(x1, reach.x1);
98
+ y1 = Math.min(y1, reach.y1);
99
+ x2 = Math.max(x2, reach.x2);
100
+ y2 = Math.max(y2, reach.y2);
101
+ bottom = Math.max(bottom, reach.y2);
102
+ }
103
+ }
74
104
  const lines = box.lines;
75
105
  if (lines) {
76
106
  let tallest = 0;
@@ -127,6 +157,8 @@ export function computePaintBounds(box, moved = false) {
127
157
  const reach = computePaintBounds(child, moved);
128
158
  if (!child.outOfFlow)
129
159
  bottom = Math.max(bottom, reach);
160
+ else
161
+ OUT_OF_FLOW_REACH.set(child, reach);
130
162
  if (child.boundsY === Infinity)
131
163
  continue;
132
164
  x1 = Math.min(x1, child.boundsX);
@@ -174,7 +206,7 @@ export function computePaintBounds(box, moved = false) {
174
206
  return bottom;
175
207
  // whether the box clips only matters where its content reaches past it,
176
208
  // and its style is one more object a walk of every box would read
177
- const end = box.y + box.height;
209
+ const end = empty ? -Infinity : box.y + box.height;
178
210
  if (bottom <= end)
179
211
  return end;
180
212
  const style = box.style;
@@ -200,6 +232,56 @@ export function hasRect(box) {
200
232
  const display = box.style.display;
201
233
  return display !== 'table-column' && display !== 'table-column-group';
202
234
  }
235
+ /**
236
+ * Where an inline box's fragments are, over the lines of the block it is
237
+ * in that hold its text: from its face's ascent and its top padding and
238
+ * border above each line's baseline to its descent and its bottom ones
239
+ * below, as `paintInlineBoxes` draws them. Null for a box with no text or
240
+ * no face.
241
+ */
242
+ function inlineFragmentsReach(box) {
243
+ const face = box.decoration ?? PADDED_FACES.get(box);
244
+ if (!face || box.subtreeTextEnd <= box.subtreeTextStart)
245
+ return null;
246
+ let block = box.parent;
247
+ while (block && block.kind === 'inline')
248
+ block = block.parent;
249
+ const lines = block?.lines;
250
+ if (!lines?.length)
251
+ return null;
252
+ const from = box.subtreeTextStart;
253
+ const to = box.subtreeTextEnd;
254
+ let lo = 0;
255
+ let hi = lines.length;
256
+ while (lo < hi) {
257
+ const mid = (lo + hi) >> 1;
258
+ if (lines[mid].textEnd > from)
259
+ hi = mid;
260
+ else
261
+ lo = mid + 1;
262
+ }
263
+ const moved = offsetOf(box);
264
+ let x1 = Infinity;
265
+ let y1 = Infinity;
266
+ let x2 = -Infinity;
267
+ let y2 = -Infinity;
268
+ for (let i = lo; i < lines.length; i += 1) {
269
+ const line = lines[i];
270
+ if (line.textStart >= to)
271
+ break;
272
+ if (line.textEnd <= from)
273
+ continue;
274
+ const raise = SHIFTED_LINES.has(line)
275
+ ? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
276
+ : 0;
277
+ const baseline = line.y + line.baseline - raise + (moved?.y ?? 0);
278
+ x1 = Math.min(x1, line.x);
279
+ x2 = Math.max(x2, line.x + line.width);
280
+ y1 = Math.min(y1, baseline - face.ascent - box.padTop - box.borderTop);
281
+ y2 = Math.max(y2, baseline + face.descent + box.padBottom + box.borderBottom);
282
+ }
283
+ return x1 === Infinity ? null : { x1, y1, x2, y2 };
284
+ }
203
285
  /** Children lists past this size get the sorted viewport index; below it a
204
286
  * linear scan is cheaper than keeping one. */
205
287
  const PAINT_INDEX_MIN = 64;
@@ -370,7 +452,7 @@ function paintCanvas(ctx, { style: source, anchor }, root, options) {
370
452
  // browser does — the stripes a short page with a gradient on its
371
453
  // body shows — or by the viewport, where it is fixed
372
454
  const box = style.backgroundAttachment === 'fixed' && options.canvas
373
- ? options.canvas
455
+ ? (options.viewport ?? options.canvas)
374
456
  : originBox(anchor, options, style);
375
457
  paintGradient(ctx, style, style.backgroundGradient, area, snapped(box.x, box.y, box.width, box.height));
376
458
  }
@@ -458,8 +540,10 @@ function paddingBox(box, options) {
458
540
  };
459
541
  }
460
542
  /** Where a layer of a box's background is placed, in window coordinates:
461
- * the box its `background-origin` names (CSS Backgrounds 3, 3.8). */
462
- function originBox(box, options, style) {
543
+ * the box its `background-origin` names (CSS Backgrounds 3, 3.8) — of the
544
+ * whole strip, for a fragment of an inline box that is sliced. */
545
+ function originBox(frame, options, style) {
546
+ const box = frame.strip ?? frame;
463
547
  const [t, r, b, l] = edgeInsets(box, style.backgroundOrigin);
464
548
  return {
465
549
  x: box.x + l + options.originX,
@@ -525,8 +609,14 @@ function paintBox(ctx, box, options) {
525
609
  ctx.save();
526
610
  ctx.globalAlpha = ctx.globalAlpha * opacity;
527
611
  }
612
+ if (masked(box.style) && paintMasked(ctx, box, options)) {
613
+ if (fade)
614
+ ctx.restore();
615
+ return;
616
+ }
528
617
  // `clip` shows the part of an absolutely positioned box it names, its own
529
- // background and borders among it (CSS 2.1 11.1.2)
618
+ // background and borders among it (CSS 2.1 11.1.2) — written out here,
619
+ // not called: a frame more a level and a deep document ran out of stack
530
620
  const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
531
621
  if (!clip || (clip.w > 0 && clip.h > 0)) {
532
622
  const clipped = !!clip && pushClip(ctx, clip, null);
@@ -537,6 +627,135 @@ function paintBox(ctx, box, options) {
537
627
  if (fade)
538
628
  ctx.restore();
539
629
  }
630
+ /** A box and what it holds, cut to its `clip` as `paintBox` cuts it. */
631
+ function paintClipped(ctx, box, options) {
632
+ const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
633
+ if (clip && !(clip.w > 0 && clip.h > 0))
634
+ return;
635
+ const clipped = !!clip && pushClip(ctx, clip, null);
636
+ paintContent(ctx, box, options);
637
+ if (clipped)
638
+ ctx.restore();
639
+ }
640
+ const MASK_LAYERS = new WeakMap();
641
+ /** No corners: a mask layer is placed in the box's rectangle, which its
642
+ * radii do not round (CSS Masking 1, 7). */
643
+ const SQUARE_RADII = [0, 0, 0, 0];
644
+ /**
645
+ * A style for each of a box's mask layers, top first, as `layersOf` makes
646
+ * a background's: the box's own style with the layer's image, repeat, size,
647
+ * position, origin and clip in its background's place, and no colour — so
648
+ * the background painter places and repeats a mask layer as it does a
649
+ * background layer, which is what CSS Masking 1 says a mask layer is (7).
650
+ * Made once a style.
651
+ */
652
+ function maskLayersOf(style) {
653
+ let layers = MASK_LAYERS.get(style);
654
+ if (layers)
655
+ return layers;
656
+ const mask = style.mask;
657
+ const nth = (list, i) => list[i % list.length];
658
+ layers = mask.images.map((image, i) => {
659
+ const layer = Object.create(style);
660
+ layer.backgroundColor = null;
661
+ layer.backgroundImages = null;
662
+ layer.backgroundImage = typeof image === 'string' ? image : null;
663
+ layer.backgroundGradient =
664
+ image !== null && typeof image !== 'string' ? image : null;
665
+ layer.backgroundRepeat = nth(mask.repeats, i);
666
+ layer.backgroundSize = nth(mask.sizes, i);
667
+ layer.backgroundAttachment = 'scroll';
668
+ [layer.backgroundPositionX, layer.backgroundPositionY] = nth(mask.positions, i);
669
+ layer.backgroundOrigin = nth(mask.origins, i);
670
+ layer.backgroundClip = nth(mask.clips, i);
671
+ layer.backgroundClipText = false;
672
+ layer.borderRadius = SQUARE_RADII;
673
+ layer.borderRadiusY = null;
674
+ return layer;
675
+ });
676
+ MASK_LAYERS.set(style, layers);
677
+ return layers;
678
+ }
679
+ /**
680
+ * A masked box (CSS Masking 1, 7): the box and what it holds drawn on a
681
+ * surface of their own, which the alpha of its mask layers — painted as a
682
+ * background's are, on a second surface, each added over the ones below
683
+ * it — then cuts with `destination-in`, and the result drawn in its place,
684
+ * as the group a mask makes. Only the mask painting area, the border box,
685
+ * can show, and only its part in the damage is drawn.
686
+ *
687
+ * A layer whose image has not arrived is transparent black (7.2), so a box
688
+ * none of whose layers can be drawn yet draws nothing: an icon masked out
689
+ * of its `background-color` is not a solid square while its image loads.
690
+ * False where there is no surface to draw on, or no `destination-in` to
691
+ * draw with, and the caller paints the box unmasked.
692
+ */
693
+ function paintMasked(ctx, box, options) {
694
+ const layers = maskLayersOf(box.style);
695
+ const drawable = layers.some((layer) => !!layer.backgroundGradient ||
696
+ (!!layer.backgroundImage &&
697
+ !!options.backgroundImageFor?.(layer.backgroundImage)));
698
+ if (!drawable)
699
+ return true;
700
+ if (!options.surface || !ctx.drawImage)
701
+ return false;
702
+ let x0 = Math.floor(box.x + options.originX);
703
+ let y0 = Math.floor(frameY(box) + options.originY);
704
+ let x1 = Math.ceil(box.x + box.width + options.originX);
705
+ let y1 = Math.ceil(frameY(box) + frameHeight(box) + options.originY);
706
+ const damage = options.damage;
707
+ if (damage) {
708
+ x0 = Math.max(x0, Math.floor(damage.x));
709
+ y0 = Math.max(y0, Math.floor(damage.y));
710
+ x1 = Math.min(x1, Math.ceil(damage.x + damage.width));
711
+ y1 = Math.min(y1, Math.ceil(damage.y + damage.height));
712
+ }
713
+ const w = x1 - x0;
714
+ const h = y1 - y0;
715
+ if (!(w > 0 && h > 0))
716
+ return true;
717
+ const content = options.surface(w, h);
718
+ const mask = content && options.surface(w, h);
719
+ try {
720
+ if (!content || !mask)
721
+ return false;
722
+ const cctx = content.getContext('2d');
723
+ const mctx = mask.getContext('2d');
724
+ // the surfaces' own coordinates, the window's moved to their corner
725
+ const on = {
726
+ ...options,
727
+ originX: options.originX - x0,
728
+ originY: options.originY - y0,
729
+ damage: { x: 0, y: 0, width: w, height: h },
730
+ canvas: options.canvas && {
731
+ ...options.canvas,
732
+ x: options.canvas.x - x0,
733
+ y: options.canvas.y - y0,
734
+ },
735
+ viewport: options.viewport && {
736
+ ...options.viewport,
737
+ x: options.viewport.x - x0,
738
+ y: options.viewport.y - y0,
739
+ },
740
+ clips: [],
741
+ };
742
+ for (let i = layers.length - 1; i >= 0; i -= 1) {
743
+ paintLayer(mctx, box, on, layers[i], frameImages(mctx, box, on));
744
+ }
745
+ paintClipped(cctx, box, on);
746
+ cctx.globalCompositeOperation = 'destination-in';
747
+ if (cctx.globalCompositeOperation !== 'destination-in')
748
+ return false;
749
+ cctx.drawImage(mask, 0, 0);
750
+ cctx.globalCompositeOperation = 'source-over';
751
+ ctx.drawImage(content, x0, y0);
752
+ return true;
753
+ }
754
+ finally {
755
+ content?.destroy?.();
756
+ mask?.destroy?.();
757
+ }
758
+ }
540
759
  function paintContent(ctx, box, options) {
541
760
  const visible = box.style.visibility === 'visible';
542
761
  if (visible)
@@ -779,9 +998,6 @@ function paintShadows(ctx, box, options, inset) {
779
998
  }, 0);
780
999
  continue;
781
1000
  }
782
- if (bakedOuter(ctx, options, s, color, shape, around, covered ? null : { rect: own, corners })) {
783
- continue;
784
- }
785
1001
  // what of the shadow falls under the box is not drawn
786
1002
  const clipped = !covered && ctx.clip && ctx.rect && ctx.save;
787
1003
  if (clipped) {
@@ -792,7 +1008,7 @@ function paintShadows(ctx, box, options, inset) {
792
1008
  ctx.clip();
793
1009
  }
794
1010
  fillShadow(ctx, s, color, (dx) => {
795
- roundedRect(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
1011
+ shadowShape(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
796
1012
  }, shape.x + shape.width + reach);
797
1013
  if (clipped)
798
1014
  ctx.restore();
@@ -842,8 +1058,6 @@ function paintShadows(ctx, box, options, inset) {
842
1058
  fillRing(ctx, pad, inner, hole, within);
843
1059
  continue;
844
1060
  }
845
- if (bakedInset(ctx, options, s, color, pad, inner, hole, within))
846
- continue;
847
1061
  if (!ctx.clip || !ctx.save || !ctx.rect)
848
1062
  continue;
849
1063
  ctx.save();
@@ -861,128 +1075,24 @@ function paintShadows(ctx, box, options, inset) {
861
1075
  fillShadow(ctx, s, color, (dx) => {
862
1076
  ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
863
1077
  if (hole.width > 0 && hole.height > 0) {
864
- roundedRect(ctx, hole.x - dx, hole.y, hole.width, hole.height, within, true, true);
1078
+ shadowShape(ctx, hole.x - dx, hole.y, hole.width, hole.height, within);
865
1079
  }
866
- }, frame.x + frame.width + shadowReach(s.blur));
1080
+ }, frame.x + frame.width + shadowReach(s.blur), 'evenodd');
867
1081
  ctx.restore();
868
1082
  }
869
1083
  }
870
- /** Corners as part of a key. */
871
- function cornerKey(c) {
872
- return `${c.x.join(',')}/${c.y.join(',')}`;
873
- }
874
- /**
875
- * An outer shadow drawn once on a surface of its own and composited: the
876
- * shape's shadow, and where `box` is given the box cut out of it — so a box
877
- * that shows what is behind it shows no shadow there, with the clip on the
878
- * small surface rather than the window. False where there is no surface.
879
- */
880
- function bakedOuter(ctx, options, s, color, shape, around, box) {
881
- if (!options.cached || !ctx.drawImage)
882
- return false;
883
- const reach = shadowReach(s.blur) + 1;
884
- const x0 = Math.floor(shape.x) - reach;
885
- const y0 = Math.floor(shape.y) - reach;
886
- const w = Math.ceil(shape.x + shape.width) + reach - x0;
887
- const h = Math.ceil(shape.y + shape.height) + reach - y0;
888
- const sx = shape.x - x0;
889
- const sy = shape.y - y0;
890
- const cut = box && {
891
- x: box.rect.x - x0,
892
- y: box.rect.y - y0,
893
- width: box.rect.width,
894
- height: box.rect.height,
895
- };
896
- const key = [
897
- 'shadow',
898
- w,
899
- h,
900
- sx,
901
- sy,
902
- shape.width,
903
- shape.height,
904
- cornerKey(around),
905
- s.blur,
906
- color,
907
- cut
908
- ? `${cut.x},${cut.y},${cut.width},${cut.height},${cornerKey(box.corners)}`
909
- : '',
910
- ].join('|');
911
- const image = options.cached(key, w, h, (sctx) => {
912
- if (cut) {
913
- sctx.save();
914
- sctx.beginPath();
915
- sctx.rect(0, 0, w, h);
916
- roundedRect(sctx, cut.x, cut.y, cut.width, cut.height, box.corners, true, true);
917
- sctx.clip();
918
- }
919
- fillShadow(sctx, s, color, (dx) => {
920
- roundedRect(sctx, sx - dx, sy, shape.width, shape.height, around);
921
- }, sx + shape.width + reach);
922
- if (cut)
923
- sctx.restore();
924
- });
925
- if (!image)
926
- return false;
927
- ctx.drawImage(image, x0, y0);
928
- return true;
929
- }
930
- /**
931
- * An inset shadow drawn once on a surface the size of the padding box and
932
- * composited: a frame around the hole, whose shadow falls inside, clipped
933
- * to the padding box's corners on the surface. False where there is none.
934
- */
935
- function bakedInset(ctx, options, s, color, pad, inner, hole, within) {
936
- if (!options.cached || !ctx.drawImage)
937
- return false;
938
- const w = Math.round(pad.width);
939
- const h = Math.round(pad.height);
940
- const hx = hole.x - pad.x;
941
- const hy = hole.y - pad.y;
942
- const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
943
- const key = [
944
- 'inset',
945
- w,
946
- h,
947
- cornerKey(inner),
948
- hx,
949
- hy,
950
- hole.width,
951
- hole.height,
952
- cornerKey(within),
953
- s.blur,
954
- color,
955
- ].join('|');
956
- const image = options.cached(key, w, h, (sctx) => {
957
- sctx.save();
958
- sctx.beginPath();
959
- roundedRect(sctx, 0, 0, w, h, inner);
960
- sctx.clip();
961
- fillShadow(sctx, s, color, (dx) => {
962
- sctx.rect(-reach - dx, -reach, w + 2 * reach, h + 2 * reach);
963
- if (hole.width > 0 && hole.height > 0) {
964
- roundedRect(sctx, hx - dx, hy, hole.width, hole.height, within, true, true);
965
- }
966
- }, w + reach + shadowReach(s.blur));
967
- sctx.restore();
968
- });
969
- if (!image)
970
- return false;
971
- ctx.drawImage(image, pad.x, pad.y);
972
- return true;
973
- }
974
1084
  /**
975
1085
  * Fill a shadow's shape: in its colour where it has no blur, and otherwise
976
1086
  * as the context's shadow of the shape drawn `dx` to the left — clear of
977
1087
  * the window, its right edge at `right` before the move — with the shadow
978
1088
  * offset back by as much, so that only the shadow lands.
979
1089
  */
980
- function fillShadow(ctx, s, color, shape, right) {
1090
+ function fillShadow(ctx, s, color, shape, right, rule = 'nonzero') {
981
1091
  if (!(s.blur > 0)) {
982
1092
  ctx.fillStyle = color;
983
1093
  ctx.beginPath();
984
1094
  shape(0);
985
- ctx.fill();
1095
+ ctx.fill(rule);
986
1096
  return;
987
1097
  }
988
1098
  const dx = Math.ceil(right) + 1;
@@ -994,9 +1104,26 @@ function fillShadow(ctx, s, color, shape, right) {
994
1104
  ctx.fillStyle = '#000000';
995
1105
  ctx.beginPath();
996
1106
  shape(dx);
997
- ctx.fill();
1107
+ ctx.fill(rule);
998
1108
  ctx.restore();
999
1109
  }
1110
+ /**
1111
+ * A shadow's shape, spelled the way a 2d context recognises one: a `rect`,
1112
+ * or a `roundRect` with each corner's own radii, elliptical where the
1113
+ * corner is. react-x11's contexts draw the blurred shadow of such a shape —
1114
+ * and of a `rect` less such a shape, filled `evenodd` — from a tile they
1115
+ * keep, where one built of curves they blur afresh on every fill.
1116
+ */
1117
+ function shadowShape(ctx, x, y, w, h, c) {
1118
+ if (c.x.every((r) => r === 0) || c.y.every((r) => r === 0)) {
1119
+ ctx.rect(x, y, w, h);
1120
+ return;
1121
+ }
1122
+ // a circular corner as the number every context has always taken; only
1123
+ // an elliptical one as the point
1124
+ const corner = (i) => c.x[i] === c.y[i] ? c.x[i] : { x: c.x[i], y: c.y[i] };
1125
+ ctx.roundRect(x, y, w, h, [corner(0), corner(1), corner(2), corner(3)]);
1126
+ }
1000
1127
  /**
1001
1128
  * The band between two rounded rectangles, the inner inside the outer, in
1002
1129
  * the colour already set. Where a side has no band the two edges meet, and
@@ -1043,6 +1170,7 @@ function inFlow(parent, child, options) {
1043
1170
  return (style.overflowX === 'visible' &&
1044
1171
  style.overflowY === 'visible' &&
1045
1172
  opacityOf(child) >= 1 &&
1173
+ !masked(style) &&
1046
1174
  // containment makes it a stacking context, painted whole
1047
1175
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1048
1176
  }
@@ -1068,6 +1196,7 @@ function flowFlex(parent, child, options) {
1068
1196
  return (style.overflowX === 'visible' &&
1069
1197
  style.overflowY === 'visible' &&
1070
1198
  !(child.outOfFlow && style.clip) &&
1199
+ !masked(style) &&
1071
1200
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1072
1201
  }
1073
1202
  /**
@@ -1294,7 +1423,55 @@ function paintPositioned(ctx, box, options) {
1294
1423
  return;
1295
1424
  }
1296
1425
  }
1297
- paintBox(ctx, box, options);
1426
+ paintBox(ctx, box, atViewport(box, options));
1427
+ }
1428
+ /**
1429
+ * The options a box is painted with where it is fixed to the viewport
1430
+ * (CSS 2.1 9.6.1): laid out against the viewport at the document's top,
1431
+ * and drawn where the viewport is now, however far the pane that scrolls
1432
+ * the element has moved it — a fixed header scrolled away with the text.
1433
+ * Once for a box and all it holds.
1434
+ */
1435
+ function atViewport(box, options) {
1436
+ const viewport = options.viewport;
1437
+ if (!viewport || options.atViewport || !fixedToViewport(box)) {
1438
+ return options;
1439
+ }
1440
+ // where the document's top left is from the viewport's, less where the
1441
+ // viewport is in the document: the scroll
1442
+ const root = rootOf(box);
1443
+ const dx = viewport.x - options.originX - root.x;
1444
+ const dy = viewport.y - options.originY - root.y;
1445
+ return {
1446
+ ...options,
1447
+ originX: options.originX + dx,
1448
+ originY: options.originY + dy,
1449
+ atViewport: true,
1450
+ };
1451
+ }
1452
+ /** Per tree, its boxes fixed to the viewport (`fixedToViewport`), found
1453
+ * as the layout positions them: what the element answers a scroll pane's
1454
+ * blit with (`HtmlViewNode.viewportFixedRects`). */
1455
+ export const FIXED_BOXES = new WeakMap();
1456
+ /** Whether a box is fixed to the viewport: `position: fixed` with no
1457
+ * transformed or contained box around it, which would be its containing
1458
+ * block instead (CSS Transforms 1, CSS Containment 2, 3.3). */
1459
+ export function fixedToViewport(box) {
1460
+ if (box.style.position !== 'fixed')
1461
+ return false;
1462
+ for (let at = box.parent; at?.parent; at = at.parent) {
1463
+ if (at.style.translate || at.style.transformTranslate)
1464
+ return false;
1465
+ if (contained(at, CONTAIN_LAYOUT | CONTAIN_PAINT))
1466
+ return false;
1467
+ }
1468
+ return true;
1469
+ }
1470
+ function rootOf(box) {
1471
+ let root = box;
1472
+ while (root.parent)
1473
+ root = root.parent;
1474
+ return root;
1298
1475
  }
1299
1476
  /**
1300
1477
  * The box an out-of-flow box is positioned in, whose clips are the ones it
@@ -1713,6 +1890,9 @@ function stacksLayers(box) {
1713
1890
  return true;
1714
1891
  if (style.opacity < 1 || FADED_BLOCKS.has(box))
1715
1892
  return true;
1893
+ // and so does a mask, which is applied to the group (CSS Masking 1, 7)
1894
+ if (masked(style))
1895
+ return true;
1716
1896
  // layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
1717
1897
  if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
1718
1898
  return true;
@@ -1732,7 +1912,7 @@ function flexItem(box) {
1732
1912
  * outside a box that clips where its containing block is, a fixed one
1733
1913
  * outside every one, and a relative one inside every one.
1734
1914
  */
1735
- function clipsFor(box, context) {
1915
+ export function clipsFor(box, context) {
1736
1916
  let from = box.parent;
1737
1917
  if (box.outOfFlow) {
1738
1918
  const fixed = box.style.position === 'fixed';
@@ -1759,6 +1939,7 @@ function clipsFor(box, context) {
1759
1939
  * parent painted it, it was. A clip of no area leaves none of it to paint.
1760
1940
  */
1761
1941
  function paintStacked(ctx, box, options) {
1942
+ options = atViewport(box, options);
1762
1943
  if (!intersects(box, options))
1763
1944
  return;
1764
1945
  const between = CLIPS_BETWEEN.get(box);
@@ -1922,7 +2103,8 @@ function paintBackground(ctx, box, options, style) {
1922
2103
  // box, which is where it starts, and repeated under the borders — or
1923
2104
  // the viewport's, where it is fixed; its line runs across the whole of
1924
2105
  // it, not the part this paint reaches
1925
- const fixed = style.backgroundAttachment === 'fixed' && options.canvas;
2106
+ const fixed = style.backgroundAttachment === 'fixed' &&
2107
+ (options.viewport ?? options.canvas);
1926
2108
  const origin = fixed || originBox(box, options, style);
1927
2109
  const at = snapped(origin.x, origin.y, origin.width, origin.height);
1928
2110
  if (rounded && style.backgroundSize !== 'auto' && ctx.clip) {
@@ -2298,7 +2480,7 @@ const MAX_TILES = 4096;
2298
2480
  */
2299
2481
  function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
2300
2482
  if (style.backgroundAttachment === 'fixed' && options.canvas) {
2301
- at = options.canvas;
2483
+ at = options.viewport ?? options.canvas;
2302
2484
  }
2303
2485
  const url = style.backgroundImage;
2304
2486
  const loaded = url ? options.backgroundImageFor?.(url) : null;
@@ -3359,7 +3541,7 @@ function paintLines(ctx, box, options) {
3359
3541
  for (const line of visible) {
3360
3542
  if (line.background)
3361
3543
  paintLineBackground(ctx, line, options);
3362
- paintInlineBoxes(ctx, line, options, line === lines[0] ? null : bleeds);
3544
+ paintInlineBoxes(ctx, line, lines, options, line === lines[0] ? null : bleeds);
3363
3545
  for (const text of line.texts) {
3364
3546
  const natural = text.layout.lines[text.layoutLine];
3365
3547
  if (natural)
@@ -3803,27 +3985,22 @@ function paintLineBackground(ctx, line, options) {
3803
3985
  style: background.style,
3804
3986
  }, options);
3805
3987
  }
3806
- function paintInlineBoxes(ctx, line, options,
3807
- /** Where a fragment that reaches up over the lines before goes, to be
3808
- * drawn again over their text; null for a block's first line. */
3809
- bleeds = null) {
3810
- let fragments = null;
3811
- const widen = (box, left, right) => {
3812
- fragments ??= new Map();
3813
- const f = fragments.get(box);
3814
- if (f) {
3815
- f.left = Math.min(f.left, left);
3816
- f.right = Math.max(f.right, right);
3817
- }
3818
- else {
3819
- fragments.set(box, { left, right, start: false, end: false });
3820
- }
3821
- };
3822
- // Every text on a line is drawn on one baseline (`finishLine`), and an
3823
- // engine's is from the top of its layout rather than of the line. Taken
3824
- // where the line has each text: a box `position: relative` moves is
3825
- // moved by its own offset below, and the boxes around it are not.
3826
- let baseline = line.y + line.baseline;
3988
+ /**
3989
+ * The fragments of the decorated inline boxes on a line: how far across it
3990
+ * each reaches, its border box, and whether the box opens or closes on it.
3991
+ * Where the line holds text `position: relative` moved, taken where the
3992
+ * text was: a fragment is moved by its own box's offset.
3993
+ *
3994
+ * A box is more than one fragment on a line where bidi reordering puts
3995
+ * what is not the box's between parts of it (CSS 2.1 9.10): a letter of
3996
+ * another element, an atomic, another element's edge. Its parts are one
3997
+ * fragment wherever nothing else comes between them, whatever room is
3998
+ * between them — a space it hangs, a box of its own inside it — so a line
3999
+ * that reads one way has one fragment of each box, as it always had.
4000
+ */
4001
+ function fragmentsOn(line) {
4002
+ /** What is on the line, where, and which decorated boxes it is in. */
4003
+ const marks = [];
3827
4004
  const shifted = SHIFTED_LINES.has(line);
3828
4005
  for (const text of line.texts) {
3829
4006
  const natural = text.layout.lines[text.layoutLine];
@@ -3831,8 +4008,6 @@ bleeds = null) {
3831
4008
  if (!natural || !boxAt)
3832
4009
  continue;
3833
4010
  const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
3834
- const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
3835
- baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
3836
4011
  const x = text.drawX - (shift?.x ?? 0) + natural.x;
3837
4012
  for (const run of natural.runs) {
3838
4013
  const owner = boxAt.call(text.spans, run.start);
@@ -3844,45 +4019,147 @@ bleeds = null) {
3844
4019
  const right = Math.min(x + natural.width, x + run.x + run.width);
3845
4020
  if (right <= left)
3846
4021
  continue;
3847
- for (const box of decoratedAncestors(owner))
3848
- widen(box, left, right);
4022
+ marks.push({ left, right, boxes: decoratedAncestors(owner) });
4023
+ }
4024
+ // and the spaces `pre-wrap` keeps that the line ends on, which hang
4025
+ // past it and are their box's all the same (`LineText.hung`)
4026
+ for (const space of text.hung ?? []) {
4027
+ const owner = boxAt.call(text.spans, space.at);
4028
+ if (!owner)
4029
+ continue;
4030
+ const left = text.drawX - (shift?.x ?? 0) + space.x;
4031
+ marks.push({
4032
+ left,
4033
+ right: left + space.width,
4034
+ boxes: decoratedAncestors(owner),
4035
+ });
3849
4036
  }
3850
4037
  }
3851
4038
  for (const placed of line.atomics) {
3852
4039
  const atomic = placed.box;
3853
- for (const box of decoratedAncestors(atomic)) {
3854
- widen(box, placed.x - atomic.marginLeft, placed.x + atomic.width + atomic.marginRight);
3855
- }
4040
+ marks.push({
4041
+ left: placed.x - atomic.marginLeft,
4042
+ right: placed.x + atomic.width + atomic.marginRight,
4043
+ boxes: decoratedAncestors(atomic),
4044
+ });
3856
4045
  }
3857
4046
  for (const edge of line.edges ?? []) {
3858
- const box = edge.box;
3859
- if (!box.decoration)
3860
- continue;
3861
- // The element's `direction` says which side its start is on, whatever
3862
- // its text reads as (CSS 2.1 8.6); the edge's margin is outside the box,
3863
- // its border and padding inside.
3864
- const onLeft = (edge.side === 'start') !== (box.style.direction === 'rtl');
3865
- if (onLeft) {
3866
- const left = edge.x + box.marginLeft;
3867
- widen(box, left, left);
4047
+ marks.push({
4048
+ left: edge.x,
4049
+ right: edge.x + edge.width,
4050
+ boxes: decoratedAncestors(edge.box),
4051
+ edge,
4052
+ });
4053
+ }
4054
+ let fragments = null;
4055
+ // each box's parts, left to right
4056
+ const parts = new Map();
4057
+ for (const mark of marks) {
4058
+ for (const box of mark.boxes) {
4059
+ let list = parts.get(box);
4060
+ if (!list)
4061
+ parts.set(box, (list = []));
4062
+ list.push(mark);
3868
4063
  }
3869
- else {
3870
- const right = edge.x + edge.width - box.marginRight;
3871
- widen(box, right, right);
4064
+ const own = mark.edge?.box;
4065
+ if (own?.decoration) {
4066
+ let list = parts.get(own);
4067
+ if (!list)
4068
+ parts.set(own, (list = []));
4069
+ list.push(mark);
3872
4070
  }
3873
- const f = fragments.get(box);
3874
- if (edge.side === 'start')
3875
- f.start = true;
3876
- else
3877
- f.end = true;
3878
4071
  }
4072
+ for (const [box, list] of parts) {
4073
+ list.sort((a, b) => a.left - b.left || a.right - b.right);
4074
+ const out = [];
4075
+ let open = null;
4076
+ let reach = -Infinity;
4077
+ for (const mark of list) {
4078
+ // The element's `direction` says which side its start is on, whatever
4079
+ // its text reads as (CSS 2.1 8.6); its own edge's margin is outside
4080
+ // the box, its border and padding inside.
4081
+ let left = mark.left;
4082
+ let right = mark.right;
4083
+ const own = mark.edge?.box === box ? mark.edge : null;
4084
+ if (own) {
4085
+ const onLeft = (own.side === 'start') !== (box.style.direction === 'rtl');
4086
+ if (onLeft)
4087
+ left = right = own.x + box.marginLeft;
4088
+ else
4089
+ left = right = own.x + own.width - box.marginRight;
4090
+ }
4091
+ if (!open || between(marks, box, reach, mark.left)) {
4092
+ open = { left, right, start: false, end: false };
4093
+ out.push(open);
4094
+ }
4095
+ else {
4096
+ open.left = Math.min(open.left, left);
4097
+ open.right = Math.max(open.right, right);
4098
+ }
4099
+ reach = Math.max(reach, mark.right);
4100
+ if (own) {
4101
+ if (own.side === 'start')
4102
+ open.start = true;
4103
+ else
4104
+ open.end = true;
4105
+ }
4106
+ }
4107
+ (fragments ??= new Map()).set(box, out);
4108
+ }
4109
+ return fragments;
4110
+ }
4111
+ /** Whether anything on a line that is not in a box lies between two of its
4112
+ * parts, from `left` to `right`: what splits it into two fragments. */
4113
+ function between(marks, box, left, right) {
4114
+ if (right - left < 0.5)
4115
+ return false;
4116
+ for (const mark of marks) {
4117
+ if (mark.right - mark.left < 0.5)
4118
+ continue;
4119
+ if (mark.boxes.includes(box) || mark.edge?.box === box)
4120
+ continue;
4121
+ const middle = (mark.left + mark.right) / 2;
4122
+ if (middle > left && middle < right)
4123
+ return true;
4124
+ }
4125
+ return false;
4126
+ }
4127
+ function paintInlineBoxes(ctx, line,
4128
+ /** The block's lines, which `line` is one of. */
4129
+ lines, options,
4130
+ /** Where a fragment that reaches up over the lines before goes, to be
4131
+ * drawn again over their text; null for a block's first line. */
4132
+ bleeds = null) {
4133
+ const fragments = fragmentsOn(line);
3879
4134
  if (!fragments)
3880
4135
  return;
4136
+ // Every text on a line is drawn on one baseline (`finishLine`), and an
4137
+ // engine's is from the top of its layout rather than of the line. Taken
4138
+ // where the line has each text: a box `position: relative` moves is
4139
+ // moved by its own offset below, and the boxes around it are not.
4140
+ let baseline = line.y + line.baseline;
4141
+ const shifted = SHIFTED_LINES.has(line);
4142
+ for (const text of line.texts) {
4143
+ const natural = text.layout.lines[text.layoutLine];
4144
+ if (!natural || !text.spans.boxAt)
4145
+ continue;
4146
+ const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
4147
+ const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
4148
+ baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
4149
+ }
3881
4150
  const boxes = [...fragments.keys()].sort((a, b) => depthOf(a) - depthOf(b));
3882
4151
  for (const box of boxes) {
3883
4152
  if (box.style.visibility !== 'visible')
3884
4153
  continue;
3885
- const f = fragments.get(box);
4154
+ for (const f of fragments.get(box)) {
4155
+ paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline);
4156
+ }
4157
+ }
4158
+ }
4159
+ /** One fragment of an inline box on a line (`paintInlineBoxes`). */
4160
+ function paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline) {
4161
+ const shifted = SHIFTED_LINES.has(line);
4162
+ {
3886
4163
  const face = box.decoration;
3887
4164
  // on its own baseline, which `vertical-align` may raise off the line's
3888
4165
  const own = shifted
@@ -3917,10 +4194,15 @@ bleeds = null) {
3917
4194
  style,
3918
4195
  };
3919
4196
  if (fragment.width <= 0)
3920
- continue;
3921
- // its images from its own padding box, as its gradients are, each
3922
- // fragment alike: `slice` would lay them out as though the fragments
3923
- // were one box end to end, which a box on one line is
4197
+ return;
4198
+ // its images and gradients from the strip its fragments make, where it
4199
+ // goes on to another line, and from its own padding box where it is
4200
+ // on this one alone or `clone` says each fragment is its own
4201
+ if (!(f.start && f.end) && sliced(box.style)) {
4202
+ const strip = stripFor(lines, line, box, fragment);
4203
+ if (strip)
4204
+ fragment.strip = strip;
4205
+ }
3924
4206
  paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
3925
4207
  paintBorders(ctx, fragment, options);
3926
4208
  if (box.style.outlineStyle !== 'none')
@@ -3930,6 +4212,76 @@ bleeds = null) {
3930
4212
  }
3931
4213
  }
3932
4214
  }
4215
+ /** Whether an inline box places background images or gradients, which
4216
+ * `slice` places across its fragments, as one box end to end. */
4217
+ function sliced(style) {
4218
+ return (style.boxDecorationBreak === 'slice' &&
4219
+ (style.backgroundImage !== null ||
4220
+ style.backgroundGradient !== null ||
4221
+ style.backgroundImages !== null));
4222
+ }
4223
+ /** Of a block's lines, by the array they are, the strip of each inline box
4224
+ * on them that `sliced` says places images across its fragments: made
4225
+ * once a layout, as `MOVED_OFF_LINES` is, so a repaint of one line reads
4226
+ * the lines before it without going over them again. What `sliced` reads
4227
+ * is never restyled in place (`HtmlViewNode`), which lays the lines out
4228
+ * again instead. */
4229
+ const STRIPS = new WeakMap();
4230
+ function stripsOf(lines) {
4231
+ const strips = new Map();
4232
+ for (const line of lines) {
4233
+ const fragments = fragmentsOn(line);
4234
+ if (!fragments)
4235
+ continue;
4236
+ for (const [box, list] of fragments) {
4237
+ if (!sliced(box.style))
4238
+ continue;
4239
+ let strip = strips.get(box);
4240
+ if (!strip) {
4241
+ strip = { width: 0, lines: 0, before: new Map() };
4242
+ strips.set(box, strip);
4243
+ }
4244
+ strip.before.set(line, strip.width);
4245
+ for (const f of list)
4246
+ strip.width += Math.max(0, f.right - f.left);
4247
+ strip.lines += 1;
4248
+ }
4249
+ }
4250
+ return strips;
4251
+ }
4252
+ /**
4253
+ * The box an inline box's fragment on `line` is a slice of (CSS
4254
+ * Fragmentation 3, 5.4): the box's fragments laid end to end in its
4255
+ * direction, as though it had not wrapped — as wide as
4256
+ * they are together, with the box's own borders and padding at its two
4257
+ * ends, placed so that this fragment is where it falls along it. So an
4258
+ * image `no-repeat` at the start is on the first fragment alone, and a
4259
+ * gradient runs once across them all. Undefined where the box is on one
4260
+ * line.
4261
+ */
4262
+ function stripFor(lines, line, box, fragment) {
4263
+ let strips = STRIPS.get(lines);
4264
+ if (!strips) {
4265
+ strips = stripsOf(lines);
4266
+ STRIPS.set(lines, strips);
4267
+ }
4268
+ const strip = strips.get(box);
4269
+ const before = strip?.before.get(line);
4270
+ if (!strip || strip.lines < 2 || before === undefined)
4271
+ return undefined;
4272
+ const x = box.style.direction === 'rtl'
4273
+ ? fragment.x + fragment.width + before - strip.width
4274
+ : fragment.x - before;
4275
+ return {
4276
+ ...fragment,
4277
+ x,
4278
+ width: strip.width,
4279
+ borderLeft: box.borderLeft,
4280
+ borderRight: box.borderRight,
4281
+ padLeft: box.padLeft,
4282
+ padRight: box.padRight,
4283
+ };
4284
+ }
3933
4285
  /** An inline box's style on a line it does not both start and end on: no
3934
4286
  * rounded corner on a side it goes on from. Made once a style and a pair
3935
4287
  * of ends, so a repaint copies no style and the layers made of it are
@@ -3990,6 +4342,20 @@ function decoratedAncestors(box) {
3990
4342
  DECORATED.set(box, found);
3991
4343
  return found;
3992
4344
  }
4345
+ /** A restyle in place (a `:hover`) gave an inline box a decoration where it
4346
+ * had none, or took it away, and the lists kept above do not know: every
4347
+ * box whose list could name it — inside it, through the inline boxes the
4348
+ * walk above climbs — looks again. A document built again has new boxes,
4349
+ * and so no lists. */
4350
+ export function forgetDecoratedAncestors(box) {
4351
+ const stack = [...box.children];
4352
+ while (stack.length) {
4353
+ const at = stack.pop();
4354
+ DECORATED.delete(at);
4355
+ if (at.kind === 'inline')
4356
+ stack.push(...at.children);
4357
+ }
4358
+ }
3993
4359
  /**
3994
4360
  * The band the document selection covers on one line.
3995
4361
  *