@react-x11/components 0.10.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 (155) hide show
  1. package/dist/code/index.d.ts.map +1 -1
  2. package/dist/code/index.js +25 -24
  3. package/dist/code/index.js.map +1 -1
  4. package/dist/code-language/index.d.ts +1 -1
  5. package/dist/code-language/index.d.ts.map +1 -1
  6. package/dist/code-language/index.js +1 -1
  7. package/dist/code-language/index.js.map +1 -1
  8. package/dist/code-language/runs.d.ts +31 -0
  9. package/dist/code-language/runs.d.ts.map +1 -1
  10. package/dist/code-language/runs.js +105 -0
  11. package/dist/code-language/runs.js.map +1 -1
  12. package/dist/codeblock/index.d.ts +13 -0
  13. package/dist/codeblock/index.d.ts.map +1 -1
  14. package/dist/codeblock/index.js +37 -1
  15. package/dist/codeblock/index.js.map +1 -1
  16. package/dist/flow/node.d.ts +14 -6
  17. package/dist/flow/node.d.ts.map +1 -1
  18. package/dist/flow/node.js +82 -64
  19. package/dist/flow/node.js.map +1 -1
  20. package/dist/html/controls.d.ts +26 -7
  21. package/dist/html/controls.d.ts.map +1 -1
  22. package/dist/html/controls.js +28 -3
  23. package/dist/html/controls.js.map +1 -1
  24. package/dist/html/css/cascade.d.ts +32 -10
  25. package/dist/html/css/cascade.d.ts.map +1 -1
  26. package/dist/html/css/cascade.js +89 -31
  27. package/dist/html/css/cascade.js.map +1 -1
  28. package/dist/html/css/color.d.ts +19 -0
  29. package/dist/html/css/color.d.ts.map +1 -1
  30. package/dist/html/css/color.js +86 -0
  31. package/dist/html/css/color.js.map +1 -1
  32. package/dist/html/css/parse.d.ts +11 -0
  33. package/dist/html/css/parse.d.ts.map +1 -1
  34. package/dist/html/css/parse.js +114 -4
  35. package/dist/html/css/parse.js.map +1 -1
  36. package/dist/html/css/style.d.ts +45 -5
  37. package/dist/html/css/style.d.ts.map +1 -1
  38. package/dist/html/css/style.js +363 -14
  39. package/dist/html/css/style.js.map +1 -1
  40. package/dist/html/css/ua.js +22 -5
  41. package/dist/html/css/ua.js.map +1 -1
  42. package/dist/html/css/values.d.ts +10 -1
  43. package/dist/html/css/values.d.ts.map +1 -1
  44. package/dist/html/css/values.js +27 -2
  45. package/dist/html/css/values.js.map +1 -1
  46. package/dist/html/css/vars.d.ts +5 -0
  47. package/dist/html/css/vars.d.ts.map +1 -1
  48. package/dist/html/css/vars.js +2 -2
  49. package/dist/html/css/vars.js.map +1 -1
  50. package/dist/html/dom.d.ts.map +1 -1
  51. package/dist/html/dom.js +35 -2
  52. package/dist/html/dom.js.map +1 -1
  53. package/dist/html/index.d.ts.map +1 -1
  54. package/dist/html/index.js +38 -1
  55. package/dist/html/index.js.map +1 -1
  56. package/dist/html/layout/block.d.ts +11 -2
  57. package/dist/html/layout/block.d.ts.map +1 -1
  58. package/dist/html/layout/block.js +306 -25
  59. package/dist/html/layout/block.js.map +1 -1
  60. package/dist/html/layout/boxes.d.ts +61 -6
  61. package/dist/html/layout/boxes.d.ts.map +1 -1
  62. package/dist/html/layout/boxes.js +65 -16
  63. package/dist/html/layout/boxes.js.map +1 -1
  64. package/dist/html/layout/cache.d.ts +2 -2
  65. package/dist/html/layout/cache.d.ts.map +1 -1
  66. package/dist/html/layout/cache.js +8 -3
  67. package/dist/html/layout/cache.js.map +1 -1
  68. package/dist/html/layout/floats.d.ts +4 -0
  69. package/dist/html/layout/floats.d.ts.map +1 -1
  70. package/dist/html/layout/floats.js +26 -0
  71. package/dist/html/layout/floats.js.map +1 -1
  72. package/dist/html/layout/inline.d.ts +29 -2
  73. package/dist/html/layout/inline.d.ts.map +1 -1
  74. package/dist/html/layout/inline.js +1454 -125
  75. package/dist/html/layout/inline.js.map +1 -1
  76. package/dist/html/node.d.ts +33 -0
  77. package/dist/html/node.d.ts.map +1 -1
  78. package/dist/html/node.js +608 -86
  79. package/dist/html/node.js.map +1 -1
  80. package/dist/html/paint.d.ts +53 -1
  81. package/dist/html/paint.d.ts.map +1 -1
  82. package/dist/html/paint.js +746 -214
  83. package/dist/html/paint.js.map +1 -1
  84. package/dist/html/surfaces.d.ts +7 -2
  85. package/dist/html/surfaces.d.ts.map +1 -1
  86. package/dist/html/surfaces.js +16 -0
  87. package/dist/html/surfaces.js.map +1 -1
  88. package/dist/html/svg.d.ts.map +1 -1
  89. package/dist/html/svg.js +27 -20
  90. package/dist/html/svg.js.map +1 -1
  91. package/dist/internal/codelines.d.ts +42 -0
  92. package/dist/internal/codelines.d.ts.map +1 -0
  93. package/dist/internal/codelines.js +170 -0
  94. package/dist/internal/codelines.js.map +1 -0
  95. package/dist/internal/window.d.ts +22 -0
  96. package/dist/internal/window.d.ts.map +1 -1
  97. package/dist/internal/window.js +35 -8
  98. package/dist/internal/window.js.map +1 -1
  99. package/dist/markdown/index.d.ts.map +1 -1
  100. package/dist/markdown/index.js +98 -26
  101. package/dist/markdown/index.js.map +1 -1
  102. package/dist/rich-text-editor/index.d.ts.map +1 -1
  103. package/dist/rich-text-editor/index.js +3 -8
  104. package/dist/rich-text-editor/index.js.map +1 -1
  105. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  106. package/dist/rich-text-editor/markdown.js +30 -1
  107. package/dist/rich-text-editor/markdown.js.map +1 -1
  108. package/dist/rich-text-editor/replace.d.ts +13 -0
  109. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  110. package/dist/rich-text-editor/replace.js +64 -0
  111. package/dist/rich-text-editor/replace.js.map +1 -0
  112. package/dist/richtext/node.d.ts +7 -0
  113. package/dist/richtext/node.d.ts.map +1 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/terminal-output/index.d.ts.map +1 -1
  116. package/dist/terminal-output/index.js +100 -25
  117. package/dist/terminal-output/index.js.map +1 -1
  118. package/dist/terminal-output/runs.d.ts +4 -1
  119. package/dist/terminal-output/runs.d.ts.map +1 -1
  120. package/dist/terminal-output/runs.js +5 -1
  121. package/dist/terminal-output/runs.js.map +1 -1
  122. package/package.json +5 -3
  123. package/src/code/index.ts +34 -32
  124. package/src/code-language/index.ts +1 -1
  125. package/src/code-language/runs.ts +111 -1
  126. package/src/codeblock/index.ts +48 -1
  127. package/src/flow/node.ts +80 -64
  128. package/src/html/controls.ts +50 -9
  129. package/src/html/css/cascade.ts +134 -43
  130. package/src/html/css/color.ts +87 -0
  131. package/src/html/css/parse.ts +107 -4
  132. package/src/html/css/style.ts +386 -23
  133. package/src/html/css/ua.ts +22 -5
  134. package/src/html/css/values.ts +31 -3
  135. package/src/html/css/vars.ts +2 -2
  136. package/src/html/dom.ts +38 -2
  137. package/src/html/index.ts +39 -1
  138. package/src/html/layout/block.ts +360 -22
  139. package/src/html/layout/boxes.ts +98 -23
  140. package/src/html/layout/cache.ts +8 -3
  141. package/src/html/layout/floats.ts +24 -0
  142. package/src/html/layout/inline.ts +1690 -119
  143. package/src/html/node.ts +648 -101
  144. package/src/html/paint.ts +883 -283
  145. package/src/html/surfaces.ts +21 -1
  146. package/src/html/svg.ts +30 -19
  147. package/src/internal/codelines.ts +200 -0
  148. package/src/internal/window.ts +36 -8
  149. package/src/markdown/index.ts +145 -26
  150. package/src/rich-text-editor/index.ts +3 -11
  151. package/src/rich-text-editor/markdown.ts +36 -1
  152. package/src/rich-text-editor/replace.ts +85 -0
  153. package/src/richtext/node.ts +7 -0
  154. package/src/terminal-output/index.ts +127 -34
  155. package/src/terminal-output/runs.ts +16 -2
@@ -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, 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;
@@ -280,7 +362,7 @@ export function paintDocument(ctx, tree, options) {
280
362
  if (!canFill(ctx))
281
363
  return;
282
364
  ctx.save();
283
- const canvas = canvasBackground(tree.root);
365
+ const canvas = canvasBackground(tree);
284
366
  if (canvas)
285
367
  paintCanvas(ctx, canvas, tree.root, options);
286
368
  paintBox(ctx, tree.root, {
@@ -296,22 +378,41 @@ export function paintDocument(ctx, tree, options) {
296
378
  * `<body>`'s, which then paints no background of its own. A document with
297
379
  * no `<html>` tag has the root box standing in for its root element, and a
298
380
  * `<body>` in it is still the body: mail often starts at `<body style>`.
299
- * `anchor` is the box the image is positioned against: the root element's,
300
- * whichever box it came from.
301
- */
302
- function canvasBackground(root) {
303
- const has = (b) => !isTransparent(b.style.backgroundColor) ||
304
- !!b.style.backgroundImage ||
305
- !!b.style.backgroundGradient ||
306
- !!b.style.backgroundImages;
381
+ * With neither tag, the root box stands in for the body, and the `<html>`
382
+ * around it is implied: an `html { … }` or a `:root { … }` background, which
383
+ * a test's reference sets on a document with no tags at all, has no box
384
+ * but the canvas to be painted on. `anchor` is the box the image is
385
+ * positioned against: the root element's, whichever box it came from.
386
+ */
387
+ function canvasBackground(tree) {
388
+ const root = tree.root;
389
+ const has = (s) => !isTransparent(s.backgroundColor) ||
390
+ !!s.backgroundImage ||
391
+ !!s.backgroundGradient ||
392
+ !!s.backgroundImages;
393
+ const implied = tree.impliedHtml;
394
+ if (implied) {
395
+ // an `<html>` that is not displayed has no background to give
396
+ if (implied.display === 'none')
397
+ return null;
398
+ if (has(implied))
399
+ return { style: implied, source: null, anchor: root };
400
+ // containment on either keeps the body's to the body (CSS Containment 2)
401
+ if (implied.contain || root.style.contain)
402
+ return null;
403
+ return has(root.style)
404
+ ? { style: root.style, source: root, anchor: root }
405
+ : null;
406
+ }
307
407
  const top = childNamed(root, 'html') ?? root;
308
- if (has(top))
309
- return { source: top, anchor: top };
408
+ if (has(top.style))
409
+ return { style: top.style, source: top, anchor: top };
310
410
  const body = childNamed(top, 'body');
311
- // containment on either keeps the body's to the body (CSS Containment 2)
312
411
  if (!body || top.style.contain || body.style.contain)
313
412
  return null;
314
- return has(body) ? { source: body, anchor: top } : null;
413
+ return has(body.style)
414
+ ? { style: body.style, source: body, anchor: top }
415
+ : null;
315
416
  }
316
417
  /** A box's child element of that name, or one inside the anonymous boxes
317
418
  * it is wrapped in: a `<body>` in an `<html>` set `display: table` is in
@@ -328,7 +429,7 @@ function childNamed(box, name) {
328
429
  }
329
430
  return null;
330
431
  }
331
- function paintCanvas(ctx, { source, anchor }, root, options) {
432
+ function paintCanvas(ctx, { style: source, anchor }, root, options) {
332
433
  const whole = options.canvas ?? {
333
434
  x: root.x + options.originX,
334
435
  y: root.y + options.originY,
@@ -338,8 +439,8 @@ function paintCanvas(ctx, { source, anchor }, root, options) {
338
439
  const area = clampRect(options, Math.round(whole.x), Math.round(whole.y), Math.ceil(whole.width), Math.ceil(whole.height));
339
440
  if (!area)
340
441
  return;
341
- const layers = layersOf(source.style) ?? [source.style];
342
- const visible = source.style.visibility === 'visible';
442
+ const layers = layersOf(source) ?? [source];
443
+ const visible = source.visibility === 'visible';
343
444
  for (let i = layers.length - 1; i >= 0; i -= 1) {
344
445
  const style = layers[i];
345
446
  if (!isTransparent(style.backgroundColor) && visible) {
@@ -351,7 +452,7 @@ function paintCanvas(ctx, { source, anchor }, root, options) {
351
452
  // browser does — the stripes a short page with a gradient on its
352
453
  // body shows — or by the viewport, where it is fixed
353
454
  const box = style.backgroundAttachment === 'fixed' && options.canvas
354
- ? options.canvas
455
+ ? (options.viewport ?? options.canvas)
355
456
  : originBox(anchor, options, style);
356
457
  paintGradient(ctx, style, style.backgroundGradient, area, snapped(box.x, box.y, box.width, box.height));
357
458
  }
@@ -403,22 +504,32 @@ function layersOf(style) {
403
504
  */
404
505
  function paintLayers(ctx, box, options, image) {
405
506
  // a background painted through the text is painted with it
406
- // (`paintClippedText`), and not as a box
407
- if (box.style.backgroundClipText)
507
+ // (`paintClippedText`), and not as a box — but for what `border-area
508
+ // text` paints in the border as well
509
+ const style = box.style;
510
+ if (style.backgroundClipText && style.backgroundClip !== 'border-area') {
408
511
  return;
409
- const layers = layersOf(box.style);
512
+ }
513
+ const layers = layersOf(style);
410
514
  if (!layers) {
411
- paintBackground(ctx, box, options, box.style);
412
- if (image && box.style.backgroundImage)
413
- image(box.style);
515
+ paintLayer(ctx, box, options, style, image);
414
516
  return;
415
517
  }
416
518
  for (let i = layers.length - 1; i >= 0; i -= 1) {
417
- paintBackground(ctx, box, options, layers[i]);
418
- if (image && layers[i].backgroundImage)
419
- image(layers[i]);
519
+ paintLayer(ctx, box, options, layers[i], image);
420
520
  }
421
521
  }
522
+ function paintLayer(ctx, box, options, layer, image) {
523
+ // painted over the border box, inside what the border paints
524
+ const area = layer.backgroundClip === 'border-area';
525
+ if (area && !pushBorderArea(ctx, box, options))
526
+ return;
527
+ paintBackground(ctx, box, options, layer);
528
+ if (image && layer.backgroundImage)
529
+ image(layer);
530
+ if (area)
531
+ ctx.restore();
532
+ }
422
533
  /** A box's padding box in window coordinates. */
423
534
  function paddingBox(box, options) {
424
535
  return {
@@ -429,8 +540,10 @@ function paddingBox(box, options) {
429
540
  };
430
541
  }
431
542
  /** Where a layer of a box's background is placed, in window coordinates:
432
- * the box its `background-origin` names (CSS Backgrounds 3, 3.8). */
433
- 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;
434
547
  const [t, r, b, l] = edgeInsets(box, style.backgroundOrigin);
435
548
  return {
436
549
  x: box.x + l + options.originX,
@@ -496,8 +609,14 @@ function paintBox(ctx, box, options) {
496
609
  ctx.save();
497
610
  ctx.globalAlpha = ctx.globalAlpha * opacity;
498
611
  }
612
+ if (masked(box.style) && paintMasked(ctx, box, options)) {
613
+ if (fade)
614
+ ctx.restore();
615
+ return;
616
+ }
499
617
  // `clip` shows the part of an absolutely positioned box it names, its own
500
- // 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
501
620
  const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
502
621
  if (!clip || (clip.w > 0 && clip.h > 0)) {
503
622
  const clipped = !!clip && pushClip(ctx, clip, null);
@@ -508,6 +627,135 @@ function paintBox(ctx, box, options) {
508
627
  if (fade)
509
628
  ctx.restore();
510
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
+ }
511
759
  function paintContent(ctx, box, options) {
512
760
  const visible = box.style.visibility === 'visible';
513
761
  if (visible)
@@ -627,14 +875,7 @@ function paintOwnBackground(ctx, box, options) {
627
875
  if (style.boxShadow)
628
876
  paintShadows(ctx, box, options, false);
629
877
  if (box !== options.canvasSource) {
630
- paintLayers(ctx, box, options, (layer) => {
631
- const painted = clipArea(box, options, layer, true);
632
- const area = painted &&
633
- clampRect(options, painted.x, painted.y, painted.w, painted.h);
634
- if (area) {
635
- paintBackgroundImage(ctx, layer, area, originBox(box, options, layer), options, painted.corners);
636
- }
637
- });
878
+ paintLayers(ctx, box, options, frameImages(ctx, box, options));
638
879
  }
639
880
  if (style.boxShadow)
640
881
  paintShadows(ctx, box, options, true);
@@ -644,6 +885,21 @@ function paintOwnBackground(ctx, box, options) {
644
885
  if (box.kind === 'table')
645
886
  paintPartBackgrounds(ctx, box, options);
646
887
  }
888
+ /**
889
+ * How `paintLayers` draws a frame's image layers: each in the area its
890
+ * `background-clip` names, from the box its `background-origin` names
891
+ * (CSS Backgrounds 3, 3.7 and 3.8) — a block's, or an inline box's
892
+ * fragment's, whose background is its images as much as its colour.
893
+ */
894
+ function frameImages(ctx, frame, options) {
895
+ return (layer) => {
896
+ const painted = clipArea(frame, options, layer, true);
897
+ const area = painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
898
+ if (area) {
899
+ paintBackgroundImage(ctx, layer, area, originBox(frame, options, layer), options, painted.corners);
900
+ }
901
+ };
902
+ }
647
903
  /** How far past its shape a shadow's blur shows: three standard deviations,
648
904
  * the blur being two (CSS Backgrounds 3, 7.1.1). */
649
905
  function shadowReach(blur) {
@@ -742,9 +998,6 @@ function paintShadows(ctx, box, options, inset) {
742
998
  }, 0);
743
999
  continue;
744
1000
  }
745
- if (bakedOuter(ctx, options, s, color, shape, around, covered ? null : { rect: own, corners })) {
746
- continue;
747
- }
748
1001
  // what of the shadow falls under the box is not drawn
749
1002
  const clipped = !covered && ctx.clip && ctx.rect && ctx.save;
750
1003
  if (clipped) {
@@ -755,7 +1008,7 @@ function paintShadows(ctx, box, options, inset) {
755
1008
  ctx.clip();
756
1009
  }
757
1010
  fillShadow(ctx, s, color, (dx) => {
758
- 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);
759
1012
  }, shape.x + shape.width + reach);
760
1013
  if (clipped)
761
1014
  ctx.restore();
@@ -805,8 +1058,6 @@ function paintShadows(ctx, box, options, inset) {
805
1058
  fillRing(ctx, pad, inner, hole, within);
806
1059
  continue;
807
1060
  }
808
- if (bakedInset(ctx, options, s, color, pad, inner, hole, within))
809
- continue;
810
1061
  if (!ctx.clip || !ctx.save || !ctx.rect)
811
1062
  continue;
812
1063
  ctx.save();
@@ -824,128 +1075,24 @@ function paintShadows(ctx, box, options, inset) {
824
1075
  fillShadow(ctx, s, color, (dx) => {
825
1076
  ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
826
1077
  if (hole.width > 0 && hole.height > 0) {
827
- 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);
828
1079
  }
829
- }, frame.x + frame.width + shadowReach(s.blur));
1080
+ }, frame.x + frame.width + shadowReach(s.blur), 'evenodd');
830
1081
  ctx.restore();
831
1082
  }
832
1083
  }
833
- /** Corners as part of a key. */
834
- function cornerKey(c) {
835
- return `${c.x.join(',')}/${c.y.join(',')}`;
836
- }
837
- /**
838
- * An outer shadow drawn once on a surface of its own and composited: the
839
- * shape's shadow, and where `box` is given the box cut out of it — so a box
840
- * that shows what is behind it shows no shadow there, with the clip on the
841
- * small surface rather than the window. False where there is no surface.
842
- */
843
- function bakedOuter(ctx, options, s, color, shape, around, box) {
844
- if (!options.cached || !ctx.drawImage)
845
- return false;
846
- const reach = shadowReach(s.blur) + 1;
847
- const x0 = Math.floor(shape.x) - reach;
848
- const y0 = Math.floor(shape.y) - reach;
849
- const w = Math.ceil(shape.x + shape.width) + reach - x0;
850
- const h = Math.ceil(shape.y + shape.height) + reach - y0;
851
- const sx = shape.x - x0;
852
- const sy = shape.y - y0;
853
- const cut = box && {
854
- x: box.rect.x - x0,
855
- y: box.rect.y - y0,
856
- width: box.rect.width,
857
- height: box.rect.height,
858
- };
859
- const key = [
860
- 'shadow',
861
- w,
862
- h,
863
- sx,
864
- sy,
865
- shape.width,
866
- shape.height,
867
- cornerKey(around),
868
- s.blur,
869
- color,
870
- cut
871
- ? `${cut.x},${cut.y},${cut.width},${cut.height},${cornerKey(box.corners)}`
872
- : '',
873
- ].join('|');
874
- const image = options.cached(key, w, h, (sctx) => {
875
- if (cut) {
876
- sctx.save();
877
- sctx.beginPath();
878
- sctx.rect(0, 0, w, h);
879
- roundedRect(sctx, cut.x, cut.y, cut.width, cut.height, box.corners, true, true);
880
- sctx.clip();
881
- }
882
- fillShadow(sctx, s, color, (dx) => {
883
- roundedRect(sctx, sx - dx, sy, shape.width, shape.height, around);
884
- }, sx + shape.width + reach);
885
- if (cut)
886
- sctx.restore();
887
- });
888
- if (!image)
889
- return false;
890
- ctx.drawImage(image, x0, y0);
891
- return true;
892
- }
893
- /**
894
- * An inset shadow drawn once on a surface the size of the padding box and
895
- * composited: a frame around the hole, whose shadow falls inside, clipped
896
- * to the padding box's corners on the surface. False where there is none.
897
- */
898
- function bakedInset(ctx, options, s, color, pad, inner, hole, within) {
899
- if (!options.cached || !ctx.drawImage)
900
- return false;
901
- const w = Math.round(pad.width);
902
- const h = Math.round(pad.height);
903
- const hx = hole.x - pad.x;
904
- const hy = hole.y - pad.y;
905
- const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
906
- const key = [
907
- 'inset',
908
- w,
909
- h,
910
- cornerKey(inner),
911
- hx,
912
- hy,
913
- hole.width,
914
- hole.height,
915
- cornerKey(within),
916
- s.blur,
917
- color,
918
- ].join('|');
919
- const image = options.cached(key, w, h, (sctx) => {
920
- sctx.save();
921
- sctx.beginPath();
922
- roundedRect(sctx, 0, 0, w, h, inner);
923
- sctx.clip();
924
- fillShadow(sctx, s, color, (dx) => {
925
- sctx.rect(-reach - dx, -reach, w + 2 * reach, h + 2 * reach);
926
- if (hole.width > 0 && hole.height > 0) {
927
- roundedRect(sctx, hx - dx, hy, hole.width, hole.height, within, true, true);
928
- }
929
- }, w + reach + shadowReach(s.blur));
930
- sctx.restore();
931
- });
932
- if (!image)
933
- return false;
934
- ctx.drawImage(image, pad.x, pad.y);
935
- return true;
936
- }
937
1084
  /**
938
1085
  * Fill a shadow's shape: in its colour where it has no blur, and otherwise
939
1086
  * as the context's shadow of the shape drawn `dx` to the left — clear of
940
1087
  * the window, its right edge at `right` before the move — with the shadow
941
1088
  * offset back by as much, so that only the shadow lands.
942
1089
  */
943
- function fillShadow(ctx, s, color, shape, right) {
1090
+ function fillShadow(ctx, s, color, shape, right, rule = 'nonzero') {
944
1091
  if (!(s.blur > 0)) {
945
1092
  ctx.fillStyle = color;
946
1093
  ctx.beginPath();
947
1094
  shape(0);
948
- ctx.fill();
1095
+ ctx.fill(rule);
949
1096
  return;
950
1097
  }
951
1098
  const dx = Math.ceil(right) + 1;
@@ -957,9 +1104,26 @@ function fillShadow(ctx, s, color, shape, right) {
957
1104
  ctx.fillStyle = '#000000';
958
1105
  ctx.beginPath();
959
1106
  shape(dx);
960
- ctx.fill();
1107
+ ctx.fill(rule);
961
1108
  ctx.restore();
962
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
+ }
963
1127
  /**
964
1128
  * The band between two rounded rectangles, the inner inside the outer, in
965
1129
  * the colour already set. Where a side has no band the two edges meet, and
@@ -1006,6 +1170,7 @@ function inFlow(parent, child, options) {
1006
1170
  return (style.overflowX === 'visible' &&
1007
1171
  style.overflowY === 'visible' &&
1008
1172
  opacityOf(child) >= 1 &&
1173
+ !masked(style) &&
1009
1174
  // containment makes it a stacking context, painted whole
1010
1175
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1011
1176
  }
@@ -1031,6 +1196,7 @@ function flowFlex(parent, child, options) {
1031
1196
  return (style.overflowX === 'visible' &&
1032
1197
  style.overflowY === 'visible' &&
1033
1198
  !(child.outOfFlow && style.clip) &&
1199
+ !masked(style) &&
1034
1200
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1035
1201
  }
1036
1202
  /**
@@ -1215,9 +1381,10 @@ function spreadCorners(c, width, height, top, right, bottom, left) {
1215
1381
  return rounded ? { x, y } : null;
1216
1382
  }
1217
1383
  /** How far in from a box's border box the box a `<visual-box>` names is:
1218
- * top, right, bottom and left. */
1384
+ * top, right, bottom and left — none for `border-area`, whose layer is
1385
+ * painted over the border box inside what the border paints. */
1219
1386
  function edgeInsets(box, which) {
1220
- if (which === 'border-box')
1387
+ if (which === 'border-box' || which === 'border-area')
1221
1388
  return NO_INSETS;
1222
1389
  const t = box.borderTop;
1223
1390
  const r = box.borderRight;
@@ -1256,7 +1423,55 @@ function paintPositioned(ctx, box, options) {
1256
1423
  return;
1257
1424
  }
1258
1425
  }
1259
- 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;
1260
1475
  }
1261
1476
  /**
1262
1477
  * The box an out-of-flow box is positioned in, whose clips are the ones it
@@ -1675,6 +1890,9 @@ function stacksLayers(box) {
1675
1890
  return true;
1676
1891
  if (style.opacity < 1 || FADED_BLOCKS.has(box))
1677
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;
1678
1896
  // layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
1679
1897
  if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
1680
1898
  return true;
@@ -1694,7 +1912,7 @@ function flexItem(box) {
1694
1912
  * outside a box that clips where its containing block is, a fixed one
1695
1913
  * outside every one, and a relative one inside every one.
1696
1914
  */
1697
- function clipsFor(box, context) {
1915
+ export function clipsFor(box, context) {
1698
1916
  let from = box.parent;
1699
1917
  if (box.outOfFlow) {
1700
1918
  const fixed = box.style.position === 'fixed';
@@ -1721,6 +1939,7 @@ function clipsFor(box, context) {
1721
1939
  * parent painted it, it was. A clip of no area leaves none of it to paint.
1722
1940
  */
1723
1941
  function paintStacked(ctx, box, options) {
1942
+ options = atViewport(box, options);
1724
1943
  if (!intersects(box, options))
1725
1944
  return;
1726
1945
  const between = CLIPS_BETWEEN.get(box);
@@ -1884,7 +2103,8 @@ function paintBackground(ctx, box, options, style) {
1884
2103
  // box, which is where it starts, and repeated under the borders — or
1885
2104
  // the viewport's, where it is fixed; its line runs across the whole of
1886
2105
  // it, not the part this paint reaches
1887
- const fixed = style.backgroundAttachment === 'fixed' && options.canvas;
2106
+ const fixed = style.backgroundAttachment === 'fixed' &&
2107
+ (options.viewport ?? options.canvas);
1888
2108
  const origin = fixed || originBox(box, options, style);
1889
2109
  const at = snapped(origin.x, origin.y, origin.width, origin.height);
1890
2110
  if (rounded && style.backgroundSize !== 'auto' && ctx.clip) {
@@ -2260,7 +2480,7 @@ const MAX_TILES = 4096;
2260
2480
  */
2261
2481
  function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
2262
2482
  if (style.backgroundAttachment === 'fixed' && options.canvas) {
2263
- at = options.canvas;
2483
+ at = options.viewport ?? options.canvas;
2264
2484
  }
2265
2485
  const url = style.backgroundImage;
2266
2486
  const loaded = url ? options.backgroundImageFor?.(url) : null;
@@ -2821,6 +3041,88 @@ function roundedRing(ctx, box, x, y, w, h, options) {
2821
3041
  }, insetCorners(corners, top, right, bottom, left));
2822
3042
  return true;
2823
3043
  }
3044
+ /**
3045
+ * Clip to what a box's border paints, for a layer of `background-clip:
3046
+ * border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
3047
+ * not its colour — a transparent border still has an area, which is what
3048
+ * the value is for. The shapes are the border painter's own, so that the
3049
+ * background shows exactly where that border would: the ring `roundedRing`
3050
+ * fills where a rounded border is solid, the ring the trapezoids of a 3D
3051
+ * style make, and otherwise the rectangles `fillEdge` fills a side at a
3052
+ * time — the dots, the dashes, the two lines of a double border. All of
3053
+ * them run clockwise and a ring's inside the other way, so the clip is the
3054
+ * union by the non-zero rule, the one every context's `clip` takes. False
3055
+ * where the border paints nothing or the context cannot clip, and nothing
3056
+ * is pushed.
3057
+ */
3058
+ function pushBorderArea(ctx, box, options) {
3059
+ const t = box.borderTop;
3060
+ const r = box.borderRight;
3061
+ const b = box.borderBottom;
3062
+ const l = box.borderLeft;
3063
+ if (!(t || r || b || l) || !ctx.beginPath || !ctx.rect || !ctx.clip) {
3064
+ return false;
3065
+ }
3066
+ const s = box.style;
3067
+ const left = box.x + options.originX;
3068
+ const top = frameY(box) + options.originY;
3069
+ const x = Math.round(left);
3070
+ const y = Math.round(top);
3071
+ const w = Math.round(left + box.width) - x;
3072
+ const h = Math.round(top + frameHeight(box)) - y;
3073
+ if (w <= 0 || h <= 0)
3074
+ return false;
3075
+ const area = clampRect(options, x, y, w, h);
3076
+ if (!area)
3077
+ return false;
3078
+ const corners = cornersOf(s, w, h);
3079
+ ctx.save();
3080
+ ctx.beginPath();
3081
+ if (corners && canCurve(ctx) && solidBorder(box)) {
3082
+ roundedRect(ctx, area.x, area.y, area.w, area.h, corners, true);
3083
+ const iw = area.w - l - r;
3084
+ const ih = area.h - t - b;
3085
+ if (iw > 0 && ih > 0) {
3086
+ const inner = insetCorners(corners, t, r, b, l);
3087
+ roundedRect(ctx, area.x + l, area.y + t, iw, ih, inner, true, true);
3088
+ }
3089
+ }
3090
+ else if (sculpted(s)) {
3091
+ // a band a side, which the trapezoids cover between them
3092
+ ctx.rect(area.x, area.y, area.w, t);
3093
+ ctx.rect(area.x, area.y + area.h - b, area.w, b);
3094
+ ctx.rect(area.x, area.y + t, l, area.h - t - b);
3095
+ ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
3096
+ }
3097
+ else {
3098
+ const path = { fillRect: ctx.rect.bind(ctx) };
3099
+ const edge = (ex, ey, ew, eh, style, horizontal) => {
3100
+ const rect = clampRect(options, ex, ey, ew, eh);
3101
+ if (rect)
3102
+ fillEdge(path, rect, horizontal ? ex : ey, style, horizontal);
3103
+ };
3104
+ const between = h - t - b;
3105
+ if (t > 0)
3106
+ edge(x, y, w, t, s.borderTopStyle, true);
3107
+ if (b > 0)
3108
+ edge(x, y + h - b, w, b, s.borderBottomStyle, true);
3109
+ if (l > 0)
3110
+ edge(x, y + t, l, between, s.borderLeftStyle, false);
3111
+ if (r > 0)
3112
+ edge(x + w - r, y + t, r, between, s.borderRightStyle, false);
3113
+ }
3114
+ ctx.clip();
3115
+ return true;
3116
+ }
3117
+ /** Whether every side a box has a border on is solid, which a rounded box
3118
+ * draws as one ring (`roundedRing`). */
3119
+ function solidBorder(box) {
3120
+ const s = box.style;
3121
+ return ((!box.borderTop || s.borderTopStyle === 'solid') &&
3122
+ (!box.borderRight || s.borderRightStyle === 'solid') &&
3123
+ (!box.borderBottom || s.borderBottomStyle === 'solid') &&
3124
+ (!box.borderLeft || s.borderLeftStyle === 'solid'));
3125
+ }
2824
3126
  /** The border styles drawn in two shades, as though lit from the top left. */
2825
3127
  const SCULPTED = new Set(['groove', 'ridge', 'inset', 'outset']);
2826
3128
  function sculpted(s) {
@@ -3215,6 +3517,20 @@ function paintLines(ctx, box, options) {
3215
3517
  if (line.y + line.height > top)
3216
3518
  visible.push(line);
3217
3519
  }
3520
+ // and a line the damage misses whose text or inline-block `position:
3521
+ // relative` moved into it: drawn by its line, it went undrawn with it
3522
+ const moved = MOVED_OFF_LINES.get(lines);
3523
+ if (moved && reachedOff(visible, moved, top, bottom)) {
3524
+ const drawn = new Set(visible);
3525
+ for (const line of moved) {
3526
+ if (movedInto(line, top, bottom))
3527
+ drawn.add(line);
3528
+ }
3529
+ visible.length = 0;
3530
+ for (const line of lines)
3531
+ if (drawn.has(line))
3532
+ visible.push(line);
3533
+ }
3218
3534
  }
3219
3535
  else {
3220
3536
  visible.push(...lines);
@@ -3225,7 +3541,7 @@ function paintLines(ctx, box, options) {
3225
3541
  for (const line of visible) {
3226
3542
  if (line.background)
3227
3543
  paintLineBackground(ctx, line, options);
3228
- paintInlineBoxes(ctx, line, options, line === lines[0] ? null : bleeds);
3544
+ paintInlineBoxes(ctx, line, lines, options, line === lines[0] ? null : bleeds);
3229
3545
  for (const text of line.texts) {
3230
3546
  const natural = text.layout.lines[text.layoutLine];
3231
3547
  if (natural)
@@ -3276,6 +3592,35 @@ function paintLines(ctx, box, options) {
3276
3592
  }
3277
3593
  }
3278
3594
  }
3595
+ /** Whether any of the lines moved content was taken off, and the damage
3596
+ * does not already draw, has it within the damage's rows. */
3597
+ function reachedOff(visible, moved, top, bottom) {
3598
+ for (const line of moved) {
3599
+ if (!visible.includes(line) && movedInto(line, top, bottom))
3600
+ return true;
3601
+ }
3602
+ return false;
3603
+ }
3604
+ /** Whether what was moved off a line — its inline-blocks and images, where
3605
+ * their ink is, and its texts, where they are drawn — reaches the rows
3606
+ * between `top` and `bottom`. */
3607
+ function movedInto(line, top, bottom) {
3608
+ for (const placed of line.atomics) {
3609
+ const box = placed.box;
3610
+ if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
3611
+ return true;
3612
+ }
3613
+ }
3614
+ for (const text of line.texts) {
3615
+ const natural = text.layout.lines[text.layoutLine];
3616
+ if (!natural)
3617
+ continue;
3618
+ const y = text.drawY + natural.y;
3619
+ if (y < bottom && y + natural.height > top)
3620
+ return true;
3621
+ }
3622
+ return false;
3623
+ }
3279
3624
  /**
3280
3625
  * The part of each such fragment above its line, drawn again over the
3281
3626
  * text there. CSS 2.1 Appendix E paints a block's inline content a line at
@@ -3297,7 +3642,7 @@ function paintBleeds(ctx, bleeds, options) {
3297
3642
  ctx.beginPath();
3298
3643
  ctx.rect(left, top, Math.ceil(fragment.width) + 2, bottom - top);
3299
3644
  ctx.clip();
3300
- paintLayers(ctx, fragment, options);
3645
+ paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
3301
3646
  paintBorders(ctx, fragment, options);
3302
3647
  ctx.restore();
3303
3648
  }
@@ -3640,27 +3985,22 @@ function paintLineBackground(ctx, line, options) {
3640
3985
  style: background.style,
3641
3986
  }, options);
3642
3987
  }
3643
- function paintInlineBoxes(ctx, line, options,
3644
- /** Where a fragment that reaches up over the lines before goes, to be
3645
- * drawn again over their text; null for a block's first line. */
3646
- bleeds = null) {
3647
- let fragments = null;
3648
- const widen = (box, left, right) => {
3649
- fragments ??= new Map();
3650
- const f = fragments.get(box);
3651
- if (f) {
3652
- f.left = Math.min(f.left, left);
3653
- f.right = Math.max(f.right, right);
3654
- }
3655
- else {
3656
- fragments.set(box, { left, right, start: false, end: false });
3657
- }
3658
- };
3659
- // Every text on a line is drawn on one baseline (`finishLine`), and an
3660
- // engine's is from the top of its layout rather than of the line. Taken
3661
- // where the line has each text: a box `position: relative` moves is
3662
- // moved by its own offset below, and the boxes around it are not.
3663
- 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 = [];
3664
4004
  const shifted = SHIFTED_LINES.has(line);
3665
4005
  for (const text of line.texts) {
3666
4006
  const natural = text.layout.lines[text.layoutLine];
@@ -3668,8 +4008,6 @@ bleeds = null) {
3668
4008
  if (!natural || !boxAt)
3669
4009
  continue;
3670
4010
  const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
3671
- const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
3672
- baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
3673
4011
  const x = text.drawX - (shift?.x ?? 0) + natural.x;
3674
4012
  for (const run of natural.runs) {
3675
4013
  const owner = boxAt.call(text.spans, run.start);
@@ -3681,45 +4019,147 @@ bleeds = null) {
3681
4019
  const right = Math.min(x + natural.width, x + run.x + run.width);
3682
4020
  if (right <= left)
3683
4021
  continue;
3684
- for (const box of decoratedAncestors(owner))
3685
- 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
+ });
3686
4036
  }
3687
4037
  }
3688
4038
  for (const placed of line.atomics) {
3689
4039
  const atomic = placed.box;
3690
- for (const box of decoratedAncestors(atomic)) {
3691
- widen(box, placed.x - atomic.marginLeft, placed.x + atomic.width + atomic.marginRight);
3692
- }
4040
+ marks.push({
4041
+ left: placed.x - atomic.marginLeft,
4042
+ right: placed.x + atomic.width + atomic.marginRight,
4043
+ boxes: decoratedAncestors(atomic),
4044
+ });
3693
4045
  }
3694
4046
  for (const edge of line.edges ?? []) {
3695
- const box = edge.box;
3696
- if (!box.decoration)
3697
- continue;
3698
- // The element's `direction` says which side its start is on, whatever
3699
- // its text reads as (CSS 2.1 8.6); the edge's margin is outside the box,
3700
- // its border and padding inside.
3701
- const onLeft = (edge.side === 'start') !== (box.style.direction === 'rtl');
3702
- if (onLeft) {
3703
- const left = edge.x + box.marginLeft;
3704
- 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);
3705
4063
  }
3706
- else {
3707
- const right = edge.x + edge.width - box.marginRight;
3708
- 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);
3709
4070
  }
3710
- const f = fragments.get(box);
3711
- if (edge.side === 'start')
3712
- f.start = true;
3713
- else
3714
- f.end = true;
3715
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);
3716
4134
  if (!fragments)
3717
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
+ }
3718
4150
  const boxes = [...fragments.keys()].sort((a, b) => depthOf(a) - depthOf(b));
3719
4151
  for (const box of boxes) {
3720
4152
  if (box.style.visibility !== 'visible')
3721
4153
  continue;
3722
- 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
+ {
3723
4163
  const face = box.decoration;
3724
4164
  // on its own baseline, which `vertical-align` may raise off the line's
3725
4165
  const own = shifted
@@ -3754,8 +4194,16 @@ bleeds = null) {
3754
4194
  style,
3755
4195
  };
3756
4196
  if (fragment.width <= 0)
3757
- continue;
3758
- paintLayers(ctx, fragment, options);
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
+ }
4206
+ paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
3759
4207
  paintBorders(ctx, fragment, options);
3760
4208
  if (box.style.outlineStyle !== 'none')
3761
4209
  paintOutline(ctx, fragment, options);
@@ -3764,6 +4212,76 @@ bleeds = null) {
3764
4212
  }
3765
4213
  }
3766
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
+ }
3767
4285
  /** An inline box's style on a line it does not both start and end on: no
3768
4286
  * rounded corner on a side it goes on from. Made once a style and a pair
3769
4287
  * of ends, so a repaint copies no style and the layers made of it are
@@ -3824,6 +4342,20 @@ function decoratedAncestors(box) {
3824
4342
  DECORATED.set(box, found);
3825
4343
  return found;
3826
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
+ }
3827
4359
  /**
3828
4360
  * The band the document selection covers on one line.
3829
4361
  *