@react-x11/components 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +545 -126
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
@@ -20,11 +20,13 @@
20
20
  // back on the laid-out lines is exactly what that module already knows how to
21
21
  // draw.
22
22
  import { canFill, lineBands, paintRunBackgrounds, paintRunRules, } from '../richtext/runs.js';
23
- import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
24
- import { blend, borderShades } from './css/color.js';
23
+ import { AUTO, alphaOf, inkColor, isTransparent, resolve, } from './css/values.js';
24
+ import { SCHEME_COLORS, blend, borderShades } from './css/color.js';
25
25
  import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, masked, scrolls, } from './css/style.js';
26
- import { contained } from './layout/block.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';
26
+ import { LARGEST } from './css/calc.js';
27
+ import { invert, mapRect, transformed } from './css/transform.js';
28
+ import { contained, placedMatrix } from './layout/block.js';
29
+ import { BOX_RAISES, LINE_BOX_RAISES, PADDED_FACES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, COLUMN_LINES, COLUMN_PIECES, COLUMN_ROWS, columned, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
28
30
  import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
29
31
  import { tableGrid } from './layout/grid.js';
30
32
  import { SvgDrawing, inlineDrawing } from './svg.js';
@@ -43,6 +45,33 @@ import { SvgDrawing, inlineDrawing } from './svg.js';
43
45
  * (`layoutDocument`).
44
46
  */
45
47
  export const OUT_OF_FLOW_REACH = new WeakMap();
48
+ /**
49
+ * How far above and below a block's lines its inline content draws, at
50
+ * most, by the array its lines are: glyphs taller than a `line-height` under
51
+ * their face's height, and an atomic's ink past the line it is on. The
52
+ * slack a paint's cull of the lines takes on top of their own rows
53
+ * (`paintLines`); none for the lines of almost every block, whose content
54
+ * is inside them.
55
+ */
56
+ const LINE_INK = new WeakMap();
57
+ /**
58
+ * For a box painted through a matrix (`placedMatrix`), the bounds of what
59
+ * it and its descendants draw before the matrix is applied: what
60
+ * `boundsX`… would have been, which are where it puts them. The paint
61
+ * inside the box, and a point taken back into it, are in these.
62
+ */
63
+ const UNTRANSFORMED = new WeakMap();
64
+ /** The bounds of what a box painted through a matrix draws, in its own
65
+ * coordinates: asked of a box `placedMatrix` answers for, whose entry the
66
+ * last `computePaintBounds` wrote. */
67
+ export function ownBounds(box) {
68
+ return (UNTRANSFORMED.get(box) ?? {
69
+ x: box.boundsX,
70
+ y: box.boundsY,
71
+ width: box.boundsWidth,
72
+ height: box.boundsHeight,
73
+ });
74
+ }
46
75
  export function computePaintBounds(box, moved = false) {
47
76
  // A box with no rectangle of its own gives only what it holds: nothing,
48
77
  // until a child with bounds is met.
@@ -170,8 +199,21 @@ export function computePaintBounds(box, moved = false) {
170
199
  // above has its bounds already: walked from its line as well, an
171
200
  // inline-block in an inline-block was walked twice a level, and twenty of
172
201
  // them took seventy milliseconds a layout.
202
+ //
203
+ // And a line's text is drawn from its face's ascent above the baseline to
204
+ // its descent below, its content area (CSS 2.1 10.6.1), which is past the
205
+ // line box wherever `line-height` is under the face's height: a line box
206
+ // is its line-height, whatever its glyphs are. Zen Garden 215's 91px title
207
+ // sits on the 20px lines its body's `line-height: 1.25em` gave it, as a
208
+ // length, and hangs 40px above them and 40px below — ink that a repaint
209
+ // of those rows has to reach, where the lines alone do not.
210
+ let above = 0;
211
+ let below = 0;
173
212
  if (lines) {
213
+ const movedOff = MOVED_OFF_LINES.get(lines);
174
214
  for (const line of lines) {
215
+ let top = line.y;
216
+ let bottom = line.y + line.height;
175
217
  for (const placed of line.atomics) {
176
218
  const atomic = placed.box;
177
219
  if (atomic.boundsY === Infinity)
@@ -180,9 +222,69 @@ export function computePaintBounds(box, moved = false) {
180
222
  y1 = Math.min(y1, atomic.boundsY);
181
223
  x2 = Math.max(x2, atomic.boundsX + atomic.boundsWidth);
182
224
  y2 = Math.max(y2, atomic.boundsY + atomic.boundsHeight);
225
+ top = Math.min(top, atomic.boundsY);
226
+ bottom = Math.max(bottom, atomic.boundsY + atomic.boundsHeight);
227
+ }
228
+ // what `position: relative` moved off a line the cull finds on its
229
+ // own (`reachedOff`), wherever it went
230
+ if (movedOff?.has(line))
231
+ continue;
232
+ for (const text of line.texts) {
233
+ const natural = text.layout.lines[text.layoutLine];
234
+ if (!natural)
235
+ continue;
236
+ const baseline = text.drawY + natural.baseline;
237
+ top = Math.min(top, baseline - (natural.ascent ?? 0));
238
+ bottom = Math.max(bottom, baseline + (natural.descent ?? 0));
239
+ }
240
+ if (top < line.y) {
241
+ y1 = Math.min(y1, top);
242
+ above = Math.max(above, line.y - top);
243
+ }
244
+ if (bottom > line.y + line.height) {
245
+ y2 = Math.max(y2, bottom);
246
+ below = Math.max(below, bottom - line.y - line.height);
183
247
  }
184
248
  }
185
249
  }
250
+ if (lines && (above > 0 || below > 0))
251
+ LINE_INK.set(lines, { above, below });
252
+ else if (lines)
253
+ LINE_INK.delete(lines);
254
+ // a `clip-path` cuts the box and all it holds: no ink past the path, and
255
+ // none where it leaves nothing — though the scrollable overflow is what
256
+ // it was, as a browser has it, which is why a page that hides a box with
257
+ // one makes it a pixel square as well
258
+ if (own && box.style.clipPath) {
259
+ const { rect } = clipPathOf(box, DOCUMENT);
260
+ x1 = Math.max(x1, rect.x);
261
+ y1 = Math.max(y1, rect.y);
262
+ x2 = Math.min(x2, rect.x + rect.w);
263
+ y2 = Math.min(y2, rect.y + rect.h);
264
+ if (!(x2 > x1 && y2 > y1))
265
+ x1 = Infinity;
266
+ }
267
+ // A box that turns, scales or skews draws what it holds through its
268
+ // matrix (`paintTransformed`): its ink is where that puts it, the
269
+ // rectangle around the one it would have filled, and the one it would
270
+ // have filled is kept for the paint inside it, which is in its own
271
+ // coordinates. One flattened to nothing draws nothing.
272
+ const matrix = own ? placedMatrix(box) : null;
273
+ if (matrix && x1 !== Infinity) {
274
+ UNTRANSFORMED.set(box, { x: x1, y: y1, width: x2 - x1, height: y2 - y1 });
275
+ if (invert(matrix)) {
276
+ // no further than a length reaches (`LARGEST`): a page's
277
+ // `scale(1e30)` is a box as large as a browser holds one, and no
278
+ // larger
279
+ const to = mapRect(matrix, x1, y1, x2 - x1, y2 - y1);
280
+ x1 = Math.max(to.x, -LARGEST);
281
+ y1 = Math.max(to.y, -LARGEST);
282
+ x2 = Math.min(to.x + to.width, LARGEST);
283
+ y2 = Math.min(to.y + to.height, LARGEST);
284
+ }
285
+ else
286
+ x1 = Infinity;
287
+ }
186
288
  if (x1 === Infinity) {
187
289
  // nothing to draw: no damage meets it, and no parent takes it in
188
290
  box.boundsX = Infinity;
@@ -207,13 +309,21 @@ export function computePaintBounds(box, moved = false) {
207
309
  // whether the box clips only matters where its content reaches past it,
208
310
  // and its style is one more object a walk of every box would read
209
311
  const end = empty ? -Infinity : box.y + box.height;
210
- if (bottom <= end)
211
- return end;
212
- const style = box.style;
213
- return box.parent &&
214
- (style.overflowX !== 'visible' || style.overflowY !== 'visible')
215
- ? end
216
- : bottom;
312
+ let reach = end;
313
+ if (bottom > end) {
314
+ const style = box.style;
315
+ reach =
316
+ box.parent &&
317
+ (style.overflowX !== 'visible' || style.overflowY !== 'visible')
318
+ ? end
319
+ : bottom;
320
+ }
321
+ // the scrollable overflow of a transformed box is where its transform
322
+ // puts it (CSS Overflow 3, 2.2)
323
+ if (!matrix || reach === -Infinity)
324
+ return reach;
325
+ const to = mapRect(matrix, box.x, box.y, box.width, reach - box.y);
326
+ return Math.min(to.y + to.height, LARGEST);
217
327
  }
218
328
  /**
219
329
  * Whether a box has a rectangle of its own to draw in. An inline box has
@@ -363,12 +473,27 @@ export function paintDocument(ctx, tree, options) {
363
473
  return;
364
474
  ctx.save();
365
475
  const canvas = canvasBackground(tree);
476
+ const scheme = rootScheme(tree);
477
+ if (scheme !== tree.paletteScheme) {
478
+ // A document is drawn on the window's own ground, the palette's, as an
479
+ // embedded one is on a transparent canvas. Where its root's colour
480
+ // scheme is not the scheme of what it is embedded in, its canvas is
481
+ // opaque, in the `Canvas` colour of its own (CSS Color Adjust 1, 2.2):
482
+ // a page that says it is light, and sets its text dark on no
483
+ // background, is not read against a dark window.
484
+ const area = canvasArea(tree.root, options);
485
+ if (area) {
486
+ ctx.fillStyle = SCHEME_COLORS[scheme].canvas;
487
+ ctx.fillRect(area.x, area.y, area.w, area.h);
488
+ }
489
+ }
366
490
  if (canvas)
367
491
  paintCanvas(ctx, canvas, tree.root, options);
368
492
  paintBox(ctx, tree.root, {
369
493
  ...options,
370
494
  canvasSource: canvas?.source,
371
495
  negative: tree.negative,
496
+ selectionStyler: options.selection ? tree.selectionStyler : null,
372
497
  });
373
498
  ctx.restore();
374
499
  }
@@ -414,6 +539,24 @@ function canvasBackground(tree) {
414
539
  ? { style: body.style, source: body, anchor: top }
415
540
  : null;
416
541
  }
542
+ /** The used colour scheme of the document's root element: the `<html>`'s,
543
+ * or that of whichever box stands in for it (`canvasBackground`). */
544
+ function rootScheme(tree) {
545
+ if (tree.impliedHtml)
546
+ return tree.impliedHtml.colorScheme;
547
+ return (childNamed(tree.root, 'html') ?? tree.root).style.colorScheme;
548
+ }
549
+ /** The canvas, cut to what is being painted: the whole element where the
550
+ * host says what that is, and else the root box. */
551
+ function canvasArea(root, options) {
552
+ const whole = options.canvas ?? {
553
+ x: root.x + options.originX,
554
+ y: root.y + options.originY,
555
+ width: root.width,
556
+ height: root.height,
557
+ };
558
+ return clampRect(options, Math.round(whole.x), Math.round(whole.y), Math.ceil(whole.width), Math.ceil(whole.height));
559
+ }
417
560
  /** A box's child element of that name, or one inside the anonymous boxes
418
561
  * it is wrapped in: a `<body>` in an `<html>` set `display: table` is in
419
562
  * an anonymous row and cell. */
@@ -430,13 +573,7 @@ function childNamed(box, name) {
430
573
  return null;
431
574
  }
432
575
  function paintCanvas(ctx, { style: source, anchor }, root, options) {
433
- const whole = options.canvas ?? {
434
- x: root.x + options.originX,
435
- y: root.y + options.originY,
436
- width: root.width,
437
- height: root.height,
438
- };
439
- const area = clampRect(options, Math.round(whole.x), Math.round(whole.y), Math.ceil(whole.width), Math.ceil(whole.height));
576
+ const area = canvasArea(root, options);
440
577
  if (!area)
441
578
  return;
442
579
  const layers = layersOf(source) ?? [source];
@@ -447,7 +584,7 @@ function paintCanvas(ctx, { style: source, anchor }, root, options) {
447
584
  ctx.fillStyle = inkColor(style.backgroundColor, style.color);
448
585
  ctx.fillRect(area.x, area.y, area.w, area.h);
449
586
  }
450
- if (style.backgroundGradient && ctx.createLinearGradient && visible) {
587
+ if (style.backgroundGradient && visible) {
451
588
  // sized by the root element's box and repeated down the canvas, as a
452
589
  // browser does — the stripes a short page with a gradient on its
453
590
  // body shows — or by the viewport, where it is fixed
@@ -615,28 +752,289 @@ function paintBox(ctx, box, options) {
615
752
  return;
616
753
  }
617
754
  // `clip` shows the part of an absolutely positioned box it names, its own
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
620
- const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
621
- if (!clip || (clip.w > 0 && clip.h > 0)) {
622
- const clipped = !!clip && pushClip(ctx, clip, null);
623
- paintContent(ctx, box, options);
624
- if (clipped)
755
+ // background and borders among it (CSS 2.1 11.1.2), and `clip-path` the
756
+ // part of any box (`pushOwnClips`) — the content painted here, not in a
757
+ // call of its own: a frame more a level and a deep document ran out of
758
+ // stack
759
+ const pushed = pushOwnClips(ctx, box, options);
760
+ if (pushed >= 0) {
761
+ paintContent(ctx, box, pathClips(box) ? underPath(options) : options);
762
+ for (let i = 0; i < pushed; i += 1)
625
763
  ctx.restore();
626
764
  }
627
765
  if (fade)
628
766
  ctx.restore();
629
767
  }
630
- /** A box and what it holds, cut to its `clip` as `paintBox` cuts it. */
768
+ /** How large a surface a transformed box is drawn on: a large window's
769
+ * worth of pixels twice over, and no side longer than a pixmap's may be. */
770
+ const RASTER_LIMIT = 16 * 1024 * 1024;
771
+ const RASTER_SIDE = 16384;
772
+ /** What XRender carries a picture's transform in is 16.16 fixed point: a
773
+ * number past this does not fit, and is thrown out of the request's
774
+ * encoder. */
775
+ const FIXED_LIMIT = 32000;
776
+ /**
777
+ * A box that turns, scales or skews (CSS Transforms 1): painted as it was
778
+ * laid out, through its matrix — about its `transform-origin`, where its
779
+ * translation has already put it (`placedMatrix`). What it holds is
780
+ * painted with it, as the stacking context it is (`stacksLayers`), culled
781
+ * against the damage taken back through the matrix.
782
+ *
783
+ * The native contexts draw everything through a matrix, a text layout's
784
+ * outlines among it, glyphs and all (`scalesText`). ntk's draws paths
785
+ * through one; a glyph it draws as it was shaped, upright and at its size,
786
+ * where the matrix puts its origin, and an image through a transform the
787
+ * server holds in fixed point, which a picture drawn small far across a
788
+ * window does not fit. So there only a box that is paths and flat colour —
789
+ * an icon, a chevron, a spinner — is drawn through the matrix, and any
790
+ * other on a surface of its own, as it was laid out, with the surface
791
+ * drawn through the matrix (`paintRaster`): a picture of its text rather
792
+ * than the text, resampled, and right about where it is and which way up.
793
+ *
794
+ * A context with no `transform` draws the box where it was laid out: the
795
+ * headless mock, which draws nothing, and a recording one.
796
+ */
797
+ function paintTransformed(ctx, box, options) {
798
+ const matrix = placedMatrix(box);
799
+ if (!matrix) {
800
+ paintBox(ctx, box, options);
801
+ return;
802
+ }
803
+ if (!intersects(box, options) ||
804
+ COLLAPSED_CELLS.has(box) ||
805
+ CLAMPED.has(box)) {
806
+ return;
807
+ }
808
+ if (typeof ctx.transform !== 'function') {
809
+ paintPlaced(ctx, box, options);
810
+ return;
811
+ }
812
+ // In the window's coordinates, where the document's origin is the
813
+ // options': T(origin) · matrix · T(-origin). And about an origin moved
814
+ // with the box's corner to the pixel it is drawn from: what is painted is
815
+ // snapped to the pixel grid a box at a time, so a box a fraction of a
816
+ // pixel down the page is drawn from a whole one, and turned about where
817
+ // it was laid out it came back off the grid — a square mirrored onto
818
+ // itself showed a line of what was under it along each side. A browser
819
+ // snaps a transformed box's corner before its transform the same way.
820
+ const left = box.x + options.originX;
821
+ const top = box.y + options.originY;
822
+ const ox = options.originX + Math.round(left) - left;
823
+ const oy = options.originY + Math.round(top) - top;
824
+ const [a, b, c, d] = matrix;
825
+ const through = [
826
+ a,
827
+ b,
828
+ c,
829
+ d,
830
+ matrix[4] + ox - (a * ox + c * oy),
831
+ matrix[5] + oy - (b * ox + d * oy),
832
+ ];
833
+ const back = invert(through);
834
+ if (!back)
835
+ return;
836
+ const damage = options.damage;
837
+ const inside = {
838
+ ...options,
839
+ damage: damage && mapRect(back, damage.x, damage.y, damage.width, damage.height),
840
+ };
841
+ if (ctx.scalesText !== true &&
842
+ !drawnAsPaths(box) &&
843
+ paintRaster(ctx, box, inside, through)) {
844
+ return;
845
+ }
846
+ ctx.save();
847
+ ctx.transform(a, b, c, d, through[4], through[5]);
848
+ paintPlaced(ctx, box, inside);
849
+ ctx.restore();
850
+ }
851
+ /**
852
+ * Whether a box's style turns, scales or skews it, which is asked before
853
+ * every box that may be one is painted (`paintPositioned`, `paintLines`) —
854
+ * there, and not in `paintBox`, whose frame is one of three a level of
855
+ * nesting costs the stack: a name more in it, and a document a thousand
856
+ * boxes deep ran out.
857
+ */
858
+ function turns(box) {
859
+ const style = box.style;
860
+ return (style.transform !== null || style.rotate !== null || style.scale !== null);
861
+ }
862
+ /**
863
+ * `paintBox` for a box whose ink bounds are not in the coordinates it is
864
+ * being painted in, and so is not culled by them: one painted through a
865
+ * matrix, in its own (`paintTransformed`).
866
+ */
867
+ function paintPlaced(ctx, box, options) {
868
+ const opacity = opacityOf(box);
869
+ if (opacity <= 0)
870
+ return;
871
+ const fade = opacity < 1 && typeof ctx.globalAlpha === 'number';
872
+ if (fade) {
873
+ ctx.save();
874
+ ctx.globalAlpha = ctx.globalAlpha * opacity;
875
+ }
876
+ if (!masked(box.style) || !paintMasked(ctx, box, options)) {
877
+ paintClipped(ctx, box, options);
878
+ }
879
+ if (fade)
880
+ ctx.restore();
881
+ }
882
+ /**
883
+ * Whether all a box and what it holds draw is paths and flat colour:
884
+ * backgrounds, borders, outlines and inline drawings — no text and no list
885
+ * marker, whose glyphs a context may not turn, and no image, gradient,
886
+ * shadow or mask, which one draws through a picture's transform
887
+ * (`paintTransformed`).
888
+ */
889
+ function drawnAsPaths(box) {
890
+ if (box.subtreeTextEnd > box.subtreeTextStart)
891
+ return false;
892
+ const stack = [box];
893
+ while (stack.length) {
894
+ const at = stack.pop();
895
+ if (at.marker || at.replaced === 'image')
896
+ return false;
897
+ const style = at.style;
898
+ if (style.backgroundImage !== null ||
899
+ style.backgroundGradient !== null ||
900
+ style.backgroundImages !== null ||
901
+ style.boxShadow !== null ||
902
+ style.borderImage.source !== null ||
903
+ masked(style)) {
904
+ return false;
905
+ }
906
+ for (const child of at.children)
907
+ stack.push(child);
908
+ }
909
+ return true;
910
+ }
911
+ /**
912
+ * A transformed box painted on a surface of its own, as it was laid out,
913
+ * and the surface drawn through its matrix: the part of what it draws that
914
+ * the damage reaches. True where that is done — or is nothing to do, or
915
+ * cannot be: a part too large for a surface, or a matrix that does not fit
916
+ * the fixed point a picture's transform is sent in, which is one that draws
917
+ * the box a thirtieth its size far across a window, or flatter than can be
918
+ * seen. None of it is drawn then. False where the context has no surface to
919
+ * draw on, and the caller draws the box through the matrix itself.
920
+ */
921
+ function paintRaster(ctx, box, options, through) {
922
+ if (!options.surface || !ctx.drawImage)
923
+ return false;
924
+ const own = ownBounds(box);
925
+ let x0 = Math.floor(own.x + options.originX);
926
+ let y0 = Math.floor(own.y + options.originY);
927
+ let x1 = Math.ceil(own.x + own.width + options.originX);
928
+ let y1 = Math.ceil(own.y + own.height + options.originY);
929
+ const damage = options.damage;
930
+ if (damage) {
931
+ // a pixel more each way: the edge of what is drawn is sampled from
932
+ // beside it
933
+ x0 = Math.max(x0, Math.floor(damage.x) - 1);
934
+ y0 = Math.max(y0, Math.floor(damage.y) - 1);
935
+ x1 = Math.min(x1, Math.ceil(damage.x + damage.width) + 1);
936
+ y1 = Math.min(y1, Math.ceil(damage.y + damage.height) + 1);
937
+ }
938
+ const w = x1 - x0;
939
+ const h = y1 - y0;
940
+ if (!(w > 0 && h > 0))
941
+ return true;
942
+ if (w > RASTER_SIDE || h > RASTER_SIDE || w * h > RASTER_LIMIT)
943
+ return true;
944
+ // what the context makes of the draw below: the picture's transform is
945
+ // the inverse of its matrix after the surface's place in it
946
+ const placed = invert([
947
+ through[0],
948
+ through[1],
949
+ through[2],
950
+ through[3],
951
+ through[0] * x0 + through[2] * y0 + through[4],
952
+ through[1] * x0 + through[3] * y0 + through[5],
953
+ ]);
954
+ if (!placed || placed.some((n) => Math.abs(n) > FIXED_LIMIT))
955
+ return true;
956
+ const surface = options.surface(w, h);
957
+ if (!surface)
958
+ return false;
959
+ try {
960
+ // the surface's own coordinates, the window's moved to its corner
961
+ const on = {
962
+ ...options,
963
+ originX: options.originX - x0,
964
+ originY: options.originY - y0,
965
+ damage: { x: 0, y: 0, width: w, height: h },
966
+ canvas: options.canvas && {
967
+ ...options.canvas,
968
+ x: options.canvas.x - x0,
969
+ y: options.canvas.y - y0,
970
+ },
971
+ viewport: options.viewport && {
972
+ ...options.viewport,
973
+ x: options.viewport.x - x0,
974
+ y: options.viewport.y - y0,
975
+ },
976
+ clips: [],
977
+ };
978
+ paintPlaced(surface.getContext('2d'), box, on);
979
+ ctx.save();
980
+ ctx.transform(through[0], through[1], through[2], through[3], through[4], through[5]);
981
+ ctx.drawImage(surface, x0, y0);
982
+ ctx.restore();
983
+ return true;
984
+ }
985
+ finally {
986
+ surface.destroy?.();
987
+ }
988
+ }
989
+ /** A box and what it holds, cut to its `clip` and its `clip-path` as
990
+ * `paintBox` cuts it. */
631
991
  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))
992
+ const pushed = pushOwnClips(ctx, box, options);
993
+ if (pushed < 0)
634
994
  return;
635
- const clipped = !!clip && pushClip(ctx, clip, null);
636
- paintContent(ctx, box, options);
637
- if (clipped)
995
+ paintContent(ctx, box, pathClips(box) ? underPath(options) : options);
996
+ for (let i = 0; i < pushed; i += 1)
638
997
  ctx.restore();
639
998
  }
999
+ /**
1000
+ * Cut what follows to what a box shows of itself: the part its `clip`
1001
+ * names, where it is absolutely positioned, and the part its `clip-path`
1002
+ * does (CSS Masking 1, 5.1). How many clips were pushed, for the caller to
1003
+ * restore — none where the context cannot clip — or -1 where they leave
1004
+ * none of the box to show, and nothing was pushed.
1005
+ */
1006
+ function pushOwnClips(ctx, box, options) {
1007
+ const style = box.style;
1008
+ const clip = box.outOfFlow && style.clip ? clipOf(box, options) : null;
1009
+ if (clip && !(clip.w > 0 && clip.h > 0))
1010
+ return -1;
1011
+ const path = pathClips(box) ? clipPathOf(box, options) : null;
1012
+ if (path && !(path.rect.w > 0 && path.rect.h > 0))
1013
+ return -1;
1014
+ let pushed = 0;
1015
+ if (clip && pushClip(ctx, clip, null))
1016
+ pushed += 1;
1017
+ if (path && pushClip(ctx, path.rect, path.radii))
1018
+ pushed += 1;
1019
+ return pushed;
1020
+ }
1021
+ /** Whether a box is cut to a `clip-path`: one with a rectangle of its own
1022
+ * for the path to be measured in. An inline box is drawn on its block's
1023
+ * lines, and is not cut. */
1024
+ export function pathClips(box) {
1025
+ return box.style.clipPath !== null && hasRect(box);
1026
+ }
1027
+ /**
1028
+ * The options what a box cut to a `clip-path` holds is painted with: no
1029
+ * clip around it for a positioned box to escape. The path cuts all the
1030
+ * element holds, whatever that is positioned from, where a box that clips
1031
+ * its overflow lets out a box whose containing block is outside it — put
1032
+ * off until that clip ended (`paintPositioned`), one in here was painted
1033
+ * with the path gone too.
1034
+ */
1035
+ function underPath(options) {
1036
+ return { ...options, clips: [] };
1037
+ }
640
1038
  const MASK_LAYERS = new WeakMap();
641
1039
  /** No corners: a mask layer is placed in the box's rectangle, which its
642
1040
  * radii do not round (CSS Masking 1, 7). */
@@ -872,6 +1270,13 @@ function paintOwnBackground(ctx, box, options) {
872
1270
  if (hidden(box))
873
1271
  return;
874
1272
  const style = box.style;
1273
+ if (columned.any) {
1274
+ const pieces = COLUMN_PIECES.get(box);
1275
+ if (pieces) {
1276
+ paintColumnPieces(ctx, box, pieces, options);
1277
+ return;
1278
+ }
1279
+ }
875
1280
  if (style.boxShadow)
876
1281
  paintShadows(ctx, box, options, false);
877
1282
  if (box !== options.canvasSource) {
@@ -885,6 +1290,70 @@ function paintOwnBackground(ctx, box, options) {
885
1290
  if (box.kind === 'table')
886
1291
  paintPartBackgrounds(ctx, box, options);
887
1292
  }
1293
+ /**
1294
+ * The background and the borders of a block a column break falls inside
1295
+ * (CSS Multi-column 1), a piece at a time: drawn in each column as the
1296
+ * whole box standing there, and cut to the piece of it that is — which is
1297
+ * `box-decoration-break: slice`, CSS's default (CSS Fragmentation 3, 6.1):
1298
+ * no border and no padding at a break, and the background going on from
1299
+ * one piece to the next. Its shadow is not drawn: a sliced box casts one as
1300
+ * a whole and this has no whole box to cast it from.
1301
+ */
1302
+ function paintColumnPieces(ctx, box, pieces, options) {
1303
+ if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
1304
+ return;
1305
+ for (const piece of pieces) {
1306
+ if (!(piece.height > 0 && piece.width > 0))
1307
+ continue;
1308
+ const frame = {
1309
+ x: piece.x,
1310
+ y: piece.wholeY,
1311
+ width: piece.width,
1312
+ height: piece.wholeHeight,
1313
+ captionTop: 0,
1314
+ captionBottom: 0,
1315
+ borderTop: box.borderTop,
1316
+ borderRight: box.borderRight,
1317
+ borderBottom: box.borderBottom,
1318
+ borderLeft: box.borderLeft,
1319
+ padTop: box.padTop,
1320
+ padRight: box.padRight,
1321
+ padBottom: box.padBottom,
1322
+ padLeft: box.padLeft,
1323
+ style: box.style,
1324
+ };
1325
+ const left = Math.round(piece.x + options.originX);
1326
+ const top = Math.round(piece.y + options.originY);
1327
+ const right = Math.round(piece.x + piece.width + options.originX);
1328
+ const bottom = Math.round(piece.y + piece.height + options.originY);
1329
+ ctx.save();
1330
+ ctx.beginPath();
1331
+ ctx.rect(left, top, right - left, bottom - top);
1332
+ ctx.clip();
1333
+ paintLayers(ctx, frame, options, frameImages(ctx, frame, options));
1334
+ paintBorders(ctx, frame, options);
1335
+ ctx.restore();
1336
+ }
1337
+ }
1338
+ /**
1339
+ * Clip to the rows of a text's layout that are in the text's column
1340
+ * (`COLUMN_ROWS`), where the layout is drawn at `left` and `top`: the rest
1341
+ * of it is another column's, drawn there. Answers whether it clipped, and
1342
+ * the caller restores.
1343
+ */
1344
+ function clipToRows(ctx, text, rows, left, top) {
1345
+ if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
1346
+ return false;
1347
+ const y1 = Math.round(top + rows.top);
1348
+ const y2 = Math.round(top + rows.bottom);
1349
+ ctx.save();
1350
+ ctx.beginPath();
1351
+ ctx.rect(Math.floor(left) - ROW_REACH, y1, Math.ceil(text.layout.width) + 2 * ROW_REACH, y2 - y1);
1352
+ ctx.clip();
1353
+ return true;
1354
+ }
1355
+ /** How far to either side of its layout a column's text may draw. */
1356
+ const ROW_REACH = 2048;
888
1357
  /**
889
1358
  * How `paintLayers` draws a frame's image layers: each in the area its
890
1359
  * `background-clip` names, from the box its `background-origin` names
@@ -1171,6 +1640,8 @@ function inFlow(parent, child, options) {
1171
1640
  style.overflowY === 'visible' &&
1172
1641
  opacityOf(child) >= 1 &&
1173
1642
  !masked(style) &&
1643
+ // cut to a path with all it holds, as one group
1644
+ !style.clipPath &&
1174
1645
  // containment makes it a stacking context, painted whole
1175
1646
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1176
1647
  }
@@ -1197,6 +1668,7 @@ function flowFlex(parent, child, options) {
1197
1668
  style.overflowY === 'visible' &&
1198
1669
  !(child.outOfFlow && style.clip) &&
1199
1670
  !masked(style) &&
1671
+ !style.clipPath &&
1200
1672
  !contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
1201
1673
  }
1202
1674
  /**
@@ -1423,7 +1895,11 @@ function paintPositioned(ctx, box, options) {
1423
1895
  return;
1424
1896
  }
1425
1897
  }
1426
- paintBox(ctx, box, atViewport(box, options));
1898
+ // through its matrix, where it turns, scales or skews
1899
+ if (turns(box))
1900
+ paintTransformed(ctx, box, atViewport(box, options));
1901
+ else
1902
+ paintBox(ctx, box, atViewport(box, options));
1427
1903
  }
1428
1904
  /**
1429
1905
  * The options a box is painted with where it is fixed to the viewport
@@ -1460,13 +1936,16 @@ export function fixedToViewport(box) {
1460
1936
  if (box.style.position !== 'fixed')
1461
1937
  return false;
1462
1938
  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))
1939
+ if (holdsFixed(at))
1466
1940
  return false;
1467
1941
  }
1468
1942
  return true;
1469
1943
  }
1944
+ /** Whether a box is the containing block of the fixed boxes in it: a
1945
+ * transformed box, or one with layout or paint containment. */
1946
+ function holdsFixed(box) {
1947
+ return (transformed(box.style) || contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT));
1948
+ }
1470
1949
  function rootOf(box) {
1471
1950
  let root = box;
1472
1951
  while (root.parent)
@@ -1476,17 +1955,22 @@ function rootOf(box) {
1476
1955
  /**
1477
1956
  * The box an out-of-flow box is positioned in, whose clips are the ones it
1478
1957
  * is under (`paintPositioned`, and the hit test's `deepestAt`): its nearest
1479
- * positioned or translated ancestor, or the root. Null for a fixed box,
1480
- * which is under none.
1958
+ * positioned or transformed ancestor, or the root. Null for a box fixed to
1959
+ * the viewport, which is under none — one in a transformed box is fixed to
1960
+ * that, and under what it is under.
1481
1961
  */
1482
1962
  export function containingBlockOf(box) {
1483
- if (box.style.position === 'fixed')
1963
+ if (box.style.position === 'fixed') {
1964
+ for (let at = box.parent; at?.parent; at = at.parent) {
1965
+ if (holdsFixed(at))
1966
+ return at;
1967
+ }
1484
1968
  return null;
1969
+ }
1485
1970
  let containing = box.parent;
1486
1971
  while (containing?.parent &&
1487
1972
  containing.style.position === 'static' &&
1488
- !containing.style.translate &&
1489
- !containing.style.transformTranslate) {
1973
+ !transformed(containing.style)) {
1490
1974
  containing = containing.parent;
1491
1975
  }
1492
1976
  return containing;
@@ -1579,6 +2063,201 @@ function clipOf(box, options) {
1579
2063
  h: Math.round(box.y + options.originY + bottom) - y,
1580
2064
  };
1581
2065
  }
2066
+ /** The document's own coordinates, as a space to place a clip in. */
2067
+ const DOCUMENT = { originX: 0, originY: 0, scale: 1, damage: null };
2068
+ /**
2069
+ * A box's `clip-path` in window coordinates (CSS Masking 1, 5.1): the
2070
+ * rectangle its shape is (CSS Shapes 1, 3.1), each edge on the pixel it
2071
+ * falls nearest as a box's are, and its corners where it has any — of no
2072
+ * area where the shape leaves none of the box to show. Measured in the box
2073
+ * the path names, the border box unless it says: `inset()` in from its
2074
+ * edges, `rect()` and `xywh()` from its top left, and the box itself, with
2075
+ * the element's own corners, where no shape is written.
2076
+ */
2077
+ function clipPathOf(box, options) {
2078
+ const path = box.style.clipPath;
2079
+ // the box it is measured in, in from the border box — or out from it
2080
+ const within = path.box;
2081
+ const [it, ir, ib, il] = within === 'margin-box'
2082
+ ? [-box.marginTop, -box.marginRight, -box.marginBottom, -box.marginLeft]
2083
+ : edgeInsets(box, within);
2084
+ const width = Math.max(0, box.width - il - ir);
2085
+ const height = Math.max(0, frameHeight(box) - it - ib);
2086
+ // how far in from that box's edges the rectangle's are
2087
+ const [a, b, c, d] = path.lengths;
2088
+ let top = 0;
2089
+ let right = 0;
2090
+ let bottom = 0;
2091
+ let left = 0;
2092
+ if (path.shape === 'inset') {
2093
+ top = resolve(a, height);
2094
+ right = resolve(b, width);
2095
+ bottom = resolve(c, height);
2096
+ left = resolve(d, width);
2097
+ // a pair that adds up to more than the box is across is reduced to
2098
+ // it, each in its proportion, as overlapping radii are (3.1):
2099
+ // `inset(50%)` and `inset(75%)` are both the box's centre, and nothing
2100
+ if (left + right > width && left > 0 && right > 0) {
2101
+ const f = Math.max(0, width) / (left + right);
2102
+ left *= f;
2103
+ right *= f;
2104
+ }
2105
+ if (top + bottom > height && top > 0 && bottom > 0) {
2106
+ const f = Math.max(0, height) / (top + bottom);
2107
+ top *= f;
2108
+ bottom *= f;
2109
+ }
2110
+ }
2111
+ else if (path.shape === 'rect') {
2112
+ // four edges from the top and the left, `auto` the box's own; the
2113
+ // right and the bottom are no less than the left and the top
2114
+ top = a === AUTO ? 0 : resolve(a, height);
2115
+ left = d === AUTO ? 0 : resolve(d, width);
2116
+ right = b === AUTO ? 0 : width - Math.max(left, resolve(b, width));
2117
+ bottom = c === AUTO ? 0 : height - Math.max(top, resolve(c, height));
2118
+ }
2119
+ else if (path.shape === 'xywh') {
2120
+ left = resolve(a, width);
2121
+ top = resolve(b, height);
2122
+ right = width - left - Math.max(0, resolve(c, width));
2123
+ bottom = height - top - Math.max(0, resolve(d, height));
2124
+ }
2125
+ const x0 = box.x + options.originX + il;
2126
+ const y0 = frameY(box) + options.originY + it;
2127
+ const x = Math.round(x0 + left);
2128
+ const y = Math.round(y0 + top);
2129
+ const rect = {
2130
+ x,
2131
+ y,
2132
+ w: Math.max(0, Math.round(x0 + width - right) - x),
2133
+ h: Math.max(0, Math.round(y0 + height - bottom) - y),
2134
+ };
2135
+ let radii = null;
2136
+ if (path.shape === null) {
2137
+ // the box alone: its edge, shaped as the element's corners shape it
2138
+ const corners = cornersOf(box.style, box.width, frameHeight(box));
2139
+ if (corners) {
2140
+ radii =
2141
+ within === 'margin-box'
2142
+ ? spreadCorners(corners, box.width, frameHeight(box), -it, -ir, -ib, -il)
2143
+ : roundedOf(insetCorners(corners, it, ir, ib, il));
2144
+ }
2145
+ }
2146
+ else if (path.radii) {
2147
+ // a percentage is of the box the shape is in, and the radii are fitted
2148
+ // to the rectangle they round
2149
+ radii = cornersFor(path.radii, path.radiiY, width, height, rect.w, rect.h);
2150
+ }
2151
+ return { rect, radii };
2152
+ }
2153
+ /** Corners, or null where every one of them is square. */
2154
+ function roundedOf(c) {
2155
+ for (let i = 0; i < 4; i += 1)
2156
+ if (c.x[i] > 0 && c.y[i] > 0)
2157
+ return c;
2158
+ return null;
2159
+ }
2160
+ /**
2161
+ * Whether a point of the document is in what a box's `clip-path` leaves of
2162
+ * it: what a hit test asks, since nothing the path cuts away — of the box
2163
+ * or of anything in it — is under a pointer (`deepestAt`).
2164
+ */
2165
+ export function inClipPath(box, x, y) {
2166
+ const { rect, radii } = clipPathOf(box, DOCUMENT);
2167
+ const right = rect.x + rect.w;
2168
+ const bottom = rect.y + rect.h;
2169
+ if (x < rect.x || x >= right || y < rect.y || y >= bottom)
2170
+ return false;
2171
+ if (!radii)
2172
+ return true;
2173
+ // inside the rectangle, and past a corner's curve: each corner is a
2174
+ // quarter of an ellipse about a centre its radii in from the two edges
2175
+ for (let i = 0; i < 4; i += 1) {
2176
+ const rx = radii.x[i];
2177
+ const ry = radii.y[i];
2178
+ if (!(rx > 0 && ry > 0))
2179
+ continue;
2180
+ const onLeft = i === 0 || i === 3;
2181
+ const cx = onLeft ? rect.x + rx : right - rx;
2182
+ const cy = i < 2 ? rect.y + ry : bottom - ry;
2183
+ if (onLeft ? x >= cx : x <= cx)
2184
+ continue;
2185
+ if (i < 2 ? y >= cy : y <= cy)
2186
+ continue;
2187
+ const dx = (x - cx) / rx;
2188
+ const dy = (y - cy) / ry;
2189
+ if (dx * dx + dy * dy > 1)
2190
+ return false;
2191
+ }
2192
+ return true;
2193
+ }
2194
+ /**
2195
+ * Whether a point of the document is in the part of a box its `clip` shows
2196
+ * (CSS 2.1 11.1.2) — anywhere, for a box that is cut to none: what a hit
2197
+ * test asks, since what the clip cuts away is not drawn (`deepestAt`). What
2198
+ * it cuts away is the box and what is painted with it: all the box holds
2199
+ * where it is a stacking context (`stacksLayers`), and elsewhere all but a
2200
+ * fixed box, which the context around it paints under no clip
2201
+ * (`clipsFor`). Chrome cuts that one too.
2202
+ */
2203
+ export function inClip(box, x, y) {
2204
+ if (!box.outOfFlow || !box.style.clip)
2205
+ return true;
2206
+ const rect = clipOf(box, DOCUMENT);
2207
+ return (x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h);
2208
+ }
2209
+ /**
2210
+ * The part of the document a box shows through, in the document's own
2211
+ * pixels, or null where nothing cuts it: the `clip` it is cut to (CSS 2.1
2212
+ * 11.1.2), and the clips of the boxes it is under — each that clips its
2213
+ * overflow or is cut to a `clip` of its own, from its containing block up
2214
+ * (11.1.1), so an absolute box is outside a box that clips where its
2215
+ * containing block is, and a fixed one outside them all. And every
2216
+ * `clip-path` from the box up, its own among them: a path cuts all its
2217
+ * element holds, whatever that is positioned from (CSS Masking 1, 5.1) —
2218
+ * to the rectangle a path's shape is, its corners left square. A rectangle
2219
+ * of no area where none of it shows. Painting cuts what it draws as it
2220
+ * walks down; this is for what is not painted here and has to be cut all
2221
+ * the same, a control's widget (`controlRectsOf`).
2222
+ */
2223
+ export function clipAround(box, scale = 1) {
2224
+ const space = { originX: 0, originY: 0, scale, damage: null };
2225
+ let x0 = -Infinity;
2226
+ let y0 = -Infinity;
2227
+ let x1 = Infinity;
2228
+ let y1 = Infinity;
2229
+ let cut = false;
2230
+ const under = (rect) => {
2231
+ cut = true;
2232
+ x0 = Math.max(x0, rect.x);
2233
+ y0 = Math.max(y0, rect.y);
2234
+ x1 = Math.min(x1, rect.x + rect.w);
2235
+ y1 = Math.min(y1, rect.y + rect.h);
2236
+ };
2237
+ if (box.outOfFlow && box.style.clip)
2238
+ under(clipOf(box, space));
2239
+ // what holds a box, as clips go: its parent, or out of the flow its
2240
+ // containing block, and the boxes between them do not clip it
2241
+ const holder = (inner) => inner.outOfFlow ? containingBlockOf(inner) : inner.parent;
2242
+ for (let outer = holder(box); outer; outer = holder(outer)) {
2243
+ if (outer.outOfFlow && outer.style.clip)
2244
+ under(clipOf(outer, space));
2245
+ if (clipsOverflow(outer))
2246
+ under(clipEdge(outer, space).rect);
2247
+ }
2248
+ for (let at = box; at; at = at.parent) {
2249
+ if (pathClips(at))
2250
+ under(clipPathOf(at, space).rect);
2251
+ }
2252
+ if (!cut)
2253
+ return null;
2254
+ return {
2255
+ x: x0,
2256
+ y: y0,
2257
+ width: Math.max(0, x1 - x0),
2258
+ height: Math.max(0, y1 - y0),
2259
+ };
2260
+ }
1582
2261
  /**
1583
2262
  * The radii of a box's padding edge — its border radii less the borders —
1584
2263
  * for a clip, or null where a rectangle clips the same: padding at least a
@@ -1637,6 +2316,16 @@ function cornersOf(style, w, h) {
1637
2316
  across[3] === 0) {
1638
2317
  return null;
1639
2318
  }
2319
+ return cornersFor(across, down, w, h, w, h);
2320
+ }
2321
+ /**
2322
+ * Radii as corners in pixels: each a length or a percentage of a box `w`
2323
+ * by `h`, across and down, and all of them reduced together where two on a
2324
+ * side of the rectangle they round — `fitW` by `fitH`, which for a
2325
+ * `border-radius` is that box, and for a `clip-path`'s `round` is the
2326
+ * rectangle inside it — would overlap. Null where every corner is square.
2327
+ */
2328
+ function cornersFor(across, down, w, h, fitW, fitH) {
1640
2329
  const vertical = down ?? across;
1641
2330
  const x = [0, 0, 0, 0];
1642
2331
  const y = [0, 0, 0, 0];
@@ -1657,10 +2346,10 @@ function cornersOf(style, w, h) {
1657
2346
  if (sum > side)
1658
2347
  f = Math.min(f, Math.max(0, side) / sum);
1659
2348
  };
1660
- fit(w, x[0] + x[1]);
1661
- fit(h, y[1] + y[2]);
1662
- fit(w, x[2] + x[3]);
1663
- fit(h, y[3] + y[0]);
2349
+ fit(fitW, x[0] + x[1]);
2350
+ fit(fitH, y[1] + y[2]);
2351
+ fit(fitW, x[2] + x[3]);
2352
+ fit(fitH, y[3] + y[0]);
1664
2353
  if (f < 1) {
1665
2354
  for (let i = 0; i < 4; i += 1) {
1666
2355
  x[i] *= f;
@@ -1789,8 +2478,7 @@ function layered(parent, child) {
1789
2478
  return true;
1790
2479
  if (style.position !== 'relative' &&
1791
2480
  style.position !== 'sticky' &&
1792
- !style.translate &&
1793
- !style.transformTranslate) {
2481
+ !transformed(style)) {
1794
2482
  return false;
1795
2483
  }
1796
2484
  return child.kind !== 'inline' && !onLine(parent, child);
@@ -1802,7 +2490,7 @@ function layered(parent, child) {
1802
2490
  * drawn twice — darker at every antialiased edge — and a translucent
1803
2491
  * background had its alpha doubled.
1804
2492
  */
1805
- function onLine(parent, child) {
2493
+ export function onLine(parent, child) {
1806
2494
  if (parent.lines === null && parent.kind !== 'inline')
1807
2495
  return false;
1808
2496
  if (child.isFloat || child.outOfFlow)
@@ -1878,24 +2566,31 @@ function settleLayers(box, list) {
1878
2566
  * it itself, and the ones below its flow: positioned with a `z-index`, or a
1879
2567
  * flex item with one; fixed or sticky with none (CSS Positioned Layout 3, as
1880
2568
  * browsers paint them — a fixed header's box set behind its content with
1881
- * `z-index: -1` went behind the page); or under full opacity (CSS Color 4
2569
+ * `z-index: -1` went behind the page); transformed (CSS Transforms 1, 3);
2570
+ * or under full opacity (CSS Color 4
1882
2571
  * 3.2), its own or the opacity it takes from an inline box it broke
1883
2572
  * (`FADED_BLOCKS`), which is painted as one group — faded with it, or at
1884
2573
  * `opacity: 0` not at all, where its root context drew a hover menu's
1885
2574
  * absolute children.
1886
2575
  */
1887
- function stacksLayers(box) {
2576
+ export function stacksLayers(box) {
1888
2577
  const style = box.style;
1889
2578
  if (style.position === 'fixed' || style.position === 'sticky')
1890
2579
  return true;
1891
2580
  if (style.opacity < 1 || FADED_BLOCKS.has(box))
1892
2581
  return true;
1893
- // and so does a mask, which is applied to the group (CSS Masking 1, 7)
1894
- if (masked(style))
2582
+ // and so does a mask, which is applied to the group (CSS Masking 1, 7),
2583
+ // and a clip path, which cuts it (5.1)
2584
+ if (masked(style) || pathClips(box))
1895
2585
  return true;
1896
2586
  // layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
1897
2587
  if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
1898
2588
  return true;
2589
+ // and a transform (CSS Transforms 1, 3), on a box one applies to: what it
2590
+ // holds is painted with it, through its matrix, and not by a context
2591
+ // outside it
2592
+ if (transformed(style) && box.kind !== 'inline')
2593
+ return true;
1899
2594
  if (typeof style.zIndex !== 'number')
1900
2595
  return false;
1901
2596
  return style.position !== 'static' || flexItem(box);
@@ -1918,7 +2613,7 @@ export function clipsFor(box, context) {
1918
2613
  const fixed = box.style.position === 'fixed';
1919
2614
  while (from && from !== context) {
1920
2615
  const style = from.style;
1921
- if (style.translate || style.transformTranslate)
2616
+ if (transformed(style))
1922
2617
  break;
1923
2618
  if (!fixed && style.position !== 'static')
1924
2619
  break;
@@ -2018,9 +2713,19 @@ function hoistFrom(box, root) {
2018
2713
  return null;
2019
2714
  }
2020
2715
  function byZIndex(a, b) {
2021
- const az = a.style.zIndex === 'auto' ? 0 : a.style.zIndex;
2022
- const bz = b.style.zIndex === 'auto' ? 0 : b.style.zIndex;
2023
- return az - bz;
2716
+ return layerOf(a) - layerOf(b);
2717
+ }
2718
+ /** The `z-index` a box is ordered by among its stacking context's layers:
2719
+ * its own where one applies — to a positioned box, and to a flex or a grid
2720
+ * item (CSS 2.1 9.9.1, CSS Flexbox 5.4) — and none for a box that is
2721
+ * among them for its transform alone, which is painted in the document's
2722
+ * order whatever `z-index` it was given. */
2723
+ function layerOf(box) {
2724
+ const style = box.style;
2725
+ const z = style.zIndex;
2726
+ if (z === 'auto')
2727
+ return 0;
2728
+ return style.position !== 'static' || flexItem(box) ? z : 0;
2024
2729
  }
2025
2730
  /** Whether anything this box or its descendants draw is in the damage. */
2026
2731
  function intersects(box, options) {
@@ -2098,7 +2803,7 @@ function paintBackground(ctx, box, options, style) {
2098
2803
  ctx.fillStyle = inkColor(color, style.color);
2099
2804
  fill();
2100
2805
  }
2101
- if (gradient && ctx.createLinearGradient) {
2806
+ if (gradient) {
2102
2807
  // over the colour, as the layer an image is: the size of the padding
2103
2808
  // box, which is where it starts, and repeated under the borders — or
2104
2809
  // the viewport's, where it is fixed; its line runs across the whole of
@@ -2121,8 +2826,9 @@ function paintBackground(ctx, box, options, style) {
2121
2826
  // with its end colours where a browser would show the next tile
2122
2827
  if (!(at.width > 0 && at.height > 0))
2123
2828
  return;
2124
- ctx.fillStyle = linearGradient(ctx, gradient, at.x, at.y, at.width, at.height, style.color, rect);
2125
- fill();
2829
+ const paint = gradientFill(ctx, gradient, at.x, at.y, at.width, at.height, style.color, rect);
2830
+ if (paint)
2831
+ fillGradient(ctx, paint, rect, rounded);
2126
2832
  }
2127
2833
  else
2128
2834
  paintGradient(ctx, style, gradient, rect, at);
@@ -2170,8 +2876,9 @@ function paintGradient(ctx, style, gradient, area, at) {
2170
2876
  MAX_TILES) {
2171
2877
  // a sliver of a root repeated down a long canvas: the one tile, and
2172
2878
  // its end colours on past it
2173
- ctx.fillStyle = linearGradient(ctx, gradient, x0, y0, w, h, style.color, area);
2174
- ctx.fillRect(area.x, area.y, area.w, area.h);
2879
+ const paint = gradientFill(ctx, gradient, x0, y0, w, h, style.color, area);
2880
+ if (paint)
2881
+ fillGradient(ctx, paint, area);
2175
2882
  return;
2176
2883
  }
2177
2884
  for (let y = fromY; y < toY; y += stepY) {
@@ -2185,8 +2892,9 @@ function paintGradient(ctx, style, gradient, area, at) {
2185
2892
  if (right <= left)
2186
2893
  continue;
2187
2894
  const tile = { x: left, y: top, w: right - left, h: bottom - top };
2188
- ctx.fillStyle = linearGradient(ctx, gradient, x, y, w, h, style.color, tile);
2189
- ctx.fillRect(tile.x, tile.y, tile.w, tile.h);
2895
+ const paint = gradientFill(ctx, gradient, x, y, w, h, style.color, tile);
2896
+ if (paint)
2897
+ fillGradient(ctx, paint, tile);
2190
2898
  }
2191
2899
  }
2192
2900
  }
@@ -2216,6 +2924,39 @@ function linearGradient(ctx, gradient, x, y, w, h, currentColor, visible = null)
2216
2924
  const dy = -Math.cos(angle);
2217
2925
  const length = Math.abs(w * dx) + Math.abs(h * dy);
2218
2926
  const stops = gradient.stops;
2927
+ const at = stopOffsets(stops, length);
2928
+ // the context's offsets are 0 to 1, so a line that stops reach past runs
2929
+ // from the first to the last, and the box sees the part of it between
2930
+ const from = Math.min(0, at[0]);
2931
+ const to = Math.max(1, at[at.length - 1]);
2932
+ const startX = x + w / 2 - (dx * length) / 2;
2933
+ const startY = y + h / 2 - (dy * length) / 2;
2934
+ const x0 = startX + dx * length * from;
2935
+ const y0 = startY + dy * length * from;
2936
+ const x1 = startX + dx * length * to;
2937
+ const y1 = startY + dy * length * to;
2938
+ const colors = stops.map((stop) => inkColor(stop.color, currentColor));
2939
+ if (visible &&
2940
+ length > 0 &&
2941
+ !(Math.abs(x0) < GRADIENT_REACH &&
2942
+ Math.abs(y0) < GRADIENT_REACH &&
2943
+ Math.abs(x1) < GRADIENT_REACH &&
2944
+ Math.abs(y1) < GRADIENT_REACH)) {
2945
+ return clippedGradient(ctx, at, colors, startX, startY, dx * length, dy * length, visible);
2946
+ }
2947
+ const g = ctx.createLinearGradient(x0, y0, x1, y1);
2948
+ for (let i = 0; i < stops.length; i += 1) {
2949
+ g.addColorStop((at[i] - from) / (to - from), colors[i]);
2950
+ }
2951
+ return g;
2952
+ }
2953
+ /**
2954
+ * Where each of a gradient's stops is along a line `length` long, as a
2955
+ * fraction of it (CSS Images 3, 3.5.1): a stop without a position is
2956
+ * spread evenly between its neighbours', the first at 0 and the last at 1
2957
+ * where they have none, and none is before the one before it.
2958
+ */
2959
+ function stopOffsets(stops, length) {
2219
2960
  const at = stops.map((stop) => stop.at === null
2220
2961
  ? null
2221
2962
  : length > 0
@@ -2239,30 +2980,195 @@ function linearGradient(ctx, gradient, x, y, w, h, currentColor, visible = null)
2239
2980
  }
2240
2981
  at[i] = Math.max(at[i], at[i - 1]);
2241
2982
  }
2242
- // the context's offsets are 0 to 1, so a line that stops reach past runs
2243
- // from the first to the last, and the box sees the part of it between
2244
- const from = Math.min(0, at[0]);
2245
- const to = Math.max(1, at[at.length - 1]);
2246
- const startX = x + w / 2 - (dx * length) / 2;
2247
- const startY = y + h / 2 - (dy * length) / 2;
2248
- const x0 = startX + dx * length * from;
2249
- const y0 = startY + dy * length * from;
2250
- const x1 = startX + dx * length * to;
2251
- const y1 = startY + dy * length * to;
2252
- const colors = stops.map((stop) => inkColor(stop.color, currentColor));
2253
- if (visible &&
2254
- length > 0 &&
2255
- !(Math.abs(x0) < GRADIENT_REACH &&
2256
- Math.abs(y0) < GRADIENT_REACH &&
2257
- Math.abs(x1) < GRADIENT_REACH &&
2258
- Math.abs(y1) < GRADIENT_REACH)) {
2259
- return clippedGradient(ctx, at, colors, startX, startY, dx * length, dy * length, visible);
2983
+ return at;
2984
+ }
2985
+ /** A gradient's fill for a box at (`x`, `y`), `w` by `h`: null where the
2986
+ * context draws no gradient of its kind. */
2987
+ function gradientFill(ctx, gradient, x, y, w, h, currentColor, visible = null) {
2988
+ if (gradient.kind === 'radial') {
2989
+ return radialGradient(ctx, gradient, x, y, w, h, currentColor, visible);
2260
2990
  }
2261
- const g = ctx.createLinearGradient(x0, y0, x1, y1);
2262
- for (let i = 0; i < stops.length; i += 1) {
2263
- g.addColorStop((at[i] - from) / (to - from), colors[i]);
2991
+ if (!ctx.createLinearGradient)
2992
+ return null;
2993
+ return {
2994
+ style: linearGradient(ctx, gradient, x, y, w, h, currentColor, visible),
2995
+ matrix: null,
2996
+ };
2997
+ }
2998
+ /**
2999
+ * A `radial-gradient()` across a box (CSS Images 3, 3.2.2): centred where
3000
+ * its position puts it, and as large as its radii, or as its keyword says
3001
+ * — the side or the corner of the box nearest the centre or furthest from
3002
+ * it, and for an ellipse through a corner, the ellipse with the shape the
3003
+ * sides would give it. The stops are spaced along the ray from the centre
3004
+ * to the ending shape's edge, which for an ellipse is its horizontal
3005
+ * radius; one before the centre, where the ray starts, gives the colour
3006
+ * there and no more, and one past the edge carries the gradient on.
3007
+ *
3008
+ * A shape with no width or no height is drawn as its last colour, as
3009
+ * Chrome draws each (crbug.com/635727): the spec draws a zero width as a
3010
+ * gradient mirrored about the centre line, which Chrome does not.
3011
+ */
3012
+ function radialGradient(ctx, gradient, x, y, w, h, currentColor, visible = null) {
3013
+ const colors = gradient.stops.map((stop) => inkColor(stop.color, currentColor));
3014
+ const cx = resolve(gradient.at[0], w);
3015
+ const cy = resolve(gradient.at[1], h);
3016
+ let rx;
3017
+ let ry;
3018
+ if (gradient.radii) {
3019
+ rx = resolve(gradient.radii[0], w);
3020
+ ry = resolve(gradient.radii[1], h);
2264
3021
  }
2265
- return g;
3022
+ else {
3023
+ const extent = gradient.extent ?? 'farthest-corner';
3024
+ const side = extent.startsWith('closest') ? Math.min : Math.max;
3025
+ const sx = side(Math.abs(cx), Math.abs(w - cx));
3026
+ const sy = side(Math.abs(cy), Math.abs(h - cy));
3027
+ const corner = extent.endsWith('corner');
3028
+ if (gradient.circle) {
3029
+ rx = ry = corner ? Math.hypot(sx, sy) : side(sx, sy);
3030
+ }
3031
+ else {
3032
+ // through the corner, in the shape the sides give it
3033
+ rx = corner ? sx * Math.SQRT2 : sx;
3034
+ ry = corner ? sy * Math.SQRT2 : sy;
3035
+ }
3036
+ }
3037
+ if (!(rx > 0 && ry > 0)) {
3038
+ return { style: colors[colors.length - 1], matrix: null };
3039
+ }
3040
+ if (!hasRadialGradients(ctx))
3041
+ return null;
3042
+ let at = stopOffsets(gradient.stops, rx);
3043
+ let inks = colors;
3044
+ if (at[0] < 0) {
3045
+ // the colour at the centre, between the stops either side of it
3046
+ const first = colorAlong(at, colors, 0);
3047
+ const i = at.findIndex((t) => t > 0);
3048
+ if (i < 0)
3049
+ return { style: first, matrix: null };
3050
+ at = [0, ...at.slice(i)];
3051
+ inks = [first, ...colors.slice(i)];
3052
+ }
3053
+ const ax = x + cx;
3054
+ const ay = y + cy;
3055
+ const seen = visible ?? { x, y, w, h };
3056
+ if (!(Math.abs(ax) < RADIAL_REACH && Math.abs(ay) < RADIAL_REACH)) {
3057
+ return farRadial(ctx, at, inks, ax, ay, rx, ry, seen);
3058
+ }
3059
+ let to = Math.max(1, at[at.length - 1]);
3060
+ if (Math.max(rx, ry) * to > GRADIENT_REACH) {
3061
+ // as far along the ray as what is seen of it reaches, and no further:
3062
+ // a circle a context cannot carry, cut to the part it draws
3063
+ let reach = 0;
3064
+ for (const px of [seen.x, seen.x + seen.w]) {
3065
+ for (const py of [seen.y, seen.y + seen.h]) {
3066
+ reach = Math.max(reach, Math.hypot((px - ax) / rx, (py - ay) / ry));
3067
+ }
3068
+ }
3069
+ if (reach > 0 && reach < to) {
3070
+ const last = colorAlong(at, inks, reach);
3071
+ const kept = at.filter((t) => t < reach).length;
3072
+ at = [...at.slice(0, kept), reach];
3073
+ inks = [...inks.slice(0, kept), last];
3074
+ to = reach;
3075
+ }
3076
+ }
3077
+ const circle = rx === ry;
3078
+ // a circle as wide as the ellipse is narrow, stretched along the other
3079
+ // axis: the matrix only ever widens, so its inverse, which a context
3080
+ // samples the gradient through, reaches no further than the centre does
3081
+ const r = Math.min(rx, ry);
3082
+ const g = circle
3083
+ ? ctx.createRadialGradient(ax, ay, 0, ax, ay, r * to)
3084
+ : ctx.createRadialGradient(0, 0, 0, 0, 0, r * to);
3085
+ for (let i = 0; i < at.length; i += 1) {
3086
+ g.addColorStop(at[i] / to, inks[i]);
3087
+ }
3088
+ return {
3089
+ style: g,
3090
+ matrix: circle ? null : [rx / r, 0, 0, ry / r, ax, ay],
3091
+ };
3092
+ }
3093
+ /**
3094
+ * How far from the origin a radial gradient's centre may be and still be
3095
+ * the context's to draw: ntk makes a gradient past what X RENDER carries at
3096
+ * a scale that fits, and its 16.16 matrix holds that scale to a fraction of
3097
+ * a pixel to here.
3098
+ */
3099
+ const RADIAL_REACH = 1 << 18;
3100
+ /**
3101
+ * A radial gradient whose centre is further off than a context carries —
3102
+ * `at 0 calc(infinity * 1px)`, which comes to the largest length there is —
3103
+ * across the part of it that is seen: from that far its rings are as good
3104
+ * as straight, so it is drawn as the linear gradient that runs the way the
3105
+ * rings spread there, as fast as they do, cut to what is seen as a linear
3106
+ * gradient's line is. The rings bend away from it by the square of what is
3107
+ * seen over eight times the distance, under a pixel from here.
3108
+ */
3109
+ function farRadial(ctx, at, colors, ax, ay, rx, ry, seen) {
3110
+ if (!ctx.createLinearGradient)
3111
+ return null;
3112
+ // the middle of what is seen, in the circle's own space, and how fast
3113
+ // the gradient runs there, a pixel
3114
+ const px = seen.x + seen.w / 2;
3115
+ const py = seen.y + seen.h / 2;
3116
+ const ux = (px - ax) / rx;
3117
+ const uy = (py - ay) / ry;
3118
+ const t = Math.hypot(ux, uy);
3119
+ const gx = ux / rx / t;
3120
+ const gy = uy / ry / t;
3121
+ const g2 = gx * gx + gy * gy;
3122
+ if (!(t > 0 && g2 > 0 && Number.isFinite(t / g2))) {
3123
+ return { style: colorAlong(at, colors, t), matrix: null };
3124
+ }
3125
+ // the line it runs along, from where it would start to where it is 1
3126
+ const lx = gx / g2;
3127
+ const ly = gy / g2;
3128
+ return {
3129
+ style: clippedGradient(ctx, at, colors, px - lx * t, py - ly * t, lx, ly, seen),
3130
+ matrix: null,
3131
+ };
3132
+ }
3133
+ /**
3134
+ * Whether a context draws radial gradients. react-x11's macOS and Windows
3135
+ * contexts have the method and paint what it returns flat, in one colour
3136
+ * — over whatever a vignette was meant to let through — and theirs takes
3137
+ * no circles, which is how it is told from one that draws them: ntk's and
3138
+ * the Wayland context's take the six numbers canvas gives it. There a
3139
+ * radial gradient is drawn as nothing, as every one was.
3140
+ */
3141
+ function hasRadialGradients(ctx) {
3142
+ return (ctx.createRadialGradient?.length ?? 0) >= 6;
3143
+ }
3144
+ /**
3145
+ * Fills a rectangle, rounded where `corners` says, with a gradient. An
3146
+ * ellipse's shape is laid down first and filled under its matrix, which a
3147
+ * context applies to the fill's paint and not to a path it already has.
3148
+ */
3149
+ function fillGradient(ctx, fill, r, corners = null) {
3150
+ ctx.fillStyle = fill.style;
3151
+ if (!fill.matrix) {
3152
+ if (corners) {
3153
+ ctx.beginPath();
3154
+ roundedRect(ctx, r.x, r.y, r.w, r.h, corners);
3155
+ ctx.fill();
3156
+ }
3157
+ else
3158
+ ctx.fillRect(r.x, r.y, r.w, r.h);
3159
+ return;
3160
+ }
3161
+ if (!ctx.transform || !ctx.beginPath || !ctx.rect || !ctx.fill)
3162
+ return;
3163
+ ctx.save();
3164
+ ctx.beginPath();
3165
+ if (corners)
3166
+ roundedRect(ctx, r.x, r.y, r.w, r.h, corners);
3167
+ else
3168
+ ctx.rect(r.x, r.y, r.w, r.h);
3169
+ ctx.transform(...fill.matrix);
3170
+ ctx.fill();
3171
+ ctx.restore();
2266
3172
  }
2267
3173
  /**
2268
3174
  * How far from the origin a gradient's line may end. X RENDER takes the
@@ -2304,29 +3210,30 @@ function clippedGradient(ctx, at, colors, sx, sy, lx, ly, visible) {
2304
3210
  const cy = (top + bottom) / 2;
2305
3211
  const tc = along(cx, cy);
2306
3212
  const g = ctx.createLinearGradient(cx + (t0 - tc) * lx, cy + (t0 - tc) * ly, cx + (t1 - tc) * lx, cy + (t1 - tc) * ly);
2307
- // the colour at a place on the line: a stop's, or between two
2308
- const colorAt = (t) => {
2309
- if (!(t > at[0]))
2310
- return colors[0];
2311
- for (let i = 1; i < at.length; i += 1) {
2312
- if (t > at[i])
2313
- continue;
2314
- const span = at[i] - at[i - 1];
2315
- const f = span > 0 ? (t - at[i - 1]) / span : 1;
2316
- return (blend(colors[i - 1], colors[i], f) ??
2317
- (f < 0.5 ? colors[i - 1] : colors[i]));
2318
- }
2319
- return colors[colors.length - 1];
2320
- };
2321
- g.addColorStop(0, colorAt(t0));
3213
+ g.addColorStop(0, colorAlong(at, colors, t0));
2322
3214
  for (let i = 0; i < at.length; i += 1) {
2323
3215
  if (at[i] > t0 && at[i] < t1) {
2324
3216
  g.addColorStop((at[i] - t0) / (t1 - t0), colors[i]);
2325
3217
  }
2326
3218
  }
2327
- g.addColorStop(1, colorAt(t1));
3219
+ g.addColorStop(1, colorAlong(at, colors, t1));
2328
3220
  return g;
2329
3221
  }
3222
+ /** The colour at a place along a gradient whose stops are at `at`: a
3223
+ * stop's, or between two. */
3224
+ function colorAlong(at, colors, t) {
3225
+ if (!(t > at[0]))
3226
+ return colors[0];
3227
+ for (let i = 1; i < at.length; i += 1) {
3228
+ if (t > at[i])
3229
+ continue;
3230
+ const span = at[i] - at[i - 1];
3231
+ const f = span > 0 ? (t - at[i - 1]) / span : 1;
3232
+ return (blend(colors[i - 1], colors[i], f) ??
3233
+ (f < 0.5 ? colors[i - 1] : colors[i]));
3234
+ }
3235
+ return colors[colors.length - 1];
3236
+ }
2330
3237
  /**
2331
3238
  * The backgrounds of a table's column groups, columns, row groups and rows
2332
3239
  * (CSS 2.1 17.5.1), in that order: over the table's own and under its
@@ -2672,16 +3579,6 @@ function tileSize(size, area, scale) {
2672
3579
  }
2673
3580
  return [area.width, area.height];
2674
3581
  }
2675
- /**
2676
- * Borders, as four rectangles.
2677
- *
2678
- * Not as a stroked path, for the reason richtext gives about its underlines:
2679
- * the mock backend has no path API, and a 1px border on a pixel grid is a
2680
- * rectangle rather than something an antialiased stroke improves. Corners are
2681
- * mitred by drawing the top and bottom full-width and the sides between them,
2682
- * which is right whenever the two sides share a colour and close enough when
2683
- * they do not.
2684
- */
2685
3582
  /**
2686
3583
  * A box's outline (CSS 2.1 18.4, CSS UI 4 5): a border of its own width,
2687
3584
  * style and colour round the border box grown by `outline-offset` — which
@@ -2733,6 +3630,25 @@ function paintOutline(ctx, box, options) {
2733
3630
  style: ring,
2734
3631
  }, options);
2735
3632
  }
3633
+ /**
3634
+ * Borders, a side at a time.
3635
+ *
3636
+ * As rectangles where they can be, the top and the bottom full width and
3637
+ * the sides between them, and not as a stroked path, for the reason
3638
+ * richtext gives about its underlines: the mock backend has no path API, and
3639
+ * a 1px border on a pixel grid is a rectangle rather than something an
3640
+ * antialiased stroke improves. That is every border whose sides share a
3641
+ * colour, where a corner looks the same whichever side it is given to.
3642
+ *
3643
+ * Where two sides that meet differ in colour — one of them transparent
3644
+ * included — the corner is cut on the diagonal from its outer point to its
3645
+ * inner one and each side takes its half (CSS Backgrounds 3, 4.3), so a side
3646
+ * is a trapezoid, and a triangle where the box inside the borders has no
3647
+ * width: the CSS triangle a dropdown's caret and a tooltip's arrow are
3648
+ * drawn with, which came out as the rectangle around it. Only a solid side
3649
+ * is cut, and only against a side that is solid or paints nothing; dots,
3650
+ * dashes and a double border's two lines keep their rectangles.
3651
+ */
2736
3652
  function paintBorders(ctx, box, options) {
2737
3653
  // most boxes have none, and are asked on every paint
2738
3654
  if (!(box.borderTop || box.borderRight || box.borderBottom || box.borderLeft))
@@ -2750,28 +3666,148 @@ function paintBorders(ctx, box, options) {
2750
3666
  return;
2751
3667
  if (sculpted(s) && paintSculpted(ctx, box, x, y, w, h, options))
2752
3668
  return;
3669
+ const t = box.borderTop;
3670
+ const r = box.borderRight;
3671
+ const b = box.borderBottom;
3672
+ const l = box.borderLeft;
3673
+ /** What a side is painted in, null where it paints nothing. */
3674
+ const ink = (width, color) => width > 0 && !isTransparent(color) ? inkColor(color, s.color) : null;
3675
+ const topInk = ink(t, s.borderTopColor);
3676
+ const rightInk = ink(r, s.borderRightColor);
3677
+ const bottomInk = ink(b, s.borderBottomColor);
3678
+ const leftInk = ink(l, s.borderLeftColor);
3679
+ // the corners cut on the diagonal; none where the context draws no path,
3680
+ // and the sides are rectangles
3681
+ const paths = !!(ctx.beginPath && ctx.moveTo && ctx.lineTo && ctx.fill);
3682
+ const topStyle = s.borderTopStyle;
3683
+ const rightStyle = s.borderRightStyle;
3684
+ const bottomStyle = s.borderBottomStyle;
3685
+ const leftStyle = s.borderLeftStyle;
3686
+ const tl = paths && mitred(t, l, topInk, leftInk, topStyle, leftStyle);
3687
+ const tr = paths && mitred(t, r, topInk, rightInk, topStyle, rightStyle);
3688
+ const br = paths && mitred(b, r, bottomInk, rightInk, bottomStyle, rightStyle);
3689
+ const bl = paths && mitred(b, l, bottomInk, leftInk, bottomStyle, leftStyle);
2753
3690
  const edge = (ex, ey, ew, eh, style, color, horizontal) => {
2754
3691
  const rect = clampRect(options, ex, ey, ew, eh);
2755
3692
  if (!rect)
2756
3693
  return;
2757
- ctx.fillStyle = inkColor(color, s.color);
3694
+ ctx.fillStyle = color;
2758
3695
  // The un-clamped start is the dash phase's origin, so the pattern does
2759
3696
  // not crawl as the viewport moves along a long edge.
2760
3697
  fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
2761
3698
  };
2762
- if (box.borderTop > 0 && !isTransparent(s.borderTopColor)) {
2763
- edge(x, y, w, box.borderTop, s.borderTopStyle, s.borderTopColor, true);
3699
+ /** A side with a cut corner: its outer edge and then its inner one. */
3700
+ const side = (points, color) => {
3701
+ const area = clampRect(options, x, y, w, h);
3702
+ if (!area)
3703
+ return;
3704
+ ctx.fillStyle = color;
3705
+ fillPolygon(ctx, points, area);
3706
+ };
3707
+ // the border box's far edges, and the padding box inside it
3708
+ const x1 = x + w;
3709
+ const y1 = y + h;
3710
+ const px0 = x + l;
3711
+ const py0 = y + t;
3712
+ const px1 = Math.max(px0, x1 - r);
3713
+ const py1 = Math.max(py0, y1 - b);
3714
+ // The left and the right are drawn after the top and the bottom. Where
3715
+ // one of them is opaque it draws its half of a cut corner over the side
3716
+ // drawn before, which keeps the whole corner: two halves each
3717
+ // antialiased along the diagonal would let what is under the border
3718
+ // show through between them.
3719
+ const over = (color) => color !== null && alphaOf(color) === 1;
3720
+ if (topInk !== null) {
3721
+ const cutLeft = tl && !over(leftInk);
3722
+ const cutRight = tr && !over(rightInk);
3723
+ if (cutLeft || cutRight) {
3724
+ side([x, y, x1, y, cutRight ? px1 : x1, py0, cutLeft ? px0 : x, py0], topInk);
3725
+ }
3726
+ else
3727
+ edge(x, y, w, t, topStyle, topInk, true);
2764
3728
  }
2765
- if (box.borderBottom > 0 && !isTransparent(s.borderBottomColor)) {
2766
- edge(x, y + h - box.borderBottom, w, box.borderBottom, s.borderBottomStyle, s.borderBottomColor, true);
3729
+ if (bottomInk !== null) {
3730
+ const cutLeft = bl && !over(leftInk);
3731
+ const cutRight = br && !over(rightInk);
3732
+ if (cutLeft || cutRight) {
3733
+ side([x1, y1, x, y1, cutLeft ? px0 : x, py1, cutRight ? px1 : x1, py1], bottomInk);
3734
+ }
3735
+ else
3736
+ edge(x, y1 - b, w, b, bottomStyle, bottomInk, true);
2767
3737
  }
2768
- if (box.borderLeft > 0 && !isTransparent(s.borderLeftColor)) {
2769
- edge(x, y + box.borderTop, box.borderLeft, h - box.borderTop - box.borderBottom, s.borderLeftStyle, s.borderLeftColor, false);
3738
+ if (leftInk !== null) {
3739
+ if (tl || bl) {
3740
+ side([x, bl ? y1 : py1, x, tl ? y : py0, px0, py0, px0, py1], leftInk);
3741
+ }
3742
+ else
3743
+ edge(x, y + t, l, h - t - b, leftStyle, leftInk, false);
2770
3744
  }
2771
- if (box.borderRight > 0 && !isTransparent(s.borderRightColor)) {
2772
- edge(x + w - box.borderRight, y + box.borderTop, box.borderRight, h - box.borderTop - box.borderBottom, s.borderRightStyle, s.borderRightColor, false);
3745
+ if (rightInk !== null) {
3746
+ if (tr || br) {
3747
+ side([x1, tr ? y : py0, x1, br ? y1 : py1, px1, py1, px1, py0], rightInk);
3748
+ }
3749
+ else
3750
+ edge(x1 - r, y + t, r, h - t - b, rightStyle, rightInk, false);
2773
3751
  }
2774
3752
  }
3753
+ /**
3754
+ * Whether the corner two sides meet at is cut on its diagonal, each side
3755
+ * taking its half (CSS Backgrounds 3, 4.3): where both have a width, they
3756
+ * are painted in different colours or only one is painted at all, and each
3757
+ * is solid or unpainted. Sides of one colour make the same corner either
3758
+ * way, and so does a corner of a single pixel, which is left to the side
3759
+ * that has it.
3760
+ */
3761
+ function mitred(a, b, inkA, inkB, styleA, styleB) {
3762
+ if (!(a > 0 && b > 0) || (a <= 1 && b <= 1) || inkA === inkB)
3763
+ return false;
3764
+ return ((inkA === null || styleA === 'solid') &&
3765
+ (inkB === null || styleB === 'solid'));
3766
+ }
3767
+ /**
3768
+ * Fill a convex polygon, its corners as `x, y` pairs, cut to `area`: the
3769
+ * part of it near what is painted, which is what keeps a side thousands of
3770
+ * pixels long inside X's coordinates. Cut edge by edge rather than by
3771
+ * moving its corners in, which would turn a diagonal that crosses the
3772
+ * painted area: a slanted divider, `border-left: 100vw solid transparent`,
3773
+ * has a corner far outside it.
3774
+ */
3775
+ function fillPolygon(ctx, points, area) {
3776
+ const bounds = [area.x, area.y, area.x + area.w, area.y + area.h];
3777
+ let cut = points;
3778
+ // each edge of the area in turn: what is inside it is kept, and an edge
3779
+ // of the polygon that crosses it ends on it (Sutherland–Hodgman)
3780
+ for (let edge = 0; edge < 4 && cut.length; edge += 1) {
3781
+ const axis = edge & 1;
3782
+ const bound = bounds[edge];
3783
+ const sign = edge < 2 ? 1 : -1;
3784
+ const from = cut;
3785
+ cut = [];
3786
+ for (let i = 0; i < from.length; i += 2) {
3787
+ const j = (i + 2) % from.length;
3788
+ const da = sign * (from[i + axis] - bound);
3789
+ const db = sign * (from[j + axis] - bound);
3790
+ if (da >= 0)
3791
+ cut.push(from[i], from[i + 1]);
3792
+ if (da < 0 !== db < 0) {
3793
+ const k = da / (da - db);
3794
+ const other = from[i + 1 - axis] + (from[j + 1 - axis] - from[i + 1 - axis]) * k;
3795
+ if (axis)
3796
+ cut.push(other, bound);
3797
+ else
3798
+ cut.push(bound, other);
3799
+ }
3800
+ }
3801
+ }
3802
+ if (cut.length < 6)
3803
+ return;
3804
+ ctx.beginPath();
3805
+ ctx.moveTo(cut[0], cut[1]);
3806
+ for (let i = 2; i < cut.length; i += 2)
3807
+ ctx.lineTo(cut[i], cut[i + 1]);
3808
+ ctx.closePath?.();
3809
+ ctx.fill();
3810
+ }
2775
3811
  /**
2776
3812
  * A box's border image in place of its border's style (CSS Backgrounds 3,
2777
3813
  * 6.2): the image cut into nine by its slices, drawn over the border image
@@ -2825,9 +3861,10 @@ function paintBorderImage(ctx, box, options) {
2825
3861
  const drawn = options.cached?.(`border-image|${key}|${w}x${h}`, w, h, (sctx) => {
2826
3862
  if (svg)
2827
3863
  svg.draw(sctx, 0, 0, w, h, scale);
2828
- else if (typeof source !== 'string' && sctx.createLinearGradient) {
2829
- sctx.fillStyle = linearGradient(sctx, source, 0, 0, w, h, box.style.color);
2830
- sctx.fillRect(0, 0, w, h);
3864
+ else if (typeof source !== 'string') {
3865
+ const paint = gradientFill(sctx, source, 0, 0, w, h, box.style.color);
3866
+ if (paint)
3867
+ fillGradient(sctx, paint, { x: 0, y: 0, w, h });
2831
3868
  }
2832
3869
  });
2833
3870
  if (!drawn)
@@ -3460,7 +4497,9 @@ function fitted(box, x, y, w, h) {
3460
4497
  };
3461
4498
  }
3462
4499
  /** An inline `<svg>`, drawn in its content box. Its `currentColor` is the
3463
- * box's `color`, as an icon's is the text's around it. */
4500
+ * box's `color`, as an icon's is the text's around it, and what it is
4501
+ * painted with the box's `fill` and `stroke`, where the document's style
4502
+ * sheets set them: an icon set's `.icon { fill: currentColor }`. */
3464
4503
  function paintSvg(ctx, box, options) {
3465
4504
  if (!box.el)
3466
4505
  return;
@@ -3468,7 +4507,11 @@ function paintSvg(ctx, box, options) {
3468
4507
  const y = Math.round(box.contentY + options.originY);
3469
4508
  const w = Math.round(box.contentX + options.originX + box.contentWidth) - x;
3470
4509
  const h = Math.round(box.contentY + options.originY + box.contentHeight) - y;
3471
- inlineDrawing(box.el).draw(ctx, x, y, w, h, options.scale ?? 1, box.style.color);
4510
+ const style = box.style;
4511
+ const paint = (value) => value === null || value === 'currentColor' || value === 'none'
4512
+ ? value
4513
+ : inkColor(value, style.color);
4514
+ inlineDrawing(box.el).draw(ctx, x, y, w, h, options.scale ?? 1, style.color, paint(style.fill), paint(style.stroke));
3472
4515
  }
3473
4516
  /**
3474
4517
  * The X protocol carries glyph positions as Int16, so anything drawn past
@@ -3502,8 +4545,15 @@ function paintLines(ctx, box, options) {
3502
4545
  const bottom = top + damage.height;
3503
4546
  let lo = 0;
3504
4547
  let hi = lines.length;
3505
- const slack = top - box.maxLineHeight;
3506
- while (lo < hi) {
4548
+ // and a line whose glyphs, or an atomic on it, reach past it
4549
+ // (`LINE_INK`) is drawn where they reach
4550
+ const ink = LINE_INK.get(lines);
4551
+ const above = ink?.above ?? 0;
4552
+ const below = ink?.below ?? 0;
4553
+ const slack = top - box.maxLineHeight - below;
4554
+ // the lines columns took apart are in no order down the page
4555
+ const apart = columned.any && COLUMN_LINES.has(lines);
4556
+ while (!apart && lo < hi) {
3507
4557
  const mid = (lo + hi) >> 1;
3508
4558
  if (lines[mid].y > slack)
3509
4559
  hi = mid;
@@ -3512,10 +4562,16 @@ function paintLines(ctx, box, options) {
3512
4562
  }
3513
4563
  for (let i = lo; i < lines.length; i += 1) {
3514
4564
  const line = lines[i];
3515
- if (line.y >= bottom)
4565
+ if (line.y - above >= bottom) {
4566
+ if (apart)
4567
+ continue;
3516
4568
  break;
3517
- if (line.y + line.height > top)
4569
+ }
4570
+ if (line.y < bottom && line.y + line.height > top)
4571
+ visible.push(line);
4572
+ else if (ink && inkInto(line, top, bottom)) {
3518
4573
  visible.push(line);
4574
+ }
3519
4575
  }
3520
4576
  // and a line the damage misses whose text or inline-block `position:
3521
4577
  // relative` moved into it: drawn by its line, it went undrawn with it
@@ -3523,7 +4579,7 @@ function paintLines(ctx, box, options) {
3523
4579
  if (moved && reachedOff(visible, moved, top, bottom)) {
3524
4580
  const drawn = new Set(visible);
3525
4581
  for (const line of moved) {
3526
- if (movedInto(line, top, bottom))
4582
+ if (inkInto(line, top, bottom))
3527
4583
  drawn.add(line);
3528
4584
  }
3529
4585
  visible.length = 0;
@@ -3544,8 +4600,13 @@ function paintLines(ctx, box, options) {
3544
4600
  paintInlineBoxes(ctx, line, lines, options, line === lines[0] ? null : bleeds);
3545
4601
  for (const text of line.texts) {
3546
4602
  const natural = text.layout.lines[text.layoutLine];
3547
- if (natural)
3548
- paintRunBackgrounds(ctx, natural, text.drawX + dx, text.drawY + dy, options.scale ?? 1);
4603
+ if (!natural)
4604
+ continue;
4605
+ for (const laid of [natural, trailOf(text, natural)]) {
4606
+ if (!laid)
4607
+ continue;
4608
+ paintRunBackgrounds(ctx, laid, text.drawX + dx, text.drawY + dy, options.scale ?? 1);
4609
+ }
3549
4610
  }
3550
4611
  paintSelection(ctx, line, options);
3551
4612
  }
@@ -3560,11 +4621,17 @@ function paintLines(ctx, box, options) {
3560
4621
  // drawing it once per line would be one X request per line for the same
3561
4622
  // batch.
3562
4623
  const drawn = new Set();
4624
+ // text a `::selection` colours is drawn in that colour where selected
4625
+ const recolored = options.selectionStyler
4626
+ ? recoloredBands(visible, options)
4627
+ : null;
3563
4628
  for (const line of visible) {
3564
4629
  for (const text of line.texts) {
3565
- if (drawn.has(text.layout))
4630
+ // once for each column its lines are in, where they are in several
4631
+ const rows = columned.any ? COLUMN_ROWS.get(text) : undefined;
4632
+ if (drawn.has(rows ?? text.layout))
3566
4633
  continue;
3567
- drawn.add(text.layout);
4634
+ drawn.add(rows ?? text.layout);
3568
4635
  const top = text.drawY + dy;
3569
4636
  if (top < -COORD_LIMIT || top + text.layout.height > COORD_LIMIT) {
3570
4637
  // A single layout so tall its own lines overflow the Int16 envelope
@@ -3574,7 +4641,14 @@ function paintLines(ctx, box, options) {
3574
4641
  // thrown from the protocol encoder.
3575
4642
  continue;
3576
4643
  }
3577
- text.layout.draw(ctx, text.drawX + dx, top);
4644
+ const bands = recolored?.get(text.layout);
4645
+ const clipped = rows !== undefined && clipToRows(ctx, text, rows, text.drawX + dx, top);
4646
+ if (bands)
4647
+ drawRecolored(ctx, text.layout, text.drawX + dx, top, bands);
4648
+ else
4649
+ text.layout.draw(ctx, text.drawX + dx, top);
4650
+ if (clipped)
4651
+ ctx.restore();
3578
4652
  }
3579
4653
  }
3580
4654
  paintRules(ctx, visible, dx, dy, options.scale ?? 1, 'over');
@@ -3588,7 +4662,10 @@ function paintLines(ctx, box, options) {
3588
4662
  for (const placed of line.atomics) {
3589
4663
  if (hoisted && HOISTED.has(placed.box))
3590
4664
  continue;
3591
- paintBox(ctx, placed.box, options);
4665
+ if (turns(placed.box))
4666
+ paintTransformed(ctx, placed.box, options);
4667
+ else
4668
+ paintBox(ctx, placed.box, options);
3592
4669
  }
3593
4670
  }
3594
4671
  }
@@ -3596,15 +4673,16 @@ function paintLines(ctx, box, options) {
3596
4673
  * does not already draw, has it within the damage's rows. */
3597
4674
  function reachedOff(visible, moved, top, bottom) {
3598
4675
  for (const line of moved) {
3599
- if (!visible.includes(line) && movedInto(line, top, bottom))
4676
+ if (!visible.includes(line) && inkInto(line, top, bottom))
3600
4677
  return true;
3601
4678
  }
3602
4679
  return false;
3603
4680
  }
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) {
4681
+ /** Whether what a line draws — its inline-blocks and images, where their
4682
+ * ink is, and its texts, where they are drawn, their glyphs from ascent to
4683
+ * descent — reaches the rows between `top` and `bottom`: what was moved off
4684
+ * it, and what hangs past it. */
4685
+ function inkInto(line, top, bottom) {
3608
4686
  for (const placed of line.atomics) {
3609
4687
  const box = placed.box;
3610
4688
  if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
@@ -3615,8 +4693,10 @@ function movedInto(line, top, bottom) {
3615
4693
  const natural = text.layout.lines[text.layoutLine];
3616
4694
  if (!natural)
3617
4695
  continue;
3618
- const y = text.drawY + natural.y;
3619
- if (y < bottom && y + natural.height > top)
4696
+ const baseline = text.drawY + natural.baseline;
4697
+ const y1 = Math.min(text.drawY + natural.y, baseline - (natural.ascent ?? 0));
4698
+ const y2 = Math.max(text.drawY + natural.y + natural.height, baseline + (natural.descent ?? 0));
4699
+ if (y1 < bottom && y2 > top)
3620
4700
  return true;
3621
4701
  }
3622
4702
  return false;
@@ -3729,7 +4809,9 @@ function paintClippedText(ctx, block, lines, options) {
3729
4809
  (style.backgroundImages?.find((image) => image !== null && typeof image !== 'string') ||
3730
4810
  null);
3731
4811
  let fill = null;
3732
- if (gradient && ctx.createLinearGradient) {
4812
+ // where the fill is a picture of the gradient, the corner it starts at
4813
+ let origin = null;
4814
+ if (gradient) {
3733
4815
  if (!(area.width > 0 && area.height > 0))
3734
4816
  continue;
3735
4817
  // the text it shows through: what the fill is drawn over
@@ -3745,9 +4827,31 @@ function paintClippedText(ctx, block, lines, options) {
3745
4827
  y1 = Math.max(y1, r.y + r.height);
3746
4828
  }
3747
4829
  }
3748
- fill = linearGradient(ctx, gradient, area.x, area.y, area.width, area.height, style.color, x1 > x0 ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null);
4830
+ const paint = gradientFill(ctx, gradient, area.x, area.y, area.width, area.height, style.color, x1 > x0 ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null);
4831
+ if (paint?.matrix) {
4832
+ // An ellipse is a circle filled through a matrix, which would draw
4833
+ // the glyphs through it too: they are filled with a picture of the
4834
+ // gradient instead, drawn once at the area's size
4835
+ const left = Math.round(area.x);
4836
+ const top = Math.round(area.y);
4837
+ const w = Math.max(1, Math.round(area.x + area.width) - left);
4838
+ const h = Math.max(1, Math.round(area.y + area.height) - top);
4839
+ const picture = ctx.createPattern && ctx.translate
4840
+ ? options.cached?.(`clip-text|${gradientKey(gradient, style.color)}|${w}x${h}`, w, h, (sctx) => {
4841
+ const p = gradientFill(sctx, gradient, 0, 0, w, h, style.color);
4842
+ if (p)
4843
+ fillGradient(sctx, p, { x: 0, y: 0, w, h });
4844
+ })
4845
+ : null;
4846
+ if (!picture)
4847
+ continue;
4848
+ fill = ctx.createPattern(picture, 'no-repeat');
4849
+ origin = { x: left, y: top };
4850
+ }
4851
+ else if (paint)
4852
+ fill = paint.style;
3749
4853
  }
3750
- else if (!isTransparent(style.backgroundColor)) {
4854
+ if (fill === null && !isTransparent(style.backgroundColor)) {
3751
4855
  fill = inkColor(style.backgroundColor, style.color);
3752
4856
  }
3753
4857
  if (fill === null)
@@ -3762,7 +4866,13 @@ function paintClippedText(ctx, block, lines, options) {
3762
4866
  ctx.rect(r.x, r.y, r.width, r.height);
3763
4867
  ctx.clip();
3764
4868
  ctx.fillStyle = fill;
3765
- inkless.draw(ctx, part.x, part.y);
4869
+ if (origin) {
4870
+ // a pattern is placed from the origin of the space it fills
4871
+ ctx.translate(origin.x, origin.y);
4872
+ inkless.draw(ctx, part.x - origin.x, part.y - origin.y);
4873
+ }
4874
+ else
4875
+ inkless.draw(ctx, part.x, part.y);
3766
4876
  ctx.restore();
3767
4877
  }
3768
4878
  }
@@ -3793,11 +4903,16 @@ function paintTextShadows(ctx, lines, options) {
3793
4903
  continue;
3794
4904
  const left = text.drawX + dx;
3795
4905
  if (cast !== MIXED) {
3796
- // every run casts the same: the layout's once for each shadow
3797
- if (whole.has(layout))
4906
+ // every run casts the same: the layout's once for each shadow, in
4907
+ // each column its lines are in
4908
+ const rows = columned.any ? COLUMN_ROWS.get(text) : undefined;
4909
+ if (whole.has(rows ?? layout))
3798
4910
  continue;
3799
- whole.add(layout);
4911
+ whole.add(rows ?? layout);
4912
+ const clipped = rows !== undefined && clipToRows(ctx, text, rows, left, top);
3800
4913
  castShadows(ctx, text, left, top, cast, null);
4914
+ if (clipped)
4915
+ ctx.restore();
3801
4916
  continue;
3802
4917
  }
3803
4918
  const natural = layout.lines[text.layoutLine];
@@ -3908,12 +5023,37 @@ function paintRules(ctx, lines, dx, dy, scale, rules) {
3908
5023
  const shifted = SHIFTED_LINES.has(line);
3909
5024
  for (const text of line.texts) {
3910
5025
  const natural = text.layout.lines[text.layoutLine];
3911
- if (natural) {
3912
- paintRunRules(ctx, natural, text.drawX + dx, text.drawY + dy + (shifted ? ruleDrop(text) : 0), scale, rules);
5026
+ if (!natural)
5027
+ continue;
5028
+ for (const laid of [natural, trailOf(text, natural)]) {
5029
+ if (!laid)
5030
+ continue;
5031
+ paintRunRules(ctx, laid, text.drawX + dx, text.drawY + dy + (shifted ? ruleDrop(text) : 0), scale, rules);
3913
5032
  }
3914
5033
  }
3915
5034
  }
3916
5035
  }
5036
+ /**
5037
+ * The spaces a text ends on where its line goes on after them
5038
+ * (`LineText.trail`), as a line of one run to draw the decorations of: the
5039
+ * run they are the end of, as wide as they are, after the line's content.
5040
+ * The engine stripped them from the piece it laid out as a line, and a
5041
+ * link's underline stopped at its last letter, a space short of where a
5042
+ * browser draws it. Null where the text has none, or reads right to left.
5043
+ */
5044
+ function trailOf(text, natural) {
5045
+ if (!text.trail || !text.trailRun)
5046
+ return null;
5047
+ const last = natural.runs[natural.runs.length - 1];
5048
+ if (!last || last.run?.direction === 'rtl')
5049
+ return null;
5050
+ return {
5051
+ ...natural,
5052
+ runs: [
5053
+ { ...last, x: natural.width, width: text.trail, span: text.trailRun },
5054
+ ],
5055
+ };
5056
+ }
3917
5057
  /**
3918
5058
  * How far below a raised text its rules go: to the baseline of the element
3919
5059
  * that decorates it. `vertical-align` moves the text and not the lines an
@@ -4361,15 +5501,34 @@ export function forgetDecoratedAncestors(box) {
4361
5501
  *
4362
5502
  * Translucent under the glyphs rather than inverted over them, so the ink
4363
5503
  * keeps its contrast on either palette — the same call `<textarea>` and
4364
- * `<richtext>` both make.
5504
+ * `<richtext>` both make. From the top of the line to its bottom, and past
5505
+ * them over the glyphs where they are taller than the line (`selectionRows`).
4365
5506
  */
4366
5507
  function paintSelection(ctx, line, options) {
5508
+ for (const band of selectedBands(line, options)) {
5509
+ const fill = band.style ? band.style.background : options.selectionColor;
5510
+ if (!fill || isTransparent(fill))
5511
+ continue;
5512
+ ctx.fillStyle = fill;
5513
+ ctx.fillRect(band.x, band.y, band.width, band.height);
5514
+ }
5515
+ }
5516
+ const NO_BANDS = [];
5517
+ /**
5518
+ * Where the document selection covers a line's text, a band for each
5519
+ * stretch of it one `::selection` styles, in window pixels as the band is
5520
+ * filled — rounded, so a band's text is drawn to its edges and no further
5521
+ * (`drawRecolored`).
5522
+ */
5523
+ function selectedBands(line, options) {
4367
5524
  const range = options.selection;
4368
- if (!range || range.end <= range.start || !options.selectionColor)
4369
- return;
4370
- if (line.textEnd <= range.start || line.textStart >= range.end)
4371
- return;
4372
- ctx.fillStyle = options.selectionColor;
5525
+ if (!range || range.end <= range.start)
5526
+ return NO_BANDS;
5527
+ if (line.textEnd <= range.start || line.textStart >= range.end) {
5528
+ return NO_BANDS;
5529
+ }
5530
+ const styler = options.selectionStyler;
5531
+ const out = [];
4373
5532
  for (const text of line.texts) {
4374
5533
  const natural = text.layout.lines[text.layoutLine];
4375
5534
  if (!natural)
@@ -4381,9 +5540,192 @@ function paintSelection(ctx, line, options) {
4381
5540
  const offsets = layoutOffsets(text.layout);
4382
5541
  const layoutFrom = layoutOffsetOf(text, from);
4383
5542
  const layoutTo = layoutOffsetOf(text, to, true);
4384
- for (const band of lineBands(text.layout, natural, offsets, layoutFrom, layoutTo)) {
4385
- ctx.fillRect(Math.round(band.x + text.drawX + options.originX), Math.round(line.y + options.originY), Math.ceil(band.width), Math.ceil(line.height));
5543
+ const rows = selectionRows(line, text, natural);
5544
+ const y = Math.round(rows.y + options.originY);
5545
+ const height = Math.ceil(rows.height);
5546
+ const pieces = styler
5547
+ ? stylePieces(text, natural, layoutFrom, layoutTo, styler)
5548
+ : [{ from: layoutFrom, to: layoutTo, style: null }];
5549
+ for (const piece of pieces) {
5550
+ for (const band of lineBands(text.layout, natural, offsets, piece.from, piece.to)) {
5551
+ out.push({
5552
+ x: Math.round(band.x + text.drawX + options.originX),
5553
+ y,
5554
+ width: Math.ceil(band.width),
5555
+ height,
5556
+ style: piece.style,
5557
+ layout: text.layout,
5558
+ });
5559
+ }
5560
+ }
5561
+ }
5562
+ return out;
5563
+ }
5564
+ /**
5565
+ * A line's selected text, `from` to `to` in the layout's offsets, cut where
5566
+ * the `::selection` over it changes: a run's is its element's, and a run of
5567
+ * no element's — an inline box's edge, laid out as a spacer — the one
5568
+ * beside it, so a styled band has no palette-coloured gap at a `<span>`.
5569
+ */
5570
+ function stylePieces(text, natural, from, to, styler) {
5571
+ const runs = natural.runs
5572
+ .filter((run) => run.end > from && run.start < to)
5573
+ .sort((a, b) => a.start - b.start);
5574
+ const boxAt = text.spans.boxAt;
5575
+ const styles = runs.map((run) => {
5576
+ const box = boxAt?.call(text.spans, run.start) ?? null;
5577
+ return box ? styler(box) : undefined;
5578
+ });
5579
+ for (let i = 1; i < styles.length; i += 1) {
5580
+ if (styles[i] === undefined)
5581
+ styles[i] = styles[i - 1];
5582
+ }
5583
+ for (let i = styles.length - 2; i >= 0; i -= 1) {
5584
+ if (styles[i] === undefined)
5585
+ styles[i] = styles[i + 1];
5586
+ }
5587
+ const out = [];
5588
+ runs.forEach((run, i) => {
5589
+ const style = styles[i] ?? null;
5590
+ const last = out[out.length - 1];
5591
+ if (last && last.style === style)
5592
+ last.to = Math.min(to, run.end);
5593
+ else {
5594
+ out.push({
5595
+ from: Math.max(from, run.start),
5596
+ to: Math.min(to, run.end),
5597
+ style,
5598
+ });
5599
+ }
5600
+ });
5601
+ return out;
5602
+ }
5603
+ /**
5604
+ * The layouts whose selected text a `::selection` gives a colour of its
5605
+ * own, each with the bands it is drawn in that colour (`drawRecolored`).
5606
+ * Null where none does, which is every document with no such rule.
5607
+ */
5608
+ function recoloredBands(lines, options) {
5609
+ let out = null;
5610
+ for (const line of lines) {
5611
+ for (const band of selectedBands(line, options)) {
5612
+ if (!band.style?.color || !(band.width > 0))
5613
+ continue;
5614
+ out ??= new Map();
5615
+ const list = out.get(band.layout);
5616
+ if (list)
5617
+ list.push(band);
5618
+ else
5619
+ out.set(band.layout, [band]);
5620
+ }
5621
+ }
5622
+ return out;
5623
+ }
5624
+ /**
5625
+ * A layout with some of its text selected in a `::selection`'s colour
5626
+ * (CSS Pseudo 4, 3.2): its own colours everywhere but those bands, and in
5627
+ * each band the glyphs in that band's colour — cast there as a hard shadow
5628
+ * from the layout drawn clear of the window, the way `castShadows` draws a
5629
+ * text shadow, since a layout draws in its runs' own colours. Drawn over
5630
+ * rather than in place of the text, the old glyphs' edges showed round the
5631
+ * new ones. Where the context cannot clip or cast, the text keeps its own
5632
+ * colours.
5633
+ */
5634
+ function drawRecolored(ctx, layout, left, top, bands) {
5635
+ if (!ctx.save ||
5636
+ !ctx.restore ||
5637
+ !ctx.beginPath ||
5638
+ !ctx.rect ||
5639
+ !ctx.clip ||
5640
+ !('shadowBlur' in ctx)) {
5641
+ layout.draw(ctx, left, top);
5642
+ return;
5643
+ }
5644
+ ctx.save();
5645
+ ctx.beginPath();
5646
+ for (const r of outsideOf(bands))
5647
+ ctx.rect(r.x, r.y, r.width, r.height);
5648
+ ctx.clip();
5649
+ layout.draw(ctx, left, top);
5650
+ ctx.restore();
5651
+ let right = layout.width;
5652
+ for (const natural of layout.lines) {
5653
+ right = Math.max(right, natural.x + natural.width);
5654
+ }
5655
+ const shift = Math.ceil(left + right) + 1;
5656
+ const colors = new Map();
5657
+ for (const band of bands) {
5658
+ const color = band.style.color;
5659
+ const group = colors.get(color);
5660
+ if (group)
5661
+ group.push(band);
5662
+ else
5663
+ colors.set(color, [band]);
5664
+ }
5665
+ for (const [color, group] of colors) {
5666
+ ctx.save();
5667
+ ctx.beginPath();
5668
+ for (const r of group)
5669
+ ctx.rect(r.x, r.y, r.width, r.height);
5670
+ ctx.clip();
5671
+ ctx.shadowColor = color;
5672
+ ctx.shadowBlur = 0.01;
5673
+ ctx.shadowOffsetX = shift;
5674
+ ctx.shadowOffsetY = 0;
5675
+ layout.draw(ctx, left - shift, top);
5676
+ ctx.restore();
5677
+ }
5678
+ }
5679
+ /** Everything but some rectangles, as rectangles: a strip between each two
5680
+ * of their edges, less what of it they cover. */
5681
+ function outsideOf(rects) {
5682
+ const far = COORD_LIMIT;
5683
+ const edges = new Set();
5684
+ for (const r of rects) {
5685
+ edges.add(r.y);
5686
+ edges.add(r.y + r.height);
5687
+ }
5688
+ const ys = [...edges].sort((a, b) => a - b);
5689
+ const out = [
5690
+ { x: -far, y: -far, width: 2 * far, height: ys[0] + far },
5691
+ {
5692
+ x: -far,
5693
+ y: ys[ys.length - 1],
5694
+ width: 2 * far,
5695
+ height: far - ys[ys.length - 1],
5696
+ },
5697
+ ];
5698
+ for (let i = 0; i + 1 < ys.length; i += 1) {
5699
+ const y0 = ys[i];
5700
+ const y1 = ys[i + 1];
5701
+ const spans = rects
5702
+ .filter((r) => r.y <= y0 && r.y + r.height >= y1)
5703
+ .sort((a, b) => a.x - b.x);
5704
+ let x = -far;
5705
+ for (const r of spans) {
5706
+ if (r.x > x)
5707
+ out.push({ x, y: y0, width: r.x - x, height: y1 - y0 });
5708
+ x = Math.max(x, r.x + r.width);
4386
5709
  }
5710
+ if (x < far)
5711
+ out.push({ x, y: y0, width: far - x, height: y1 - y0 });
4387
5712
  }
5713
+ return out;
5714
+ }
5715
+ /**
5716
+ * The rows a highlight over a line's text covers: the line box's, united
5717
+ * with the text's content area — its face's ascent above the baseline to
5718
+ * its descent below (CSS 2.1 10.6.1) — which is taller wherever
5719
+ * `line-height` is under the face's height. Blink unites the two in the
5720
+ * block direction (`ExpandSelectionRectToLineHeight`), so a selection over
5721
+ * a tall line fills it and one over tall glyphs on a short line covers
5722
+ * them: Zen Garden 215's 91px title on 20px lines had a band a fifth of
5723
+ * its letters' height through their middle.
5724
+ */
5725
+ export function selectionRows(line, text, natural) {
5726
+ const baseline = text.drawY + natural.baseline;
5727
+ const top = Math.min(line.y, baseline - (natural.ascent ?? 0));
5728
+ const bottom = Math.max(line.y + line.height, baseline + (natural.descent ?? 0));
5729
+ return { y: top, height: bottom - top };
4388
5730
  }
4389
5731
  //# sourceMappingURL=paint.js.map