@react-x11/components 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +25 -24
- package/dist/code/index.js.map +1 -1
- package/dist/code-language/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.js.map +1 -1
- package/dist/code-language/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.js.map +1 -1
- package/dist/codeblock/index.d.ts +13 -0
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +37 -1
- package/dist/codeblock/index.js.map +1 -1
- package/dist/flow/node.d.ts +14 -6
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +82 -64
- package/dist/flow/node.js.map +1 -1
- package/dist/html/controls.d.ts +26 -7
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +28 -3
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +32 -10
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +89 -31
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +19 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +86 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +11 -0
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +114 -4
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +45 -5
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +363 -14
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +22 -5
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +10 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +27 -2
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +5 -0
- package/dist/html/css/vars.d.ts.map +1 -1
- package/dist/html/css/vars.js +2 -2
- package/dist/html/css/vars.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +35 -2
- package/dist/html/dom.js.map +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +38 -1
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +11 -2
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +306 -25
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +61 -6
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +65 -16
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +2 -2
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +8 -3
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/floats.d.ts +4 -0
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +26 -0
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/inline.d.ts +29 -2
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +1454 -125
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +33 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +608 -86
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +53 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +746 -214
- package/dist/html/paint.js.map +1 -1
- package/dist/html/surfaces.d.ts +7 -2
- package/dist/html/surfaces.d.ts.map +1 -1
- package/dist/html/surfaces.js +16 -0
- package/dist/html/surfaces.js.map +1 -1
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +27 -20
- package/dist/html/svg.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- package/dist/internal/window.d.ts +22 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +35 -8
- package/dist/internal/window.js.map +1 -1
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +98 -26
- package/dist/markdown/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +3 -8
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/richtext/node.d.ts +7 -0
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +100 -25
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.js.map +1 -1
- package/package.json +5 -3
- package/src/code/index.ts +34 -32
- package/src/code-language/index.ts +1 -1
- package/src/code-language/runs.ts +111 -1
- package/src/codeblock/index.ts +48 -1
- package/src/flow/node.ts +80 -64
- package/src/html/controls.ts +50 -9
- package/src/html/css/cascade.ts +134 -43
- package/src/html/css/color.ts +87 -0
- package/src/html/css/parse.ts +107 -4
- package/src/html/css/style.ts +386 -23
- package/src/html/css/ua.ts +22 -5
- package/src/html/css/values.ts +31 -3
- package/src/html/css/vars.ts +2 -2
- package/src/html/dom.ts +38 -2
- package/src/html/index.ts +39 -1
- package/src/html/layout/block.ts +360 -22
- package/src/html/layout/boxes.ts +98 -23
- package/src/html/layout/cache.ts +8 -3
- package/src/html/layout/floats.ts +24 -0
- package/src/html/layout/inline.ts +1690 -119
- package/src/html/node.ts +648 -101
- package/src/html/paint.ts +883 -283
- package/src/html/surfaces.ts +21 -1
- package/src/html/svg.ts +30 -19
- package/src/internal/codelines.ts +200 -0
- package/src/internal/window.ts +36 -8
- package/src/markdown/index.ts +145 -26
- package/src/rich-text-editor/index.ts +3 -11
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/richtext/node.ts +7 -0
- package/src/terminal-output/index.ts +127 -34
- package/src/terminal-output/runs.ts +16 -2
package/dist/html/paint.js
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
import { canFill, lineBands, paintRunBackgrounds, paintRunRules, } from '../richtext/runs.js';
|
|
23
23
|
import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
|
|
24
24
|
import { blend, borderShades } from './css/color.js';
|
|
25
|
-
import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, scrolls, } from './css/style.js';
|
|
25
|
+
import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, masked, scrolls, } from './css/style.js';
|
|
26
26
|
import { contained } from './layout/block.js';
|
|
27
|
-
import { BOX_RAISES, LINE_BOX_RAISES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
27
|
+
import { BOX_RAISES, LINE_BOX_RAISES, PADDED_FACES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
28
28
|
import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
|
|
29
29
|
import { tableGrid } from './layout/grid.js';
|
|
30
30
|
import { SvgDrawing, inlineDrawing } from './svg.js';
|
|
@@ -36,6 +36,13 @@ import { SvgDrawing, inlineDrawing } from './svg.js';
|
|
|
36
36
|
* document's height — handed up rather than kept on every box, where
|
|
37
37
|
* writing it and reading it back cost this walk a fifth of its time.
|
|
38
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* How far down an out-of-flow box's scrollable overflow reaches: its border
|
|
41
|
+
* box, and what it holds where it does not clip it (`computePaintBounds`),
|
|
42
|
+
* which the box it is in does not take in and the document does
|
|
43
|
+
* (`layoutDocument`).
|
|
44
|
+
*/
|
|
45
|
+
export const OUT_OF_FLOW_REACH = new WeakMap();
|
|
39
46
|
export function computePaintBounds(box, moved = false) {
|
|
40
47
|
// A box with no rectangle of its own gives only what it holds: nothing,
|
|
41
48
|
// until a child with bounds is met.
|
|
@@ -47,8 +54,12 @@ export function computePaintBounds(box, moved = false) {
|
|
|
47
54
|
// How far down the content reaches, for the document's height: the
|
|
48
55
|
// border box, and every box and line under it, but not past a box that
|
|
49
56
|
// clips what it holds — where the scrollable overflow ends. Out-of-flow
|
|
50
|
-
// boxes are the layout's to count.
|
|
51
|
-
|
|
57
|
+
// boxes are the layout's to count. A border box of no area reaches
|
|
58
|
+
// nowhere, as a browser has it (Blink's `ScrollableOverflowCalculator`
|
|
59
|
+
// adds no empty rect): the empty blocks a design leaves at its end,
|
|
60
|
+
// below the last one's bottom margin, made the page that margin taller
|
|
61
|
+
const empty = own && !(box.width > 0 && box.height > 0);
|
|
62
|
+
let bottom = empty ? -Infinity : y2;
|
|
52
63
|
// A shadow is ink past the box, and no overflow: a repaint of the strip
|
|
53
64
|
// under a card has to reach the card, and the document is no taller.
|
|
54
65
|
const shadows = own ? box.style.boxShadow : null;
|
|
@@ -71,6 +82,25 @@ export function computePaintBounds(box, moved = false) {
|
|
|
71
82
|
x2 = Math.max(x2, box.x + box.width + reach);
|
|
72
83
|
y2 = Math.max(y2, box.y + box.height + reach);
|
|
73
84
|
}
|
|
85
|
+
// An inline box's fragments are boxes on the lines they are on, and its
|
|
86
|
+
// padding and border reach above and below those lines: they count in
|
|
87
|
+
// the scrollable overflow of the block it is in (CSS Overflow 3, 2.2) —
|
|
88
|
+
// Blink adds each inline box fragment's border box
|
|
89
|
+
// (`ScrollableOverflowCalculator::AddItemsInternal`). Design 150's footer
|
|
90
|
+
// links, 50px of padding under their text, made Chrome's page 19px
|
|
91
|
+
// taller than the box they end. Only a box with padding or a border
|
|
92
|
+
// above or below its text can reach past its lines.
|
|
93
|
+
if (box.kind === 'inline' &&
|
|
94
|
+
box.padTop + box.padBottom + box.borderTop + box.borderBottom > 0) {
|
|
95
|
+
const reach = inlineFragmentsReach(box);
|
|
96
|
+
if (reach) {
|
|
97
|
+
x1 = Math.min(x1, reach.x1);
|
|
98
|
+
y1 = Math.min(y1, reach.y1);
|
|
99
|
+
x2 = Math.max(x2, reach.x2);
|
|
100
|
+
y2 = Math.max(y2, reach.y2);
|
|
101
|
+
bottom = Math.max(bottom, reach.y2);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
74
104
|
const lines = box.lines;
|
|
75
105
|
if (lines) {
|
|
76
106
|
let tallest = 0;
|
|
@@ -127,6 +157,8 @@ export function computePaintBounds(box, moved = false) {
|
|
|
127
157
|
const reach = computePaintBounds(child, moved);
|
|
128
158
|
if (!child.outOfFlow)
|
|
129
159
|
bottom = Math.max(bottom, reach);
|
|
160
|
+
else
|
|
161
|
+
OUT_OF_FLOW_REACH.set(child, reach);
|
|
130
162
|
if (child.boundsY === Infinity)
|
|
131
163
|
continue;
|
|
132
164
|
x1 = Math.min(x1, child.boundsX);
|
|
@@ -174,7 +206,7 @@ export function computePaintBounds(box, moved = false) {
|
|
|
174
206
|
return bottom;
|
|
175
207
|
// whether the box clips only matters where its content reaches past it,
|
|
176
208
|
// and its style is one more object a walk of every box would read
|
|
177
|
-
const end = box.y + box.height;
|
|
209
|
+
const end = empty ? -Infinity : box.y + box.height;
|
|
178
210
|
if (bottom <= end)
|
|
179
211
|
return end;
|
|
180
212
|
const style = box.style;
|
|
@@ -200,6 +232,56 @@ export function hasRect(box) {
|
|
|
200
232
|
const display = box.style.display;
|
|
201
233
|
return display !== 'table-column' && display !== 'table-column-group';
|
|
202
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Where an inline box's fragments are, over the lines of the block it is
|
|
237
|
+
* in that hold its text: from its face's ascent and its top padding and
|
|
238
|
+
* border above each line's baseline to its descent and its bottom ones
|
|
239
|
+
* below, as `paintInlineBoxes` draws them. Null for a box with no text or
|
|
240
|
+
* no face.
|
|
241
|
+
*/
|
|
242
|
+
function inlineFragmentsReach(box) {
|
|
243
|
+
const face = box.decoration ?? PADDED_FACES.get(box);
|
|
244
|
+
if (!face || box.subtreeTextEnd <= box.subtreeTextStart)
|
|
245
|
+
return null;
|
|
246
|
+
let block = box.parent;
|
|
247
|
+
while (block && block.kind === 'inline')
|
|
248
|
+
block = block.parent;
|
|
249
|
+
const lines = block?.lines;
|
|
250
|
+
if (!lines?.length)
|
|
251
|
+
return null;
|
|
252
|
+
const from = box.subtreeTextStart;
|
|
253
|
+
const to = box.subtreeTextEnd;
|
|
254
|
+
let lo = 0;
|
|
255
|
+
let hi = lines.length;
|
|
256
|
+
while (lo < hi) {
|
|
257
|
+
const mid = (lo + hi) >> 1;
|
|
258
|
+
if (lines[mid].textEnd > from)
|
|
259
|
+
hi = mid;
|
|
260
|
+
else
|
|
261
|
+
lo = mid + 1;
|
|
262
|
+
}
|
|
263
|
+
const moved = offsetOf(box);
|
|
264
|
+
let x1 = Infinity;
|
|
265
|
+
let y1 = Infinity;
|
|
266
|
+
let x2 = -Infinity;
|
|
267
|
+
let y2 = -Infinity;
|
|
268
|
+
for (let i = lo; i < lines.length; i += 1) {
|
|
269
|
+
const line = lines[i];
|
|
270
|
+
if (line.textStart >= to)
|
|
271
|
+
break;
|
|
272
|
+
if (line.textEnd <= from)
|
|
273
|
+
continue;
|
|
274
|
+
const raise = SHIFTED_LINES.has(line)
|
|
275
|
+
? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
|
|
276
|
+
: 0;
|
|
277
|
+
const baseline = line.y + line.baseline - raise + (moved?.y ?? 0);
|
|
278
|
+
x1 = Math.min(x1, line.x);
|
|
279
|
+
x2 = Math.max(x2, line.x + line.width);
|
|
280
|
+
y1 = Math.min(y1, baseline - face.ascent - box.padTop - box.borderTop);
|
|
281
|
+
y2 = Math.max(y2, baseline + face.descent + box.padBottom + box.borderBottom);
|
|
282
|
+
}
|
|
283
|
+
return x1 === Infinity ? null : { x1, y1, x2, y2 };
|
|
284
|
+
}
|
|
203
285
|
/** Children lists past this size get the sorted viewport index; below it a
|
|
204
286
|
* linear scan is cheaper than keeping one. */
|
|
205
287
|
const PAINT_INDEX_MIN = 64;
|
|
@@ -280,7 +362,7 @@ export function paintDocument(ctx, tree, options) {
|
|
|
280
362
|
if (!canFill(ctx))
|
|
281
363
|
return;
|
|
282
364
|
ctx.save();
|
|
283
|
-
const canvas = canvasBackground(tree
|
|
365
|
+
const canvas = canvasBackground(tree);
|
|
284
366
|
if (canvas)
|
|
285
367
|
paintCanvas(ctx, canvas, tree.root, options);
|
|
286
368
|
paintBox(ctx, tree.root, {
|
|
@@ -296,22 +378,41 @@ export function paintDocument(ctx, tree, options) {
|
|
|
296
378
|
* `<body>`'s, which then paints no background of its own. A document with
|
|
297
379
|
* no `<html>` tag has the root box standing in for its root element, and a
|
|
298
380
|
* `<body>` in it is still the body: mail often starts at `<body style>`.
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
381
|
+
* With neither tag, the root box stands in for the body, and the `<html>`
|
|
382
|
+
* around it is implied: an `html { … }` or a `:root { … }` background, which
|
|
383
|
+
* a test's reference sets on a document with no tags at all, has no box
|
|
384
|
+
* but the canvas to be painted on. `anchor` is the box the image is
|
|
385
|
+
* positioned against: the root element's, whichever box it came from.
|
|
386
|
+
*/
|
|
387
|
+
function canvasBackground(tree) {
|
|
388
|
+
const root = tree.root;
|
|
389
|
+
const has = (s) => !isTransparent(s.backgroundColor) ||
|
|
390
|
+
!!s.backgroundImage ||
|
|
391
|
+
!!s.backgroundGradient ||
|
|
392
|
+
!!s.backgroundImages;
|
|
393
|
+
const implied = tree.impliedHtml;
|
|
394
|
+
if (implied) {
|
|
395
|
+
// an `<html>` that is not displayed has no background to give
|
|
396
|
+
if (implied.display === 'none')
|
|
397
|
+
return null;
|
|
398
|
+
if (has(implied))
|
|
399
|
+
return { style: implied, source: null, anchor: root };
|
|
400
|
+
// containment on either keeps the body's to the body (CSS Containment 2)
|
|
401
|
+
if (implied.contain || root.style.contain)
|
|
402
|
+
return null;
|
|
403
|
+
return has(root.style)
|
|
404
|
+
? { style: root.style, source: root, anchor: root }
|
|
405
|
+
: null;
|
|
406
|
+
}
|
|
307
407
|
const top = childNamed(root, 'html') ?? root;
|
|
308
|
-
if (has(top))
|
|
309
|
-
return { source: top, anchor: top };
|
|
408
|
+
if (has(top.style))
|
|
409
|
+
return { style: top.style, source: top, anchor: top };
|
|
310
410
|
const body = childNamed(top, 'body');
|
|
311
|
-
// containment on either keeps the body's to the body (CSS Containment 2)
|
|
312
411
|
if (!body || top.style.contain || body.style.contain)
|
|
313
412
|
return null;
|
|
314
|
-
return has(body)
|
|
413
|
+
return has(body.style)
|
|
414
|
+
? { style: body.style, source: body, anchor: top }
|
|
415
|
+
: null;
|
|
315
416
|
}
|
|
316
417
|
/** A box's child element of that name, or one inside the anonymous boxes
|
|
317
418
|
* it is wrapped in: a `<body>` in an `<html>` set `display: table` is in
|
|
@@ -328,7 +429,7 @@ function childNamed(box, name) {
|
|
|
328
429
|
}
|
|
329
430
|
return null;
|
|
330
431
|
}
|
|
331
|
-
function paintCanvas(ctx, { source, anchor }, root, options) {
|
|
432
|
+
function paintCanvas(ctx, { style: source, anchor }, root, options) {
|
|
332
433
|
const whole = options.canvas ?? {
|
|
333
434
|
x: root.x + options.originX,
|
|
334
435
|
y: root.y + options.originY,
|
|
@@ -338,8 +439,8 @@ function paintCanvas(ctx, { source, anchor }, root, options) {
|
|
|
338
439
|
const area = clampRect(options, Math.round(whole.x), Math.round(whole.y), Math.ceil(whole.width), Math.ceil(whole.height));
|
|
339
440
|
if (!area)
|
|
340
441
|
return;
|
|
341
|
-
const layers = layersOf(source
|
|
342
|
-
const visible = source.
|
|
442
|
+
const layers = layersOf(source) ?? [source];
|
|
443
|
+
const visible = source.visibility === 'visible';
|
|
343
444
|
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
344
445
|
const style = layers[i];
|
|
345
446
|
if (!isTransparent(style.backgroundColor) && visible) {
|
|
@@ -351,7 +452,7 @@ function paintCanvas(ctx, { source, anchor }, root, options) {
|
|
|
351
452
|
// browser does — the stripes a short page with a gradient on its
|
|
352
453
|
// body shows — or by the viewport, where it is fixed
|
|
353
454
|
const box = style.backgroundAttachment === 'fixed' && options.canvas
|
|
354
|
-
? options.canvas
|
|
455
|
+
? (options.viewport ?? options.canvas)
|
|
355
456
|
: originBox(anchor, options, style);
|
|
356
457
|
paintGradient(ctx, style, style.backgroundGradient, area, snapped(box.x, box.y, box.width, box.height));
|
|
357
458
|
}
|
|
@@ -403,22 +504,32 @@ function layersOf(style) {
|
|
|
403
504
|
*/
|
|
404
505
|
function paintLayers(ctx, box, options, image) {
|
|
405
506
|
// a background painted through the text is painted with it
|
|
406
|
-
// (`paintClippedText`), and not as a box
|
|
407
|
-
|
|
507
|
+
// (`paintClippedText`), and not as a box — but for what `border-area
|
|
508
|
+
// text` paints in the border as well
|
|
509
|
+
const style = box.style;
|
|
510
|
+
if (style.backgroundClipText && style.backgroundClip !== 'border-area') {
|
|
408
511
|
return;
|
|
409
|
-
|
|
512
|
+
}
|
|
513
|
+
const layers = layersOf(style);
|
|
410
514
|
if (!layers) {
|
|
411
|
-
|
|
412
|
-
if (image && box.style.backgroundImage)
|
|
413
|
-
image(box.style);
|
|
515
|
+
paintLayer(ctx, box, options, style, image);
|
|
414
516
|
return;
|
|
415
517
|
}
|
|
416
518
|
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
417
|
-
|
|
418
|
-
if (image && layers[i].backgroundImage)
|
|
419
|
-
image(layers[i]);
|
|
519
|
+
paintLayer(ctx, box, options, layers[i], image);
|
|
420
520
|
}
|
|
421
521
|
}
|
|
522
|
+
function paintLayer(ctx, box, options, layer, image) {
|
|
523
|
+
// painted over the border box, inside what the border paints
|
|
524
|
+
const area = layer.backgroundClip === 'border-area';
|
|
525
|
+
if (area && !pushBorderArea(ctx, box, options))
|
|
526
|
+
return;
|
|
527
|
+
paintBackground(ctx, box, options, layer);
|
|
528
|
+
if (image && layer.backgroundImage)
|
|
529
|
+
image(layer);
|
|
530
|
+
if (area)
|
|
531
|
+
ctx.restore();
|
|
532
|
+
}
|
|
422
533
|
/** A box's padding box in window coordinates. */
|
|
423
534
|
function paddingBox(box, options) {
|
|
424
535
|
return {
|
|
@@ -429,8 +540,10 @@ function paddingBox(box, options) {
|
|
|
429
540
|
};
|
|
430
541
|
}
|
|
431
542
|
/** Where a layer of a box's background is placed, in window coordinates:
|
|
432
|
-
* the box its `background-origin` names (CSS Backgrounds 3, 3.8)
|
|
433
|
-
|
|
543
|
+
* the box its `background-origin` names (CSS Backgrounds 3, 3.8) — of the
|
|
544
|
+
* whole strip, for a fragment of an inline box that is sliced. */
|
|
545
|
+
function originBox(frame, options, style) {
|
|
546
|
+
const box = frame.strip ?? frame;
|
|
434
547
|
const [t, r, b, l] = edgeInsets(box, style.backgroundOrigin);
|
|
435
548
|
return {
|
|
436
549
|
x: box.x + l + options.originX,
|
|
@@ -496,8 +609,14 @@ function paintBox(ctx, box, options) {
|
|
|
496
609
|
ctx.save();
|
|
497
610
|
ctx.globalAlpha = ctx.globalAlpha * opacity;
|
|
498
611
|
}
|
|
612
|
+
if (masked(box.style) && paintMasked(ctx, box, options)) {
|
|
613
|
+
if (fade)
|
|
614
|
+
ctx.restore();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
499
617
|
// `clip` shows the part of an absolutely positioned box it names, its own
|
|
500
|
-
// background and borders among it (CSS 2.1 11.1.2)
|
|
618
|
+
// background and borders among it (CSS 2.1 11.1.2) — written out here,
|
|
619
|
+
// not called: a frame more a level and a deep document ran out of stack
|
|
501
620
|
const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
|
|
502
621
|
if (!clip || (clip.w > 0 && clip.h > 0)) {
|
|
503
622
|
const clipped = !!clip && pushClip(ctx, clip, null);
|
|
@@ -508,6 +627,135 @@ function paintBox(ctx, box, options) {
|
|
|
508
627
|
if (fade)
|
|
509
628
|
ctx.restore();
|
|
510
629
|
}
|
|
630
|
+
/** A box and what it holds, cut to its `clip` as `paintBox` cuts it. */
|
|
631
|
+
function paintClipped(ctx, box, options) {
|
|
632
|
+
const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
|
|
633
|
+
if (clip && !(clip.w > 0 && clip.h > 0))
|
|
634
|
+
return;
|
|
635
|
+
const clipped = !!clip && pushClip(ctx, clip, null);
|
|
636
|
+
paintContent(ctx, box, options);
|
|
637
|
+
if (clipped)
|
|
638
|
+
ctx.restore();
|
|
639
|
+
}
|
|
640
|
+
const MASK_LAYERS = new WeakMap();
|
|
641
|
+
/** No corners: a mask layer is placed in the box's rectangle, which its
|
|
642
|
+
* radii do not round (CSS Masking 1, 7). */
|
|
643
|
+
const SQUARE_RADII = [0, 0, 0, 0];
|
|
644
|
+
/**
|
|
645
|
+
* A style for each of a box's mask layers, top first, as `layersOf` makes
|
|
646
|
+
* a background's: the box's own style with the layer's image, repeat, size,
|
|
647
|
+
* position, origin and clip in its background's place, and no colour — so
|
|
648
|
+
* the background painter places and repeats a mask layer as it does a
|
|
649
|
+
* background layer, which is what CSS Masking 1 says a mask layer is (7).
|
|
650
|
+
* Made once a style.
|
|
651
|
+
*/
|
|
652
|
+
function maskLayersOf(style) {
|
|
653
|
+
let layers = MASK_LAYERS.get(style);
|
|
654
|
+
if (layers)
|
|
655
|
+
return layers;
|
|
656
|
+
const mask = style.mask;
|
|
657
|
+
const nth = (list, i) => list[i % list.length];
|
|
658
|
+
layers = mask.images.map((image, i) => {
|
|
659
|
+
const layer = Object.create(style);
|
|
660
|
+
layer.backgroundColor = null;
|
|
661
|
+
layer.backgroundImages = null;
|
|
662
|
+
layer.backgroundImage = typeof image === 'string' ? image : null;
|
|
663
|
+
layer.backgroundGradient =
|
|
664
|
+
image !== null && typeof image !== 'string' ? image : null;
|
|
665
|
+
layer.backgroundRepeat = nth(mask.repeats, i);
|
|
666
|
+
layer.backgroundSize = nth(mask.sizes, i);
|
|
667
|
+
layer.backgroundAttachment = 'scroll';
|
|
668
|
+
[layer.backgroundPositionX, layer.backgroundPositionY] = nth(mask.positions, i);
|
|
669
|
+
layer.backgroundOrigin = nth(mask.origins, i);
|
|
670
|
+
layer.backgroundClip = nth(mask.clips, i);
|
|
671
|
+
layer.backgroundClipText = false;
|
|
672
|
+
layer.borderRadius = SQUARE_RADII;
|
|
673
|
+
layer.borderRadiusY = null;
|
|
674
|
+
return layer;
|
|
675
|
+
});
|
|
676
|
+
MASK_LAYERS.set(style, layers);
|
|
677
|
+
return layers;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* A masked box (CSS Masking 1, 7): the box and what it holds drawn on a
|
|
681
|
+
* surface of their own, which the alpha of its mask layers — painted as a
|
|
682
|
+
* background's are, on a second surface, each added over the ones below
|
|
683
|
+
* it — then cuts with `destination-in`, and the result drawn in its place,
|
|
684
|
+
* as the group a mask makes. Only the mask painting area, the border box,
|
|
685
|
+
* can show, and only its part in the damage is drawn.
|
|
686
|
+
*
|
|
687
|
+
* A layer whose image has not arrived is transparent black (7.2), so a box
|
|
688
|
+
* none of whose layers can be drawn yet draws nothing: an icon masked out
|
|
689
|
+
* of its `background-color` is not a solid square while its image loads.
|
|
690
|
+
* False where there is no surface to draw on, or no `destination-in` to
|
|
691
|
+
* draw with, and the caller paints the box unmasked.
|
|
692
|
+
*/
|
|
693
|
+
function paintMasked(ctx, box, options) {
|
|
694
|
+
const layers = maskLayersOf(box.style);
|
|
695
|
+
const drawable = layers.some((layer) => !!layer.backgroundGradient ||
|
|
696
|
+
(!!layer.backgroundImage &&
|
|
697
|
+
!!options.backgroundImageFor?.(layer.backgroundImage)));
|
|
698
|
+
if (!drawable)
|
|
699
|
+
return true;
|
|
700
|
+
if (!options.surface || !ctx.drawImage)
|
|
701
|
+
return false;
|
|
702
|
+
let x0 = Math.floor(box.x + options.originX);
|
|
703
|
+
let y0 = Math.floor(frameY(box) + options.originY);
|
|
704
|
+
let x1 = Math.ceil(box.x + box.width + options.originX);
|
|
705
|
+
let y1 = Math.ceil(frameY(box) + frameHeight(box) + options.originY);
|
|
706
|
+
const damage = options.damage;
|
|
707
|
+
if (damage) {
|
|
708
|
+
x0 = Math.max(x0, Math.floor(damage.x));
|
|
709
|
+
y0 = Math.max(y0, Math.floor(damage.y));
|
|
710
|
+
x1 = Math.min(x1, Math.ceil(damage.x + damage.width));
|
|
711
|
+
y1 = Math.min(y1, Math.ceil(damage.y + damage.height));
|
|
712
|
+
}
|
|
713
|
+
const w = x1 - x0;
|
|
714
|
+
const h = y1 - y0;
|
|
715
|
+
if (!(w > 0 && h > 0))
|
|
716
|
+
return true;
|
|
717
|
+
const content = options.surface(w, h);
|
|
718
|
+
const mask = content && options.surface(w, h);
|
|
719
|
+
try {
|
|
720
|
+
if (!content || !mask)
|
|
721
|
+
return false;
|
|
722
|
+
const cctx = content.getContext('2d');
|
|
723
|
+
const mctx = mask.getContext('2d');
|
|
724
|
+
// the surfaces' own coordinates, the window's moved to their corner
|
|
725
|
+
const on = {
|
|
726
|
+
...options,
|
|
727
|
+
originX: options.originX - x0,
|
|
728
|
+
originY: options.originY - y0,
|
|
729
|
+
damage: { x: 0, y: 0, width: w, height: h },
|
|
730
|
+
canvas: options.canvas && {
|
|
731
|
+
...options.canvas,
|
|
732
|
+
x: options.canvas.x - x0,
|
|
733
|
+
y: options.canvas.y - y0,
|
|
734
|
+
},
|
|
735
|
+
viewport: options.viewport && {
|
|
736
|
+
...options.viewport,
|
|
737
|
+
x: options.viewport.x - x0,
|
|
738
|
+
y: options.viewport.y - y0,
|
|
739
|
+
},
|
|
740
|
+
clips: [],
|
|
741
|
+
};
|
|
742
|
+
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
743
|
+
paintLayer(mctx, box, on, layers[i], frameImages(mctx, box, on));
|
|
744
|
+
}
|
|
745
|
+
paintClipped(cctx, box, on);
|
|
746
|
+
cctx.globalCompositeOperation = 'destination-in';
|
|
747
|
+
if (cctx.globalCompositeOperation !== 'destination-in')
|
|
748
|
+
return false;
|
|
749
|
+
cctx.drawImage(mask, 0, 0);
|
|
750
|
+
cctx.globalCompositeOperation = 'source-over';
|
|
751
|
+
ctx.drawImage(content, x0, y0);
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
finally {
|
|
755
|
+
content?.destroy?.();
|
|
756
|
+
mask?.destroy?.();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
511
759
|
function paintContent(ctx, box, options) {
|
|
512
760
|
const visible = box.style.visibility === 'visible';
|
|
513
761
|
if (visible)
|
|
@@ -627,14 +875,7 @@ function paintOwnBackground(ctx, box, options) {
|
|
|
627
875
|
if (style.boxShadow)
|
|
628
876
|
paintShadows(ctx, box, options, false);
|
|
629
877
|
if (box !== options.canvasSource) {
|
|
630
|
-
paintLayers(ctx, box, options, (
|
|
631
|
-
const painted = clipArea(box, options, layer, true);
|
|
632
|
-
const area = painted &&
|
|
633
|
-
clampRect(options, painted.x, painted.y, painted.w, painted.h);
|
|
634
|
-
if (area) {
|
|
635
|
-
paintBackgroundImage(ctx, layer, area, originBox(box, options, layer), options, painted.corners);
|
|
636
|
-
}
|
|
637
|
-
});
|
|
878
|
+
paintLayers(ctx, box, options, frameImages(ctx, box, options));
|
|
638
879
|
}
|
|
639
880
|
if (style.boxShadow)
|
|
640
881
|
paintShadows(ctx, box, options, true);
|
|
@@ -644,6 +885,21 @@ function paintOwnBackground(ctx, box, options) {
|
|
|
644
885
|
if (box.kind === 'table')
|
|
645
886
|
paintPartBackgrounds(ctx, box, options);
|
|
646
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* How `paintLayers` draws a frame's image layers: each in the area its
|
|
890
|
+
* `background-clip` names, from the box its `background-origin` names
|
|
891
|
+
* (CSS Backgrounds 3, 3.7 and 3.8) — a block's, or an inline box's
|
|
892
|
+
* fragment's, whose background is its images as much as its colour.
|
|
893
|
+
*/
|
|
894
|
+
function frameImages(ctx, frame, options) {
|
|
895
|
+
return (layer) => {
|
|
896
|
+
const painted = clipArea(frame, options, layer, true);
|
|
897
|
+
const area = painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
|
|
898
|
+
if (area) {
|
|
899
|
+
paintBackgroundImage(ctx, layer, area, originBox(frame, options, layer), options, painted.corners);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
647
903
|
/** How far past its shape a shadow's blur shows: three standard deviations,
|
|
648
904
|
* the blur being two (CSS Backgrounds 3, 7.1.1). */
|
|
649
905
|
function shadowReach(blur) {
|
|
@@ -742,9 +998,6 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
742
998
|
}, 0);
|
|
743
999
|
continue;
|
|
744
1000
|
}
|
|
745
|
-
if (bakedOuter(ctx, options, s, color, shape, around, covered ? null : { rect: own, corners })) {
|
|
746
|
-
continue;
|
|
747
|
-
}
|
|
748
1001
|
// what of the shadow falls under the box is not drawn
|
|
749
1002
|
const clipped = !covered && ctx.clip && ctx.rect && ctx.save;
|
|
750
1003
|
if (clipped) {
|
|
@@ -755,7 +1008,7 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
755
1008
|
ctx.clip();
|
|
756
1009
|
}
|
|
757
1010
|
fillShadow(ctx, s, color, (dx) => {
|
|
758
|
-
|
|
1011
|
+
shadowShape(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
|
|
759
1012
|
}, shape.x + shape.width + reach);
|
|
760
1013
|
if (clipped)
|
|
761
1014
|
ctx.restore();
|
|
@@ -805,8 +1058,6 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
805
1058
|
fillRing(ctx, pad, inner, hole, within);
|
|
806
1059
|
continue;
|
|
807
1060
|
}
|
|
808
|
-
if (bakedInset(ctx, options, s, color, pad, inner, hole, within))
|
|
809
|
-
continue;
|
|
810
1061
|
if (!ctx.clip || !ctx.save || !ctx.rect)
|
|
811
1062
|
continue;
|
|
812
1063
|
ctx.save();
|
|
@@ -824,128 +1075,24 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
824
1075
|
fillShadow(ctx, s, color, (dx) => {
|
|
825
1076
|
ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
|
|
826
1077
|
if (hole.width > 0 && hole.height > 0) {
|
|
827
|
-
|
|
1078
|
+
shadowShape(ctx, hole.x - dx, hole.y, hole.width, hole.height, within);
|
|
828
1079
|
}
|
|
829
|
-
}, frame.x + frame.width + shadowReach(s.blur));
|
|
1080
|
+
}, frame.x + frame.width + shadowReach(s.blur), 'evenodd');
|
|
830
1081
|
ctx.restore();
|
|
831
1082
|
}
|
|
832
1083
|
}
|
|
833
|
-
/** Corners as part of a key. */
|
|
834
|
-
function cornerKey(c) {
|
|
835
|
-
return `${c.x.join(',')}/${c.y.join(',')}`;
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* An outer shadow drawn once on a surface of its own and composited: the
|
|
839
|
-
* shape's shadow, and where `box` is given the box cut out of it — so a box
|
|
840
|
-
* that shows what is behind it shows no shadow there, with the clip on the
|
|
841
|
-
* small surface rather than the window. False where there is no surface.
|
|
842
|
-
*/
|
|
843
|
-
function bakedOuter(ctx, options, s, color, shape, around, box) {
|
|
844
|
-
if (!options.cached || !ctx.drawImage)
|
|
845
|
-
return false;
|
|
846
|
-
const reach = shadowReach(s.blur) + 1;
|
|
847
|
-
const x0 = Math.floor(shape.x) - reach;
|
|
848
|
-
const y0 = Math.floor(shape.y) - reach;
|
|
849
|
-
const w = Math.ceil(shape.x + shape.width) + reach - x0;
|
|
850
|
-
const h = Math.ceil(shape.y + shape.height) + reach - y0;
|
|
851
|
-
const sx = shape.x - x0;
|
|
852
|
-
const sy = shape.y - y0;
|
|
853
|
-
const cut = box && {
|
|
854
|
-
x: box.rect.x - x0,
|
|
855
|
-
y: box.rect.y - y0,
|
|
856
|
-
width: box.rect.width,
|
|
857
|
-
height: box.rect.height,
|
|
858
|
-
};
|
|
859
|
-
const key = [
|
|
860
|
-
'shadow',
|
|
861
|
-
w,
|
|
862
|
-
h,
|
|
863
|
-
sx,
|
|
864
|
-
sy,
|
|
865
|
-
shape.width,
|
|
866
|
-
shape.height,
|
|
867
|
-
cornerKey(around),
|
|
868
|
-
s.blur,
|
|
869
|
-
color,
|
|
870
|
-
cut
|
|
871
|
-
? `${cut.x},${cut.y},${cut.width},${cut.height},${cornerKey(box.corners)}`
|
|
872
|
-
: '',
|
|
873
|
-
].join('|');
|
|
874
|
-
const image = options.cached(key, w, h, (sctx) => {
|
|
875
|
-
if (cut) {
|
|
876
|
-
sctx.save();
|
|
877
|
-
sctx.beginPath();
|
|
878
|
-
sctx.rect(0, 0, w, h);
|
|
879
|
-
roundedRect(sctx, cut.x, cut.y, cut.width, cut.height, box.corners, true, true);
|
|
880
|
-
sctx.clip();
|
|
881
|
-
}
|
|
882
|
-
fillShadow(sctx, s, color, (dx) => {
|
|
883
|
-
roundedRect(sctx, sx - dx, sy, shape.width, shape.height, around);
|
|
884
|
-
}, sx + shape.width + reach);
|
|
885
|
-
if (cut)
|
|
886
|
-
sctx.restore();
|
|
887
|
-
});
|
|
888
|
-
if (!image)
|
|
889
|
-
return false;
|
|
890
|
-
ctx.drawImage(image, x0, y0);
|
|
891
|
-
return true;
|
|
892
|
-
}
|
|
893
|
-
/**
|
|
894
|
-
* An inset shadow drawn once on a surface the size of the padding box and
|
|
895
|
-
* composited: a frame around the hole, whose shadow falls inside, clipped
|
|
896
|
-
* to the padding box's corners on the surface. False where there is none.
|
|
897
|
-
*/
|
|
898
|
-
function bakedInset(ctx, options, s, color, pad, inner, hole, within) {
|
|
899
|
-
if (!options.cached || !ctx.drawImage)
|
|
900
|
-
return false;
|
|
901
|
-
const w = Math.round(pad.width);
|
|
902
|
-
const h = Math.round(pad.height);
|
|
903
|
-
const hx = hole.x - pad.x;
|
|
904
|
-
const hy = hole.y - pad.y;
|
|
905
|
-
const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
|
|
906
|
-
const key = [
|
|
907
|
-
'inset',
|
|
908
|
-
w,
|
|
909
|
-
h,
|
|
910
|
-
cornerKey(inner),
|
|
911
|
-
hx,
|
|
912
|
-
hy,
|
|
913
|
-
hole.width,
|
|
914
|
-
hole.height,
|
|
915
|
-
cornerKey(within),
|
|
916
|
-
s.blur,
|
|
917
|
-
color,
|
|
918
|
-
].join('|');
|
|
919
|
-
const image = options.cached(key, w, h, (sctx) => {
|
|
920
|
-
sctx.save();
|
|
921
|
-
sctx.beginPath();
|
|
922
|
-
roundedRect(sctx, 0, 0, w, h, inner);
|
|
923
|
-
sctx.clip();
|
|
924
|
-
fillShadow(sctx, s, color, (dx) => {
|
|
925
|
-
sctx.rect(-reach - dx, -reach, w + 2 * reach, h + 2 * reach);
|
|
926
|
-
if (hole.width > 0 && hole.height > 0) {
|
|
927
|
-
roundedRect(sctx, hx - dx, hy, hole.width, hole.height, within, true, true);
|
|
928
|
-
}
|
|
929
|
-
}, w + reach + shadowReach(s.blur));
|
|
930
|
-
sctx.restore();
|
|
931
|
-
});
|
|
932
|
-
if (!image)
|
|
933
|
-
return false;
|
|
934
|
-
ctx.drawImage(image, pad.x, pad.y);
|
|
935
|
-
return true;
|
|
936
|
-
}
|
|
937
1084
|
/**
|
|
938
1085
|
* Fill a shadow's shape: in its colour where it has no blur, and otherwise
|
|
939
1086
|
* as the context's shadow of the shape drawn `dx` to the left — clear of
|
|
940
1087
|
* the window, its right edge at `right` before the move — with the shadow
|
|
941
1088
|
* offset back by as much, so that only the shadow lands.
|
|
942
1089
|
*/
|
|
943
|
-
function fillShadow(ctx, s, color, shape, right) {
|
|
1090
|
+
function fillShadow(ctx, s, color, shape, right, rule = 'nonzero') {
|
|
944
1091
|
if (!(s.blur > 0)) {
|
|
945
1092
|
ctx.fillStyle = color;
|
|
946
1093
|
ctx.beginPath();
|
|
947
1094
|
shape(0);
|
|
948
|
-
ctx.fill();
|
|
1095
|
+
ctx.fill(rule);
|
|
949
1096
|
return;
|
|
950
1097
|
}
|
|
951
1098
|
const dx = Math.ceil(right) + 1;
|
|
@@ -957,9 +1104,26 @@ function fillShadow(ctx, s, color, shape, right) {
|
|
|
957
1104
|
ctx.fillStyle = '#000000';
|
|
958
1105
|
ctx.beginPath();
|
|
959
1106
|
shape(dx);
|
|
960
|
-
ctx.fill();
|
|
1107
|
+
ctx.fill(rule);
|
|
961
1108
|
ctx.restore();
|
|
962
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* A shadow's shape, spelled the way a 2d context recognises one: a `rect`,
|
|
1112
|
+
* or a `roundRect` with each corner's own radii, elliptical where the
|
|
1113
|
+
* corner is. react-x11's contexts draw the blurred shadow of such a shape —
|
|
1114
|
+
* and of a `rect` less such a shape, filled `evenodd` — from a tile they
|
|
1115
|
+
* keep, where one built of curves they blur afresh on every fill.
|
|
1116
|
+
*/
|
|
1117
|
+
function shadowShape(ctx, x, y, w, h, c) {
|
|
1118
|
+
if (c.x.every((r) => r === 0) || c.y.every((r) => r === 0)) {
|
|
1119
|
+
ctx.rect(x, y, w, h);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
// a circular corner as the number every context has always taken; only
|
|
1123
|
+
// an elliptical one as the point
|
|
1124
|
+
const corner = (i) => c.x[i] === c.y[i] ? c.x[i] : { x: c.x[i], y: c.y[i] };
|
|
1125
|
+
ctx.roundRect(x, y, w, h, [corner(0), corner(1), corner(2), corner(3)]);
|
|
1126
|
+
}
|
|
963
1127
|
/**
|
|
964
1128
|
* The band between two rounded rectangles, the inner inside the outer, in
|
|
965
1129
|
* the colour already set. Where a side has no band the two edges meet, and
|
|
@@ -1006,6 +1170,7 @@ function inFlow(parent, child, options) {
|
|
|
1006
1170
|
return (style.overflowX === 'visible' &&
|
|
1007
1171
|
style.overflowY === 'visible' &&
|
|
1008
1172
|
opacityOf(child) >= 1 &&
|
|
1173
|
+
!masked(style) &&
|
|
1009
1174
|
// containment makes it a stacking context, painted whole
|
|
1010
1175
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
|
|
1011
1176
|
}
|
|
@@ -1031,6 +1196,7 @@ function flowFlex(parent, child, options) {
|
|
|
1031
1196
|
return (style.overflowX === 'visible' &&
|
|
1032
1197
|
style.overflowY === 'visible' &&
|
|
1033
1198
|
!(child.outOfFlow && style.clip) &&
|
|
1199
|
+
!masked(style) &&
|
|
1034
1200
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
|
|
1035
1201
|
}
|
|
1036
1202
|
/**
|
|
@@ -1215,9 +1381,10 @@ function spreadCorners(c, width, height, top, right, bottom, left) {
|
|
|
1215
1381
|
return rounded ? { x, y } : null;
|
|
1216
1382
|
}
|
|
1217
1383
|
/** How far in from a box's border box the box a `<visual-box>` names is:
|
|
1218
|
-
* top, right, bottom and left
|
|
1384
|
+
* top, right, bottom and left — none for `border-area`, whose layer is
|
|
1385
|
+
* painted over the border box inside what the border paints. */
|
|
1219
1386
|
function edgeInsets(box, which) {
|
|
1220
|
-
if (which === 'border-box')
|
|
1387
|
+
if (which === 'border-box' || which === 'border-area')
|
|
1221
1388
|
return NO_INSETS;
|
|
1222
1389
|
const t = box.borderTop;
|
|
1223
1390
|
const r = box.borderRight;
|
|
@@ -1256,7 +1423,55 @@ function paintPositioned(ctx, box, options) {
|
|
|
1256
1423
|
return;
|
|
1257
1424
|
}
|
|
1258
1425
|
}
|
|
1259
|
-
paintBox(ctx, box, options);
|
|
1426
|
+
paintBox(ctx, box, atViewport(box, options));
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* The options a box is painted with where it is fixed to the viewport
|
|
1430
|
+
* (CSS 2.1 9.6.1): laid out against the viewport at the document's top,
|
|
1431
|
+
* and drawn where the viewport is now, however far the pane that scrolls
|
|
1432
|
+
* the element has moved it — a fixed header scrolled away with the text.
|
|
1433
|
+
* Once for a box and all it holds.
|
|
1434
|
+
*/
|
|
1435
|
+
function atViewport(box, options) {
|
|
1436
|
+
const viewport = options.viewport;
|
|
1437
|
+
if (!viewport || options.atViewport || !fixedToViewport(box)) {
|
|
1438
|
+
return options;
|
|
1439
|
+
}
|
|
1440
|
+
// where the document's top left is from the viewport's, less where the
|
|
1441
|
+
// viewport is in the document: the scroll
|
|
1442
|
+
const root = rootOf(box);
|
|
1443
|
+
const dx = viewport.x - options.originX - root.x;
|
|
1444
|
+
const dy = viewport.y - options.originY - root.y;
|
|
1445
|
+
return {
|
|
1446
|
+
...options,
|
|
1447
|
+
originX: options.originX + dx,
|
|
1448
|
+
originY: options.originY + dy,
|
|
1449
|
+
atViewport: true,
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
/** Per tree, its boxes fixed to the viewport (`fixedToViewport`), found
|
|
1453
|
+
* as the layout positions them: what the element answers a scroll pane's
|
|
1454
|
+
* blit with (`HtmlViewNode.viewportFixedRects`). */
|
|
1455
|
+
export const FIXED_BOXES = new WeakMap();
|
|
1456
|
+
/** Whether a box is fixed to the viewport: `position: fixed` with no
|
|
1457
|
+
* transformed or contained box around it, which would be its containing
|
|
1458
|
+
* block instead (CSS Transforms 1, CSS Containment 2, 3.3). */
|
|
1459
|
+
export function fixedToViewport(box) {
|
|
1460
|
+
if (box.style.position !== 'fixed')
|
|
1461
|
+
return false;
|
|
1462
|
+
for (let at = box.parent; at?.parent; at = at.parent) {
|
|
1463
|
+
if (at.style.translate || at.style.transformTranslate)
|
|
1464
|
+
return false;
|
|
1465
|
+
if (contained(at, CONTAIN_LAYOUT | CONTAIN_PAINT))
|
|
1466
|
+
return false;
|
|
1467
|
+
}
|
|
1468
|
+
return true;
|
|
1469
|
+
}
|
|
1470
|
+
function rootOf(box) {
|
|
1471
|
+
let root = box;
|
|
1472
|
+
while (root.parent)
|
|
1473
|
+
root = root.parent;
|
|
1474
|
+
return root;
|
|
1260
1475
|
}
|
|
1261
1476
|
/**
|
|
1262
1477
|
* The box an out-of-flow box is positioned in, whose clips are the ones it
|
|
@@ -1675,6 +1890,9 @@ function stacksLayers(box) {
|
|
|
1675
1890
|
return true;
|
|
1676
1891
|
if (style.opacity < 1 || FADED_BLOCKS.has(box))
|
|
1677
1892
|
return true;
|
|
1893
|
+
// and so does a mask, which is applied to the group (CSS Masking 1, 7)
|
|
1894
|
+
if (masked(style))
|
|
1895
|
+
return true;
|
|
1678
1896
|
// layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
|
|
1679
1897
|
if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
|
|
1680
1898
|
return true;
|
|
@@ -1694,7 +1912,7 @@ function flexItem(box) {
|
|
|
1694
1912
|
* outside a box that clips where its containing block is, a fixed one
|
|
1695
1913
|
* outside every one, and a relative one inside every one.
|
|
1696
1914
|
*/
|
|
1697
|
-
function clipsFor(box, context) {
|
|
1915
|
+
export function clipsFor(box, context) {
|
|
1698
1916
|
let from = box.parent;
|
|
1699
1917
|
if (box.outOfFlow) {
|
|
1700
1918
|
const fixed = box.style.position === 'fixed';
|
|
@@ -1721,6 +1939,7 @@ function clipsFor(box, context) {
|
|
|
1721
1939
|
* parent painted it, it was. A clip of no area leaves none of it to paint.
|
|
1722
1940
|
*/
|
|
1723
1941
|
function paintStacked(ctx, box, options) {
|
|
1942
|
+
options = atViewport(box, options);
|
|
1724
1943
|
if (!intersects(box, options))
|
|
1725
1944
|
return;
|
|
1726
1945
|
const between = CLIPS_BETWEEN.get(box);
|
|
@@ -1884,7 +2103,8 @@ function paintBackground(ctx, box, options, style) {
|
|
|
1884
2103
|
// box, which is where it starts, and repeated under the borders — or
|
|
1885
2104
|
// the viewport's, where it is fixed; its line runs across the whole of
|
|
1886
2105
|
// it, not the part this paint reaches
|
|
1887
|
-
const fixed = style.backgroundAttachment === 'fixed' &&
|
|
2106
|
+
const fixed = style.backgroundAttachment === 'fixed' &&
|
|
2107
|
+
(options.viewport ?? options.canvas);
|
|
1888
2108
|
const origin = fixed || originBox(box, options, style);
|
|
1889
2109
|
const at = snapped(origin.x, origin.y, origin.width, origin.height);
|
|
1890
2110
|
if (rounded && style.backgroundSize !== 'auto' && ctx.clip) {
|
|
@@ -2260,7 +2480,7 @@ const MAX_TILES = 4096;
|
|
|
2260
2480
|
*/
|
|
2261
2481
|
function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
|
|
2262
2482
|
if (style.backgroundAttachment === 'fixed' && options.canvas) {
|
|
2263
|
-
at = options.canvas;
|
|
2483
|
+
at = options.viewport ?? options.canvas;
|
|
2264
2484
|
}
|
|
2265
2485
|
const url = style.backgroundImage;
|
|
2266
2486
|
const loaded = url ? options.backgroundImageFor?.(url) : null;
|
|
@@ -2821,6 +3041,88 @@ function roundedRing(ctx, box, x, y, w, h, options) {
|
|
|
2821
3041
|
}, insetCorners(corners, top, right, bottom, left));
|
|
2822
3042
|
return true;
|
|
2823
3043
|
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Clip to what a box's border paints, for a layer of `background-clip:
|
|
3046
|
+
* border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
|
|
3047
|
+
* not its colour — a transparent border still has an area, which is what
|
|
3048
|
+
* the value is for. The shapes are the border painter's own, so that the
|
|
3049
|
+
* background shows exactly where that border would: the ring `roundedRing`
|
|
3050
|
+
* fills where a rounded border is solid, the ring the trapezoids of a 3D
|
|
3051
|
+
* style make, and otherwise the rectangles `fillEdge` fills a side at a
|
|
3052
|
+
* time — the dots, the dashes, the two lines of a double border. All of
|
|
3053
|
+
* them run clockwise and a ring's inside the other way, so the clip is the
|
|
3054
|
+
* union by the non-zero rule, the one every context's `clip` takes. False
|
|
3055
|
+
* where the border paints nothing or the context cannot clip, and nothing
|
|
3056
|
+
* is pushed.
|
|
3057
|
+
*/
|
|
3058
|
+
function pushBorderArea(ctx, box, options) {
|
|
3059
|
+
const t = box.borderTop;
|
|
3060
|
+
const r = box.borderRight;
|
|
3061
|
+
const b = box.borderBottom;
|
|
3062
|
+
const l = box.borderLeft;
|
|
3063
|
+
if (!(t || r || b || l) || !ctx.beginPath || !ctx.rect || !ctx.clip) {
|
|
3064
|
+
return false;
|
|
3065
|
+
}
|
|
3066
|
+
const s = box.style;
|
|
3067
|
+
const left = box.x + options.originX;
|
|
3068
|
+
const top = frameY(box) + options.originY;
|
|
3069
|
+
const x = Math.round(left);
|
|
3070
|
+
const y = Math.round(top);
|
|
3071
|
+
const w = Math.round(left + box.width) - x;
|
|
3072
|
+
const h = Math.round(top + frameHeight(box)) - y;
|
|
3073
|
+
if (w <= 0 || h <= 0)
|
|
3074
|
+
return false;
|
|
3075
|
+
const area = clampRect(options, x, y, w, h);
|
|
3076
|
+
if (!area)
|
|
3077
|
+
return false;
|
|
3078
|
+
const corners = cornersOf(s, w, h);
|
|
3079
|
+
ctx.save();
|
|
3080
|
+
ctx.beginPath();
|
|
3081
|
+
if (corners && canCurve(ctx) && solidBorder(box)) {
|
|
3082
|
+
roundedRect(ctx, area.x, area.y, area.w, area.h, corners, true);
|
|
3083
|
+
const iw = area.w - l - r;
|
|
3084
|
+
const ih = area.h - t - b;
|
|
3085
|
+
if (iw > 0 && ih > 0) {
|
|
3086
|
+
const inner = insetCorners(corners, t, r, b, l);
|
|
3087
|
+
roundedRect(ctx, area.x + l, area.y + t, iw, ih, inner, true, true);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
else if (sculpted(s)) {
|
|
3091
|
+
// a band a side, which the trapezoids cover between them
|
|
3092
|
+
ctx.rect(area.x, area.y, area.w, t);
|
|
3093
|
+
ctx.rect(area.x, area.y + area.h - b, area.w, b);
|
|
3094
|
+
ctx.rect(area.x, area.y + t, l, area.h - t - b);
|
|
3095
|
+
ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
|
|
3096
|
+
}
|
|
3097
|
+
else {
|
|
3098
|
+
const path = { fillRect: ctx.rect.bind(ctx) };
|
|
3099
|
+
const edge = (ex, ey, ew, eh, style, horizontal) => {
|
|
3100
|
+
const rect = clampRect(options, ex, ey, ew, eh);
|
|
3101
|
+
if (rect)
|
|
3102
|
+
fillEdge(path, rect, horizontal ? ex : ey, style, horizontal);
|
|
3103
|
+
};
|
|
3104
|
+
const between = h - t - b;
|
|
3105
|
+
if (t > 0)
|
|
3106
|
+
edge(x, y, w, t, s.borderTopStyle, true);
|
|
3107
|
+
if (b > 0)
|
|
3108
|
+
edge(x, y + h - b, w, b, s.borderBottomStyle, true);
|
|
3109
|
+
if (l > 0)
|
|
3110
|
+
edge(x, y + t, l, between, s.borderLeftStyle, false);
|
|
3111
|
+
if (r > 0)
|
|
3112
|
+
edge(x + w - r, y + t, r, between, s.borderRightStyle, false);
|
|
3113
|
+
}
|
|
3114
|
+
ctx.clip();
|
|
3115
|
+
return true;
|
|
3116
|
+
}
|
|
3117
|
+
/** Whether every side a box has a border on is solid, which a rounded box
|
|
3118
|
+
* draws as one ring (`roundedRing`). */
|
|
3119
|
+
function solidBorder(box) {
|
|
3120
|
+
const s = box.style;
|
|
3121
|
+
return ((!box.borderTop || s.borderTopStyle === 'solid') &&
|
|
3122
|
+
(!box.borderRight || s.borderRightStyle === 'solid') &&
|
|
3123
|
+
(!box.borderBottom || s.borderBottomStyle === 'solid') &&
|
|
3124
|
+
(!box.borderLeft || s.borderLeftStyle === 'solid'));
|
|
3125
|
+
}
|
|
2824
3126
|
/** The border styles drawn in two shades, as though lit from the top left. */
|
|
2825
3127
|
const SCULPTED = new Set(['groove', 'ridge', 'inset', 'outset']);
|
|
2826
3128
|
function sculpted(s) {
|
|
@@ -3215,6 +3517,20 @@ function paintLines(ctx, box, options) {
|
|
|
3215
3517
|
if (line.y + line.height > top)
|
|
3216
3518
|
visible.push(line);
|
|
3217
3519
|
}
|
|
3520
|
+
// and a line the damage misses whose text or inline-block `position:
|
|
3521
|
+
// relative` moved into it: drawn by its line, it went undrawn with it
|
|
3522
|
+
const moved = MOVED_OFF_LINES.get(lines);
|
|
3523
|
+
if (moved && reachedOff(visible, moved, top, bottom)) {
|
|
3524
|
+
const drawn = new Set(visible);
|
|
3525
|
+
for (const line of moved) {
|
|
3526
|
+
if (movedInto(line, top, bottom))
|
|
3527
|
+
drawn.add(line);
|
|
3528
|
+
}
|
|
3529
|
+
visible.length = 0;
|
|
3530
|
+
for (const line of lines)
|
|
3531
|
+
if (drawn.has(line))
|
|
3532
|
+
visible.push(line);
|
|
3533
|
+
}
|
|
3218
3534
|
}
|
|
3219
3535
|
else {
|
|
3220
3536
|
visible.push(...lines);
|
|
@@ -3225,7 +3541,7 @@ function paintLines(ctx, box, options) {
|
|
|
3225
3541
|
for (const line of visible) {
|
|
3226
3542
|
if (line.background)
|
|
3227
3543
|
paintLineBackground(ctx, line, options);
|
|
3228
|
-
paintInlineBoxes(ctx, line, options, line === lines[0] ? null : bleeds);
|
|
3544
|
+
paintInlineBoxes(ctx, line, lines, options, line === lines[0] ? null : bleeds);
|
|
3229
3545
|
for (const text of line.texts) {
|
|
3230
3546
|
const natural = text.layout.lines[text.layoutLine];
|
|
3231
3547
|
if (natural)
|
|
@@ -3276,6 +3592,35 @@ function paintLines(ctx, box, options) {
|
|
|
3276
3592
|
}
|
|
3277
3593
|
}
|
|
3278
3594
|
}
|
|
3595
|
+
/** Whether any of the lines moved content was taken off, and the damage
|
|
3596
|
+
* does not already draw, has it within the damage's rows. */
|
|
3597
|
+
function reachedOff(visible, moved, top, bottom) {
|
|
3598
|
+
for (const line of moved) {
|
|
3599
|
+
if (!visible.includes(line) && movedInto(line, top, bottom))
|
|
3600
|
+
return true;
|
|
3601
|
+
}
|
|
3602
|
+
return false;
|
|
3603
|
+
}
|
|
3604
|
+
/** Whether what was moved off a line — its inline-blocks and images, where
|
|
3605
|
+
* their ink is, and its texts, where they are drawn — reaches the rows
|
|
3606
|
+
* between `top` and `bottom`. */
|
|
3607
|
+
function movedInto(line, top, bottom) {
|
|
3608
|
+
for (const placed of line.atomics) {
|
|
3609
|
+
const box = placed.box;
|
|
3610
|
+
if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
|
|
3611
|
+
return true;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
for (const text of line.texts) {
|
|
3615
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
3616
|
+
if (!natural)
|
|
3617
|
+
continue;
|
|
3618
|
+
const y = text.drawY + natural.y;
|
|
3619
|
+
if (y < bottom && y + natural.height > top)
|
|
3620
|
+
return true;
|
|
3621
|
+
}
|
|
3622
|
+
return false;
|
|
3623
|
+
}
|
|
3279
3624
|
/**
|
|
3280
3625
|
* The part of each such fragment above its line, drawn again over the
|
|
3281
3626
|
* text there. CSS 2.1 Appendix E paints a block's inline content a line at
|
|
@@ -3297,7 +3642,7 @@ function paintBleeds(ctx, bleeds, options) {
|
|
|
3297
3642
|
ctx.beginPath();
|
|
3298
3643
|
ctx.rect(left, top, Math.ceil(fragment.width) + 2, bottom - top);
|
|
3299
3644
|
ctx.clip();
|
|
3300
|
-
paintLayers(ctx, fragment, options);
|
|
3645
|
+
paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
|
|
3301
3646
|
paintBorders(ctx, fragment, options);
|
|
3302
3647
|
ctx.restore();
|
|
3303
3648
|
}
|
|
@@ -3640,27 +3985,22 @@ function paintLineBackground(ctx, line, options) {
|
|
|
3640
3985
|
style: background.style,
|
|
3641
3986
|
}, options);
|
|
3642
3987
|
}
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
*
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
// Every text on a line is drawn on one baseline (`finishLine`), and an
|
|
3660
|
-
// engine's is from the top of its layout rather than of the line. Taken
|
|
3661
|
-
// where the line has each text: a box `position: relative` moves is
|
|
3662
|
-
// moved by its own offset below, and the boxes around it are not.
|
|
3663
|
-
let baseline = line.y + line.baseline;
|
|
3988
|
+
/**
|
|
3989
|
+
* The fragments of the decorated inline boxes on a line: how far across it
|
|
3990
|
+
* each reaches, its border box, and whether the box opens or closes on it.
|
|
3991
|
+
* Where the line holds text `position: relative` moved, taken where the
|
|
3992
|
+
* text was: a fragment is moved by its own box's offset.
|
|
3993
|
+
*
|
|
3994
|
+
* A box is more than one fragment on a line where bidi reordering puts
|
|
3995
|
+
* what is not the box's between parts of it (CSS 2.1 9.10): a letter of
|
|
3996
|
+
* another element, an atomic, another element's edge. Its parts are one
|
|
3997
|
+
* fragment wherever nothing else comes between them, whatever room is
|
|
3998
|
+
* between them — a space it hangs, a box of its own inside it — so a line
|
|
3999
|
+
* that reads one way has one fragment of each box, as it always had.
|
|
4000
|
+
*/
|
|
4001
|
+
function fragmentsOn(line) {
|
|
4002
|
+
/** What is on the line, where, and which decorated boxes it is in. */
|
|
4003
|
+
const marks = [];
|
|
3664
4004
|
const shifted = SHIFTED_LINES.has(line);
|
|
3665
4005
|
for (const text of line.texts) {
|
|
3666
4006
|
const natural = text.layout.lines[text.layoutLine];
|
|
@@ -3668,8 +4008,6 @@ bleeds = null) {
|
|
|
3668
4008
|
if (!natural || !boxAt)
|
|
3669
4009
|
continue;
|
|
3670
4010
|
const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
|
|
3671
|
-
const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
|
|
3672
|
-
baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
|
|
3673
4011
|
const x = text.drawX - (shift?.x ?? 0) + natural.x;
|
|
3674
4012
|
for (const run of natural.runs) {
|
|
3675
4013
|
const owner = boxAt.call(text.spans, run.start);
|
|
@@ -3681,45 +4019,147 @@ bleeds = null) {
|
|
|
3681
4019
|
const right = Math.min(x + natural.width, x + run.x + run.width);
|
|
3682
4020
|
if (right <= left)
|
|
3683
4021
|
continue;
|
|
3684
|
-
|
|
3685
|
-
|
|
4022
|
+
marks.push({ left, right, boxes: decoratedAncestors(owner) });
|
|
4023
|
+
}
|
|
4024
|
+
// and the spaces `pre-wrap` keeps that the line ends on, which hang
|
|
4025
|
+
// past it and are their box's all the same (`LineText.hung`)
|
|
4026
|
+
for (const space of text.hung ?? []) {
|
|
4027
|
+
const owner = boxAt.call(text.spans, space.at);
|
|
4028
|
+
if (!owner)
|
|
4029
|
+
continue;
|
|
4030
|
+
const left = text.drawX - (shift?.x ?? 0) + space.x;
|
|
4031
|
+
marks.push({
|
|
4032
|
+
left,
|
|
4033
|
+
right: left + space.width,
|
|
4034
|
+
boxes: decoratedAncestors(owner),
|
|
4035
|
+
});
|
|
3686
4036
|
}
|
|
3687
4037
|
}
|
|
3688
4038
|
for (const placed of line.atomics) {
|
|
3689
4039
|
const atomic = placed.box;
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
4040
|
+
marks.push({
|
|
4041
|
+
left: placed.x - atomic.marginLeft,
|
|
4042
|
+
right: placed.x + atomic.width + atomic.marginRight,
|
|
4043
|
+
boxes: decoratedAncestors(atomic),
|
|
4044
|
+
});
|
|
3693
4045
|
}
|
|
3694
4046
|
for (const edge of line.edges ?? []) {
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
4047
|
+
marks.push({
|
|
4048
|
+
left: edge.x,
|
|
4049
|
+
right: edge.x + edge.width,
|
|
4050
|
+
boxes: decoratedAncestors(edge.box),
|
|
4051
|
+
edge,
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
let fragments = null;
|
|
4055
|
+
// each box's parts, left to right
|
|
4056
|
+
const parts = new Map();
|
|
4057
|
+
for (const mark of marks) {
|
|
4058
|
+
for (const box of mark.boxes) {
|
|
4059
|
+
let list = parts.get(box);
|
|
4060
|
+
if (!list)
|
|
4061
|
+
parts.set(box, (list = []));
|
|
4062
|
+
list.push(mark);
|
|
3705
4063
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
4064
|
+
const own = mark.edge?.box;
|
|
4065
|
+
if (own?.decoration) {
|
|
4066
|
+
let list = parts.get(own);
|
|
4067
|
+
if (!list)
|
|
4068
|
+
parts.set(own, (list = []));
|
|
4069
|
+
list.push(mark);
|
|
3709
4070
|
}
|
|
3710
|
-
const f = fragments.get(box);
|
|
3711
|
-
if (edge.side === 'start')
|
|
3712
|
-
f.start = true;
|
|
3713
|
-
else
|
|
3714
|
-
f.end = true;
|
|
3715
4071
|
}
|
|
4072
|
+
for (const [box, list] of parts) {
|
|
4073
|
+
list.sort((a, b) => a.left - b.left || a.right - b.right);
|
|
4074
|
+
const out = [];
|
|
4075
|
+
let open = null;
|
|
4076
|
+
let reach = -Infinity;
|
|
4077
|
+
for (const mark of list) {
|
|
4078
|
+
// The element's `direction` says which side its start is on, whatever
|
|
4079
|
+
// its text reads as (CSS 2.1 8.6); its own edge's margin is outside
|
|
4080
|
+
// the box, its border and padding inside.
|
|
4081
|
+
let left = mark.left;
|
|
4082
|
+
let right = mark.right;
|
|
4083
|
+
const own = mark.edge?.box === box ? mark.edge : null;
|
|
4084
|
+
if (own) {
|
|
4085
|
+
const onLeft = (own.side === 'start') !== (box.style.direction === 'rtl');
|
|
4086
|
+
if (onLeft)
|
|
4087
|
+
left = right = own.x + box.marginLeft;
|
|
4088
|
+
else
|
|
4089
|
+
left = right = own.x + own.width - box.marginRight;
|
|
4090
|
+
}
|
|
4091
|
+
if (!open || between(marks, box, reach, mark.left)) {
|
|
4092
|
+
open = { left, right, start: false, end: false };
|
|
4093
|
+
out.push(open);
|
|
4094
|
+
}
|
|
4095
|
+
else {
|
|
4096
|
+
open.left = Math.min(open.left, left);
|
|
4097
|
+
open.right = Math.max(open.right, right);
|
|
4098
|
+
}
|
|
4099
|
+
reach = Math.max(reach, mark.right);
|
|
4100
|
+
if (own) {
|
|
4101
|
+
if (own.side === 'start')
|
|
4102
|
+
open.start = true;
|
|
4103
|
+
else
|
|
4104
|
+
open.end = true;
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
(fragments ??= new Map()).set(box, out);
|
|
4108
|
+
}
|
|
4109
|
+
return fragments;
|
|
4110
|
+
}
|
|
4111
|
+
/** Whether anything on a line that is not in a box lies between two of its
|
|
4112
|
+
* parts, from `left` to `right`: what splits it into two fragments. */
|
|
4113
|
+
function between(marks, box, left, right) {
|
|
4114
|
+
if (right - left < 0.5)
|
|
4115
|
+
return false;
|
|
4116
|
+
for (const mark of marks) {
|
|
4117
|
+
if (mark.right - mark.left < 0.5)
|
|
4118
|
+
continue;
|
|
4119
|
+
if (mark.boxes.includes(box) || mark.edge?.box === box)
|
|
4120
|
+
continue;
|
|
4121
|
+
const middle = (mark.left + mark.right) / 2;
|
|
4122
|
+
if (middle > left && middle < right)
|
|
4123
|
+
return true;
|
|
4124
|
+
}
|
|
4125
|
+
return false;
|
|
4126
|
+
}
|
|
4127
|
+
function paintInlineBoxes(ctx, line,
|
|
4128
|
+
/** The block's lines, which `line` is one of. */
|
|
4129
|
+
lines, options,
|
|
4130
|
+
/** Where a fragment that reaches up over the lines before goes, to be
|
|
4131
|
+
* drawn again over their text; null for a block's first line. */
|
|
4132
|
+
bleeds = null) {
|
|
4133
|
+
const fragments = fragmentsOn(line);
|
|
3716
4134
|
if (!fragments)
|
|
3717
4135
|
return;
|
|
4136
|
+
// Every text on a line is drawn on one baseline (`finishLine`), and an
|
|
4137
|
+
// engine's is from the top of its layout rather than of the line. Taken
|
|
4138
|
+
// where the line has each text: a box `position: relative` moves is
|
|
4139
|
+
// moved by its own offset below, and the boxes around it are not.
|
|
4140
|
+
let baseline = line.y + line.baseline;
|
|
4141
|
+
const shifted = SHIFTED_LINES.has(line);
|
|
4142
|
+
for (const text of line.texts) {
|
|
4143
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
4144
|
+
if (!natural || !text.spans.boxAt)
|
|
4145
|
+
continue;
|
|
4146
|
+
const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
|
|
4147
|
+
const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
|
|
4148
|
+
baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
|
|
4149
|
+
}
|
|
3718
4150
|
const boxes = [...fragments.keys()].sort((a, b) => depthOf(a) - depthOf(b));
|
|
3719
4151
|
for (const box of boxes) {
|
|
3720
4152
|
if (box.style.visibility !== 'visible')
|
|
3721
4153
|
continue;
|
|
3722
|
-
const f
|
|
4154
|
+
for (const f of fragments.get(box)) {
|
|
4155
|
+
paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
/** One fragment of an inline box on a line (`paintInlineBoxes`). */
|
|
4160
|
+
function paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline) {
|
|
4161
|
+
const shifted = SHIFTED_LINES.has(line);
|
|
4162
|
+
{
|
|
3723
4163
|
const face = box.decoration;
|
|
3724
4164
|
// on its own baseline, which `vertical-align` may raise off the line's
|
|
3725
4165
|
const own = shifted
|
|
@@ -3754,8 +4194,16 @@ bleeds = null) {
|
|
|
3754
4194
|
style,
|
|
3755
4195
|
};
|
|
3756
4196
|
if (fragment.width <= 0)
|
|
3757
|
-
|
|
3758
|
-
|
|
4197
|
+
return;
|
|
4198
|
+
// its images and gradients from the strip its fragments make, where it
|
|
4199
|
+
// goes on to another line, and from its own padding box where it is
|
|
4200
|
+
// on this one alone or `clone` says each fragment is its own
|
|
4201
|
+
if (!(f.start && f.end) && sliced(box.style)) {
|
|
4202
|
+
const strip = stripFor(lines, line, box, fragment);
|
|
4203
|
+
if (strip)
|
|
4204
|
+
fragment.strip = strip;
|
|
4205
|
+
}
|
|
4206
|
+
paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
|
|
3759
4207
|
paintBorders(ctx, fragment, options);
|
|
3760
4208
|
if (box.style.outlineStyle !== 'none')
|
|
3761
4209
|
paintOutline(ctx, fragment, options);
|
|
@@ -3764,6 +4212,76 @@ bleeds = null) {
|
|
|
3764
4212
|
}
|
|
3765
4213
|
}
|
|
3766
4214
|
}
|
|
4215
|
+
/** Whether an inline box places background images or gradients, which
|
|
4216
|
+
* `slice` places across its fragments, as one box end to end. */
|
|
4217
|
+
function sliced(style) {
|
|
4218
|
+
return (style.boxDecorationBreak === 'slice' &&
|
|
4219
|
+
(style.backgroundImage !== null ||
|
|
4220
|
+
style.backgroundGradient !== null ||
|
|
4221
|
+
style.backgroundImages !== null));
|
|
4222
|
+
}
|
|
4223
|
+
/** Of a block's lines, by the array they are, the strip of each inline box
|
|
4224
|
+
* on them that `sliced` says places images across its fragments: made
|
|
4225
|
+
* once a layout, as `MOVED_OFF_LINES` is, so a repaint of one line reads
|
|
4226
|
+
* the lines before it without going over them again. What `sliced` reads
|
|
4227
|
+
* is never restyled in place (`HtmlViewNode`), which lays the lines out
|
|
4228
|
+
* again instead. */
|
|
4229
|
+
const STRIPS = new WeakMap();
|
|
4230
|
+
function stripsOf(lines) {
|
|
4231
|
+
const strips = new Map();
|
|
4232
|
+
for (const line of lines) {
|
|
4233
|
+
const fragments = fragmentsOn(line);
|
|
4234
|
+
if (!fragments)
|
|
4235
|
+
continue;
|
|
4236
|
+
for (const [box, list] of fragments) {
|
|
4237
|
+
if (!sliced(box.style))
|
|
4238
|
+
continue;
|
|
4239
|
+
let strip = strips.get(box);
|
|
4240
|
+
if (!strip) {
|
|
4241
|
+
strip = { width: 0, lines: 0, before: new Map() };
|
|
4242
|
+
strips.set(box, strip);
|
|
4243
|
+
}
|
|
4244
|
+
strip.before.set(line, strip.width);
|
|
4245
|
+
for (const f of list)
|
|
4246
|
+
strip.width += Math.max(0, f.right - f.left);
|
|
4247
|
+
strip.lines += 1;
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
return strips;
|
|
4251
|
+
}
|
|
4252
|
+
/**
|
|
4253
|
+
* The box an inline box's fragment on `line` is a slice of (CSS
|
|
4254
|
+
* Fragmentation 3, 5.4): the box's fragments laid end to end in its
|
|
4255
|
+
* direction, as though it had not wrapped — as wide as
|
|
4256
|
+
* they are together, with the box's own borders and padding at its two
|
|
4257
|
+
* ends, placed so that this fragment is where it falls along it. So an
|
|
4258
|
+
* image `no-repeat` at the start is on the first fragment alone, and a
|
|
4259
|
+
* gradient runs once across them all. Undefined where the box is on one
|
|
4260
|
+
* line.
|
|
4261
|
+
*/
|
|
4262
|
+
function stripFor(lines, line, box, fragment) {
|
|
4263
|
+
let strips = STRIPS.get(lines);
|
|
4264
|
+
if (!strips) {
|
|
4265
|
+
strips = stripsOf(lines);
|
|
4266
|
+
STRIPS.set(lines, strips);
|
|
4267
|
+
}
|
|
4268
|
+
const strip = strips.get(box);
|
|
4269
|
+
const before = strip?.before.get(line);
|
|
4270
|
+
if (!strip || strip.lines < 2 || before === undefined)
|
|
4271
|
+
return undefined;
|
|
4272
|
+
const x = box.style.direction === 'rtl'
|
|
4273
|
+
? fragment.x + fragment.width + before - strip.width
|
|
4274
|
+
: fragment.x - before;
|
|
4275
|
+
return {
|
|
4276
|
+
...fragment,
|
|
4277
|
+
x,
|
|
4278
|
+
width: strip.width,
|
|
4279
|
+
borderLeft: box.borderLeft,
|
|
4280
|
+
borderRight: box.borderRight,
|
|
4281
|
+
padLeft: box.padLeft,
|
|
4282
|
+
padRight: box.padRight,
|
|
4283
|
+
};
|
|
4284
|
+
}
|
|
3767
4285
|
/** An inline box's style on a line it does not both start and end on: no
|
|
3768
4286
|
* rounded corner on a side it goes on from. Made once a style and a pair
|
|
3769
4287
|
* of ends, so a repaint copies no style and the layers made of it are
|
|
@@ -3824,6 +4342,20 @@ function decoratedAncestors(box) {
|
|
|
3824
4342
|
DECORATED.set(box, found);
|
|
3825
4343
|
return found;
|
|
3826
4344
|
}
|
|
4345
|
+
/** A restyle in place (a `:hover`) gave an inline box a decoration where it
|
|
4346
|
+
* had none, or took it away, and the lists kept above do not know: every
|
|
4347
|
+
* box whose list could name it — inside it, through the inline boxes the
|
|
4348
|
+
* walk above climbs — looks again. A document built again has new boxes,
|
|
4349
|
+
* and so no lists. */
|
|
4350
|
+
export function forgetDecoratedAncestors(box) {
|
|
4351
|
+
const stack = [...box.children];
|
|
4352
|
+
while (stack.length) {
|
|
4353
|
+
const at = stack.pop();
|
|
4354
|
+
DECORATED.delete(at);
|
|
4355
|
+
if (at.kind === 'inline')
|
|
4356
|
+
stack.push(...at.children);
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
3827
4359
|
/**
|
|
3828
4360
|
* The band the document selection covers on one line.
|
|
3829
4361
|
*
|