@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
@@ -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,22 @@ 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();
46
+ /**
47
+ * How far above and below a block's lines its inline content draws, at
48
+ * most, by the array its lines are: glyphs taller than a `line-height` under
49
+ * their face's height, and an atomic's ink past the line it is on. The
50
+ * slack a paint's cull of the lines takes on top of their own rows
51
+ * (`paintLines`); none for the lines of almost every block, whose content
52
+ * is inside them.
53
+ */
54
+ const LINE_INK = new WeakMap();
39
55
  export function computePaintBounds(box, moved = false) {
40
56
  // A box with no rectangle of its own gives only what it holds: nothing,
41
57
  // until a child with bounds is met.
@@ -47,8 +63,12 @@ export function computePaintBounds(box, moved = false) {
47
63
  // How far down the content reaches, for the document's height: the
48
64
  // border box, and every box and line under it, but not past a box that
49
65
  // clips what it holds — where the scrollable overflow ends. Out-of-flow
50
- // boxes are the layout's to count.
51
- let bottom = y2;
66
+ // boxes are the layout's to count. A border box of no area reaches
67
+ // nowhere, as a browser has it (Blink's `ScrollableOverflowCalculator`
68
+ // adds no empty rect): the empty blocks a design leaves at its end,
69
+ // below the last one's bottom margin, made the page that margin taller
70
+ const empty = own && !(box.width > 0 && box.height > 0);
71
+ let bottom = empty ? -Infinity : y2;
52
72
  // A shadow is ink past the box, and no overflow: a repaint of the strip
53
73
  // under a card has to reach the card, and the document is no taller.
54
74
  const shadows = own ? box.style.boxShadow : null;
@@ -71,6 +91,25 @@ export function computePaintBounds(box, moved = false) {
71
91
  x2 = Math.max(x2, box.x + box.width + reach);
72
92
  y2 = Math.max(y2, box.y + box.height + reach);
73
93
  }
94
+ // An inline box's fragments are boxes on the lines they are on, and its
95
+ // padding and border reach above and below those lines: they count in
96
+ // the scrollable overflow of the block it is in (CSS Overflow 3, 2.2) —
97
+ // Blink adds each inline box fragment's border box
98
+ // (`ScrollableOverflowCalculator::AddItemsInternal`). Design 150's footer
99
+ // links, 50px of padding under their text, made Chrome's page 19px
100
+ // taller than the box they end. Only a box with padding or a border
101
+ // above or below its text can reach past its lines.
102
+ if (box.kind === 'inline' &&
103
+ box.padTop + box.padBottom + box.borderTop + box.borderBottom > 0) {
104
+ const reach = inlineFragmentsReach(box);
105
+ if (reach) {
106
+ x1 = Math.min(x1, reach.x1);
107
+ y1 = Math.min(y1, reach.y1);
108
+ x2 = Math.max(x2, reach.x2);
109
+ y2 = Math.max(y2, reach.y2);
110
+ bottom = Math.max(bottom, reach.y2);
111
+ }
112
+ }
74
113
  const lines = box.lines;
75
114
  if (lines) {
76
115
  let tallest = 0;
@@ -127,6 +166,8 @@ export function computePaintBounds(box, moved = false) {
127
166
  const reach = computePaintBounds(child, moved);
128
167
  if (!child.outOfFlow)
129
168
  bottom = Math.max(bottom, reach);
169
+ else
170
+ OUT_OF_FLOW_REACH.set(child, reach);
130
171
  if (child.boundsY === Infinity)
131
172
  continue;
132
173
  x1 = Math.min(x1, child.boundsX);
@@ -138,8 +179,21 @@ export function computePaintBounds(box, moved = false) {
138
179
  // above has its bounds already: walked from its line as well, an
139
180
  // inline-block in an inline-block was walked twice a level, and twenty of
140
181
  // them took seventy milliseconds a layout.
182
+ //
183
+ // And a line's text is drawn from its face's ascent above the baseline to
184
+ // its descent below, its content area (CSS 2.1 10.6.1), which is past the
185
+ // line box wherever `line-height` is under the face's height: a line box
186
+ // is its line-height, whatever its glyphs are. Zen Garden 215's 91px title
187
+ // sits on the 20px lines its body's `line-height: 1.25em` gave it, as a
188
+ // length, and hangs 40px above them and 40px below — ink that a repaint
189
+ // of those rows has to reach, where the lines alone do not.
190
+ let above = 0;
191
+ let below = 0;
141
192
  if (lines) {
193
+ const movedOff = MOVED_OFF_LINES.get(lines);
142
194
  for (const line of lines) {
195
+ let top = line.y;
196
+ let bottom = line.y + line.height;
143
197
  for (const placed of line.atomics) {
144
198
  const atomic = placed.box;
145
199
  if (atomic.boundsY === Infinity)
@@ -148,9 +202,35 @@ export function computePaintBounds(box, moved = false) {
148
202
  y1 = Math.min(y1, atomic.boundsY);
149
203
  x2 = Math.max(x2, atomic.boundsX + atomic.boundsWidth);
150
204
  y2 = Math.max(y2, atomic.boundsY + atomic.boundsHeight);
205
+ top = Math.min(top, atomic.boundsY);
206
+ bottom = Math.max(bottom, atomic.boundsY + atomic.boundsHeight);
207
+ }
208
+ // what `position: relative` moved off a line the cull finds on its
209
+ // own (`reachedOff`), wherever it went
210
+ if (movedOff?.has(line))
211
+ continue;
212
+ for (const text of line.texts) {
213
+ const natural = text.layout.lines[text.layoutLine];
214
+ if (!natural)
215
+ continue;
216
+ const baseline = text.drawY + natural.baseline;
217
+ top = Math.min(top, baseline - (natural.ascent ?? 0));
218
+ bottom = Math.max(bottom, baseline + (natural.descent ?? 0));
219
+ }
220
+ if (top < line.y) {
221
+ y1 = Math.min(y1, top);
222
+ above = Math.max(above, line.y - top);
223
+ }
224
+ if (bottom > line.y + line.height) {
225
+ y2 = Math.max(y2, bottom);
226
+ below = Math.max(below, bottom - line.y - line.height);
151
227
  }
152
228
  }
153
229
  }
230
+ if (lines && (above > 0 || below > 0))
231
+ LINE_INK.set(lines, { above, below });
232
+ else if (lines)
233
+ LINE_INK.delete(lines);
154
234
  if (x1 === Infinity) {
155
235
  // nothing to draw: no damage meets it, and no parent takes it in
156
236
  box.boundsX = Infinity;
@@ -174,7 +254,7 @@ export function computePaintBounds(box, moved = false) {
174
254
  return bottom;
175
255
  // whether the box clips only matters where its content reaches past it,
176
256
  // and its style is one more object a walk of every box would read
177
- const end = box.y + box.height;
257
+ const end = empty ? -Infinity : box.y + box.height;
178
258
  if (bottom <= end)
179
259
  return end;
180
260
  const style = box.style;
@@ -200,6 +280,56 @@ export function hasRect(box) {
200
280
  const display = box.style.display;
201
281
  return display !== 'table-column' && display !== 'table-column-group';
202
282
  }
283
+ /**
284
+ * Where an inline box's fragments are, over the lines of the block it is
285
+ * in that hold its text: from its face's ascent and its top padding and
286
+ * border above each line's baseline to its descent and its bottom ones
287
+ * below, as `paintInlineBoxes` draws them. Null for a box with no text or
288
+ * no face.
289
+ */
290
+ function inlineFragmentsReach(box) {
291
+ const face = box.decoration ?? PADDED_FACES.get(box);
292
+ if (!face || box.subtreeTextEnd <= box.subtreeTextStart)
293
+ return null;
294
+ let block = box.parent;
295
+ while (block && block.kind === 'inline')
296
+ block = block.parent;
297
+ const lines = block?.lines;
298
+ if (!lines?.length)
299
+ return null;
300
+ const from = box.subtreeTextStart;
301
+ const to = box.subtreeTextEnd;
302
+ let lo = 0;
303
+ let hi = lines.length;
304
+ while (lo < hi) {
305
+ const mid = (lo + hi) >> 1;
306
+ if (lines[mid].textEnd > from)
307
+ hi = mid;
308
+ else
309
+ lo = mid + 1;
310
+ }
311
+ const moved = offsetOf(box);
312
+ let x1 = Infinity;
313
+ let y1 = Infinity;
314
+ let x2 = -Infinity;
315
+ let y2 = -Infinity;
316
+ for (let i = lo; i < lines.length; i += 1) {
317
+ const line = lines[i];
318
+ if (line.textStart >= to)
319
+ break;
320
+ if (line.textEnd <= from)
321
+ continue;
322
+ const raise = SHIFTED_LINES.has(line)
323
+ ? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
324
+ : 0;
325
+ const baseline = line.y + line.baseline - raise + (moved?.y ?? 0);
326
+ x1 = Math.min(x1, line.x);
327
+ x2 = Math.max(x2, line.x + line.width);
328
+ y1 = Math.min(y1, baseline - face.ascent - box.padTop - box.borderTop);
329
+ y2 = Math.max(y2, baseline + face.descent + box.padBottom + box.borderBottom);
330
+ }
331
+ return x1 === Infinity ? null : { x1, y1, x2, y2 };
332
+ }
203
333
  /** Children lists past this size get the sorted viewport index; below it a
204
334
  * linear scan is cheaper than keeping one. */
205
335
  const PAINT_INDEX_MIN = 64;
@@ -287,6 +417,7 @@ export function paintDocument(ctx, tree, options) {
287
417
  ...options,
288
418
  canvasSource: canvas?.source,
289
419
  negative: tree.negative,
420
+ selectionStyler: options.selection ? tree.selectionStyler : null,
290
421
  });
291
422
  ctx.restore();
292
423
  }
@@ -370,7 +501,7 @@ function paintCanvas(ctx, { style: source, anchor }, root, options) {
370
501
  // browser does — the stripes a short page with a gradient on its
371
502
  // body shows — or by the viewport, where it is fixed
372
503
  const box = style.backgroundAttachment === 'fixed' && options.canvas
373
- ? options.canvas
504
+ ? (options.viewport ?? options.canvas)
374
505
  : originBox(anchor, options, style);
375
506
  paintGradient(ctx, style, style.backgroundGradient, area, snapped(box.x, box.y, box.width, box.height));
376
507
  }
@@ -458,8 +589,10 @@ function paddingBox(box, options) {
458
589
  };
459
590
  }
460
591
  /** 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) {
592
+ * the box its `background-origin` names (CSS Backgrounds 3, 3.8) — of the
593
+ * whole strip, for a fragment of an inline box that is sliced. */
594
+ function originBox(frame, options, style) {
595
+ const box = frame.strip ?? frame;
463
596
  const [t, r, b, l] = edgeInsets(box, style.backgroundOrigin);
464
597
  return {
465
598
  x: box.x + l + options.originX,
@@ -525,8 +658,14 @@ function paintBox(ctx, box, options) {
525
658
  ctx.save();
526
659
  ctx.globalAlpha = ctx.globalAlpha * opacity;
527
660
  }
661
+ if (masked(box.style) && paintMasked(ctx, box, options)) {
662
+ if (fade)
663
+ ctx.restore();
664
+ return;
665
+ }
528
666
  // `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)
667
+ // background and borders among it (CSS 2.1 11.1.2) — written out here,
668
+ // not called: a frame more a level and a deep document ran out of stack
530
669
  const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
531
670
  if (!clip || (clip.w > 0 && clip.h > 0)) {
532
671
  const clipped = !!clip && pushClip(ctx, clip, null);
@@ -537,6 +676,135 @@ function paintBox(ctx, box, options) {
537
676
  if (fade)
538
677
  ctx.restore();
539
678
  }
679
+ /** A box and what it holds, cut to its `clip` as `paintBox` cuts it. */
680
+ function paintClipped(ctx, box, options) {
681
+ const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
682
+ if (clip && !(clip.w > 0 && clip.h > 0))
683
+ return;
684
+ const clipped = !!clip && pushClip(ctx, clip, null);
685
+ paintContent(ctx, box, options);
686
+ if (clipped)
687
+ ctx.restore();
688
+ }
689
+ const MASK_LAYERS = new WeakMap();
690
+ /** No corners: a mask layer is placed in the box's rectangle, which its
691
+ * radii do not round (CSS Masking 1, 7). */
692
+ const SQUARE_RADII = [0, 0, 0, 0];
693
+ /**
694
+ * A style for each of a box's mask layers, top first, as `layersOf` makes
695
+ * a background's: the box's own style with the layer's image, repeat, size,
696
+ * position, origin and clip in its background's place, and no colour — so
697
+ * the background painter places and repeats a mask layer as it does a
698
+ * background layer, which is what CSS Masking 1 says a mask layer is (7).
699
+ * Made once a style.
700
+ */
701
+ function maskLayersOf(style) {
702
+ let layers = MASK_LAYERS.get(style);
703
+ if (layers)
704
+ return layers;
705
+ const mask = style.mask;
706
+ const nth = (list, i) => list[i % list.length];
707
+ layers = mask.images.map((image, i) => {
708
+ const layer = Object.create(style);
709
+ layer.backgroundColor = null;
710
+ layer.backgroundImages = null;
711
+ layer.backgroundImage = typeof image === 'string' ? image : null;
712
+ layer.backgroundGradient =
713
+ image !== null && typeof image !== 'string' ? image : null;
714
+ layer.backgroundRepeat = nth(mask.repeats, i);
715
+ layer.backgroundSize = nth(mask.sizes, i);
716
+ layer.backgroundAttachment = 'scroll';
717
+ [layer.backgroundPositionX, layer.backgroundPositionY] = nth(mask.positions, i);
718
+ layer.backgroundOrigin = nth(mask.origins, i);
719
+ layer.backgroundClip = nth(mask.clips, i);
720
+ layer.backgroundClipText = false;
721
+ layer.borderRadius = SQUARE_RADII;
722
+ layer.borderRadiusY = null;
723
+ return layer;
724
+ });
725
+ MASK_LAYERS.set(style, layers);
726
+ return layers;
727
+ }
728
+ /**
729
+ * A masked box (CSS Masking 1, 7): the box and what it holds drawn on a
730
+ * surface of their own, which the alpha of its mask layers — painted as a
731
+ * background's are, on a second surface, each added over the ones below
732
+ * it — then cuts with `destination-in`, and the result drawn in its place,
733
+ * as the group a mask makes. Only the mask painting area, the border box,
734
+ * can show, and only its part in the damage is drawn.
735
+ *
736
+ * A layer whose image has not arrived is transparent black (7.2), so a box
737
+ * none of whose layers can be drawn yet draws nothing: an icon masked out
738
+ * of its `background-color` is not a solid square while its image loads.
739
+ * False where there is no surface to draw on, or no `destination-in` to
740
+ * draw with, and the caller paints the box unmasked.
741
+ */
742
+ function paintMasked(ctx, box, options) {
743
+ const layers = maskLayersOf(box.style);
744
+ const drawable = layers.some((layer) => !!layer.backgroundGradient ||
745
+ (!!layer.backgroundImage &&
746
+ !!options.backgroundImageFor?.(layer.backgroundImage)));
747
+ if (!drawable)
748
+ return true;
749
+ if (!options.surface || !ctx.drawImage)
750
+ return false;
751
+ let x0 = Math.floor(box.x + options.originX);
752
+ let y0 = Math.floor(frameY(box) + options.originY);
753
+ let x1 = Math.ceil(box.x + box.width + options.originX);
754
+ let y1 = Math.ceil(frameY(box) + frameHeight(box) + options.originY);
755
+ const damage = options.damage;
756
+ if (damage) {
757
+ x0 = Math.max(x0, Math.floor(damage.x));
758
+ y0 = Math.max(y0, Math.floor(damage.y));
759
+ x1 = Math.min(x1, Math.ceil(damage.x + damage.width));
760
+ y1 = Math.min(y1, Math.ceil(damage.y + damage.height));
761
+ }
762
+ const w = x1 - x0;
763
+ const h = y1 - y0;
764
+ if (!(w > 0 && h > 0))
765
+ return true;
766
+ const content = options.surface(w, h);
767
+ const mask = content && options.surface(w, h);
768
+ try {
769
+ if (!content || !mask)
770
+ return false;
771
+ const cctx = content.getContext('2d');
772
+ const mctx = mask.getContext('2d');
773
+ // the surfaces' own coordinates, the window's moved to their corner
774
+ const on = {
775
+ ...options,
776
+ originX: options.originX - x0,
777
+ originY: options.originY - y0,
778
+ damage: { x: 0, y: 0, width: w, height: h },
779
+ canvas: options.canvas && {
780
+ ...options.canvas,
781
+ x: options.canvas.x - x0,
782
+ y: options.canvas.y - y0,
783
+ },
784
+ viewport: options.viewport && {
785
+ ...options.viewport,
786
+ x: options.viewport.x - x0,
787
+ y: options.viewport.y - y0,
788
+ },
789
+ clips: [],
790
+ };
791
+ for (let i = layers.length - 1; i >= 0; i -= 1) {
792
+ paintLayer(mctx, box, on, layers[i], frameImages(mctx, box, on));
793
+ }
794
+ paintClipped(cctx, box, on);
795
+ cctx.globalCompositeOperation = 'destination-in';
796
+ if (cctx.globalCompositeOperation !== 'destination-in')
797
+ return false;
798
+ cctx.drawImage(mask, 0, 0);
799
+ cctx.globalCompositeOperation = 'source-over';
800
+ ctx.drawImage(content, x0, y0);
801
+ return true;
802
+ }
803
+ finally {
804
+ content?.destroy?.();
805
+ mask?.destroy?.();
806
+ }
807
+ }
540
808
  function paintContent(ctx, box, options) {
541
809
  const visible = box.style.visibility === 'visible';
542
810
  if (visible)
@@ -779,9 +1047,6 @@ function paintShadows(ctx, box, options, inset) {
779
1047
  }, 0);
780
1048
  continue;
781
1049
  }
782
- if (bakedOuter(ctx, options, s, color, shape, around, covered ? null : { rect: own, corners })) {
783
- continue;
784
- }
785
1050
  // what of the shadow falls under the box is not drawn
786
1051
  const clipped = !covered && ctx.clip && ctx.rect && ctx.save;
787
1052
  if (clipped) {
@@ -792,7 +1057,7 @@ function paintShadows(ctx, box, options, inset) {
792
1057
  ctx.clip();
793
1058
  }
794
1059
  fillShadow(ctx, s, color, (dx) => {
795
- roundedRect(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
1060
+ shadowShape(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
796
1061
  }, shape.x + shape.width + reach);
797
1062
  if (clipped)
798
1063
  ctx.restore();
@@ -842,8 +1107,6 @@ function paintShadows(ctx, box, options, inset) {
842
1107
  fillRing(ctx, pad, inner, hole, within);
843
1108
  continue;
844
1109
  }
845
- if (bakedInset(ctx, options, s, color, pad, inner, hole, within))
846
- continue;
847
1110
  if (!ctx.clip || !ctx.save || !ctx.rect)
848
1111
  continue;
849
1112
  ctx.save();
@@ -861,128 +1124,24 @@ function paintShadows(ctx, box, options, inset) {
861
1124
  fillShadow(ctx, s, color, (dx) => {
862
1125
  ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
863
1126
  if (hole.width > 0 && hole.height > 0) {
864
- roundedRect(ctx, hole.x - dx, hole.y, hole.width, hole.height, within, true, true);
1127
+ shadowShape(ctx, hole.x - dx, hole.y, hole.width, hole.height, within);
865
1128
  }
866
- }, frame.x + frame.width + shadowReach(s.blur));
1129
+ }, frame.x + frame.width + shadowReach(s.blur), 'evenodd');
867
1130
  ctx.restore();
868
1131
  }
869
1132
  }
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
1133
  /**
975
1134
  * Fill a shadow's shape: in its colour where it has no blur, and otherwise
976
1135
  * as the context's shadow of the shape drawn `dx` to the left — clear of
977
1136
  * the window, its right edge at `right` before the move — with the shadow
978
1137
  * offset back by as much, so that only the shadow lands.
979
1138
  */
980
- function fillShadow(ctx, s, color, shape, right) {
1139
+ function fillShadow(ctx, s, color, shape, right, rule = 'nonzero') {
981
1140
  if (!(s.blur > 0)) {
982
1141
  ctx.fillStyle = color;
983
1142
  ctx.beginPath();
984
1143
  shape(0);
985
- ctx.fill();
1144
+ ctx.fill(rule);
986
1145
  return;
987
1146
  }
988
1147
  const dx = Math.ceil(right) + 1;
@@ -994,9 +1153,26 @@ function fillShadow(ctx, s, color, shape, right) {
994
1153
  ctx.fillStyle = '#000000';
995
1154
  ctx.beginPath();
996
1155
  shape(dx);
997
- ctx.fill();
1156
+ ctx.fill(rule);
998
1157
  ctx.restore();
999
1158
  }
1159
+ /**
1160
+ * A shadow's shape, spelled the way a 2d context recognises one: a `rect`,
1161
+ * or a `roundRect` with each corner's own radii, elliptical where the
1162
+ * corner is. react-x11's contexts draw the blurred shadow of such a shape —
1163
+ * and of a `rect` less such a shape, filled `evenodd` — from a tile they
1164
+ * keep, where one built of curves they blur afresh on every fill.
1165
+ */
1166
+ function shadowShape(ctx, x, y, w, h, c) {
1167
+ if (c.x.every((r) => r === 0) || c.y.every((r) => r === 0)) {
1168
+ ctx.rect(x, y, w, h);
1169
+ return;
1170
+ }
1171
+ // a circular corner as the number every context has always taken; only
1172
+ // an elliptical one as the point
1173
+ const corner = (i) => c.x[i] === c.y[i] ? c.x[i] : { x: c.x[i], y: c.y[i] };
1174
+ ctx.roundRect(x, y, w, h, [corner(0), corner(1), corner(2), corner(3)]);
1175
+ }
1000
1176
  /**
1001
1177
  * The band between two rounded rectangles, the inner inside the outer, in
1002
1178
  * the colour already set. Where a side has no band the two edges meet, and
@@ -1043,6 +1219,7 @@ function inFlow(parent, child, options) {
1043
1219
  return (style.overflowX === 'visible' &&
1044
1220
  style.overflowY === 'visible' &&
1045
1221
  opacityOf(child) >= 1 &&
1222
+ !masked(style) &&
1046
1223
  // containment makes it a stacking context, painted whole
1047
1224
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1048
1225
  }
@@ -1068,6 +1245,7 @@ function flowFlex(parent, child, options) {
1068
1245
  return (style.overflowX === 'visible' &&
1069
1246
  style.overflowY === 'visible' &&
1070
1247
  !(child.outOfFlow && style.clip) &&
1248
+ !masked(style) &&
1071
1249
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1072
1250
  }
1073
1251
  /**
@@ -1294,7 +1472,55 @@ function paintPositioned(ctx, box, options) {
1294
1472
  return;
1295
1473
  }
1296
1474
  }
1297
- paintBox(ctx, box, options);
1475
+ paintBox(ctx, box, atViewport(box, options));
1476
+ }
1477
+ /**
1478
+ * The options a box is painted with where it is fixed to the viewport
1479
+ * (CSS 2.1 9.6.1): laid out against the viewport at the document's top,
1480
+ * and drawn where the viewport is now, however far the pane that scrolls
1481
+ * the element has moved it — a fixed header scrolled away with the text.
1482
+ * Once for a box and all it holds.
1483
+ */
1484
+ function atViewport(box, options) {
1485
+ const viewport = options.viewport;
1486
+ if (!viewport || options.atViewport || !fixedToViewport(box)) {
1487
+ return options;
1488
+ }
1489
+ // where the document's top left is from the viewport's, less where the
1490
+ // viewport is in the document: the scroll
1491
+ const root = rootOf(box);
1492
+ const dx = viewport.x - options.originX - root.x;
1493
+ const dy = viewport.y - options.originY - root.y;
1494
+ return {
1495
+ ...options,
1496
+ originX: options.originX + dx,
1497
+ originY: options.originY + dy,
1498
+ atViewport: true,
1499
+ };
1500
+ }
1501
+ /** Per tree, its boxes fixed to the viewport (`fixedToViewport`), found
1502
+ * as the layout positions them: what the element answers a scroll pane's
1503
+ * blit with (`HtmlViewNode.viewportFixedRects`). */
1504
+ export const FIXED_BOXES = new WeakMap();
1505
+ /** Whether a box is fixed to the viewport: `position: fixed` with no
1506
+ * transformed or contained box around it, which would be its containing
1507
+ * block instead (CSS Transforms 1, CSS Containment 2, 3.3). */
1508
+ export function fixedToViewport(box) {
1509
+ if (box.style.position !== 'fixed')
1510
+ return false;
1511
+ for (let at = box.parent; at?.parent; at = at.parent) {
1512
+ if (at.style.translate || at.style.transformTranslate)
1513
+ return false;
1514
+ if (contained(at, CONTAIN_LAYOUT | CONTAIN_PAINT))
1515
+ return false;
1516
+ }
1517
+ return true;
1518
+ }
1519
+ function rootOf(box) {
1520
+ let root = box;
1521
+ while (root.parent)
1522
+ root = root.parent;
1523
+ return root;
1298
1524
  }
1299
1525
  /**
1300
1526
  * The box an out-of-flow box is positioned in, whose clips are the ones it
@@ -1713,6 +1939,9 @@ function stacksLayers(box) {
1713
1939
  return true;
1714
1940
  if (style.opacity < 1 || FADED_BLOCKS.has(box))
1715
1941
  return true;
1942
+ // and so does a mask, which is applied to the group (CSS Masking 1, 7)
1943
+ if (masked(style))
1944
+ return true;
1716
1945
  // layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
1717
1946
  if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
1718
1947
  return true;
@@ -1732,7 +1961,7 @@ function flexItem(box) {
1732
1961
  * outside a box that clips where its containing block is, a fixed one
1733
1962
  * outside every one, and a relative one inside every one.
1734
1963
  */
1735
- function clipsFor(box, context) {
1964
+ export function clipsFor(box, context) {
1736
1965
  let from = box.parent;
1737
1966
  if (box.outOfFlow) {
1738
1967
  const fixed = box.style.position === 'fixed';
@@ -1759,6 +1988,7 @@ function clipsFor(box, context) {
1759
1988
  * parent painted it, it was. A clip of no area leaves none of it to paint.
1760
1989
  */
1761
1990
  function paintStacked(ctx, box, options) {
1991
+ options = atViewport(box, options);
1762
1992
  if (!intersects(box, options))
1763
1993
  return;
1764
1994
  const between = CLIPS_BETWEEN.get(box);
@@ -1922,7 +2152,8 @@ function paintBackground(ctx, box, options, style) {
1922
2152
  // box, which is where it starts, and repeated under the borders — or
1923
2153
  // the viewport's, where it is fixed; its line runs across the whole of
1924
2154
  // it, not the part this paint reaches
1925
- const fixed = style.backgroundAttachment === 'fixed' && options.canvas;
2155
+ const fixed = style.backgroundAttachment === 'fixed' &&
2156
+ (options.viewport ?? options.canvas);
1926
2157
  const origin = fixed || originBox(box, options, style);
1927
2158
  const at = snapped(origin.x, origin.y, origin.width, origin.height);
1928
2159
  if (rounded && style.backgroundSize !== 'auto' && ctx.clip) {
@@ -2298,7 +2529,7 @@ const MAX_TILES = 4096;
2298
2529
  */
2299
2530
  function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
2300
2531
  if (style.backgroundAttachment === 'fixed' && options.canvas) {
2301
- at = options.canvas;
2532
+ at = options.viewport ?? options.canvas;
2302
2533
  }
2303
2534
  const url = style.backgroundImage;
2304
2535
  const loaded = url ? options.backgroundImageFor?.(url) : null;
@@ -3320,7 +3551,12 @@ function paintLines(ctx, box, options) {
3320
3551
  const bottom = top + damage.height;
3321
3552
  let lo = 0;
3322
3553
  let hi = lines.length;
3323
- const slack = top - box.maxLineHeight;
3554
+ // and a line whose glyphs, or an atomic on it, reach past it
3555
+ // (`LINE_INK`) is drawn where they reach
3556
+ const ink = LINE_INK.get(lines);
3557
+ const above = ink?.above ?? 0;
3558
+ const below = ink?.below ?? 0;
3559
+ const slack = top - box.maxLineHeight - below;
3324
3560
  while (lo < hi) {
3325
3561
  const mid = (lo + hi) >> 1;
3326
3562
  if (lines[mid].y > slack)
@@ -3330,10 +3566,13 @@ function paintLines(ctx, box, options) {
3330
3566
  }
3331
3567
  for (let i = lo; i < lines.length; i += 1) {
3332
3568
  const line = lines[i];
3333
- if (line.y >= bottom)
3569
+ if (line.y - above >= bottom)
3334
3570
  break;
3335
- if (line.y + line.height > top)
3571
+ if (line.y < bottom && line.y + line.height > top)
3336
3572
  visible.push(line);
3573
+ else if (ink && inkInto(line, top, bottom)) {
3574
+ visible.push(line);
3575
+ }
3337
3576
  }
3338
3577
  // and a line the damage misses whose text or inline-block `position:
3339
3578
  // relative` moved into it: drawn by its line, it went undrawn with it
@@ -3341,7 +3580,7 @@ function paintLines(ctx, box, options) {
3341
3580
  if (moved && reachedOff(visible, moved, top, bottom)) {
3342
3581
  const drawn = new Set(visible);
3343
3582
  for (const line of moved) {
3344
- if (movedInto(line, top, bottom))
3583
+ if (inkInto(line, top, bottom))
3345
3584
  drawn.add(line);
3346
3585
  }
3347
3586
  visible.length = 0;
@@ -3359,7 +3598,7 @@ function paintLines(ctx, box, options) {
3359
3598
  for (const line of visible) {
3360
3599
  if (line.background)
3361
3600
  paintLineBackground(ctx, line, options);
3362
- paintInlineBoxes(ctx, line, options, line === lines[0] ? null : bleeds);
3601
+ paintInlineBoxes(ctx, line, lines, options, line === lines[0] ? null : bleeds);
3363
3602
  for (const text of line.texts) {
3364
3603
  const natural = text.layout.lines[text.layoutLine];
3365
3604
  if (natural)
@@ -3378,6 +3617,10 @@ function paintLines(ctx, box, options) {
3378
3617
  // drawing it once per line would be one X request per line for the same
3379
3618
  // batch.
3380
3619
  const drawn = new Set();
3620
+ // text a `::selection` colours is drawn in that colour where selected
3621
+ const recolored = options.selectionStyler
3622
+ ? recoloredBands(visible, options)
3623
+ : null;
3381
3624
  for (const line of visible) {
3382
3625
  for (const text of line.texts) {
3383
3626
  if (drawn.has(text.layout))
@@ -3392,7 +3635,11 @@ function paintLines(ctx, box, options) {
3392
3635
  // thrown from the protocol encoder.
3393
3636
  continue;
3394
3637
  }
3395
- text.layout.draw(ctx, text.drawX + dx, top);
3638
+ const bands = recolored?.get(text.layout);
3639
+ if (bands)
3640
+ drawRecolored(ctx, text.layout, text.drawX + dx, top, bands);
3641
+ else
3642
+ text.layout.draw(ctx, text.drawX + dx, top);
3396
3643
  }
3397
3644
  }
3398
3645
  paintRules(ctx, visible, dx, dy, options.scale ?? 1, 'over');
@@ -3414,15 +3661,16 @@ function paintLines(ctx, box, options) {
3414
3661
  * does not already draw, has it within the damage's rows. */
3415
3662
  function reachedOff(visible, moved, top, bottom) {
3416
3663
  for (const line of moved) {
3417
- if (!visible.includes(line) && movedInto(line, top, bottom))
3664
+ if (!visible.includes(line) && inkInto(line, top, bottom))
3418
3665
  return true;
3419
3666
  }
3420
3667
  return false;
3421
3668
  }
3422
- /** Whether what was moved off a line — its inline-blocks and images, where
3423
- * their ink is, and its texts, where they are drawn — reaches the rows
3424
- * between `top` and `bottom`. */
3425
- function movedInto(line, top, bottom) {
3669
+ /** Whether what a line draws — its inline-blocks and images, where their
3670
+ * ink is, and its texts, where they are drawn, their glyphs from ascent to
3671
+ * descent — reaches the rows between `top` and `bottom`: what was moved off
3672
+ * it, and what hangs past it. */
3673
+ function inkInto(line, top, bottom) {
3426
3674
  for (const placed of line.atomics) {
3427
3675
  const box = placed.box;
3428
3676
  if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
@@ -3433,8 +3681,10 @@ function movedInto(line, top, bottom) {
3433
3681
  const natural = text.layout.lines[text.layoutLine];
3434
3682
  if (!natural)
3435
3683
  continue;
3436
- const y = text.drawY + natural.y;
3437
- if (y < bottom && y + natural.height > top)
3684
+ const baseline = text.drawY + natural.baseline;
3685
+ const y1 = Math.min(text.drawY + natural.y, baseline - (natural.ascent ?? 0));
3686
+ const y2 = Math.max(text.drawY + natural.y + natural.height, baseline + (natural.descent ?? 0));
3687
+ if (y1 < bottom && y2 > top)
3438
3688
  return true;
3439
3689
  }
3440
3690
  return false;
@@ -3803,27 +4053,22 @@ function paintLineBackground(ctx, line, options) {
3803
4053
  style: background.style,
3804
4054
  }, options);
3805
4055
  }
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;
4056
+ /**
4057
+ * The fragments of the decorated inline boxes on a line: how far across it
4058
+ * each reaches, its border box, and whether the box opens or closes on it.
4059
+ * Where the line holds text `position: relative` moved, taken where the
4060
+ * text was: a fragment is moved by its own box's offset.
4061
+ *
4062
+ * A box is more than one fragment on a line where bidi reordering puts
4063
+ * what is not the box's between parts of it (CSS 2.1 9.10): a letter of
4064
+ * another element, an atomic, another element's edge. Its parts are one
4065
+ * fragment wherever nothing else comes between them, whatever room is
4066
+ * between them — a space it hangs, a box of its own inside it — so a line
4067
+ * that reads one way has one fragment of each box, as it always had.
4068
+ */
4069
+ function fragmentsOn(line) {
4070
+ /** What is on the line, where, and which decorated boxes it is in. */
4071
+ const marks = [];
3827
4072
  const shifted = SHIFTED_LINES.has(line);
3828
4073
  for (const text of line.texts) {
3829
4074
  const natural = text.layout.lines[text.layoutLine];
@@ -3831,8 +4076,6 @@ bleeds = null) {
3831
4076
  if (!natural || !boxAt)
3832
4077
  continue;
3833
4078
  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
4079
  const x = text.drawX - (shift?.x ?? 0) + natural.x;
3837
4080
  for (const run of natural.runs) {
3838
4081
  const owner = boxAt.call(text.spans, run.start);
@@ -3844,45 +4087,147 @@ bleeds = null) {
3844
4087
  const right = Math.min(x + natural.width, x + run.x + run.width);
3845
4088
  if (right <= left)
3846
4089
  continue;
3847
- for (const box of decoratedAncestors(owner))
3848
- widen(box, left, right);
4090
+ marks.push({ left, right, boxes: decoratedAncestors(owner) });
4091
+ }
4092
+ // and the spaces `pre-wrap` keeps that the line ends on, which hang
4093
+ // past it and are their box's all the same (`LineText.hung`)
4094
+ for (const space of text.hung ?? []) {
4095
+ const owner = boxAt.call(text.spans, space.at);
4096
+ if (!owner)
4097
+ continue;
4098
+ const left = text.drawX - (shift?.x ?? 0) + space.x;
4099
+ marks.push({
4100
+ left,
4101
+ right: left + space.width,
4102
+ boxes: decoratedAncestors(owner),
4103
+ });
3849
4104
  }
3850
4105
  }
3851
4106
  for (const placed of line.atomics) {
3852
4107
  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
- }
4108
+ marks.push({
4109
+ left: placed.x - atomic.marginLeft,
4110
+ right: placed.x + atomic.width + atomic.marginRight,
4111
+ boxes: decoratedAncestors(atomic),
4112
+ });
3856
4113
  }
3857
4114
  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);
4115
+ marks.push({
4116
+ left: edge.x,
4117
+ right: edge.x + edge.width,
4118
+ boxes: decoratedAncestors(edge.box),
4119
+ edge,
4120
+ });
4121
+ }
4122
+ let fragments = null;
4123
+ // each box's parts, left to right
4124
+ const parts = new Map();
4125
+ for (const mark of marks) {
4126
+ for (const box of mark.boxes) {
4127
+ let list = parts.get(box);
4128
+ if (!list)
4129
+ parts.set(box, (list = []));
4130
+ list.push(mark);
3868
4131
  }
3869
- else {
3870
- const right = edge.x + edge.width - box.marginRight;
3871
- widen(box, right, right);
4132
+ const own = mark.edge?.box;
4133
+ if (own?.decoration) {
4134
+ let list = parts.get(own);
4135
+ if (!list)
4136
+ parts.set(own, (list = []));
4137
+ list.push(mark);
3872
4138
  }
3873
- const f = fragments.get(box);
3874
- if (edge.side === 'start')
3875
- f.start = true;
3876
- else
3877
- f.end = true;
3878
4139
  }
4140
+ for (const [box, list] of parts) {
4141
+ list.sort((a, b) => a.left - b.left || a.right - b.right);
4142
+ const out = [];
4143
+ let open = null;
4144
+ let reach = -Infinity;
4145
+ for (const mark of list) {
4146
+ // The element's `direction` says which side its start is on, whatever
4147
+ // its text reads as (CSS 2.1 8.6); its own edge's margin is outside
4148
+ // the box, its border and padding inside.
4149
+ let left = mark.left;
4150
+ let right = mark.right;
4151
+ const own = mark.edge?.box === box ? mark.edge : null;
4152
+ if (own) {
4153
+ const onLeft = (own.side === 'start') !== (box.style.direction === 'rtl');
4154
+ if (onLeft)
4155
+ left = right = own.x + box.marginLeft;
4156
+ else
4157
+ left = right = own.x + own.width - box.marginRight;
4158
+ }
4159
+ if (!open || between(marks, box, reach, mark.left)) {
4160
+ open = { left, right, start: false, end: false };
4161
+ out.push(open);
4162
+ }
4163
+ else {
4164
+ open.left = Math.min(open.left, left);
4165
+ open.right = Math.max(open.right, right);
4166
+ }
4167
+ reach = Math.max(reach, mark.right);
4168
+ if (own) {
4169
+ if (own.side === 'start')
4170
+ open.start = true;
4171
+ else
4172
+ open.end = true;
4173
+ }
4174
+ }
4175
+ (fragments ??= new Map()).set(box, out);
4176
+ }
4177
+ return fragments;
4178
+ }
4179
+ /** Whether anything on a line that is not in a box lies between two of its
4180
+ * parts, from `left` to `right`: what splits it into two fragments. */
4181
+ function between(marks, box, left, right) {
4182
+ if (right - left < 0.5)
4183
+ return false;
4184
+ for (const mark of marks) {
4185
+ if (mark.right - mark.left < 0.5)
4186
+ continue;
4187
+ if (mark.boxes.includes(box) || mark.edge?.box === box)
4188
+ continue;
4189
+ const middle = (mark.left + mark.right) / 2;
4190
+ if (middle > left && middle < right)
4191
+ return true;
4192
+ }
4193
+ return false;
4194
+ }
4195
+ function paintInlineBoxes(ctx, line,
4196
+ /** The block's lines, which `line` is one of. */
4197
+ lines, options,
4198
+ /** Where a fragment that reaches up over the lines before goes, to be
4199
+ * drawn again over their text; null for a block's first line. */
4200
+ bleeds = null) {
4201
+ const fragments = fragmentsOn(line);
3879
4202
  if (!fragments)
3880
4203
  return;
4204
+ // Every text on a line is drawn on one baseline (`finishLine`), and an
4205
+ // engine's is from the top of its layout rather than of the line. Taken
4206
+ // where the line has each text: a box `position: relative` moves is
4207
+ // moved by its own offset below, and the boxes around it are not.
4208
+ let baseline = line.y + line.baseline;
4209
+ const shifted = SHIFTED_LINES.has(line);
4210
+ for (const text of line.texts) {
4211
+ const natural = text.layout.lines[text.layoutLine];
4212
+ if (!natural || !text.spans.boxAt)
4213
+ continue;
4214
+ const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
4215
+ const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
4216
+ baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
4217
+ }
3881
4218
  const boxes = [...fragments.keys()].sort((a, b) => depthOf(a) - depthOf(b));
3882
4219
  for (const box of boxes) {
3883
4220
  if (box.style.visibility !== 'visible')
3884
4221
  continue;
3885
- const f = fragments.get(box);
4222
+ for (const f of fragments.get(box)) {
4223
+ paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline);
4224
+ }
4225
+ }
4226
+ }
4227
+ /** One fragment of an inline box on a line (`paintInlineBoxes`). */
4228
+ function paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline) {
4229
+ const shifted = SHIFTED_LINES.has(line);
4230
+ {
3886
4231
  const face = box.decoration;
3887
4232
  // on its own baseline, which `vertical-align` may raise off the line's
3888
4233
  const own = shifted
@@ -3917,10 +4262,15 @@ bleeds = null) {
3917
4262
  style,
3918
4263
  };
3919
4264
  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
4265
+ return;
4266
+ // its images and gradients from the strip its fragments make, where it
4267
+ // goes on to another line, and from its own padding box where it is
4268
+ // on this one alone or `clone` says each fragment is its own
4269
+ if (!(f.start && f.end) && sliced(box.style)) {
4270
+ const strip = stripFor(lines, line, box, fragment);
4271
+ if (strip)
4272
+ fragment.strip = strip;
4273
+ }
3924
4274
  paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
3925
4275
  paintBorders(ctx, fragment, options);
3926
4276
  if (box.style.outlineStyle !== 'none')
@@ -3930,6 +4280,76 @@ bleeds = null) {
3930
4280
  }
3931
4281
  }
3932
4282
  }
4283
+ /** Whether an inline box places background images or gradients, which
4284
+ * `slice` places across its fragments, as one box end to end. */
4285
+ function sliced(style) {
4286
+ return (style.boxDecorationBreak === 'slice' &&
4287
+ (style.backgroundImage !== null ||
4288
+ style.backgroundGradient !== null ||
4289
+ style.backgroundImages !== null));
4290
+ }
4291
+ /** Of a block's lines, by the array they are, the strip of each inline box
4292
+ * on them that `sliced` says places images across its fragments: made
4293
+ * once a layout, as `MOVED_OFF_LINES` is, so a repaint of one line reads
4294
+ * the lines before it without going over them again. What `sliced` reads
4295
+ * is never restyled in place (`HtmlViewNode`), which lays the lines out
4296
+ * again instead. */
4297
+ const STRIPS = new WeakMap();
4298
+ function stripsOf(lines) {
4299
+ const strips = new Map();
4300
+ for (const line of lines) {
4301
+ const fragments = fragmentsOn(line);
4302
+ if (!fragments)
4303
+ continue;
4304
+ for (const [box, list] of fragments) {
4305
+ if (!sliced(box.style))
4306
+ continue;
4307
+ let strip = strips.get(box);
4308
+ if (!strip) {
4309
+ strip = { width: 0, lines: 0, before: new Map() };
4310
+ strips.set(box, strip);
4311
+ }
4312
+ strip.before.set(line, strip.width);
4313
+ for (const f of list)
4314
+ strip.width += Math.max(0, f.right - f.left);
4315
+ strip.lines += 1;
4316
+ }
4317
+ }
4318
+ return strips;
4319
+ }
4320
+ /**
4321
+ * The box an inline box's fragment on `line` is a slice of (CSS
4322
+ * Fragmentation 3, 5.4): the box's fragments laid end to end in its
4323
+ * direction, as though it had not wrapped — as wide as
4324
+ * they are together, with the box's own borders and padding at its two
4325
+ * ends, placed so that this fragment is where it falls along it. So an
4326
+ * image `no-repeat` at the start is on the first fragment alone, and a
4327
+ * gradient runs once across them all. Undefined where the box is on one
4328
+ * line.
4329
+ */
4330
+ function stripFor(lines, line, box, fragment) {
4331
+ let strips = STRIPS.get(lines);
4332
+ if (!strips) {
4333
+ strips = stripsOf(lines);
4334
+ STRIPS.set(lines, strips);
4335
+ }
4336
+ const strip = strips.get(box);
4337
+ const before = strip?.before.get(line);
4338
+ if (!strip || strip.lines < 2 || before === undefined)
4339
+ return undefined;
4340
+ const x = box.style.direction === 'rtl'
4341
+ ? fragment.x + fragment.width + before - strip.width
4342
+ : fragment.x - before;
4343
+ return {
4344
+ ...fragment,
4345
+ x,
4346
+ width: strip.width,
4347
+ borderLeft: box.borderLeft,
4348
+ borderRight: box.borderRight,
4349
+ padLeft: box.padLeft,
4350
+ padRight: box.padRight,
4351
+ };
4352
+ }
3933
4353
  /** An inline box's style on a line it does not both start and end on: no
3934
4354
  * rounded corner on a side it goes on from. Made once a style and a pair
3935
4355
  * of ends, so a repaint copies no style and the layers made of it are
@@ -3990,20 +4410,53 @@ function decoratedAncestors(box) {
3990
4410
  DECORATED.set(box, found);
3991
4411
  return found;
3992
4412
  }
4413
+ /** A restyle in place (a `:hover`) gave an inline box a decoration where it
4414
+ * had none, or took it away, and the lists kept above do not know: every
4415
+ * box whose list could name it — inside it, through the inline boxes the
4416
+ * walk above climbs — looks again. A document built again has new boxes,
4417
+ * and so no lists. */
4418
+ export function forgetDecoratedAncestors(box) {
4419
+ const stack = [...box.children];
4420
+ while (stack.length) {
4421
+ const at = stack.pop();
4422
+ DECORATED.delete(at);
4423
+ if (at.kind === 'inline')
4424
+ stack.push(...at.children);
4425
+ }
4426
+ }
3993
4427
  /**
3994
4428
  * The band the document selection covers on one line.
3995
4429
  *
3996
4430
  * Translucent under the glyphs rather than inverted over them, so the ink
3997
4431
  * keeps its contrast on either palette — the same call `<textarea>` and
3998
- * `<richtext>` both make.
4432
+ * `<richtext>` both make. From the top of the line to its bottom, and past
4433
+ * them over the glyphs where they are taller than the line (`selectionRows`).
3999
4434
  */
4000
4435
  function paintSelection(ctx, line, options) {
4436
+ for (const band of selectedBands(line, options)) {
4437
+ const fill = band.style ? band.style.background : options.selectionColor;
4438
+ if (!fill || isTransparent(fill))
4439
+ continue;
4440
+ ctx.fillStyle = fill;
4441
+ ctx.fillRect(band.x, band.y, band.width, band.height);
4442
+ }
4443
+ }
4444
+ const NO_BANDS = [];
4445
+ /**
4446
+ * Where the document selection covers a line's text, a band for each
4447
+ * stretch of it one `::selection` styles, in window pixels as the band is
4448
+ * filled — rounded, so a band's text is drawn to its edges and no further
4449
+ * (`drawRecolored`).
4450
+ */
4451
+ function selectedBands(line, options) {
4001
4452
  const range = options.selection;
4002
- if (!range || range.end <= range.start || !options.selectionColor)
4003
- return;
4004
- if (line.textEnd <= range.start || line.textStart >= range.end)
4005
- return;
4006
- ctx.fillStyle = options.selectionColor;
4453
+ if (!range || range.end <= range.start)
4454
+ return NO_BANDS;
4455
+ if (line.textEnd <= range.start || line.textStart >= range.end) {
4456
+ return NO_BANDS;
4457
+ }
4458
+ const styler = options.selectionStyler;
4459
+ const out = [];
4007
4460
  for (const text of line.texts) {
4008
4461
  const natural = text.layout.lines[text.layoutLine];
4009
4462
  if (!natural)
@@ -4015,9 +4468,192 @@ function paintSelection(ctx, line, options) {
4015
4468
  const offsets = layoutOffsets(text.layout);
4016
4469
  const layoutFrom = layoutOffsetOf(text, from);
4017
4470
  const layoutTo = layoutOffsetOf(text, to, true);
4018
- for (const band of lineBands(text.layout, natural, offsets, layoutFrom, layoutTo)) {
4019
- ctx.fillRect(Math.round(band.x + text.drawX + options.originX), Math.round(line.y + options.originY), Math.ceil(band.width), Math.ceil(line.height));
4471
+ const rows = selectionRows(line, text, natural);
4472
+ const y = Math.round(rows.y + options.originY);
4473
+ const height = Math.ceil(rows.height);
4474
+ const pieces = styler
4475
+ ? stylePieces(text, natural, layoutFrom, layoutTo, styler)
4476
+ : [{ from: layoutFrom, to: layoutTo, style: null }];
4477
+ for (const piece of pieces) {
4478
+ for (const band of lineBands(text.layout, natural, offsets, piece.from, piece.to)) {
4479
+ out.push({
4480
+ x: Math.round(band.x + text.drawX + options.originX),
4481
+ y,
4482
+ width: Math.ceil(band.width),
4483
+ height,
4484
+ style: piece.style,
4485
+ layout: text.layout,
4486
+ });
4487
+ }
4488
+ }
4489
+ }
4490
+ return out;
4491
+ }
4492
+ /**
4493
+ * A line's selected text, `from` to `to` in the layout's offsets, cut where
4494
+ * the `::selection` over it changes: a run's is its element's, and a run of
4495
+ * no element's — an inline box's edge, laid out as a spacer — the one
4496
+ * beside it, so a styled band has no palette-coloured gap at a `<span>`.
4497
+ */
4498
+ function stylePieces(text, natural, from, to, styler) {
4499
+ const runs = natural.runs
4500
+ .filter((run) => run.end > from && run.start < to)
4501
+ .sort((a, b) => a.start - b.start);
4502
+ const boxAt = text.spans.boxAt;
4503
+ const styles = runs.map((run) => {
4504
+ const box = boxAt?.call(text.spans, run.start) ?? null;
4505
+ return box ? styler(box) : undefined;
4506
+ });
4507
+ for (let i = 1; i < styles.length; i += 1) {
4508
+ if (styles[i] === undefined)
4509
+ styles[i] = styles[i - 1];
4510
+ }
4511
+ for (let i = styles.length - 2; i >= 0; i -= 1) {
4512
+ if (styles[i] === undefined)
4513
+ styles[i] = styles[i + 1];
4514
+ }
4515
+ const out = [];
4516
+ runs.forEach((run, i) => {
4517
+ const style = styles[i] ?? null;
4518
+ const last = out[out.length - 1];
4519
+ if (last && last.style === style)
4520
+ last.to = Math.min(to, run.end);
4521
+ else {
4522
+ out.push({
4523
+ from: Math.max(from, run.start),
4524
+ to: Math.min(to, run.end),
4525
+ style,
4526
+ });
4527
+ }
4528
+ });
4529
+ return out;
4530
+ }
4531
+ /**
4532
+ * The layouts whose selected text a `::selection` gives a colour of its
4533
+ * own, each with the bands it is drawn in that colour (`drawRecolored`).
4534
+ * Null where none does, which is every document with no such rule.
4535
+ */
4536
+ function recoloredBands(lines, options) {
4537
+ let out = null;
4538
+ for (const line of lines) {
4539
+ for (const band of selectedBands(line, options)) {
4540
+ if (!band.style?.color || !(band.width > 0))
4541
+ continue;
4542
+ out ??= new Map();
4543
+ const list = out.get(band.layout);
4544
+ if (list)
4545
+ list.push(band);
4546
+ else
4547
+ out.set(band.layout, [band]);
4548
+ }
4549
+ }
4550
+ return out;
4551
+ }
4552
+ /**
4553
+ * A layout with some of its text selected in a `::selection`'s colour
4554
+ * (CSS Pseudo 4, 3.2): its own colours everywhere but those bands, and in
4555
+ * each band the glyphs in that band's colour — cast there as a hard shadow
4556
+ * from the layout drawn clear of the window, the way `castShadows` draws a
4557
+ * text shadow, since a layout draws in its runs' own colours. Drawn over
4558
+ * rather than in place of the text, the old glyphs' edges showed round the
4559
+ * new ones. Where the context cannot clip or cast, the text keeps its own
4560
+ * colours.
4561
+ */
4562
+ function drawRecolored(ctx, layout, left, top, bands) {
4563
+ if (!ctx.save ||
4564
+ !ctx.restore ||
4565
+ !ctx.beginPath ||
4566
+ !ctx.rect ||
4567
+ !ctx.clip ||
4568
+ !('shadowBlur' in ctx)) {
4569
+ layout.draw(ctx, left, top);
4570
+ return;
4571
+ }
4572
+ ctx.save();
4573
+ ctx.beginPath();
4574
+ for (const r of outsideOf(bands))
4575
+ ctx.rect(r.x, r.y, r.width, r.height);
4576
+ ctx.clip();
4577
+ layout.draw(ctx, left, top);
4578
+ ctx.restore();
4579
+ let right = layout.width;
4580
+ for (const natural of layout.lines) {
4581
+ right = Math.max(right, natural.x + natural.width);
4582
+ }
4583
+ const shift = Math.ceil(left + right) + 1;
4584
+ const colors = new Map();
4585
+ for (const band of bands) {
4586
+ const color = band.style.color;
4587
+ const group = colors.get(color);
4588
+ if (group)
4589
+ group.push(band);
4590
+ else
4591
+ colors.set(color, [band]);
4592
+ }
4593
+ for (const [color, group] of colors) {
4594
+ ctx.save();
4595
+ ctx.beginPath();
4596
+ for (const r of group)
4597
+ ctx.rect(r.x, r.y, r.width, r.height);
4598
+ ctx.clip();
4599
+ ctx.shadowColor = color;
4600
+ ctx.shadowBlur = 0.01;
4601
+ ctx.shadowOffsetX = shift;
4602
+ ctx.shadowOffsetY = 0;
4603
+ layout.draw(ctx, left - shift, top);
4604
+ ctx.restore();
4605
+ }
4606
+ }
4607
+ /** Everything but some rectangles, as rectangles: a strip between each two
4608
+ * of their edges, less what of it they cover. */
4609
+ function outsideOf(rects) {
4610
+ const far = COORD_LIMIT;
4611
+ const edges = new Set();
4612
+ for (const r of rects) {
4613
+ edges.add(r.y);
4614
+ edges.add(r.y + r.height);
4615
+ }
4616
+ const ys = [...edges].sort((a, b) => a - b);
4617
+ const out = [
4618
+ { x: -far, y: -far, width: 2 * far, height: ys[0] + far },
4619
+ {
4620
+ x: -far,
4621
+ y: ys[ys.length - 1],
4622
+ width: 2 * far,
4623
+ height: far - ys[ys.length - 1],
4624
+ },
4625
+ ];
4626
+ for (let i = 0; i + 1 < ys.length; i += 1) {
4627
+ const y0 = ys[i];
4628
+ const y1 = ys[i + 1];
4629
+ const spans = rects
4630
+ .filter((r) => r.y <= y0 && r.y + r.height >= y1)
4631
+ .sort((a, b) => a.x - b.x);
4632
+ let x = -far;
4633
+ for (const r of spans) {
4634
+ if (r.x > x)
4635
+ out.push({ x, y: y0, width: r.x - x, height: y1 - y0 });
4636
+ x = Math.max(x, r.x + r.width);
4020
4637
  }
4638
+ if (x < far)
4639
+ out.push({ x, y: y0, width: far - x, height: y1 - y0 });
4021
4640
  }
4641
+ return out;
4642
+ }
4643
+ /**
4644
+ * The rows a highlight over a line's text covers: the line box's, united
4645
+ * with the text's content area — its face's ascent above the baseline to
4646
+ * its descent below (CSS 2.1 10.6.1) — which is taller wherever
4647
+ * `line-height` is under the face's height. Blink unites the two in the
4648
+ * block direction (`ExpandSelectionRectToLineHeight`), so a selection over
4649
+ * a tall line fills it and one over tall glyphs on a short line covers
4650
+ * them: Zen Garden 215's 91px title on 20px lines had a band a fifth of
4651
+ * its letters' height through their middle.
4652
+ */
4653
+ export function selectionRows(line, text, natural) {
4654
+ const baseline = text.drawY + natural.baseline;
4655
+ const top = Math.min(line.y, baseline - (natural.ascent ?? 0));
4656
+ const bottom = Math.max(line.y + line.height, baseline + (natural.descent ?? 0));
4657
+ return { y: top, height: bottom - top };
4022
4658
  }
4023
4659
  //# sourceMappingURL=paint.js.map