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