@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/src/html/paint.ts
CHANGED
|
@@ -30,25 +30,27 @@ import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
|
|
|
30
30
|
import { blend, borderShades } from './css/color.js';
|
|
31
31
|
import type { Len } from './css/values.js';
|
|
32
32
|
import type {
|
|
33
|
+
BackgroundClip,
|
|
33
34
|
BackgroundRepeat,
|
|
34
35
|
BoxShadow,
|
|
35
36
|
ComputedStyle,
|
|
36
37
|
ImageRepeat,
|
|
37
38
|
LinearGradient,
|
|
38
39
|
RepeatMode,
|
|
39
|
-
VisualBox,
|
|
40
40
|
} from './css/style.js';
|
|
41
41
|
import {
|
|
42
42
|
CONTAIN_LAYOUT,
|
|
43
43
|
CONTAIN_PAINT,
|
|
44
44
|
CONTAIN_SIZE,
|
|
45
45
|
copyStyle,
|
|
46
|
+
masked,
|
|
46
47
|
scrolls,
|
|
47
48
|
} from './css/style.js';
|
|
48
49
|
import { contained } from './layout/block.js';
|
|
49
50
|
import {
|
|
50
51
|
BOX_RAISES,
|
|
51
52
|
LINE_BOX_RAISES,
|
|
53
|
+
PADDED_FACES,
|
|
52
54
|
Box,
|
|
53
55
|
CLAMPED,
|
|
54
56
|
CLIPPED_CELLS,
|
|
@@ -56,12 +58,19 @@ import {
|
|
|
56
58
|
FADED_BLOCKS,
|
|
57
59
|
PAINT_ORDER,
|
|
58
60
|
INLINE_OFFSETS,
|
|
61
|
+
MOVED_OFF_LINES,
|
|
59
62
|
SHADOWED_TEXT,
|
|
60
63
|
SHIFTED_LINES,
|
|
61
64
|
TEXT_RAISES,
|
|
62
65
|
TEXT_SHIFTS,
|
|
63
66
|
} from './layout/boxes.js';
|
|
64
|
-
import type {
|
|
67
|
+
import type {
|
|
68
|
+
BoxTree,
|
|
69
|
+
EdgePlacement,
|
|
70
|
+
LineBox,
|
|
71
|
+
LineText,
|
|
72
|
+
Marker,
|
|
73
|
+
} from './layout/boxes.js';
|
|
65
74
|
import {
|
|
66
75
|
CLIPPED_TEXT,
|
|
67
76
|
clipBoxOf,
|
|
@@ -88,7 +97,13 @@ export interface Rect {
|
|
|
88
97
|
export interface PaintContext extends FillContext {
|
|
89
98
|
beginPath?(): void;
|
|
90
99
|
rect?(x: number, y: number, w: number, h: number): void;
|
|
91
|
-
roundRect?(
|
|
100
|
+
roundRect?(
|
|
101
|
+
x: number,
|
|
102
|
+
y: number,
|
|
103
|
+
w: number,
|
|
104
|
+
h: number,
|
|
105
|
+
radii: (number | { x: number; y: number })[],
|
|
106
|
+
): void;
|
|
92
107
|
moveTo?(x: number, y: number): void;
|
|
93
108
|
lineTo?(x: number, y: number): void;
|
|
94
109
|
bezierCurveTo?(
|
|
@@ -119,6 +134,15 @@ export interface PaintContext extends FillContext {
|
|
|
119
134
|
translate?(x: number, y: number): void;
|
|
120
135
|
/** What every drawing is multiplied by: an element's `opacity`. */
|
|
121
136
|
globalAlpha?: number;
|
|
137
|
+
/** How a drawing meets what is under it; a value a backend does not
|
|
138
|
+
* have does not stick, and reads back as the one before. */
|
|
139
|
+
globalCompositeOperation?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** An offscreen surface, as painting uses one. */
|
|
143
|
+
export interface Offscreen {
|
|
144
|
+
getContext(kind: '2d'): unknown;
|
|
145
|
+
destroy?(): void;
|
|
122
146
|
}
|
|
123
147
|
|
|
124
148
|
export interface PaintOptions {
|
|
@@ -141,6 +165,18 @@ export interface PaintOptions {
|
|
|
141
165
|
/** The whole element in window coordinates: the canvas the root's
|
|
142
166
|
* background covers (CSS 2.1 14.2). Absent, the root box is it. */
|
|
143
167
|
canvas?: Rect;
|
|
168
|
+
/** The viewport the document is seen through, in window coordinates:
|
|
169
|
+
* the scroll pane's, where one scrolls the element. What a fixed
|
|
170
|
+
* background is placed against (CSS 2.1 14.2.1) and where a fixed box
|
|
171
|
+
* is drawn (9.6.1), both laid out against the viewport at the
|
|
172
|
+
* document's top. Absent, the element is it. */
|
|
173
|
+
viewport?: Rect;
|
|
174
|
+
/** @internal Painting a fixed box already moved to the viewport. */
|
|
175
|
+
atViewport?: boolean;
|
|
176
|
+
/** A transparent surface `width` by `height` to draw on and composite
|
|
177
|
+
* with `drawImage`, the caller's to `destroy` once it has: a masked
|
|
178
|
+
* element is drawn on one. Null where there is no surface to be had. */
|
|
179
|
+
surface?(width: number, height: number): Offscreen | null;
|
|
144
180
|
/** A drawing made once for its key on a surface `width` by `height` and
|
|
145
181
|
* kept, to be drawn with `drawImage`: a blurred shadow, whose blur is
|
|
146
182
|
* the cost. Null where there is no surface to be had. */
|
|
@@ -166,6 +202,14 @@ export interface PaintOptions {
|
|
|
166
202
|
* document's height — handed up rather than kept on every box, where
|
|
167
203
|
* writing it and reading it back cost this walk a fifth of its time.
|
|
168
204
|
*/
|
|
205
|
+
/**
|
|
206
|
+
* How far down an out-of-flow box's scrollable overflow reaches: its border
|
|
207
|
+
* box, and what it holds where it does not clip it (`computePaintBounds`),
|
|
208
|
+
* which the box it is in does not take in and the document does
|
|
209
|
+
* (`layoutDocument`).
|
|
210
|
+
*/
|
|
211
|
+
export const OUT_OF_FLOW_REACH = new WeakMap<Box, number>();
|
|
212
|
+
|
|
169
213
|
export function computePaintBounds(box: Box, moved = false): number {
|
|
170
214
|
// A box with no rectangle of its own gives only what it holds: nothing,
|
|
171
215
|
// until a child with bounds is met.
|
|
@@ -177,8 +221,12 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
177
221
|
// How far down the content reaches, for the document's height: the
|
|
178
222
|
// border box, and every box and line under it, but not past a box that
|
|
179
223
|
// clips what it holds — where the scrollable overflow ends. Out-of-flow
|
|
180
|
-
// boxes are the layout's to count.
|
|
181
|
-
|
|
224
|
+
// boxes are the layout's to count. A border box of no area reaches
|
|
225
|
+
// nowhere, as a browser has it (Blink's `ScrollableOverflowCalculator`
|
|
226
|
+
// adds no empty rect): the empty blocks a design leaves at its end,
|
|
227
|
+
// below the last one's bottom margin, made the page that margin taller
|
|
228
|
+
const empty = own && !(box.width > 0 && box.height > 0);
|
|
229
|
+
let bottom = empty ? -Infinity : y2;
|
|
182
230
|
// A shadow is ink past the box, and no overflow: a repaint of the strip
|
|
183
231
|
// under a card has to reach the card, and the document is no taller.
|
|
184
232
|
const shadows = own ? box.style.boxShadow : null;
|
|
@@ -200,6 +248,27 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
200
248
|
x2 = Math.max(x2, box.x + box.width + reach);
|
|
201
249
|
y2 = Math.max(y2, box.y + box.height + reach);
|
|
202
250
|
}
|
|
251
|
+
// An inline box's fragments are boxes on the lines they are on, and its
|
|
252
|
+
// padding and border reach above and below those lines: they count in
|
|
253
|
+
// the scrollable overflow of the block it is in (CSS Overflow 3, 2.2) —
|
|
254
|
+
// Blink adds each inline box fragment's border box
|
|
255
|
+
// (`ScrollableOverflowCalculator::AddItemsInternal`). Design 150's footer
|
|
256
|
+
// links, 50px of padding under their text, made Chrome's page 19px
|
|
257
|
+
// taller than the box they end. Only a box with padding or a border
|
|
258
|
+
// above or below its text can reach past its lines.
|
|
259
|
+
if (
|
|
260
|
+
box.kind === 'inline' &&
|
|
261
|
+
box.padTop + box.padBottom + box.borderTop + box.borderBottom > 0
|
|
262
|
+
) {
|
|
263
|
+
const reach = inlineFragmentsReach(box);
|
|
264
|
+
if (reach) {
|
|
265
|
+
x1 = Math.min(x1, reach.x1);
|
|
266
|
+
y1 = Math.min(y1, reach.y1);
|
|
267
|
+
x2 = Math.max(x2, reach.x2);
|
|
268
|
+
y2 = Math.max(y2, reach.y2);
|
|
269
|
+
bottom = Math.max(bottom, reach.y2);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
203
272
|
const lines = box.lines;
|
|
204
273
|
if (lines) {
|
|
205
274
|
let tallest = 0;
|
|
@@ -253,6 +322,7 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
253
322
|
}
|
|
254
323
|
const reach = computePaintBounds(child, moved);
|
|
255
324
|
if (!child.outOfFlow) bottom = Math.max(bottom, reach);
|
|
325
|
+
else OUT_OF_FLOW_REACH.set(child, reach);
|
|
256
326
|
if (child.boundsY === Infinity) continue;
|
|
257
327
|
x1 = Math.min(x1, child.boundsX);
|
|
258
328
|
y1 = Math.min(y1, child.boundsY);
|
|
@@ -296,7 +366,7 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
296
366
|
if (!own) return bottom;
|
|
297
367
|
// whether the box clips only matters where its content reaches past it,
|
|
298
368
|
// and its style is one more object a walk of every box would read
|
|
299
|
-
const end = box.y + box.height;
|
|
369
|
+
const end = empty ? -Infinity : box.y + box.height;
|
|
300
370
|
if (bottom <= end) return end;
|
|
301
371
|
const style = box.style;
|
|
302
372
|
return box.parent &&
|
|
@@ -321,6 +391,55 @@ export function hasRect(box: Box): boolean {
|
|
|
321
391
|
return display !== 'table-column' && display !== 'table-column-group';
|
|
322
392
|
}
|
|
323
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Where an inline box's fragments are, over the lines of the block it is
|
|
396
|
+
* in that hold its text: from its face's ascent and its top padding and
|
|
397
|
+
* border above each line's baseline to its descent and its bottom ones
|
|
398
|
+
* below, as `paintInlineBoxes` draws them. Null for a box with no text or
|
|
399
|
+
* no face.
|
|
400
|
+
*/
|
|
401
|
+
function inlineFragmentsReach(
|
|
402
|
+
box: Box,
|
|
403
|
+
): { x1: number; y1: number; x2: number; y2: number } | null {
|
|
404
|
+
const face = box.decoration ?? PADDED_FACES.get(box);
|
|
405
|
+
if (!face || box.subtreeTextEnd <= box.subtreeTextStart) return null;
|
|
406
|
+
let block = box.parent;
|
|
407
|
+
while (block && block.kind === 'inline') block = block.parent;
|
|
408
|
+
const lines = block?.lines;
|
|
409
|
+
if (!lines?.length) return null;
|
|
410
|
+
const from = box.subtreeTextStart;
|
|
411
|
+
const to = box.subtreeTextEnd;
|
|
412
|
+
let lo = 0;
|
|
413
|
+
let hi = lines.length;
|
|
414
|
+
while (lo < hi) {
|
|
415
|
+
const mid = (lo + hi) >> 1;
|
|
416
|
+
if (lines[mid].textEnd > from) hi = mid;
|
|
417
|
+
else lo = mid + 1;
|
|
418
|
+
}
|
|
419
|
+
const moved = offsetOf(box);
|
|
420
|
+
let x1 = Infinity;
|
|
421
|
+
let y1 = Infinity;
|
|
422
|
+
let x2 = -Infinity;
|
|
423
|
+
let y2 = -Infinity;
|
|
424
|
+
for (let i = lo; i < lines.length; i += 1) {
|
|
425
|
+
const line = lines[i];
|
|
426
|
+
if (line.textStart >= to) break;
|
|
427
|
+
if (line.textEnd <= from) continue;
|
|
428
|
+
const raise = SHIFTED_LINES.has(line)
|
|
429
|
+
? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
|
|
430
|
+
: 0;
|
|
431
|
+
const baseline = line.y + line.baseline - raise + (moved?.y ?? 0);
|
|
432
|
+
x1 = Math.min(x1, line.x);
|
|
433
|
+
x2 = Math.max(x2, line.x + line.width);
|
|
434
|
+
y1 = Math.min(y1, baseline - face.ascent - box.padTop - box.borderTop);
|
|
435
|
+
y2 = Math.max(
|
|
436
|
+
y2,
|
|
437
|
+
baseline + face.descent + box.padBottom + box.borderBottom,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return x1 === Infinity ? null : { x1, y1, x2, y2 };
|
|
441
|
+
}
|
|
442
|
+
|
|
324
443
|
/** Children lists past this size get the sorted viewport index; below it a
|
|
325
444
|
* linear scan is cheaper than keeping one. */
|
|
326
445
|
const PAINT_INDEX_MIN = 64;
|
|
@@ -401,7 +520,7 @@ export function paintDocument(
|
|
|
401
520
|
): void {
|
|
402
521
|
if (!canFill(ctx)) return;
|
|
403
522
|
ctx.save();
|
|
404
|
-
const canvas = canvasBackground(tree
|
|
523
|
+
const canvas = canvasBackground(tree);
|
|
405
524
|
if (canvas) paintCanvas(ctx, canvas, tree.root, options);
|
|
406
525
|
paintBox(ctx, tree.root, {
|
|
407
526
|
...options,
|
|
@@ -411,27 +530,51 @@ export function paintDocument(
|
|
|
411
530
|
ctx.restore();
|
|
412
531
|
}
|
|
413
532
|
|
|
533
|
+
/** What covers the canvas: a style's background, the box that would have
|
|
534
|
+
* painted it and now paints none, and the box its image is placed by. */
|
|
535
|
+
interface CanvasBackground {
|
|
536
|
+
style: ComputedStyle;
|
|
537
|
+
source: Box | null;
|
|
538
|
+
anchor: Box;
|
|
539
|
+
}
|
|
540
|
+
|
|
414
541
|
/**
|
|
415
542
|
* Whose background covers the canvas (CSS 2.1 14.2): the root element's,
|
|
416
543
|
* or — where `<html>` has neither a colour nor an image — the first
|
|
417
544
|
* `<body>`'s, which then paints no background of its own. A document with
|
|
418
545
|
* no `<html>` tag has the root box standing in for its root element, and a
|
|
419
546
|
* `<body>` in it is still the body: mail often starts at `<body style>`.
|
|
420
|
-
*
|
|
421
|
-
*
|
|
547
|
+
* With neither tag, the root box stands in for the body, and the `<html>`
|
|
548
|
+
* around it is implied: an `html { … }` or a `:root { … }` background, which
|
|
549
|
+
* a test's reference sets on a document with no tags at all, has no box
|
|
550
|
+
* but the canvas to be painted on. `anchor` is the box the image is
|
|
551
|
+
* positioned against: the root element's, whichever box it came from.
|
|
422
552
|
*/
|
|
423
|
-
function canvasBackground(
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
!!
|
|
428
|
-
!!
|
|
553
|
+
function canvasBackground(tree: BoxTree): CanvasBackground | null {
|
|
554
|
+
const root = tree.root;
|
|
555
|
+
const has = (s: ComputedStyle) =>
|
|
556
|
+
!isTransparent(s.backgroundColor) ||
|
|
557
|
+
!!s.backgroundImage ||
|
|
558
|
+
!!s.backgroundGradient ||
|
|
559
|
+
!!s.backgroundImages;
|
|
560
|
+
const implied = tree.impliedHtml;
|
|
561
|
+
if (implied) {
|
|
562
|
+
// an `<html>` that is not displayed has no background to give
|
|
563
|
+
if (implied.display === 'none') return null;
|
|
564
|
+
if (has(implied)) return { style: implied, source: null, anchor: root };
|
|
565
|
+
// containment on either keeps the body's to the body (CSS Containment 2)
|
|
566
|
+
if (implied.contain || root.style.contain) return null;
|
|
567
|
+
return has(root.style)
|
|
568
|
+
? { style: root.style, source: root, anchor: root }
|
|
569
|
+
: null;
|
|
570
|
+
}
|
|
429
571
|
const top = childNamed(root, 'html') ?? root;
|
|
430
|
-
if (has(top)) return { source: top, anchor: top };
|
|
572
|
+
if (has(top.style)) return { style: top.style, source: top, anchor: top };
|
|
431
573
|
const body = childNamed(top, 'body');
|
|
432
|
-
// containment on either keeps the body's to the body (CSS Containment 2)
|
|
433
574
|
if (!body || top.style.contain || body.style.contain) return null;
|
|
434
|
-
return has(body)
|
|
575
|
+
return has(body.style)
|
|
576
|
+
? { style: body.style, source: body, anchor: top }
|
|
577
|
+
: null;
|
|
435
578
|
}
|
|
436
579
|
|
|
437
580
|
/** A box's child element of that name, or one inside the anonymous boxes
|
|
@@ -450,7 +593,7 @@ function childNamed(box: Box, name: string): Box | null {
|
|
|
450
593
|
|
|
451
594
|
function paintCanvas(
|
|
452
595
|
ctx: PaintContext,
|
|
453
|
-
{ source, anchor }:
|
|
596
|
+
{ style: source, anchor }: CanvasBackground,
|
|
454
597
|
root: Box,
|
|
455
598
|
options: PaintOptions,
|
|
456
599
|
): void {
|
|
@@ -468,8 +611,8 @@ function paintCanvas(
|
|
|
468
611
|
Math.ceil(whole.height),
|
|
469
612
|
);
|
|
470
613
|
if (!area) return;
|
|
471
|
-
const layers = layersOf(source
|
|
472
|
-
const visible = source.
|
|
614
|
+
const layers = layersOf(source) ?? [source];
|
|
615
|
+
const visible = source.visibility === 'visible';
|
|
473
616
|
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
474
617
|
const style = layers[i];
|
|
475
618
|
if (!isTransparent(style.backgroundColor) && visible) {
|
|
@@ -482,7 +625,7 @@ function paintCanvas(
|
|
|
482
625
|
// body shows — or by the viewport, where it is fixed
|
|
483
626
|
const box =
|
|
484
627
|
style.backgroundAttachment === 'fixed' && options.canvas
|
|
485
|
-
? options.canvas
|
|
628
|
+
? (options.viewport ?? options.canvas)
|
|
486
629
|
: originBox(anchor, options, style);
|
|
487
630
|
paintGradient(
|
|
488
631
|
ctx,
|
|
@@ -568,20 +711,37 @@ function paintLayers(
|
|
|
568
711
|
image?: (layer: ComputedStyle) => void,
|
|
569
712
|
): void {
|
|
570
713
|
// a background painted through the text is painted with it
|
|
571
|
-
// (`paintClippedText`), and not as a box
|
|
572
|
-
|
|
573
|
-
const
|
|
714
|
+
// (`paintClippedText`), and not as a box — but for what `border-area
|
|
715
|
+
// text` paints in the border as well
|
|
716
|
+
const style = box.style;
|
|
717
|
+
if (style.backgroundClipText && style.backgroundClip !== 'border-area') {
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
const layers = layersOf(style);
|
|
574
721
|
if (!layers) {
|
|
575
|
-
|
|
576
|
-
if (image && box.style.backgroundImage) image(box.style);
|
|
722
|
+
paintLayer(ctx, box, options, style, image);
|
|
577
723
|
return;
|
|
578
724
|
}
|
|
579
725
|
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
580
|
-
|
|
581
|
-
if (image && layers[i].backgroundImage) image(layers[i]);
|
|
726
|
+
paintLayer(ctx, box, options, layers[i], image);
|
|
582
727
|
}
|
|
583
728
|
}
|
|
584
729
|
|
|
730
|
+
function paintLayer(
|
|
731
|
+
ctx: PaintContext,
|
|
732
|
+
box: Frame,
|
|
733
|
+
options: PaintOptions,
|
|
734
|
+
layer: ComputedStyle,
|
|
735
|
+
image?: (layer: ComputedStyle) => void,
|
|
736
|
+
): void {
|
|
737
|
+
// painted over the border box, inside what the border paints
|
|
738
|
+
const area = layer.backgroundClip === 'border-area';
|
|
739
|
+
if (area && !pushBorderArea(ctx, box, options)) return;
|
|
740
|
+
paintBackground(ctx, box, options, layer);
|
|
741
|
+
if (image && layer.backgroundImage) image(layer);
|
|
742
|
+
if (area) ctx.restore();
|
|
743
|
+
}
|
|
744
|
+
|
|
585
745
|
/** A box's padding box in window coordinates. */
|
|
586
746
|
function paddingBox(box: Box, options: PaintOptions): Rect {
|
|
587
747
|
return {
|
|
@@ -593,12 +753,14 @@ function paddingBox(box: Box, options: PaintOptions): Rect {
|
|
|
593
753
|
}
|
|
594
754
|
|
|
595
755
|
/** Where a layer of a box's background is placed, in window coordinates:
|
|
596
|
-
* the box its `background-origin` names (CSS Backgrounds 3, 3.8)
|
|
756
|
+
* the box its `background-origin` names (CSS Backgrounds 3, 3.8) — of the
|
|
757
|
+
* whole strip, for a fragment of an inline box that is sliced. */
|
|
597
758
|
function originBox(
|
|
598
|
-
|
|
759
|
+
frame: Frame,
|
|
599
760
|
options: PaintOptions,
|
|
600
761
|
style: ComputedStyle,
|
|
601
762
|
): Rect {
|
|
763
|
+
const box = frame.strip ?? frame;
|
|
602
764
|
const [t, r, b, l] = edgeInsets(box, style.backgroundOrigin);
|
|
603
765
|
return {
|
|
604
766
|
x: box.x + l + options.originX,
|
|
@@ -676,7 +838,12 @@ type Frame = Pick<
|
|
|
676
838
|
| 'style'
|
|
677
839
|
> &
|
|
678
840
|
// the padding a `content-box` background is inset by, where it has any
|
|
679
|
-
Partial<Pick<Box, 'padTop' | 'padRight' | 'padBottom' | 'padLeft'
|
|
841
|
+
Partial<Pick<Box, 'padTop' | 'padRight' | 'padBottom' | 'padLeft'>> & {
|
|
842
|
+
/** Of an inline box's fragment, where the box goes on to another line:
|
|
843
|
+
* its fragments laid end to end as one box, which its images and
|
|
844
|
+
* gradients are placed in (`box-decoration-break: slice`). */
|
|
845
|
+
strip?: Frame;
|
|
846
|
+
};
|
|
680
847
|
|
|
681
848
|
/** Where a box's background and border go: its border box, which for a
|
|
682
849
|
* table leaves out the captions around it (CSS 2.1 17.4). */
|
|
@@ -710,8 +877,13 @@ function paintBox(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
710
877
|
ctx.save();
|
|
711
878
|
ctx.globalAlpha = ctx.globalAlpha! * opacity;
|
|
712
879
|
}
|
|
880
|
+
if (masked(box.style) && paintMasked(ctx, box, options)) {
|
|
881
|
+
if (fade) ctx.restore();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
713
884
|
// `clip` shows the part of an absolutely positioned box it names, its own
|
|
714
|
-
// background and borders among it (CSS 2.1 11.1.2)
|
|
885
|
+
// background and borders among it (CSS 2.1 11.1.2) — written out here,
|
|
886
|
+
// not called: a frame more a level and a deep document ran out of stack
|
|
715
887
|
const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
|
|
716
888
|
if (!clip || (clip.w > 0 && clip.h > 0)) {
|
|
717
889
|
const clipped = !!clip && pushClip(ctx, clip, null);
|
|
@@ -721,6 +893,145 @@ function paintBox(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
721
893
|
if (fade) ctx.restore();
|
|
722
894
|
}
|
|
723
895
|
|
|
896
|
+
/** A box and what it holds, cut to its `clip` as `paintBox` cuts it. */
|
|
897
|
+
function paintClipped(
|
|
898
|
+
ctx: PaintContext,
|
|
899
|
+
box: Box,
|
|
900
|
+
options: PaintOptions,
|
|
901
|
+
): void {
|
|
902
|
+
const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
|
|
903
|
+
if (clip && !(clip.w > 0 && clip.h > 0)) return;
|
|
904
|
+
const clipped = !!clip && pushClip(ctx, clip, null);
|
|
905
|
+
paintContent(ctx, box, options);
|
|
906
|
+
if (clipped) ctx.restore();
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const MASK_LAYERS = new WeakMap<ComputedStyle, ComputedStyle[]>();
|
|
910
|
+
|
|
911
|
+
/** No corners: a mask layer is placed in the box's rectangle, which its
|
|
912
|
+
* radii do not round (CSS Masking 1, 7). */
|
|
913
|
+
const SQUARE_RADII: ComputedStyle['borderRadius'] = [0, 0, 0, 0];
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* A style for each of a box's mask layers, top first, as `layersOf` makes
|
|
917
|
+
* a background's: the box's own style with the layer's image, repeat, size,
|
|
918
|
+
* position, origin and clip in its background's place, and no colour — so
|
|
919
|
+
* the background painter places and repeats a mask layer as it does a
|
|
920
|
+
* background layer, which is what CSS Masking 1 says a mask layer is (7).
|
|
921
|
+
* Made once a style.
|
|
922
|
+
*/
|
|
923
|
+
function maskLayersOf(style: ComputedStyle): ComputedStyle[] {
|
|
924
|
+
let layers = MASK_LAYERS.get(style);
|
|
925
|
+
if (layers) return layers;
|
|
926
|
+
const mask = style.mask;
|
|
927
|
+
const nth = <T>(list: readonly T[], i: number): T => list[i % list.length];
|
|
928
|
+
layers = mask.images.map((image, i) => {
|
|
929
|
+
const layer = Object.create(style) as ComputedStyle;
|
|
930
|
+
layer.backgroundColor = null;
|
|
931
|
+
layer.backgroundImages = null;
|
|
932
|
+
layer.backgroundImage = typeof image === 'string' ? image : null;
|
|
933
|
+
layer.backgroundGradient =
|
|
934
|
+
image !== null && typeof image !== 'string' ? image : null;
|
|
935
|
+
layer.backgroundRepeat = nth(mask.repeats, i);
|
|
936
|
+
layer.backgroundSize = nth(mask.sizes, i);
|
|
937
|
+
layer.backgroundAttachment = 'scroll';
|
|
938
|
+
[layer.backgroundPositionX, layer.backgroundPositionY] = nth(
|
|
939
|
+
mask.positions,
|
|
940
|
+
i,
|
|
941
|
+
);
|
|
942
|
+
layer.backgroundOrigin = nth(mask.origins, i);
|
|
943
|
+
layer.backgroundClip = nth(mask.clips, i);
|
|
944
|
+
layer.backgroundClipText = false;
|
|
945
|
+
layer.borderRadius = SQUARE_RADII;
|
|
946
|
+
layer.borderRadiusY = null;
|
|
947
|
+
return layer;
|
|
948
|
+
});
|
|
949
|
+
MASK_LAYERS.set(style, layers);
|
|
950
|
+
return layers;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* A masked box (CSS Masking 1, 7): the box and what it holds drawn on a
|
|
955
|
+
* surface of their own, which the alpha of its mask layers — painted as a
|
|
956
|
+
* background's are, on a second surface, each added over the ones below
|
|
957
|
+
* it — then cuts with `destination-in`, and the result drawn in its place,
|
|
958
|
+
* as the group a mask makes. Only the mask painting area, the border box,
|
|
959
|
+
* can show, and only its part in the damage is drawn.
|
|
960
|
+
*
|
|
961
|
+
* A layer whose image has not arrived is transparent black (7.2), so a box
|
|
962
|
+
* none of whose layers can be drawn yet draws nothing: an icon masked out
|
|
963
|
+
* of its `background-color` is not a solid square while its image loads.
|
|
964
|
+
* False where there is no surface to draw on, or no `destination-in` to
|
|
965
|
+
* draw with, and the caller paints the box unmasked.
|
|
966
|
+
*/
|
|
967
|
+
function paintMasked(
|
|
968
|
+
ctx: PaintContext,
|
|
969
|
+
box: Box,
|
|
970
|
+
options: PaintOptions,
|
|
971
|
+
): boolean {
|
|
972
|
+
const layers = maskLayersOf(box.style);
|
|
973
|
+
const drawable = layers.some(
|
|
974
|
+
(layer) =>
|
|
975
|
+
!!layer.backgroundGradient ||
|
|
976
|
+
(!!layer.backgroundImage &&
|
|
977
|
+
!!options.backgroundImageFor?.(layer.backgroundImage)),
|
|
978
|
+
);
|
|
979
|
+
if (!drawable) return true;
|
|
980
|
+
if (!options.surface || !ctx.drawImage) return false;
|
|
981
|
+
let x0 = Math.floor(box.x + options.originX);
|
|
982
|
+
let y0 = Math.floor(frameY(box) + options.originY);
|
|
983
|
+
let x1 = Math.ceil(box.x + box.width + options.originX);
|
|
984
|
+
let y1 = Math.ceil(frameY(box) + frameHeight(box) + options.originY);
|
|
985
|
+
const damage = options.damage;
|
|
986
|
+
if (damage) {
|
|
987
|
+
x0 = Math.max(x0, Math.floor(damage.x));
|
|
988
|
+
y0 = Math.max(y0, Math.floor(damage.y));
|
|
989
|
+
x1 = Math.min(x1, Math.ceil(damage.x + damage.width));
|
|
990
|
+
y1 = Math.min(y1, Math.ceil(damage.y + damage.height));
|
|
991
|
+
}
|
|
992
|
+
const w = x1 - x0;
|
|
993
|
+
const h = y1 - y0;
|
|
994
|
+
if (!(w > 0 && h > 0)) return true;
|
|
995
|
+
const content = options.surface(w, h);
|
|
996
|
+
const mask = content && options.surface(w, h);
|
|
997
|
+
try {
|
|
998
|
+
if (!content || !mask) return false;
|
|
999
|
+
const cctx = content.getContext('2d') as PaintContext;
|
|
1000
|
+
const mctx = mask.getContext('2d') as PaintContext;
|
|
1001
|
+
// the surfaces' own coordinates, the window's moved to their corner
|
|
1002
|
+
const on: PaintOptions = {
|
|
1003
|
+
...options,
|
|
1004
|
+
originX: options.originX - x0,
|
|
1005
|
+
originY: options.originY - y0,
|
|
1006
|
+
damage: { x: 0, y: 0, width: w, height: h },
|
|
1007
|
+
canvas: options.canvas && {
|
|
1008
|
+
...options.canvas,
|
|
1009
|
+
x: options.canvas.x - x0,
|
|
1010
|
+
y: options.canvas.y - y0,
|
|
1011
|
+
},
|
|
1012
|
+
viewport: options.viewport && {
|
|
1013
|
+
...options.viewport,
|
|
1014
|
+
x: options.viewport.x - x0,
|
|
1015
|
+
y: options.viewport.y - y0,
|
|
1016
|
+
},
|
|
1017
|
+
clips: [],
|
|
1018
|
+
};
|
|
1019
|
+
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
1020
|
+
paintLayer(mctx, box, on, layers[i], frameImages(mctx, box, on));
|
|
1021
|
+
}
|
|
1022
|
+
paintClipped(cctx, box, on);
|
|
1023
|
+
cctx.globalCompositeOperation = 'destination-in';
|
|
1024
|
+
if (cctx.globalCompositeOperation !== 'destination-in') return false;
|
|
1025
|
+
cctx.drawImage!(mask, 0, 0);
|
|
1026
|
+
cctx.globalCompositeOperation = 'source-over';
|
|
1027
|
+
ctx.drawImage(content, x0, y0);
|
|
1028
|
+
return true;
|
|
1029
|
+
} finally {
|
|
1030
|
+
content?.destroy?.();
|
|
1031
|
+
mask?.destroy?.();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
724
1035
|
function paintContent(
|
|
725
1036
|
ctx: PaintContext,
|
|
726
1037
|
box: Box,
|
|
@@ -837,22 +1148,7 @@ function paintOwnBackground(
|
|
|
837
1148
|
const style = box.style;
|
|
838
1149
|
if (style.boxShadow) paintShadows(ctx, box, options, false);
|
|
839
1150
|
if (box !== options.canvasSource) {
|
|
840
|
-
paintLayers(ctx, box, options, (
|
|
841
|
-
const painted = clipArea(box, options, layer, true);
|
|
842
|
-
const area =
|
|
843
|
-
painted &&
|
|
844
|
-
clampRect(options, painted.x, painted.y, painted.w, painted.h);
|
|
845
|
-
if (area) {
|
|
846
|
-
paintBackgroundImage(
|
|
847
|
-
ctx,
|
|
848
|
-
layer,
|
|
849
|
-
area,
|
|
850
|
-
originBox(box, options, layer),
|
|
851
|
-
options,
|
|
852
|
-
painted.corners,
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
});
|
|
1151
|
+
paintLayers(ctx, box, options, frameImages(ctx, box, options));
|
|
856
1152
|
}
|
|
857
1153
|
if (style.boxShadow) paintShadows(ctx, box, options, true);
|
|
858
1154
|
if (!box.bordersCollapsed && !paintBorderImage(ctx, box, options)) {
|
|
@@ -861,6 +1157,34 @@ function paintOwnBackground(
|
|
|
861
1157
|
if (box.kind === 'table') paintPartBackgrounds(ctx, box, options);
|
|
862
1158
|
}
|
|
863
1159
|
|
|
1160
|
+
/**
|
|
1161
|
+
* How `paintLayers` draws a frame's image layers: each in the area its
|
|
1162
|
+
* `background-clip` names, from the box its `background-origin` names
|
|
1163
|
+
* (CSS Backgrounds 3, 3.7 and 3.8) — a block's, or an inline box's
|
|
1164
|
+
* fragment's, whose background is its images as much as its colour.
|
|
1165
|
+
*/
|
|
1166
|
+
function frameImages(
|
|
1167
|
+
ctx: PaintContext,
|
|
1168
|
+
frame: Frame,
|
|
1169
|
+
options: PaintOptions,
|
|
1170
|
+
): (layer: ComputedStyle) => void {
|
|
1171
|
+
return (layer) => {
|
|
1172
|
+
const painted = clipArea(frame, options, layer, true);
|
|
1173
|
+
const area =
|
|
1174
|
+
painted && clampRect(options, painted.x, painted.y, painted.w, painted.h);
|
|
1175
|
+
if (area) {
|
|
1176
|
+
paintBackgroundImage(
|
|
1177
|
+
ctx,
|
|
1178
|
+
layer,
|
|
1179
|
+
area,
|
|
1180
|
+
originBox(frame, options, layer),
|
|
1181
|
+
options,
|
|
1182
|
+
painted.corners,
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
|
|
864
1188
|
/** How far past its shape a shadow's blur shows: three standard deviations,
|
|
865
1189
|
* the blur being two (CSS Backgrounds 3, 7.1.1). */
|
|
866
1190
|
function shadowReach(blur: number): number {
|
|
@@ -992,19 +1316,6 @@ function paintShadows(
|
|
|
992
1316
|
);
|
|
993
1317
|
continue;
|
|
994
1318
|
}
|
|
995
|
-
if (
|
|
996
|
-
bakedOuter(
|
|
997
|
-
ctx,
|
|
998
|
-
options,
|
|
999
|
-
s,
|
|
1000
|
-
color,
|
|
1001
|
-
shape,
|
|
1002
|
-
around,
|
|
1003
|
-
covered ? null : { rect: own, corners },
|
|
1004
|
-
)
|
|
1005
|
-
) {
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
1319
|
// what of the shadow falls under the box is not drawn
|
|
1009
1320
|
const clipped = !covered && ctx.clip && ctx.rect && ctx.save;
|
|
1010
1321
|
if (clipped) {
|
|
@@ -1033,7 +1344,7 @@ function paintShadows(
|
|
|
1033
1344
|
s,
|
|
1034
1345
|
color,
|
|
1035
1346
|
(dx) => {
|
|
1036
|
-
|
|
1347
|
+
shadowShape(
|
|
1037
1348
|
ctx,
|
|
1038
1349
|
shape.x - dx,
|
|
1039
1350
|
shape.y,
|
|
@@ -1096,7 +1407,6 @@ function paintShadows(
|
|
|
1096
1407
|
fillRing(ctx, pad, inner, hole, within);
|
|
1097
1408
|
continue;
|
|
1098
1409
|
}
|
|
1099
|
-
if (bakedInset(ctx, options, s, color, pad, inner, hole, within)) continue;
|
|
1100
1410
|
if (!ctx.clip || !ctx.save || !ctx.rect) continue;
|
|
1101
1411
|
ctx.save();
|
|
1102
1412
|
ctx.beginPath();
|
|
@@ -1117,173 +1427,23 @@ function paintShadows(
|
|
|
1117
1427
|
(dx) => {
|
|
1118
1428
|
ctx.rect!(frame.x - dx, frame.y, frame.width, frame.height);
|
|
1119
1429
|
if (hole.width > 0 && hole.height > 0) {
|
|
1120
|
-
|
|
1430
|
+
shadowShape(
|
|
1121
1431
|
ctx,
|
|
1122
1432
|
hole.x - dx,
|
|
1123
1433
|
hole.y,
|
|
1124
1434
|
hole.width,
|
|
1125
1435
|
hole.height,
|
|
1126
1436
|
within,
|
|
1127
|
-
true,
|
|
1128
|
-
true,
|
|
1129
1437
|
);
|
|
1130
1438
|
}
|
|
1131
1439
|
},
|
|
1132
1440
|
frame.x + frame.width + shadowReach(s.blur),
|
|
1441
|
+
'evenodd',
|
|
1133
1442
|
);
|
|
1134
1443
|
ctx.restore();
|
|
1135
1444
|
}
|
|
1136
1445
|
}
|
|
1137
1446
|
|
|
1138
|
-
/** Corners as part of a key. */
|
|
1139
|
-
function cornerKey(c: Corners): string {
|
|
1140
|
-
return `${c.x.join(',')}/${c.y.join(',')}`;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
/**
|
|
1144
|
-
* An outer shadow drawn once on a surface of its own and composited: the
|
|
1145
|
-
* shape's shadow, and where `box` is given the box cut out of it — so a box
|
|
1146
|
-
* that shows what is behind it shows no shadow there, with the clip on the
|
|
1147
|
-
* small surface rather than the window. False where there is no surface.
|
|
1148
|
-
*/
|
|
1149
|
-
function bakedOuter(
|
|
1150
|
-
ctx: PaintContext,
|
|
1151
|
-
options: PaintOptions,
|
|
1152
|
-
s: BoxShadow,
|
|
1153
|
-
color: string,
|
|
1154
|
-
shape: Rect,
|
|
1155
|
-
around: Corners,
|
|
1156
|
-
box: { rect: Rect; corners: Corners } | null,
|
|
1157
|
-
): boolean {
|
|
1158
|
-
if (!options.cached || !ctx.drawImage) return false;
|
|
1159
|
-
const reach = shadowReach(s.blur) + 1;
|
|
1160
|
-
const x0 = Math.floor(shape.x) - reach;
|
|
1161
|
-
const y0 = Math.floor(shape.y) - reach;
|
|
1162
|
-
const w = Math.ceil(shape.x + shape.width) + reach - x0;
|
|
1163
|
-
const h = Math.ceil(shape.y + shape.height) + reach - y0;
|
|
1164
|
-
const sx = shape.x - x0;
|
|
1165
|
-
const sy = shape.y - y0;
|
|
1166
|
-
const cut = box && {
|
|
1167
|
-
x: box.rect.x - x0,
|
|
1168
|
-
y: box.rect.y - y0,
|
|
1169
|
-
width: box.rect.width,
|
|
1170
|
-
height: box.rect.height,
|
|
1171
|
-
};
|
|
1172
|
-
const key = [
|
|
1173
|
-
'shadow',
|
|
1174
|
-
w,
|
|
1175
|
-
h,
|
|
1176
|
-
sx,
|
|
1177
|
-
sy,
|
|
1178
|
-
shape.width,
|
|
1179
|
-
shape.height,
|
|
1180
|
-
cornerKey(around),
|
|
1181
|
-
s.blur,
|
|
1182
|
-
color,
|
|
1183
|
-
cut
|
|
1184
|
-
? `${cut.x},${cut.y},${cut.width},${cut.height},${cornerKey(box.corners)}`
|
|
1185
|
-
: '',
|
|
1186
|
-
].join('|');
|
|
1187
|
-
const image = options.cached(key, w, h, (sctx) => {
|
|
1188
|
-
if (cut) {
|
|
1189
|
-
sctx.save();
|
|
1190
|
-
sctx.beginPath!();
|
|
1191
|
-
sctx.rect!(0, 0, w, h);
|
|
1192
|
-
roundedRect(
|
|
1193
|
-
sctx,
|
|
1194
|
-
cut.x,
|
|
1195
|
-
cut.y,
|
|
1196
|
-
cut.width,
|
|
1197
|
-
cut.height,
|
|
1198
|
-
box.corners,
|
|
1199
|
-
true,
|
|
1200
|
-
true,
|
|
1201
|
-
);
|
|
1202
|
-
sctx.clip!();
|
|
1203
|
-
}
|
|
1204
|
-
fillShadow(
|
|
1205
|
-
sctx,
|
|
1206
|
-
s,
|
|
1207
|
-
color,
|
|
1208
|
-
(dx) => {
|
|
1209
|
-
roundedRect(sctx, sx - dx, sy, shape.width, shape.height, around);
|
|
1210
|
-
},
|
|
1211
|
-
sx + shape.width + reach,
|
|
1212
|
-
);
|
|
1213
|
-
if (cut) sctx.restore();
|
|
1214
|
-
});
|
|
1215
|
-
if (!image) return false;
|
|
1216
|
-
ctx.drawImage(image, x0, y0);
|
|
1217
|
-
return true;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
/**
|
|
1221
|
-
* An inset shadow drawn once on a surface the size of the padding box and
|
|
1222
|
-
* composited: a frame around the hole, whose shadow falls inside, clipped
|
|
1223
|
-
* to the padding box's corners on the surface. False where there is none.
|
|
1224
|
-
*/
|
|
1225
|
-
function bakedInset(
|
|
1226
|
-
ctx: PaintContext,
|
|
1227
|
-
options: PaintOptions,
|
|
1228
|
-
s: BoxShadow,
|
|
1229
|
-
color: string,
|
|
1230
|
-
pad: Rect,
|
|
1231
|
-
inner: Corners,
|
|
1232
|
-
hole: Rect,
|
|
1233
|
-
within: Corners,
|
|
1234
|
-
): boolean {
|
|
1235
|
-
if (!options.cached || !ctx.drawImage) return false;
|
|
1236
|
-
const w = Math.round(pad.width);
|
|
1237
|
-
const h = Math.round(pad.height);
|
|
1238
|
-
const hx = hole.x - pad.x;
|
|
1239
|
-
const hy = hole.y - pad.y;
|
|
1240
|
-
const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
|
|
1241
|
-
const key = [
|
|
1242
|
-
'inset',
|
|
1243
|
-
w,
|
|
1244
|
-
h,
|
|
1245
|
-
cornerKey(inner),
|
|
1246
|
-
hx,
|
|
1247
|
-
hy,
|
|
1248
|
-
hole.width,
|
|
1249
|
-
hole.height,
|
|
1250
|
-
cornerKey(within),
|
|
1251
|
-
s.blur,
|
|
1252
|
-
color,
|
|
1253
|
-
].join('|');
|
|
1254
|
-
const image = options.cached(key, w, h, (sctx) => {
|
|
1255
|
-
sctx.save();
|
|
1256
|
-
sctx.beginPath!();
|
|
1257
|
-
roundedRect(sctx, 0, 0, w, h, inner);
|
|
1258
|
-
sctx.clip!();
|
|
1259
|
-
fillShadow(
|
|
1260
|
-
sctx,
|
|
1261
|
-
s,
|
|
1262
|
-
color,
|
|
1263
|
-
(dx) => {
|
|
1264
|
-
sctx.rect!(-reach - dx, -reach, w + 2 * reach, h + 2 * reach);
|
|
1265
|
-
if (hole.width > 0 && hole.height > 0) {
|
|
1266
|
-
roundedRect(
|
|
1267
|
-
sctx,
|
|
1268
|
-
hx - dx,
|
|
1269
|
-
hy,
|
|
1270
|
-
hole.width,
|
|
1271
|
-
hole.height,
|
|
1272
|
-
within,
|
|
1273
|
-
true,
|
|
1274
|
-
true,
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
},
|
|
1278
|
-
w + reach + shadowReach(s.blur),
|
|
1279
|
-
);
|
|
1280
|
-
sctx.restore();
|
|
1281
|
-
});
|
|
1282
|
-
if (!image) return false;
|
|
1283
|
-
ctx.drawImage(image, pad.x, pad.y);
|
|
1284
|
-
return true;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
1447
|
/**
|
|
1288
1448
|
* Fill a shadow's shape: in its colour where it has no blur, and otherwise
|
|
1289
1449
|
* as the context's shadow of the shape drawn `dx` to the left — clear of
|
|
@@ -1296,12 +1456,13 @@ function fillShadow(
|
|
|
1296
1456
|
color: string,
|
|
1297
1457
|
shape: (dx: number) => void,
|
|
1298
1458
|
right: number,
|
|
1459
|
+
rule: 'nonzero' | 'evenodd' = 'nonzero',
|
|
1299
1460
|
): void {
|
|
1300
1461
|
if (!(s.blur > 0)) {
|
|
1301
1462
|
ctx.fillStyle = color;
|
|
1302
1463
|
ctx.beginPath!();
|
|
1303
1464
|
shape(0);
|
|
1304
|
-
ctx.fill!();
|
|
1465
|
+
ctx.fill!(rule);
|
|
1305
1466
|
return;
|
|
1306
1467
|
}
|
|
1307
1468
|
const dx = Math.ceil(right) + 1;
|
|
@@ -1313,10 +1474,36 @@ function fillShadow(
|
|
|
1313
1474
|
ctx.fillStyle = '#000000';
|
|
1314
1475
|
ctx.beginPath!();
|
|
1315
1476
|
shape(dx);
|
|
1316
|
-
ctx.fill!();
|
|
1477
|
+
ctx.fill!(rule);
|
|
1317
1478
|
ctx.restore();
|
|
1318
1479
|
}
|
|
1319
1480
|
|
|
1481
|
+
/**
|
|
1482
|
+
* A shadow's shape, spelled the way a 2d context recognises one: a `rect`,
|
|
1483
|
+
* or a `roundRect` with each corner's own radii, elliptical where the
|
|
1484
|
+
* corner is. react-x11's contexts draw the blurred shadow of such a shape —
|
|
1485
|
+
* and of a `rect` less such a shape, filled `evenodd` — from a tile they
|
|
1486
|
+
* keep, where one built of curves they blur afresh on every fill.
|
|
1487
|
+
*/
|
|
1488
|
+
function shadowShape(
|
|
1489
|
+
ctx: PaintContext,
|
|
1490
|
+
x: number,
|
|
1491
|
+
y: number,
|
|
1492
|
+
w: number,
|
|
1493
|
+
h: number,
|
|
1494
|
+
c: Corners,
|
|
1495
|
+
): void {
|
|
1496
|
+
if (c.x.every((r) => r === 0) || c.y.every((r) => r === 0)) {
|
|
1497
|
+
ctx.rect!(x, y, w, h);
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
// a circular corner as the number every context has always taken; only
|
|
1501
|
+
// an elliptical one as the point
|
|
1502
|
+
const corner = (i: number) =>
|
|
1503
|
+
c.x[i] === c.y[i] ? c.x[i] : { x: c.x[i], y: c.y[i] };
|
|
1504
|
+
ctx.roundRect!(x, y, w, h, [corner(0), corner(1), corner(2), corner(3)]);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1320
1507
|
/**
|
|
1321
1508
|
* The band between two rounded rectangles, the inner inside the outer, in
|
|
1322
1509
|
* the colour already set. Where a side has no band the two edges meet, and
|
|
@@ -1390,6 +1577,7 @@ function inFlow(parent: Box, child: Box, options: PaintOptions): boolean {
|
|
|
1390
1577
|
style.overflowX === 'visible' &&
|
|
1391
1578
|
style.overflowY === 'visible' &&
|
|
1392
1579
|
opacityOf(child) >= 1 &&
|
|
1580
|
+
!masked(style) &&
|
|
1393
1581
|
// containment makes it a stacking context, painted whole
|
|
1394
1582
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT)
|
|
1395
1583
|
);
|
|
@@ -1417,6 +1605,7 @@ function flowFlex(parent: Box, child: Box, options: PaintOptions): boolean {
|
|
|
1417
1605
|
style.overflowX === 'visible' &&
|
|
1418
1606
|
style.overflowY === 'visible' &&
|
|
1419
1607
|
!(child.outOfFlow && style.clip) &&
|
|
1608
|
+
!masked(style) &&
|
|
1420
1609
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT)
|
|
1421
1610
|
);
|
|
1422
1611
|
}
|
|
@@ -1634,12 +1823,13 @@ function spreadCorners(
|
|
|
1634
1823
|
}
|
|
1635
1824
|
|
|
1636
1825
|
/** How far in from a box's border box the box a `<visual-box>` names is:
|
|
1637
|
-
* top, right, bottom and left
|
|
1826
|
+
* top, right, bottom and left — none for `border-area`, whose layer is
|
|
1827
|
+
* painted over the border box inside what the border paints. */
|
|
1638
1828
|
function edgeInsets(
|
|
1639
1829
|
box: Frame,
|
|
1640
|
-
which:
|
|
1830
|
+
which: BackgroundClip,
|
|
1641
1831
|
): readonly [number, number, number, number] {
|
|
1642
|
-
if (which === 'border-box') return NO_INSETS;
|
|
1832
|
+
if (which === 'border-box' || which === 'border-area') return NO_INSETS;
|
|
1643
1833
|
const t = box.borderTop;
|
|
1644
1834
|
const r = box.borderRight;
|
|
1645
1835
|
const b = box.borderBottom;
|
|
@@ -1688,7 +1878,55 @@ function paintPositioned(
|
|
|
1688
1878
|
return;
|
|
1689
1879
|
}
|
|
1690
1880
|
}
|
|
1691
|
-
paintBox(ctx, box, options);
|
|
1881
|
+
paintBox(ctx, box, atViewport(box, options));
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
/**
|
|
1885
|
+
* The options a box is painted with where it is fixed to the viewport
|
|
1886
|
+
* (CSS 2.1 9.6.1): laid out against the viewport at the document's top,
|
|
1887
|
+
* and drawn where the viewport is now, however far the pane that scrolls
|
|
1888
|
+
* the element has moved it — a fixed header scrolled away with the text.
|
|
1889
|
+
* Once for a box and all it holds.
|
|
1890
|
+
*/
|
|
1891
|
+
function atViewport(box: Box, options: PaintOptions): PaintOptions {
|
|
1892
|
+
const viewport = options.viewport;
|
|
1893
|
+
if (!viewport || options.atViewport || !fixedToViewport(box)) {
|
|
1894
|
+
return options;
|
|
1895
|
+
}
|
|
1896
|
+
// where the document's top left is from the viewport's, less where the
|
|
1897
|
+
// viewport is in the document: the scroll
|
|
1898
|
+
const root = rootOf(box);
|
|
1899
|
+
const dx = viewport.x - options.originX - root.x;
|
|
1900
|
+
const dy = viewport.y - options.originY - root.y;
|
|
1901
|
+
return {
|
|
1902
|
+
...options,
|
|
1903
|
+
originX: options.originX + dx,
|
|
1904
|
+
originY: options.originY + dy,
|
|
1905
|
+
atViewport: true,
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
/** Per tree, its boxes fixed to the viewport (`fixedToViewport`), found
|
|
1910
|
+
* as the layout positions them: what the element answers a scroll pane's
|
|
1911
|
+
* blit with (`HtmlViewNode.viewportFixedRects`). */
|
|
1912
|
+
export const FIXED_BOXES = new WeakMap<object, Box[]>();
|
|
1913
|
+
|
|
1914
|
+
/** Whether a box is fixed to the viewport: `position: fixed` with no
|
|
1915
|
+
* transformed or contained box around it, which would be its containing
|
|
1916
|
+
* block instead (CSS Transforms 1, CSS Containment 2, 3.3). */
|
|
1917
|
+
export function fixedToViewport(box: Box): boolean {
|
|
1918
|
+
if (box.style.position !== 'fixed') return false;
|
|
1919
|
+
for (let at = box.parent; at?.parent; at = at.parent) {
|
|
1920
|
+
if (at.style.translate || at.style.transformTranslate) return false;
|
|
1921
|
+
if (contained(at, CONTAIN_LAYOUT | CONTAIN_PAINT)) return false;
|
|
1922
|
+
}
|
|
1923
|
+
return true;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function rootOf(box: Box): Box {
|
|
1927
|
+
let root = box;
|
|
1928
|
+
while (root.parent) root = root.parent;
|
|
1929
|
+
return root;
|
|
1692
1930
|
}
|
|
1693
1931
|
|
|
1694
1932
|
/**
|
|
@@ -2156,6 +2394,8 @@ function stacksLayers(box: Box): boolean {
|
|
|
2156
2394
|
const style = box.style;
|
|
2157
2395
|
if (style.position === 'fixed' || style.position === 'sticky') return true;
|
|
2158
2396
|
if (style.opacity < 1 || FADED_BLOCKS.has(box)) return true;
|
|
2397
|
+
// and so does a mask, which is applied to the group (CSS Masking 1, 7)
|
|
2398
|
+
if (masked(style)) return true;
|
|
2159
2399
|
// layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
|
|
2160
2400
|
if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT)) return true;
|
|
2161
2401
|
if (typeof style.zIndex !== 'number') return false;
|
|
@@ -2175,7 +2415,7 @@ function flexItem(box: Box): boolean {
|
|
|
2175
2415
|
* outside a box that clips where its containing block is, a fixed one
|
|
2176
2416
|
* outside every one, and a relative one inside every one.
|
|
2177
2417
|
*/
|
|
2178
|
-
function clipsFor(box: Box, context: Box): Box[] {
|
|
2418
|
+
export function clipsFor(box: Box, context: Box): Box[] {
|
|
2179
2419
|
let from: Box | null = box.parent;
|
|
2180
2420
|
if (box.outOfFlow) {
|
|
2181
2421
|
const fixed = box.style.position === 'fixed';
|
|
@@ -2205,6 +2445,7 @@ function paintStacked(
|
|
|
2205
2445
|
box: Box,
|
|
2206
2446
|
options: PaintOptions,
|
|
2207
2447
|
): void {
|
|
2448
|
+
options = atViewport(box, options);
|
|
2208
2449
|
if (!intersects(box, options)) return;
|
|
2209
2450
|
const between = CLIPS_BETWEEN.get(box);
|
|
2210
2451
|
if (!between) {
|
|
@@ -2383,7 +2624,9 @@ function paintBackground(
|
|
|
2383
2624
|
// box, which is where it starts, and repeated under the borders — or
|
|
2384
2625
|
// the viewport's, where it is fixed; its line runs across the whole of
|
|
2385
2626
|
// it, not the part this paint reaches
|
|
2386
|
-
const fixed =
|
|
2627
|
+
const fixed =
|
|
2628
|
+
style.backgroundAttachment === 'fixed' &&
|
|
2629
|
+
(options.viewport ?? options.canvas);
|
|
2387
2630
|
const origin = fixed || originBox(box, options, style);
|
|
2388
2631
|
const at = snapped(origin.x, origin.y, origin.width, origin.height);
|
|
2389
2632
|
if (rounded && style.backgroundSize !== 'auto' && ctx.clip) {
|
|
@@ -2873,7 +3116,7 @@ function paintBackgroundImage(
|
|
|
2873
3116
|
corners: Corners | null = null,
|
|
2874
3117
|
): void {
|
|
2875
3118
|
if (style.backgroundAttachment === 'fixed' && options.canvas) {
|
|
2876
|
-
at = options.canvas;
|
|
3119
|
+
at = options.viewport ?? options.canvas;
|
|
2877
3120
|
}
|
|
2878
3121
|
const url = style.backgroundImage;
|
|
2879
3122
|
const loaded = url ? options.backgroundImageFor?.(url) : null;
|
|
@@ -3559,6 +3802,94 @@ function roundedRing(
|
|
|
3559
3802
|
return true;
|
|
3560
3803
|
}
|
|
3561
3804
|
|
|
3805
|
+
/**
|
|
3806
|
+
* Clip to what a box's border paints, for a layer of `background-clip:
|
|
3807
|
+
* border-area` (CSS Backgrounds 4, 2.1): each side's width and style, and
|
|
3808
|
+
* not its colour — a transparent border still has an area, which is what
|
|
3809
|
+
* the value is for. The shapes are the border painter's own, so that the
|
|
3810
|
+
* background shows exactly where that border would: the ring `roundedRing`
|
|
3811
|
+
* fills where a rounded border is solid, the ring the trapezoids of a 3D
|
|
3812
|
+
* style make, and otherwise the rectangles `fillEdge` fills a side at a
|
|
3813
|
+
* time — the dots, the dashes, the two lines of a double border. All of
|
|
3814
|
+
* them run clockwise and a ring's inside the other way, so the clip is the
|
|
3815
|
+
* union by the non-zero rule, the one every context's `clip` takes. False
|
|
3816
|
+
* where the border paints nothing or the context cannot clip, and nothing
|
|
3817
|
+
* is pushed.
|
|
3818
|
+
*/
|
|
3819
|
+
function pushBorderArea(
|
|
3820
|
+
ctx: PaintContext,
|
|
3821
|
+
box: Frame,
|
|
3822
|
+
options: PaintOptions,
|
|
3823
|
+
): boolean {
|
|
3824
|
+
const t = box.borderTop;
|
|
3825
|
+
const r = box.borderRight;
|
|
3826
|
+
const b = box.borderBottom;
|
|
3827
|
+
const l = box.borderLeft;
|
|
3828
|
+
if (!(t || r || b || l) || !ctx.beginPath || !ctx.rect || !ctx.clip) {
|
|
3829
|
+
return false;
|
|
3830
|
+
}
|
|
3831
|
+
const s = box.style;
|
|
3832
|
+
const left = box.x + options.originX;
|
|
3833
|
+
const top = frameY(box) + options.originY;
|
|
3834
|
+
const x = Math.round(left);
|
|
3835
|
+
const y = Math.round(top);
|
|
3836
|
+
const w = Math.round(left + box.width) - x;
|
|
3837
|
+
const h = Math.round(top + frameHeight(box)) - y;
|
|
3838
|
+
if (w <= 0 || h <= 0) return false;
|
|
3839
|
+
const area = clampRect(options, x, y, w, h);
|
|
3840
|
+
if (!area) return false;
|
|
3841
|
+
const corners = cornersOf(s, w, h);
|
|
3842
|
+
ctx.save();
|
|
3843
|
+
ctx.beginPath();
|
|
3844
|
+
if (corners && canCurve(ctx) && solidBorder(box)) {
|
|
3845
|
+
roundedRect(ctx, area.x, area.y, area.w, area.h, corners, true);
|
|
3846
|
+
const iw = area.w - l - r;
|
|
3847
|
+
const ih = area.h - t - b;
|
|
3848
|
+
if (iw > 0 && ih > 0) {
|
|
3849
|
+
const inner = insetCorners(corners, t, r, b, l);
|
|
3850
|
+
roundedRect(ctx, area.x + l, area.y + t, iw, ih, inner, true, true);
|
|
3851
|
+
}
|
|
3852
|
+
} else if (sculpted(s)) {
|
|
3853
|
+
// a band a side, which the trapezoids cover between them
|
|
3854
|
+
ctx.rect(area.x, area.y, area.w, t);
|
|
3855
|
+
ctx.rect(area.x, area.y + area.h - b, area.w, b);
|
|
3856
|
+
ctx.rect(area.x, area.y + t, l, area.h - t - b);
|
|
3857
|
+
ctx.rect(area.x + area.w - r, area.y + t, r, area.h - t - b);
|
|
3858
|
+
} else {
|
|
3859
|
+
const path = { fillRect: ctx.rect.bind(ctx) };
|
|
3860
|
+
const edge = (
|
|
3861
|
+
ex: number,
|
|
3862
|
+
ey: number,
|
|
3863
|
+
ew: number,
|
|
3864
|
+
eh: number,
|
|
3865
|
+
style: ComputedStyle['borderTopStyle'],
|
|
3866
|
+
horizontal: boolean,
|
|
3867
|
+
): void => {
|
|
3868
|
+
const rect = clampRect(options, ex, ey, ew, eh);
|
|
3869
|
+
if (rect) fillEdge(path, rect, horizontal ? ex : ey, style, horizontal);
|
|
3870
|
+
};
|
|
3871
|
+
const between = h - t - b;
|
|
3872
|
+
if (t > 0) edge(x, y, w, t, s.borderTopStyle, true);
|
|
3873
|
+
if (b > 0) edge(x, y + h - b, w, b, s.borderBottomStyle, true);
|
|
3874
|
+
if (l > 0) edge(x, y + t, l, between, s.borderLeftStyle, false);
|
|
3875
|
+
if (r > 0) edge(x + w - r, y + t, r, between, s.borderRightStyle, false);
|
|
3876
|
+
}
|
|
3877
|
+
ctx.clip();
|
|
3878
|
+
return true;
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
/** Whether every side a box has a border on is solid, which a rounded box
|
|
3882
|
+
* draws as one ring (`roundedRing`). */
|
|
3883
|
+
function solidBorder(box: Frame): boolean {
|
|
3884
|
+
const s = box.style;
|
|
3885
|
+
return (
|
|
3886
|
+
(!box.borderTop || s.borderTopStyle === 'solid') &&
|
|
3887
|
+
(!box.borderRight || s.borderRightStyle === 'solid') &&
|
|
3888
|
+
(!box.borderBottom || s.borderBottomStyle === 'solid') &&
|
|
3889
|
+
(!box.borderLeft || s.borderLeftStyle === 'solid')
|
|
3890
|
+
);
|
|
3891
|
+
}
|
|
3892
|
+
|
|
3562
3893
|
/** The border styles drawn in two shades, as though lit from the top left. */
|
|
3563
3894
|
const SCULPTED = new Set(['groove', 'ridge', 'inset', 'outset']);
|
|
3564
3895
|
|
|
@@ -3757,7 +4088,7 @@ function paintCollapsedBorders(
|
|
|
3757
4088
|
}
|
|
3758
4089
|
|
|
3759
4090
|
function fillEdge(
|
|
3760
|
-
ctx:
|
|
4091
|
+
ctx: Pick<FillContext, 'fillRect'>,
|
|
3761
4092
|
rect: { x: number; y: number; w: number; h: number },
|
|
3762
4093
|
phaseOrigin: number,
|
|
3763
4094
|
style: ComputedStyle['borderTopStyle'],
|
|
@@ -3994,6 +4325,17 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
3994
4325
|
if (line.y >= bottom) break;
|
|
3995
4326
|
if (line.y + line.height > top) visible.push(line);
|
|
3996
4327
|
}
|
|
4328
|
+
// and a line the damage misses whose text or inline-block `position:
|
|
4329
|
+
// relative` moved into it: drawn by its line, it went undrawn with it
|
|
4330
|
+
const moved = MOVED_OFF_LINES.get(lines);
|
|
4331
|
+
if (moved && reachedOff(visible, moved, top, bottom)) {
|
|
4332
|
+
const drawn = new Set(visible);
|
|
4333
|
+
for (const line of moved) {
|
|
4334
|
+
if (movedInto(line, top, bottom)) drawn.add(line);
|
|
4335
|
+
}
|
|
4336
|
+
visible.length = 0;
|
|
4337
|
+
for (const line of lines) if (drawn.has(line)) visible.push(line);
|
|
4338
|
+
}
|
|
3997
4339
|
} else {
|
|
3998
4340
|
visible.push(...lines);
|
|
3999
4341
|
}
|
|
@@ -4002,7 +4344,13 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4002
4344
|
const bleeds: Bleed[] = [];
|
|
4003
4345
|
for (const line of visible) {
|
|
4004
4346
|
if (line.background) paintLineBackground(ctx, line, options);
|
|
4005
|
-
paintInlineBoxes(
|
|
4347
|
+
paintInlineBoxes(
|
|
4348
|
+
ctx,
|
|
4349
|
+
line,
|
|
4350
|
+
lines,
|
|
4351
|
+
options,
|
|
4352
|
+
line === lines[0] ? null : bleeds,
|
|
4353
|
+
);
|
|
4006
4354
|
for (const text of line.texts) {
|
|
4007
4355
|
const natural = text.layout.lines[text.layoutLine];
|
|
4008
4356
|
if (natural)
|
|
@@ -4060,6 +4408,39 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4060
4408
|
}
|
|
4061
4409
|
}
|
|
4062
4410
|
|
|
4411
|
+
/** Whether any of the lines moved content was taken off, and the damage
|
|
4412
|
+
* does not already draw, has it within the damage's rows. */
|
|
4413
|
+
function reachedOff(
|
|
4414
|
+
visible: readonly LineBox[],
|
|
4415
|
+
moved: ReadonlySet<LineBox>,
|
|
4416
|
+
top: number,
|
|
4417
|
+
bottom: number,
|
|
4418
|
+
): boolean {
|
|
4419
|
+
for (const line of moved) {
|
|
4420
|
+
if (!visible.includes(line) && movedInto(line, top, bottom)) return true;
|
|
4421
|
+
}
|
|
4422
|
+
return false;
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
/** Whether what was moved off a line — its inline-blocks and images, where
|
|
4426
|
+
* their ink is, and its texts, where they are drawn — reaches the rows
|
|
4427
|
+
* between `top` and `bottom`. */
|
|
4428
|
+
function movedInto(line: LineBox, top: number, bottom: number): boolean {
|
|
4429
|
+
for (const placed of line.atomics) {
|
|
4430
|
+
const box = placed.box;
|
|
4431
|
+
if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
|
|
4432
|
+
return true;
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
for (const text of line.texts) {
|
|
4436
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
4437
|
+
if (!natural) continue;
|
|
4438
|
+
const y = text.drawY + natural.y;
|
|
4439
|
+
if (y < bottom && y + natural.height > top) return true;
|
|
4440
|
+
}
|
|
4441
|
+
return false;
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4063
4444
|
/** An inline box's fragment whose padding or border reaches up over the
|
|
4064
4445
|
* lines before its own, and where its own line starts. */
|
|
4065
4446
|
interface Bleed {
|
|
@@ -4091,7 +4472,7 @@ function paintBleeds(
|
|
|
4091
4472
|
ctx.beginPath();
|
|
4092
4473
|
ctx.rect(left, top, Math.ceil(fragment.width) + 2, bottom - top);
|
|
4093
4474
|
ctx.clip();
|
|
4094
|
-
paintLayers(ctx, fragment, options);
|
|
4475
|
+
paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
|
|
4095
4476
|
paintBorders(ctx, fragment, options);
|
|
4096
4477
|
ctx.restore();
|
|
4097
4478
|
}
|
|
@@ -4502,38 +4883,34 @@ function paintLineBackground(
|
|
|
4502
4883
|
);
|
|
4503
4884
|
}
|
|
4504
4885
|
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
):
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
// moved by its own offset below, and the boxes around it are not.
|
|
4528
|
-
let baseline = line.y + line.baseline;
|
|
4886
|
+
/**
|
|
4887
|
+
* The fragments of the decorated inline boxes on a line: how far across it
|
|
4888
|
+
* each reaches, its border box, and whether the box opens or closes on it.
|
|
4889
|
+
* Where the line holds text `position: relative` moved, taken where the
|
|
4890
|
+
* text was: a fragment is moved by its own box's offset.
|
|
4891
|
+
*
|
|
4892
|
+
* A box is more than one fragment on a line where bidi reordering puts
|
|
4893
|
+
* what is not the box's between parts of it (CSS 2.1 9.10): a letter of
|
|
4894
|
+
* another element, an atomic, another element's edge. Its parts are one
|
|
4895
|
+
* fragment wherever nothing else comes between them, whatever room is
|
|
4896
|
+
* between them — a space it hangs, a box of its own inside it — so a line
|
|
4897
|
+
* that reads one way has one fragment of each box, as it always had.
|
|
4898
|
+
*/
|
|
4899
|
+
function fragmentsOn(line: LineBox): Map<Box, InlineFragment[]> | null {
|
|
4900
|
+
/** What is on the line, where, and which decorated boxes it is in. */
|
|
4901
|
+
const marks: {
|
|
4902
|
+
left: number;
|
|
4903
|
+
right: number;
|
|
4904
|
+
boxes: readonly Box[];
|
|
4905
|
+
/** An edge's own box, whose fragment it opens or closes. */
|
|
4906
|
+
edge?: EdgePlacement;
|
|
4907
|
+
}[] = [];
|
|
4529
4908
|
const shifted = SHIFTED_LINES.has(line);
|
|
4530
4909
|
for (const text of line.texts) {
|
|
4531
4910
|
const natural = text.layout.lines[text.layoutLine];
|
|
4532
4911
|
const boxAt = text.spans.boxAt;
|
|
4533
4912
|
if (!natural || !boxAt) continue;
|
|
4534
4913
|
const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
|
|
4535
|
-
const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
|
|
4536
|
-
baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
|
|
4537
4914
|
const x = text.drawX - (shift?.x ?? 0) + natural.x;
|
|
4538
4915
|
for (const run of natural.runs) {
|
|
4539
4916
|
const owner = boxAt.call(text.spans, run.start);
|
|
@@ -4543,44 +4920,159 @@ function paintInlineBoxes(
|
|
|
4543
4920
|
const left = Math.max(x, x + run.x);
|
|
4544
4921
|
const right = Math.min(x + natural.width, x + run.x + run.width);
|
|
4545
4922
|
if (right <= left) continue;
|
|
4546
|
-
|
|
4923
|
+
marks.push({ left, right, boxes: decoratedAncestors(owner) });
|
|
4924
|
+
}
|
|
4925
|
+
// and the spaces `pre-wrap` keeps that the line ends on, which hang
|
|
4926
|
+
// past it and are their box's all the same (`LineText.hung`)
|
|
4927
|
+
for (const space of text.hung ?? []) {
|
|
4928
|
+
const owner = boxAt.call(text.spans, space.at);
|
|
4929
|
+
if (!owner) continue;
|
|
4930
|
+
const left = text.drawX - (shift?.x ?? 0) + space.x;
|
|
4931
|
+
marks.push({
|
|
4932
|
+
left,
|
|
4933
|
+
right: left + space.width,
|
|
4934
|
+
boxes: decoratedAncestors(owner),
|
|
4935
|
+
});
|
|
4547
4936
|
}
|
|
4548
4937
|
}
|
|
4549
4938
|
for (const placed of line.atomics) {
|
|
4550
4939
|
const atomic = placed.box;
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
);
|
|
4557
|
-
}
|
|
4940
|
+
marks.push({
|
|
4941
|
+
left: placed.x - atomic.marginLeft,
|
|
4942
|
+
right: placed.x + atomic.width + atomic.marginRight,
|
|
4943
|
+
boxes: decoratedAncestors(atomic),
|
|
4944
|
+
});
|
|
4558
4945
|
}
|
|
4559
4946
|
for (const edge of line.edges ?? []) {
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4947
|
+
marks.push({
|
|
4948
|
+
left: edge.x,
|
|
4949
|
+
right: edge.x + edge.width,
|
|
4950
|
+
boxes: decoratedAncestors(edge.box),
|
|
4951
|
+
edge,
|
|
4952
|
+
});
|
|
4953
|
+
}
|
|
4954
|
+
let fragments: Map<Box, InlineFragment[]> | null = null;
|
|
4955
|
+
// each box's parts, left to right
|
|
4956
|
+
const parts = new Map<Box, typeof marks>();
|
|
4957
|
+
for (const mark of marks) {
|
|
4958
|
+
for (const box of mark.boxes) {
|
|
4959
|
+
let list = parts.get(box);
|
|
4960
|
+
if (!list) parts.set(box, (list = []));
|
|
4961
|
+
list.push(mark);
|
|
4572
4962
|
}
|
|
4573
|
-
const
|
|
4574
|
-
if (
|
|
4575
|
-
|
|
4963
|
+
const own = mark.edge?.box;
|
|
4964
|
+
if (own?.decoration) {
|
|
4965
|
+
let list = parts.get(own);
|
|
4966
|
+
if (!list) parts.set(own, (list = []));
|
|
4967
|
+
list.push(mark);
|
|
4968
|
+
}
|
|
4969
|
+
}
|
|
4970
|
+
for (const [box, list] of parts) {
|
|
4971
|
+
list.sort((a, b) => a.left - b.left || a.right - b.right);
|
|
4972
|
+
const out: InlineFragment[] = [];
|
|
4973
|
+
let open: InlineFragment | null = null;
|
|
4974
|
+
let reach = -Infinity;
|
|
4975
|
+
for (const mark of list) {
|
|
4976
|
+
// The element's `direction` says which side its start is on, whatever
|
|
4977
|
+
// its text reads as (CSS 2.1 8.6); its own edge's margin is outside
|
|
4978
|
+
// the box, its border and padding inside.
|
|
4979
|
+
let left = mark.left;
|
|
4980
|
+
let right = mark.right;
|
|
4981
|
+
const own = mark.edge?.box === box ? mark.edge : null;
|
|
4982
|
+
if (own) {
|
|
4983
|
+
const onLeft =
|
|
4984
|
+
(own.side === 'start') !== (box.style.direction === 'rtl');
|
|
4985
|
+
if (onLeft) left = right = own.x + box.marginLeft;
|
|
4986
|
+
else left = right = own.x + own.width - box.marginRight;
|
|
4987
|
+
}
|
|
4988
|
+
if (!open || between(marks, box, reach, mark.left)) {
|
|
4989
|
+
open = { left, right, start: false, end: false };
|
|
4990
|
+
out.push(open);
|
|
4991
|
+
} else {
|
|
4992
|
+
open.left = Math.min(open.left, left);
|
|
4993
|
+
open.right = Math.max(open.right, right);
|
|
4994
|
+
}
|
|
4995
|
+
reach = Math.max(reach, mark.right);
|
|
4996
|
+
if (own) {
|
|
4997
|
+
if (own.side === 'start') open.start = true;
|
|
4998
|
+
else open.end = true;
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
(fragments ??= new Map()).set(box, out);
|
|
5002
|
+
}
|
|
5003
|
+
return fragments;
|
|
5004
|
+
}
|
|
5005
|
+
|
|
5006
|
+
/** Whether anything on a line that is not in a box lies between two of its
|
|
5007
|
+
* parts, from `left` to `right`: what splits it into two fragments. */
|
|
5008
|
+
function between(
|
|
5009
|
+
marks: readonly {
|
|
5010
|
+
left: number;
|
|
5011
|
+
right: number;
|
|
5012
|
+
boxes: readonly Box[];
|
|
5013
|
+
edge?: EdgePlacement;
|
|
5014
|
+
}[],
|
|
5015
|
+
box: Box,
|
|
5016
|
+
left: number,
|
|
5017
|
+
right: number,
|
|
5018
|
+
): boolean {
|
|
5019
|
+
if (right - left < 0.5) return false;
|
|
5020
|
+
for (const mark of marks) {
|
|
5021
|
+
if (mark.right - mark.left < 0.5) continue;
|
|
5022
|
+
if (mark.boxes.includes(box) || mark.edge?.box === box) continue;
|
|
5023
|
+
const middle = (mark.left + mark.right) / 2;
|
|
5024
|
+
if (middle > left && middle < right) return true;
|
|
4576
5025
|
}
|
|
5026
|
+
return false;
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
function paintInlineBoxes(
|
|
5030
|
+
ctx: PaintContext,
|
|
5031
|
+
line: LineBox,
|
|
5032
|
+
/** The block's lines, which `line` is one of. */
|
|
5033
|
+
lines: readonly LineBox[],
|
|
5034
|
+
options: PaintOptions,
|
|
5035
|
+
/** Where a fragment that reaches up over the lines before goes, to be
|
|
5036
|
+
* drawn again over their text; null for a block's first line. */
|
|
5037
|
+
bleeds: Bleed[] | null = null,
|
|
5038
|
+
): void {
|
|
5039
|
+
const fragments = fragmentsOn(line);
|
|
4577
5040
|
if (!fragments) return;
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
5041
|
+
// Every text on a line is drawn on one baseline (`finishLine`), and an
|
|
5042
|
+
// engine's is from the top of its layout rather than of the line. Taken
|
|
5043
|
+
// where the line has each text: a box `position: relative` moves is
|
|
5044
|
+
// moved by its own offset below, and the boxes around it are not.
|
|
5045
|
+
let baseline = line.y + line.baseline;
|
|
5046
|
+
const shifted = SHIFTED_LINES.has(line);
|
|
5047
|
+
for (const text of line.texts) {
|
|
5048
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
5049
|
+
if (!natural || !text.spans.boxAt) continue;
|
|
5050
|
+
const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
|
|
5051
|
+
const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
|
|
5052
|
+
baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
|
|
5053
|
+
}
|
|
5054
|
+
const boxes = [...fragments.keys()].sort((a, b) => depthOf(a) - depthOf(b));
|
|
4581
5055
|
for (const box of boxes) {
|
|
4582
5056
|
if (box.style.visibility !== 'visible') continue;
|
|
4583
|
-
const f
|
|
5057
|
+
for (const f of fragments.get(box)!) {
|
|
5058
|
+
paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline);
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
/** One fragment of an inline box on a line (`paintInlineBoxes`). */
|
|
5064
|
+
function paintInlineFragment(
|
|
5065
|
+
ctx: PaintContext,
|
|
5066
|
+
line: LineBox,
|
|
5067
|
+
lines: readonly LineBox[],
|
|
5068
|
+
options: PaintOptions,
|
|
5069
|
+
bleeds: Bleed[] | null,
|
|
5070
|
+
box: Box,
|
|
5071
|
+
f: InlineFragment,
|
|
5072
|
+
baseline: number,
|
|
5073
|
+
): void {
|
|
5074
|
+
const shifted = SHIFTED_LINES.has(line);
|
|
5075
|
+
{
|
|
4584
5076
|
const face = box.decoration!;
|
|
4585
5077
|
// on its own baseline, which `vertical-align` may raise off the line's
|
|
4586
5078
|
const own = shifted
|
|
@@ -4614,8 +5106,15 @@ function paintInlineBoxes(
|
|
|
4614
5106
|
padRight: rightEnds ? box.padRight : 0,
|
|
4615
5107
|
style,
|
|
4616
5108
|
};
|
|
4617
|
-
if (fragment.width <= 0)
|
|
4618
|
-
|
|
5109
|
+
if (fragment.width <= 0) return;
|
|
5110
|
+
// its images and gradients from the strip its fragments make, where it
|
|
5111
|
+
// goes on to another line, and from its own padding box where it is
|
|
5112
|
+
// on this one alone or `clone` says each fragment is its own
|
|
5113
|
+
if (!(f.start && f.end) && sliced(box.style)) {
|
|
5114
|
+
const strip = stripFor(lines, line, box, fragment);
|
|
5115
|
+
if (strip) fragment.strip = strip;
|
|
5116
|
+
}
|
|
5117
|
+
paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
|
|
4619
5118
|
paintBorders(ctx, fragment, options);
|
|
4620
5119
|
if (box.style.outlineStyle !== 'none') paintOutline(ctx, fragment, options);
|
|
4621
5120
|
if (bleeds && fragment.y < line.y - 0.5) {
|
|
@@ -4624,6 +5123,93 @@ function paintInlineBoxes(
|
|
|
4624
5123
|
}
|
|
4625
5124
|
}
|
|
4626
5125
|
|
|
5126
|
+
/** Whether an inline box places background images or gradients, which
|
|
5127
|
+
* `slice` places across its fragments, as one box end to end. */
|
|
5128
|
+
function sliced(style: ComputedStyle): boolean {
|
|
5129
|
+
return (
|
|
5130
|
+
style.boxDecorationBreak === 'slice' &&
|
|
5131
|
+
(style.backgroundImage !== null ||
|
|
5132
|
+
style.backgroundGradient !== null ||
|
|
5133
|
+
style.backgroundImages !== null)
|
|
5134
|
+
);
|
|
5135
|
+
}
|
|
5136
|
+
|
|
5137
|
+
/** Where an inline box's fragments sit along the strip they make laid end
|
|
5138
|
+
* to end: how wide it is, on how many lines, and how far along it each
|
|
5139
|
+
* line's fragment starts, in the box's direction. */
|
|
5140
|
+
interface Strip {
|
|
5141
|
+
width: number;
|
|
5142
|
+
lines: number;
|
|
5143
|
+
before: Map<LineBox, number>;
|
|
5144
|
+
}
|
|
5145
|
+
|
|
5146
|
+
/** Of a block's lines, by the array they are, the strip of each inline box
|
|
5147
|
+
* on them that `sliced` says places images across its fragments: made
|
|
5148
|
+
* once a layout, as `MOVED_OFF_LINES` is, so a repaint of one line reads
|
|
5149
|
+
* the lines before it without going over them again. What `sliced` reads
|
|
5150
|
+
* is never restyled in place (`HtmlViewNode`), which lays the lines out
|
|
5151
|
+
* again instead. */
|
|
5152
|
+
const STRIPS = new WeakMap<readonly LineBox[], Map<Box, Strip>>();
|
|
5153
|
+
|
|
5154
|
+
function stripsOf(lines: readonly LineBox[]): Map<Box, Strip> {
|
|
5155
|
+
const strips = new Map<Box, Strip>();
|
|
5156
|
+
for (const line of lines) {
|
|
5157
|
+
const fragments = fragmentsOn(line);
|
|
5158
|
+
if (!fragments) continue;
|
|
5159
|
+
for (const [box, list] of fragments) {
|
|
5160
|
+
if (!sliced(box.style)) continue;
|
|
5161
|
+
let strip = strips.get(box);
|
|
5162
|
+
if (!strip) {
|
|
5163
|
+
strip = { width: 0, lines: 0, before: new Map() };
|
|
5164
|
+
strips.set(box, strip);
|
|
5165
|
+
}
|
|
5166
|
+
strip.before.set(line, strip.width);
|
|
5167
|
+
for (const f of list) strip.width += Math.max(0, f.right - f.left);
|
|
5168
|
+
strip.lines += 1;
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
return strips;
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5174
|
+
/**
|
|
5175
|
+
* The box an inline box's fragment on `line` is a slice of (CSS
|
|
5176
|
+
* Fragmentation 3, 5.4): the box's fragments laid end to end in its
|
|
5177
|
+
* direction, as though it had not wrapped — as wide as
|
|
5178
|
+
* they are together, with the box's own borders and padding at its two
|
|
5179
|
+
* ends, placed so that this fragment is where it falls along it. So an
|
|
5180
|
+
* image `no-repeat` at the start is on the first fragment alone, and a
|
|
5181
|
+
* gradient runs once across them all. Undefined where the box is on one
|
|
5182
|
+
* line.
|
|
5183
|
+
*/
|
|
5184
|
+
function stripFor(
|
|
5185
|
+
lines: readonly LineBox[],
|
|
5186
|
+
line: LineBox,
|
|
5187
|
+
box: Box,
|
|
5188
|
+
fragment: Frame,
|
|
5189
|
+
): Frame | undefined {
|
|
5190
|
+
let strips = STRIPS.get(lines);
|
|
5191
|
+
if (!strips) {
|
|
5192
|
+
strips = stripsOf(lines);
|
|
5193
|
+
STRIPS.set(lines, strips);
|
|
5194
|
+
}
|
|
5195
|
+
const strip = strips.get(box);
|
|
5196
|
+
const before = strip?.before.get(line);
|
|
5197
|
+
if (!strip || strip.lines < 2 || before === undefined) return undefined;
|
|
5198
|
+
const x =
|
|
5199
|
+
box.style.direction === 'rtl'
|
|
5200
|
+
? fragment.x + fragment.width + before - strip.width
|
|
5201
|
+
: fragment.x - before;
|
|
5202
|
+
return {
|
|
5203
|
+
...fragment,
|
|
5204
|
+
x,
|
|
5205
|
+
width: strip.width,
|
|
5206
|
+
borderLeft: box.borderLeft,
|
|
5207
|
+
borderRight: box.borderRight,
|
|
5208
|
+
padLeft: box.padLeft,
|
|
5209
|
+
padRight: box.padRight,
|
|
5210
|
+
};
|
|
5211
|
+
}
|
|
5212
|
+
|
|
4627
5213
|
/** An inline box's style on a line it does not both start and end on: no
|
|
4628
5214
|
* rounded corner on a side it goes on from. Made once a style and a pair
|
|
4629
5215
|
* of ends, so a repaint copies no style and the layers made of it are
|
|
@@ -4701,6 +5287,20 @@ function decoratedAncestors(box: Box): Box[] {
|
|
|
4701
5287
|
return found;
|
|
4702
5288
|
}
|
|
4703
5289
|
|
|
5290
|
+
/** A restyle in place (a `:hover`) gave an inline box a decoration where it
|
|
5291
|
+
* had none, or took it away, and the lists kept above do not know: every
|
|
5292
|
+
* box whose list could name it — inside it, through the inline boxes the
|
|
5293
|
+
* walk above climbs — looks again. A document built again has new boxes,
|
|
5294
|
+
* and so no lists. */
|
|
5295
|
+
export function forgetDecoratedAncestors(box: Box): void {
|
|
5296
|
+
const stack = [...box.children];
|
|
5297
|
+
while (stack.length) {
|
|
5298
|
+
const at = stack.pop()!;
|
|
5299
|
+
DECORATED.delete(at);
|
|
5300
|
+
if (at.kind === 'inline') stack.push(...at.children);
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
|
|
4704
5304
|
/**
|
|
4705
5305
|
* The band the document selection covers on one line.
|
|
4706
5306
|
*
|