@react-x11/components 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html/controls.d.ts +41 -5
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +85 -57
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +545 -126
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +47 -6
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +336 -30
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +142 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +563 -147
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +73 -9
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +78 -4
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +373 -19
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +93 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +393 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +90 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +74 -10
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +90 -18
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +574 -84
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +154 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +62 -18
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +90 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +771 -186
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +101 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1502 -160
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +15 -2
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +249 -17
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +700 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +2 -2
- package/src/html/controls.ts +114 -51
- package/src/html/css/cascade.ts +614 -140
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +388 -33
- package/src/html/css/style.ts +688 -150
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +77 -10
- package/src/html/css/values.ts +4 -0
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +417 -24
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +447 -37
- package/src/html/layout/boxes.ts +143 -14
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +95 -22
- package/src/html/layout/flex.ts +675 -78
- package/src/html/layout/inline.ts +159 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +62 -19
- package/src/html/node.ts +798 -172
- package/src/html/paint.ts +1742 -241
- package/src/html/svg.ts +277 -19
- package/src/html/widgets.ts +821 -0
- package/src/html/woff2.ts +612 -0
- package/src/index.ts +1 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/src/html/paint.ts
CHANGED
|
@@ -26,16 +26,25 @@ import {
|
|
|
26
26
|
paintRunRules,
|
|
27
27
|
} from '../richtext/runs.js';
|
|
28
28
|
import type { FillContext } from '../richtext/runs.js';
|
|
29
|
-
import {
|
|
30
|
-
|
|
29
|
+
import {
|
|
30
|
+
AUTO,
|
|
31
|
+
alphaOf,
|
|
32
|
+
inkColor,
|
|
33
|
+
isTransparent,
|
|
34
|
+
resolve,
|
|
35
|
+
} from './css/values.js';
|
|
36
|
+
import { SCHEME_COLORS, blend, borderShades } from './css/color.js';
|
|
31
37
|
import type { Len } from './css/values.js';
|
|
32
38
|
import type {
|
|
33
39
|
BackgroundClip,
|
|
34
40
|
BackgroundRepeat,
|
|
35
41
|
BoxShadow,
|
|
36
42
|
ComputedStyle,
|
|
43
|
+
Gradient,
|
|
44
|
+
GradientStop,
|
|
37
45
|
ImageRepeat,
|
|
38
46
|
LinearGradient,
|
|
47
|
+
RadialGradient,
|
|
39
48
|
RepeatMode,
|
|
40
49
|
} from './css/style.js';
|
|
41
50
|
import {
|
|
@@ -46,7 +55,10 @@ import {
|
|
|
46
55
|
masked,
|
|
47
56
|
scrolls,
|
|
48
57
|
} from './css/style.js';
|
|
49
|
-
import {
|
|
58
|
+
import { LARGEST } from './css/calc.js';
|
|
59
|
+
import { invert, mapRect, transformed } from './css/transform.js';
|
|
60
|
+
import type { Matrix } from './css/transform.js';
|
|
61
|
+
import { contained, placedMatrix } from './layout/block.js';
|
|
50
62
|
import {
|
|
51
63
|
BOX_RAISES,
|
|
52
64
|
LINE_BOX_RAISES,
|
|
@@ -55,6 +67,10 @@ import {
|
|
|
55
67
|
CLAMPED,
|
|
56
68
|
CLIPPED_CELLS,
|
|
57
69
|
COLLAPSED_CELLS,
|
|
70
|
+
COLUMN_LINES,
|
|
71
|
+
COLUMN_PIECES,
|
|
72
|
+
COLUMN_ROWS,
|
|
73
|
+
columned,
|
|
58
74
|
FADED_BLOCKS,
|
|
59
75
|
PAINT_ORDER,
|
|
60
76
|
INLINE_OFFSETS,
|
|
@@ -66,11 +82,14 @@ import {
|
|
|
66
82
|
} from './layout/boxes.js';
|
|
67
83
|
import type {
|
|
68
84
|
BoxTree,
|
|
85
|
+
ColumnPiece,
|
|
69
86
|
EdgePlacement,
|
|
70
87
|
LineBox,
|
|
71
88
|
LineText,
|
|
72
89
|
Marker,
|
|
90
|
+
SelectionStyler,
|
|
73
91
|
} from './layout/boxes.js';
|
|
92
|
+
import type { SelectionStyle } from './css/cascade.js';
|
|
74
93
|
import {
|
|
75
94
|
CLIPPED_TEXT,
|
|
76
95
|
clipBoxOf,
|
|
@@ -122,6 +141,24 @@ export interface PaintContext extends FillContext {
|
|
|
122
141
|
x1: number,
|
|
123
142
|
y1: number,
|
|
124
143
|
): { addColorStop(offset: number, color: string): void };
|
|
144
|
+
createRadialGradient?(
|
|
145
|
+
x0: number,
|
|
146
|
+
y0: number,
|
|
147
|
+
r0: number,
|
|
148
|
+
x1: number,
|
|
149
|
+
y1: number,
|
|
150
|
+
r1: number,
|
|
151
|
+
): { addColorStop(offset: number, color: string): void };
|
|
152
|
+
/** Multiplies the current matrix, which a fill's paint is sampled
|
|
153
|
+
* through and a path already laid down is not. */
|
|
154
|
+
transform?(
|
|
155
|
+
a: number,
|
|
156
|
+
b: number,
|
|
157
|
+
c: number,
|
|
158
|
+
d: number,
|
|
159
|
+
e: number,
|
|
160
|
+
f: number,
|
|
161
|
+
): void;
|
|
125
162
|
clip?(): void;
|
|
126
163
|
/** Canvas shadows: ntk bakes and caches the blur, CoreGraphics draws it. */
|
|
127
164
|
shadowColor?: string;
|
|
@@ -132,6 +169,20 @@ export interface PaintContext extends FillContext {
|
|
|
132
169
|
/** ntk's X11 context has patterns; the Cocoa one does not, and tiles. */
|
|
133
170
|
createPattern?(image: unknown, repetition: string): unknown;
|
|
134
171
|
translate?(x: number, y: number): void;
|
|
172
|
+
/** Multiplies the context's matrix: what a transformed box is drawn
|
|
173
|
+
* through (`paintTransformed`). */
|
|
174
|
+
transform?(
|
|
175
|
+
a: number,
|
|
176
|
+
b: number,
|
|
177
|
+
c: number,
|
|
178
|
+
d: number,
|
|
179
|
+
e: number,
|
|
180
|
+
f: number,
|
|
181
|
+
): void;
|
|
182
|
+
/** Whether text drawn under a matrix that turns or scales is drawn
|
|
183
|
+
* turned and scaled, glyphs and all: the native contexts', and not
|
|
184
|
+
* ntk's, whose glyphs are drawn as they were shaped. */
|
|
185
|
+
scalesText?: boolean;
|
|
135
186
|
/** What every drawing is multiplied by: an element's `opacity`. */
|
|
136
187
|
globalAlpha?: number;
|
|
137
188
|
/** How a drawing meets what is under it; a value a backend does not
|
|
@@ -149,8 +200,9 @@ export interface PaintOptions {
|
|
|
149
200
|
/** Where the document's origin sits in the window. Scrolling is this. */
|
|
150
201
|
originX: number;
|
|
151
202
|
originY: number;
|
|
152
|
-
/** Device pixels per logical pixel, for the run rules —
|
|
153
|
-
*
|
|
203
|
+
/** Device pixels per logical pixel, for the run rules — the two lines
|
|
204
|
+
* of a double one are a logical pixel apart, not a device one; a rule's
|
|
205
|
+
* thickness is its style's, in device pixels already. Default 1. */
|
|
154
206
|
scale?: number;
|
|
155
207
|
/** The rectangle being repainted, in window coordinates, or null for all. */
|
|
156
208
|
damage: Rect | null;
|
|
@@ -192,6 +244,9 @@ export interface PaintOptions {
|
|
|
192
244
|
clips?: ClipLevel[];
|
|
193
245
|
/** @internal Whether the tree has a layer below the flow (`hoistNegative`). */
|
|
194
246
|
negative?: boolean;
|
|
247
|
+
/** @internal Each box's `::selection`, where a rule styles one
|
|
248
|
+
* (`BoxTree.selectionStyler`). */
|
|
249
|
+
selectionStyler?: SelectionStyler | null;
|
|
195
250
|
}
|
|
196
251
|
|
|
197
252
|
/**
|
|
@@ -210,6 +265,41 @@ export interface PaintOptions {
|
|
|
210
265
|
*/
|
|
211
266
|
export const OUT_OF_FLOW_REACH = new WeakMap<Box, number>();
|
|
212
267
|
|
|
268
|
+
/**
|
|
269
|
+
* How far above and below a block's lines its inline content draws, at
|
|
270
|
+
* most, by the array its lines are: glyphs taller than a `line-height` under
|
|
271
|
+
* their face's height, and an atomic's ink past the line it is on. The
|
|
272
|
+
* slack a paint's cull of the lines takes on top of their own rows
|
|
273
|
+
* (`paintLines`); none for the lines of almost every block, whose content
|
|
274
|
+
* is inside them.
|
|
275
|
+
*/
|
|
276
|
+
const LINE_INK = new WeakMap<
|
|
277
|
+
readonly LineBox[],
|
|
278
|
+
{ above: number; below: number }
|
|
279
|
+
>();
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* For a box painted through a matrix (`placedMatrix`), the bounds of what
|
|
283
|
+
* it and its descendants draw before the matrix is applied: what
|
|
284
|
+
* `boundsX`… would have been, which are where it puts them. The paint
|
|
285
|
+
* inside the box, and a point taken back into it, are in these.
|
|
286
|
+
*/
|
|
287
|
+
const UNTRANSFORMED = new WeakMap<Box, Rect>();
|
|
288
|
+
|
|
289
|
+
/** The bounds of what a box painted through a matrix draws, in its own
|
|
290
|
+
* coordinates: asked of a box `placedMatrix` answers for, whose entry the
|
|
291
|
+
* last `computePaintBounds` wrote. */
|
|
292
|
+
export function ownBounds(box: Box): Rect {
|
|
293
|
+
return (
|
|
294
|
+
UNTRANSFORMED.get(box) ?? {
|
|
295
|
+
x: box.boundsX,
|
|
296
|
+
y: box.boundsY,
|
|
297
|
+
width: box.boundsWidth,
|
|
298
|
+
height: box.boundsHeight,
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
213
303
|
export function computePaintBounds(box: Box, moved = false): number {
|
|
214
304
|
// A box with no rectangle of its own gives only what it holds: nothing,
|
|
215
305
|
// until a child with bounds is met.
|
|
@@ -333,8 +423,21 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
333
423
|
// above has its bounds already: walked from its line as well, an
|
|
334
424
|
// inline-block in an inline-block was walked twice a level, and twenty of
|
|
335
425
|
// them took seventy milliseconds a layout.
|
|
426
|
+
//
|
|
427
|
+
// And a line's text is drawn from its face's ascent above the baseline to
|
|
428
|
+
// its descent below, its content area (CSS 2.1 10.6.1), which is past the
|
|
429
|
+
// line box wherever `line-height` is under the face's height: a line box
|
|
430
|
+
// is its line-height, whatever its glyphs are. Zen Garden 215's 91px title
|
|
431
|
+
// sits on the 20px lines its body's `line-height: 1.25em` gave it, as a
|
|
432
|
+
// length, and hangs 40px above them and 40px below — ink that a repaint
|
|
433
|
+
// of those rows has to reach, where the lines alone do not.
|
|
434
|
+
let above = 0;
|
|
435
|
+
let below = 0;
|
|
336
436
|
if (lines) {
|
|
437
|
+
const movedOff = MOVED_OFF_LINES.get(lines);
|
|
337
438
|
for (const line of lines) {
|
|
439
|
+
let top = line.y;
|
|
440
|
+
let bottom = line.y + line.height;
|
|
338
441
|
for (const placed of line.atomics) {
|
|
339
442
|
const atomic = placed.box;
|
|
340
443
|
if (atomic.boundsY === Infinity) continue;
|
|
@@ -342,9 +445,62 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
342
445
|
y1 = Math.min(y1, atomic.boundsY);
|
|
343
446
|
x2 = Math.max(x2, atomic.boundsX + atomic.boundsWidth);
|
|
344
447
|
y2 = Math.max(y2, atomic.boundsY + atomic.boundsHeight);
|
|
448
|
+
top = Math.min(top, atomic.boundsY);
|
|
449
|
+
bottom = Math.max(bottom, atomic.boundsY + atomic.boundsHeight);
|
|
450
|
+
}
|
|
451
|
+
// what `position: relative` moved off a line the cull finds on its
|
|
452
|
+
// own (`reachedOff`), wherever it went
|
|
453
|
+
if (movedOff?.has(line)) continue;
|
|
454
|
+
for (const text of line.texts) {
|
|
455
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
456
|
+
if (!natural) continue;
|
|
457
|
+
const baseline = text.drawY + natural.baseline;
|
|
458
|
+
top = Math.min(top, baseline - (natural.ascent ?? 0));
|
|
459
|
+
bottom = Math.max(bottom, baseline + (natural.descent ?? 0));
|
|
460
|
+
}
|
|
461
|
+
if (top < line.y) {
|
|
462
|
+
y1 = Math.min(y1, top);
|
|
463
|
+
above = Math.max(above, line.y - top);
|
|
464
|
+
}
|
|
465
|
+
if (bottom > line.y + line.height) {
|
|
466
|
+
y2 = Math.max(y2, bottom);
|
|
467
|
+
below = Math.max(below, bottom - line.y - line.height);
|
|
345
468
|
}
|
|
346
469
|
}
|
|
347
470
|
}
|
|
471
|
+
if (lines && (above > 0 || below > 0)) LINE_INK.set(lines, { above, below });
|
|
472
|
+
else if (lines) LINE_INK.delete(lines);
|
|
473
|
+
// a `clip-path` cuts the box and all it holds: no ink past the path, and
|
|
474
|
+
// none where it leaves nothing — though the scrollable overflow is what
|
|
475
|
+
// it was, as a browser has it, which is why a page that hides a box with
|
|
476
|
+
// one makes it a pixel square as well
|
|
477
|
+
if (own && box.style.clipPath) {
|
|
478
|
+
const { rect } = clipPathOf(box, DOCUMENT);
|
|
479
|
+
x1 = Math.max(x1, rect.x);
|
|
480
|
+
y1 = Math.max(y1, rect.y);
|
|
481
|
+
x2 = Math.min(x2, rect.x + rect.w);
|
|
482
|
+
y2 = Math.min(y2, rect.y + rect.h);
|
|
483
|
+
if (!(x2 > x1 && y2 > y1)) x1 = Infinity;
|
|
484
|
+
}
|
|
485
|
+
// A box that turns, scales or skews draws what it holds through its
|
|
486
|
+
// matrix (`paintTransformed`): its ink is where that puts it, the
|
|
487
|
+
// rectangle around the one it would have filled, and the one it would
|
|
488
|
+
// have filled is kept for the paint inside it, which is in its own
|
|
489
|
+
// coordinates. One flattened to nothing draws nothing.
|
|
490
|
+
const matrix = own ? placedMatrix(box) : null;
|
|
491
|
+
if (matrix && x1 !== Infinity) {
|
|
492
|
+
UNTRANSFORMED.set(box, { x: x1, y: y1, width: x2 - x1, height: y2 - y1 });
|
|
493
|
+
if (invert(matrix)) {
|
|
494
|
+
// no further than a length reaches (`LARGEST`): a page's
|
|
495
|
+
// `scale(1e30)` is a box as large as a browser holds one, and no
|
|
496
|
+
// larger
|
|
497
|
+
const to = mapRect(matrix, x1, y1, x2 - x1, y2 - y1);
|
|
498
|
+
x1 = Math.max(to.x, -LARGEST);
|
|
499
|
+
y1 = Math.max(to.y, -LARGEST);
|
|
500
|
+
x2 = Math.min(to.x + to.width, LARGEST);
|
|
501
|
+
y2 = Math.min(to.y + to.height, LARGEST);
|
|
502
|
+
} else x1 = Infinity;
|
|
503
|
+
}
|
|
348
504
|
if (x1 === Infinity) {
|
|
349
505
|
// nothing to draw: no damage meets it, and no parent takes it in
|
|
350
506
|
box.boundsX = Infinity;
|
|
@@ -367,12 +523,20 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
367
523
|
// whether the box clips only matters where its content reaches past it,
|
|
368
524
|
// and its style is one more object a walk of every box would read
|
|
369
525
|
const end = empty ? -Infinity : box.y + box.height;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
526
|
+
let reach = end;
|
|
527
|
+
if (bottom > end) {
|
|
528
|
+
const style = box.style;
|
|
529
|
+
reach =
|
|
530
|
+
box.parent &&
|
|
531
|
+
(style.overflowX !== 'visible' || style.overflowY !== 'visible')
|
|
532
|
+
? end
|
|
533
|
+
: bottom;
|
|
534
|
+
}
|
|
535
|
+
// the scrollable overflow of a transformed box is where its transform
|
|
536
|
+
// puts it (CSS Overflow 3, 2.2)
|
|
537
|
+
if (!matrix || reach === -Infinity) return reach;
|
|
538
|
+
const to = mapRect(matrix, box.x, box.y, box.width, reach - box.y);
|
|
539
|
+
return Math.min(to.y + to.height, LARGEST);
|
|
376
540
|
}
|
|
377
541
|
|
|
378
542
|
/**
|
|
@@ -521,11 +685,26 @@ export function paintDocument(
|
|
|
521
685
|
if (!canFill(ctx)) return;
|
|
522
686
|
ctx.save();
|
|
523
687
|
const canvas = canvasBackground(tree);
|
|
688
|
+
const scheme = rootScheme(tree);
|
|
689
|
+
if (scheme !== tree.paletteScheme) {
|
|
690
|
+
// A document is drawn on the window's own ground, the palette's, as an
|
|
691
|
+
// embedded one is on a transparent canvas. Where its root's colour
|
|
692
|
+
// scheme is not the scheme of what it is embedded in, its canvas is
|
|
693
|
+
// opaque, in the `Canvas` colour of its own (CSS Color Adjust 1, 2.2):
|
|
694
|
+
// a page that says it is light, and sets its text dark on no
|
|
695
|
+
// background, is not read against a dark window.
|
|
696
|
+
const area = canvasArea(tree.root, options);
|
|
697
|
+
if (area) {
|
|
698
|
+
ctx.fillStyle = SCHEME_COLORS[scheme].canvas;
|
|
699
|
+
ctx.fillRect(area.x, area.y, area.w, area.h);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
524
702
|
if (canvas) paintCanvas(ctx, canvas, tree.root, options);
|
|
525
703
|
paintBox(ctx, tree.root, {
|
|
526
704
|
...options,
|
|
527
705
|
canvasSource: canvas?.source,
|
|
528
706
|
negative: tree.negative,
|
|
707
|
+
selectionStyler: options.selection ? tree.selectionStyler : null,
|
|
529
708
|
});
|
|
530
709
|
ctx.restore();
|
|
531
710
|
}
|
|
@@ -577,6 +756,34 @@ function canvasBackground(tree: BoxTree): CanvasBackground | null {
|
|
|
577
756
|
: null;
|
|
578
757
|
}
|
|
579
758
|
|
|
759
|
+
/** The used colour scheme of the document's root element: the `<html>`'s,
|
|
760
|
+
* or that of whichever box stands in for it (`canvasBackground`). */
|
|
761
|
+
function rootScheme(tree: BoxTree): 'light' | 'dark' {
|
|
762
|
+
if (tree.impliedHtml) return tree.impliedHtml.colorScheme;
|
|
763
|
+
return (childNamed(tree.root, 'html') ?? tree.root).style.colorScheme;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** The canvas, cut to what is being painted: the whole element where the
|
|
767
|
+
* host says what that is, and else the root box. */
|
|
768
|
+
function canvasArea(
|
|
769
|
+
root: Box,
|
|
770
|
+
options: PaintOptions,
|
|
771
|
+
): { x: number; y: number; w: number; h: number } | null {
|
|
772
|
+
const whole = options.canvas ?? {
|
|
773
|
+
x: root.x + options.originX,
|
|
774
|
+
y: root.y + options.originY,
|
|
775
|
+
width: root.width,
|
|
776
|
+
height: root.height,
|
|
777
|
+
};
|
|
778
|
+
return clampRect(
|
|
779
|
+
options,
|
|
780
|
+
Math.round(whole.x),
|
|
781
|
+
Math.round(whole.y),
|
|
782
|
+
Math.ceil(whole.width),
|
|
783
|
+
Math.ceil(whole.height),
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
580
787
|
/** A box's child element of that name, or one inside the anonymous boxes
|
|
581
788
|
* it is wrapped in: a `<body>` in an `<html>` set `display: table` is in
|
|
582
789
|
* an anonymous row and cell. */
|
|
@@ -597,19 +804,7 @@ function paintCanvas(
|
|
|
597
804
|
root: Box,
|
|
598
805
|
options: PaintOptions,
|
|
599
806
|
): void {
|
|
600
|
-
const
|
|
601
|
-
x: root.x + options.originX,
|
|
602
|
-
y: root.y + options.originY,
|
|
603
|
-
width: root.width,
|
|
604
|
-
height: root.height,
|
|
605
|
-
};
|
|
606
|
-
const area = clampRect(
|
|
607
|
-
options,
|
|
608
|
-
Math.round(whole.x),
|
|
609
|
-
Math.round(whole.y),
|
|
610
|
-
Math.ceil(whole.width),
|
|
611
|
-
Math.ceil(whole.height),
|
|
612
|
-
);
|
|
807
|
+
const area = canvasArea(root, options);
|
|
613
808
|
if (!area) return;
|
|
614
809
|
const layers = layersOf(source) ?? [source];
|
|
615
810
|
const visible = source.visibility === 'visible';
|
|
@@ -619,7 +814,7 @@ function paintCanvas(
|
|
|
619
814
|
ctx.fillStyle = inkColor(style.backgroundColor as string, style.color);
|
|
620
815
|
ctx.fillRect(area.x, area.y, area.w, area.h);
|
|
621
816
|
}
|
|
622
|
-
if (style.backgroundGradient &&
|
|
817
|
+
if (style.backgroundGradient && visible) {
|
|
623
818
|
// sized by the root element's box and repeated down the canvas, as a
|
|
624
819
|
// browser does — the stripes a short page with a gradient on its
|
|
625
820
|
// body shows — or by the viewport, where it is fixed
|
|
@@ -882,28 +1077,312 @@ function paintBox(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
882
1077
|
return;
|
|
883
1078
|
}
|
|
884
1079
|
// `clip` shows the part of an absolutely positioned box it names, its own
|
|
885
|
-
// background and borders among it (CSS 2.1 11.1.2)
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1080
|
+
// background and borders among it (CSS 2.1 11.1.2), and `clip-path` the
|
|
1081
|
+
// part of any box (`pushOwnClips`) — the content painted here, not in a
|
|
1082
|
+
// call of its own: a frame more a level and a deep document ran out of
|
|
1083
|
+
// stack
|
|
1084
|
+
const pushed = pushOwnClips(ctx, box, options);
|
|
1085
|
+
if (pushed >= 0) {
|
|
1086
|
+
paintContent(ctx, box, pathClips(box) ? underPath(options) : options);
|
|
1087
|
+
for (let i = 0; i < pushed; i += 1) ctx.restore();
|
|
892
1088
|
}
|
|
893
1089
|
if (fade) ctx.restore();
|
|
894
1090
|
}
|
|
895
1091
|
|
|
896
|
-
/**
|
|
1092
|
+
/** How large a surface a transformed box is drawn on: a large window's
|
|
1093
|
+
* worth of pixels twice over, and no side longer than a pixmap's may be. */
|
|
1094
|
+
const RASTER_LIMIT = 16 * 1024 * 1024;
|
|
1095
|
+
const RASTER_SIDE = 16384;
|
|
1096
|
+
|
|
1097
|
+
/** What XRender carries a picture's transform in is 16.16 fixed point: a
|
|
1098
|
+
* number past this does not fit, and is thrown out of the request's
|
|
1099
|
+
* encoder. */
|
|
1100
|
+
const FIXED_LIMIT = 32000;
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* A box that turns, scales or skews (CSS Transforms 1): painted as it was
|
|
1104
|
+
* laid out, through its matrix — about its `transform-origin`, where its
|
|
1105
|
+
* translation has already put it (`placedMatrix`). What it holds is
|
|
1106
|
+
* painted with it, as the stacking context it is (`stacksLayers`), culled
|
|
1107
|
+
* against the damage taken back through the matrix.
|
|
1108
|
+
*
|
|
1109
|
+
* The native contexts draw everything through a matrix, a text layout's
|
|
1110
|
+
* outlines among it, glyphs and all (`scalesText`). ntk's draws paths
|
|
1111
|
+
* through one; a glyph it draws as it was shaped, upright and at its size,
|
|
1112
|
+
* where the matrix puts its origin, and an image through a transform the
|
|
1113
|
+
* server holds in fixed point, which a picture drawn small far across a
|
|
1114
|
+
* window does not fit. So there only a box that is paths and flat colour —
|
|
1115
|
+
* an icon, a chevron, a spinner — is drawn through the matrix, and any
|
|
1116
|
+
* other on a surface of its own, as it was laid out, with the surface
|
|
1117
|
+
* drawn through the matrix (`paintRaster`): a picture of its text rather
|
|
1118
|
+
* than the text, resampled, and right about where it is and which way up.
|
|
1119
|
+
*
|
|
1120
|
+
* A context with no `transform` draws the box where it was laid out: the
|
|
1121
|
+
* headless mock, which draws nothing, and a recording one.
|
|
1122
|
+
*/
|
|
1123
|
+
function paintTransformed(
|
|
1124
|
+
ctx: PaintContext,
|
|
1125
|
+
box: Box,
|
|
1126
|
+
options: PaintOptions,
|
|
1127
|
+
): void {
|
|
1128
|
+
const matrix = placedMatrix(box);
|
|
1129
|
+
if (!matrix) {
|
|
1130
|
+
paintBox(ctx, box, options);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
if (
|
|
1134
|
+
!intersects(box, options) ||
|
|
1135
|
+
COLLAPSED_CELLS.has(box) ||
|
|
1136
|
+
CLAMPED.has(box)
|
|
1137
|
+
) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (typeof ctx.transform !== 'function') {
|
|
1141
|
+
paintPlaced(ctx, box, options);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
// In the window's coordinates, where the document's origin is the
|
|
1145
|
+
// options': T(origin) · matrix · T(-origin). And about an origin moved
|
|
1146
|
+
// with the box's corner to the pixel it is drawn from: what is painted is
|
|
1147
|
+
// snapped to the pixel grid a box at a time, so a box a fraction of a
|
|
1148
|
+
// pixel down the page is drawn from a whole one, and turned about where
|
|
1149
|
+
// it was laid out it came back off the grid — a square mirrored onto
|
|
1150
|
+
// itself showed a line of what was under it along each side. A browser
|
|
1151
|
+
// snaps a transformed box's corner before its transform the same way.
|
|
1152
|
+
const left = box.x + options.originX;
|
|
1153
|
+
const top = box.y + options.originY;
|
|
1154
|
+
const ox = options.originX + Math.round(left) - left;
|
|
1155
|
+
const oy = options.originY + Math.round(top) - top;
|
|
1156
|
+
const [a, b, c, d] = matrix;
|
|
1157
|
+
const through: Matrix = [
|
|
1158
|
+
a,
|
|
1159
|
+
b,
|
|
1160
|
+
c,
|
|
1161
|
+
d,
|
|
1162
|
+
matrix[4] + ox - (a * ox + c * oy),
|
|
1163
|
+
matrix[5] + oy - (b * ox + d * oy),
|
|
1164
|
+
];
|
|
1165
|
+
const back = invert(through);
|
|
1166
|
+
if (!back) return;
|
|
1167
|
+
const damage = options.damage;
|
|
1168
|
+
const inside: PaintOptions = {
|
|
1169
|
+
...options,
|
|
1170
|
+
damage:
|
|
1171
|
+
damage && mapRect(back, damage.x, damage.y, damage.width, damage.height),
|
|
1172
|
+
};
|
|
1173
|
+
if (
|
|
1174
|
+
ctx.scalesText !== true &&
|
|
1175
|
+
!drawnAsPaths(box) &&
|
|
1176
|
+
paintRaster(ctx, box, inside, through)
|
|
1177
|
+
) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
ctx.save();
|
|
1181
|
+
ctx.transform(a, b, c, d, through[4], through[5]);
|
|
1182
|
+
paintPlaced(ctx, box, inside);
|
|
1183
|
+
ctx.restore();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Whether a box's style turns, scales or skews it, which is asked before
|
|
1188
|
+
* every box that may be one is painted (`paintPositioned`, `paintLines`) —
|
|
1189
|
+
* there, and not in `paintBox`, whose frame is one of three a level of
|
|
1190
|
+
* nesting costs the stack: a name more in it, and a document a thousand
|
|
1191
|
+
* boxes deep ran out.
|
|
1192
|
+
*/
|
|
1193
|
+
function turns(box: Box): boolean {
|
|
1194
|
+
const style = box.style;
|
|
1195
|
+
return (
|
|
1196
|
+
style.transform !== null || style.rotate !== null || style.scale !== null
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* `paintBox` for a box whose ink bounds are not in the coordinates it is
|
|
1202
|
+
* being painted in, and so is not culled by them: one painted through a
|
|
1203
|
+
* matrix, in its own (`paintTransformed`).
|
|
1204
|
+
*/
|
|
1205
|
+
function paintPlaced(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
1206
|
+
const opacity = opacityOf(box);
|
|
1207
|
+
if (opacity <= 0) return;
|
|
1208
|
+
const fade = opacity < 1 && typeof ctx.globalAlpha === 'number';
|
|
1209
|
+
if (fade) {
|
|
1210
|
+
ctx.save();
|
|
1211
|
+
ctx.globalAlpha = ctx.globalAlpha! * opacity;
|
|
1212
|
+
}
|
|
1213
|
+
if (!masked(box.style) || !paintMasked(ctx, box, options)) {
|
|
1214
|
+
paintClipped(ctx, box, options);
|
|
1215
|
+
}
|
|
1216
|
+
if (fade) ctx.restore();
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
/**
|
|
1220
|
+
* Whether all a box and what it holds draw is paths and flat colour:
|
|
1221
|
+
* backgrounds, borders, outlines and inline drawings — no text and no list
|
|
1222
|
+
* marker, whose glyphs a context may not turn, and no image, gradient,
|
|
1223
|
+
* shadow or mask, which one draws through a picture's transform
|
|
1224
|
+
* (`paintTransformed`).
|
|
1225
|
+
*/
|
|
1226
|
+
function drawnAsPaths(box: Box): boolean {
|
|
1227
|
+
if (box.subtreeTextEnd > box.subtreeTextStart) return false;
|
|
1228
|
+
const stack: Box[] = [box];
|
|
1229
|
+
while (stack.length) {
|
|
1230
|
+
const at = stack.pop()!;
|
|
1231
|
+
if (at.marker || at.replaced === 'image') return false;
|
|
1232
|
+
const style = at.style;
|
|
1233
|
+
if (
|
|
1234
|
+
style.backgroundImage !== null ||
|
|
1235
|
+
style.backgroundGradient !== null ||
|
|
1236
|
+
style.backgroundImages !== null ||
|
|
1237
|
+
style.boxShadow !== null ||
|
|
1238
|
+
style.borderImage.source !== null ||
|
|
1239
|
+
masked(style)
|
|
1240
|
+
) {
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
for (const child of at.children) stack.push(child);
|
|
1244
|
+
}
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* A transformed box painted on a surface of its own, as it was laid out,
|
|
1250
|
+
* and the surface drawn through its matrix: the part of what it draws that
|
|
1251
|
+
* the damage reaches. True where that is done — or is nothing to do, or
|
|
1252
|
+
* cannot be: a part too large for a surface, or a matrix that does not fit
|
|
1253
|
+
* the fixed point a picture's transform is sent in, which is one that draws
|
|
1254
|
+
* the box a thirtieth its size far across a window, or flatter than can be
|
|
1255
|
+
* seen. None of it is drawn then. False where the context has no surface to
|
|
1256
|
+
* draw on, and the caller draws the box through the matrix itself.
|
|
1257
|
+
*/
|
|
1258
|
+
function paintRaster(
|
|
1259
|
+
ctx: PaintContext,
|
|
1260
|
+
box: Box,
|
|
1261
|
+
options: PaintOptions,
|
|
1262
|
+
through: Matrix,
|
|
1263
|
+
): boolean {
|
|
1264
|
+
if (!options.surface || !ctx.drawImage) return false;
|
|
1265
|
+
const own = ownBounds(box);
|
|
1266
|
+
let x0 = Math.floor(own.x + options.originX);
|
|
1267
|
+
let y0 = Math.floor(own.y + options.originY);
|
|
1268
|
+
let x1 = Math.ceil(own.x + own.width + options.originX);
|
|
1269
|
+
let y1 = Math.ceil(own.y + own.height + options.originY);
|
|
1270
|
+
const damage = options.damage;
|
|
1271
|
+
if (damage) {
|
|
1272
|
+
// a pixel more each way: the edge of what is drawn is sampled from
|
|
1273
|
+
// beside it
|
|
1274
|
+
x0 = Math.max(x0, Math.floor(damage.x) - 1);
|
|
1275
|
+
y0 = Math.max(y0, Math.floor(damage.y) - 1);
|
|
1276
|
+
x1 = Math.min(x1, Math.ceil(damage.x + damage.width) + 1);
|
|
1277
|
+
y1 = Math.min(y1, Math.ceil(damage.y + damage.height) + 1);
|
|
1278
|
+
}
|
|
1279
|
+
const w = x1 - x0;
|
|
1280
|
+
const h = y1 - y0;
|
|
1281
|
+
if (!(w > 0 && h > 0)) return true;
|
|
1282
|
+
if (w > RASTER_SIDE || h > RASTER_SIDE || w * h > RASTER_LIMIT) return true;
|
|
1283
|
+
// what the context makes of the draw below: the picture's transform is
|
|
1284
|
+
// the inverse of its matrix after the surface's place in it
|
|
1285
|
+
const placed = invert([
|
|
1286
|
+
through[0],
|
|
1287
|
+
through[1],
|
|
1288
|
+
through[2],
|
|
1289
|
+
through[3],
|
|
1290
|
+
through[0] * x0 + through[2] * y0 + through[4],
|
|
1291
|
+
through[1] * x0 + through[3] * y0 + through[5],
|
|
1292
|
+
]);
|
|
1293
|
+
if (!placed || placed.some((n) => Math.abs(n) > FIXED_LIMIT)) return true;
|
|
1294
|
+
const surface = options.surface(w, h);
|
|
1295
|
+
if (!surface) return false;
|
|
1296
|
+
try {
|
|
1297
|
+
// the surface's own coordinates, the window's moved to its corner
|
|
1298
|
+
const on: PaintOptions = {
|
|
1299
|
+
...options,
|
|
1300
|
+
originX: options.originX - x0,
|
|
1301
|
+
originY: options.originY - y0,
|
|
1302
|
+
damage: { x: 0, y: 0, width: w, height: h },
|
|
1303
|
+
canvas: options.canvas && {
|
|
1304
|
+
...options.canvas,
|
|
1305
|
+
x: options.canvas.x - x0,
|
|
1306
|
+
y: options.canvas.y - y0,
|
|
1307
|
+
},
|
|
1308
|
+
viewport: options.viewport && {
|
|
1309
|
+
...options.viewport,
|
|
1310
|
+
x: options.viewport.x - x0,
|
|
1311
|
+
y: options.viewport.y - y0,
|
|
1312
|
+
},
|
|
1313
|
+
clips: [],
|
|
1314
|
+
};
|
|
1315
|
+
paintPlaced(surface.getContext('2d') as PaintContext, box, on);
|
|
1316
|
+
ctx.save();
|
|
1317
|
+
ctx.transform!(
|
|
1318
|
+
through[0],
|
|
1319
|
+
through[1],
|
|
1320
|
+
through[2],
|
|
1321
|
+
through[3],
|
|
1322
|
+
through[4],
|
|
1323
|
+
through[5],
|
|
1324
|
+
);
|
|
1325
|
+
ctx.drawImage(surface, x0, y0);
|
|
1326
|
+
ctx.restore();
|
|
1327
|
+
return true;
|
|
1328
|
+
} finally {
|
|
1329
|
+
surface.destroy?.();
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/** A box and what it holds, cut to its `clip` and its `clip-path` as
|
|
1334
|
+
* `paintBox` cuts it. */
|
|
897
1335
|
function paintClipped(
|
|
898
1336
|
ctx: PaintContext,
|
|
899
1337
|
box: Box,
|
|
900
1338
|
options: PaintOptions,
|
|
901
1339
|
): void {
|
|
902
|
-
const
|
|
903
|
-
if (
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1340
|
+
const pushed = pushOwnClips(ctx, box, options);
|
|
1341
|
+
if (pushed < 0) return;
|
|
1342
|
+
paintContent(ctx, box, pathClips(box) ? underPath(options) : options);
|
|
1343
|
+
for (let i = 0; i < pushed; i += 1) ctx.restore();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Cut what follows to what a box shows of itself: the part its `clip`
|
|
1348
|
+
* names, where it is absolutely positioned, and the part its `clip-path`
|
|
1349
|
+
* does (CSS Masking 1, 5.1). How many clips were pushed, for the caller to
|
|
1350
|
+
* restore — none where the context cannot clip — or -1 where they leave
|
|
1351
|
+
* none of the box to show, and nothing was pushed.
|
|
1352
|
+
*/
|
|
1353
|
+
function pushOwnClips(
|
|
1354
|
+
ctx: PaintContext,
|
|
1355
|
+
box: Box,
|
|
1356
|
+
options: PaintOptions,
|
|
1357
|
+
): number {
|
|
1358
|
+
const style = box.style;
|
|
1359
|
+
const clip = box.outOfFlow && style.clip ? clipOf(box, options) : null;
|
|
1360
|
+
if (clip && !(clip.w > 0 && clip.h > 0)) return -1;
|
|
1361
|
+
const path = pathClips(box) ? clipPathOf(box, options) : null;
|
|
1362
|
+
if (path && !(path.rect.w > 0 && path.rect.h > 0)) return -1;
|
|
1363
|
+
let pushed = 0;
|
|
1364
|
+
if (clip && pushClip(ctx, clip, null)) pushed += 1;
|
|
1365
|
+
if (path && pushClip(ctx, path.rect, path.radii)) pushed += 1;
|
|
1366
|
+
return pushed;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/** Whether a box is cut to a `clip-path`: one with a rectangle of its own
|
|
1370
|
+
* for the path to be measured in. An inline box is drawn on its block's
|
|
1371
|
+
* lines, and is not cut. */
|
|
1372
|
+
export function pathClips(box: Box): boolean {
|
|
1373
|
+
return box.style.clipPath !== null && hasRect(box);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* The options what a box cut to a `clip-path` holds is painted with: no
|
|
1378
|
+
* clip around it for a positioned box to escape. The path cuts all the
|
|
1379
|
+
* element holds, whatever that is positioned from, where a box that clips
|
|
1380
|
+
* its overflow lets out a box whose containing block is outside it — put
|
|
1381
|
+
* off until that clip ended (`paintPositioned`), one in here was painted
|
|
1382
|
+
* with the path gone too.
|
|
1383
|
+
*/
|
|
1384
|
+
function underPath(options: PaintOptions): PaintOptions {
|
|
1385
|
+
return { ...options, clips: [] };
|
|
907
1386
|
}
|
|
908
1387
|
|
|
909
1388
|
const MASK_LAYERS = new WeakMap<ComputedStyle, ComputedStyle[]>();
|
|
@@ -1146,6 +1625,13 @@ function paintOwnBackground(
|
|
|
1146
1625
|
if (box.kind === 'table-row' || box.kind === 'table-row-group') return;
|
|
1147
1626
|
if (hidden(box)) return;
|
|
1148
1627
|
const style = box.style;
|
|
1628
|
+
if (columned.any) {
|
|
1629
|
+
const pieces = COLUMN_PIECES.get(box);
|
|
1630
|
+
if (pieces) {
|
|
1631
|
+
paintColumnPieces(ctx, box, pieces, options);
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1149
1635
|
if (style.boxShadow) paintShadows(ctx, box, options, false);
|
|
1150
1636
|
if (box !== options.canvasSource) {
|
|
1151
1637
|
paintLayers(ctx, box, options, frameImages(ctx, box, options));
|
|
@@ -1157,6 +1643,88 @@ function paintOwnBackground(
|
|
|
1157
1643
|
if (box.kind === 'table') paintPartBackgrounds(ctx, box, options);
|
|
1158
1644
|
}
|
|
1159
1645
|
|
|
1646
|
+
/**
|
|
1647
|
+
* The background and the borders of a block a column break falls inside
|
|
1648
|
+
* (CSS Multi-column 1), a piece at a time: drawn in each column as the
|
|
1649
|
+
* whole box standing there, and cut to the piece of it that is — which is
|
|
1650
|
+
* `box-decoration-break: slice`, CSS's default (CSS Fragmentation 3, 6.1):
|
|
1651
|
+
* no border and no padding at a break, and the background going on from
|
|
1652
|
+
* one piece to the next. Its shadow is not drawn: a sliced box casts one as
|
|
1653
|
+
* a whole and this has no whole box to cast it from.
|
|
1654
|
+
*/
|
|
1655
|
+
function paintColumnPieces(
|
|
1656
|
+
ctx: PaintContext,
|
|
1657
|
+
box: Box,
|
|
1658
|
+
pieces: readonly ColumnPiece[],
|
|
1659
|
+
options: PaintOptions,
|
|
1660
|
+
): void {
|
|
1661
|
+
if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
|
|
1662
|
+
return;
|
|
1663
|
+
for (const piece of pieces) {
|
|
1664
|
+
if (!(piece.height > 0 && piece.width > 0)) continue;
|
|
1665
|
+
const frame: Frame = {
|
|
1666
|
+
x: piece.x,
|
|
1667
|
+
y: piece.wholeY,
|
|
1668
|
+
width: piece.width,
|
|
1669
|
+
height: piece.wholeHeight,
|
|
1670
|
+
captionTop: 0,
|
|
1671
|
+
captionBottom: 0,
|
|
1672
|
+
borderTop: box.borderTop,
|
|
1673
|
+
borderRight: box.borderRight,
|
|
1674
|
+
borderBottom: box.borderBottom,
|
|
1675
|
+
borderLeft: box.borderLeft,
|
|
1676
|
+
padTop: box.padTop,
|
|
1677
|
+
padRight: box.padRight,
|
|
1678
|
+
padBottom: box.padBottom,
|
|
1679
|
+
padLeft: box.padLeft,
|
|
1680
|
+
style: box.style,
|
|
1681
|
+
};
|
|
1682
|
+
const left = Math.round(piece.x + options.originX);
|
|
1683
|
+
const top = Math.round(piece.y + options.originY);
|
|
1684
|
+
const right = Math.round(piece.x + piece.width + options.originX);
|
|
1685
|
+
const bottom = Math.round(piece.y + piece.height + options.originY);
|
|
1686
|
+
ctx.save();
|
|
1687
|
+
ctx.beginPath();
|
|
1688
|
+
ctx.rect(left, top, right - left, bottom - top);
|
|
1689
|
+
ctx.clip();
|
|
1690
|
+
paintLayers(ctx, frame, options, frameImages(ctx, frame, options));
|
|
1691
|
+
paintBorders(ctx, frame, options);
|
|
1692
|
+
ctx.restore();
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Clip to the rows of a text's layout that are in the text's column
|
|
1698
|
+
* (`COLUMN_ROWS`), where the layout is drawn at `left` and `top`: the rest
|
|
1699
|
+
* of it is another column's, drawn there. Answers whether it clipped, and
|
|
1700
|
+
* the caller restores.
|
|
1701
|
+
*/
|
|
1702
|
+
function clipToRows(
|
|
1703
|
+
ctx: PaintContext,
|
|
1704
|
+
text: LineText,
|
|
1705
|
+
rows: { top: number; bottom: number },
|
|
1706
|
+
left: number,
|
|
1707
|
+
top: number,
|
|
1708
|
+
): boolean {
|
|
1709
|
+
if (!ctx.save || !ctx.restore || !ctx.beginPath || !ctx.rect || !ctx.clip)
|
|
1710
|
+
return false;
|
|
1711
|
+
const y1 = Math.round(top + rows.top);
|
|
1712
|
+
const y2 = Math.round(top + rows.bottom);
|
|
1713
|
+
ctx.save();
|
|
1714
|
+
ctx.beginPath();
|
|
1715
|
+
ctx.rect(
|
|
1716
|
+
Math.floor(left) - ROW_REACH,
|
|
1717
|
+
y1,
|
|
1718
|
+
Math.ceil(text.layout.width) + 2 * ROW_REACH,
|
|
1719
|
+
y2 - y1,
|
|
1720
|
+
);
|
|
1721
|
+
ctx.clip();
|
|
1722
|
+
return true;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
/** How far to either side of its layout a column's text may draw. */
|
|
1726
|
+
const ROW_REACH = 2048;
|
|
1727
|
+
|
|
1160
1728
|
/**
|
|
1161
1729
|
* How `paintLayers` draws a frame's image layers: each in the area its
|
|
1162
1730
|
* `background-clip` names, from the box its `background-origin` names
|
|
@@ -1578,6 +2146,8 @@ function inFlow(parent: Box, child: Box, options: PaintOptions): boolean {
|
|
|
1578
2146
|
style.overflowY === 'visible' &&
|
|
1579
2147
|
opacityOf(child) >= 1 &&
|
|
1580
2148
|
!masked(style) &&
|
|
2149
|
+
// cut to a path with all it holds, as one group
|
|
2150
|
+
!style.clipPath &&
|
|
1581
2151
|
// containment makes it a stacking context, painted whole
|
|
1582
2152
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT)
|
|
1583
2153
|
);
|
|
@@ -1606,6 +2176,7 @@ function flowFlex(parent: Box, child: Box, options: PaintOptions): boolean {
|
|
|
1606
2176
|
style.overflowY === 'visible' &&
|
|
1607
2177
|
!(child.outOfFlow && style.clip) &&
|
|
1608
2178
|
!masked(style) &&
|
|
2179
|
+
!style.clipPath &&
|
|
1609
2180
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT)
|
|
1610
2181
|
);
|
|
1611
2182
|
}
|
|
@@ -1696,7 +2267,7 @@ function paintFlowLines(
|
|
|
1696
2267
|
* beyond its background's edge. */
|
|
1697
2268
|
function overflowClip(
|
|
1698
2269
|
box: Box,
|
|
1699
|
-
options:
|
|
2270
|
+
options: ClipSpace,
|
|
1700
2271
|
): { x: number; y: number; w: number; h: number } {
|
|
1701
2272
|
const left = box.x + options.originX;
|
|
1702
2273
|
const top = frameY(box) + options.originY;
|
|
@@ -1722,7 +2293,7 @@ function overflowClip(
|
|
|
1722
2293
|
*/
|
|
1723
2294
|
function clipEdge(
|
|
1724
2295
|
box: Box,
|
|
1725
|
-
options:
|
|
2296
|
+
options: ClipSpace,
|
|
1726
2297
|
): {
|
|
1727
2298
|
rect: { x: number; y: number; w: number; h: number };
|
|
1728
2299
|
radii: Corners | null;
|
|
@@ -1878,7 +2449,9 @@ function paintPositioned(
|
|
|
1878
2449
|
return;
|
|
1879
2450
|
}
|
|
1880
2451
|
}
|
|
1881
|
-
|
|
2452
|
+
// through its matrix, where it turns, scales or skews
|
|
2453
|
+
if (turns(box)) paintTransformed(ctx, box, atViewport(box, options));
|
|
2454
|
+
else paintBox(ctx, box, atViewport(box, options));
|
|
1882
2455
|
}
|
|
1883
2456
|
|
|
1884
2457
|
/**
|
|
@@ -1917,12 +2490,19 @@ export const FIXED_BOXES = new WeakMap<object, Box[]>();
|
|
|
1917
2490
|
export function fixedToViewport(box: Box): boolean {
|
|
1918
2491
|
if (box.style.position !== 'fixed') return false;
|
|
1919
2492
|
for (let at = box.parent; at?.parent; at = at.parent) {
|
|
1920
|
-
if (at
|
|
1921
|
-
if (contained(at, CONTAIN_LAYOUT | CONTAIN_PAINT)) return false;
|
|
2493
|
+
if (holdsFixed(at)) return false;
|
|
1922
2494
|
}
|
|
1923
2495
|
return true;
|
|
1924
2496
|
}
|
|
1925
2497
|
|
|
2498
|
+
/** Whether a box is the containing block of the fixed boxes in it: a
|
|
2499
|
+
* transformed box, or one with layout or paint containment. */
|
|
2500
|
+
function holdsFixed(box: Box): boolean {
|
|
2501
|
+
return (
|
|
2502
|
+
transformed(box.style) || contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT)
|
|
2503
|
+
);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
1926
2506
|
function rootOf(box: Box): Box {
|
|
1927
2507
|
let root = box;
|
|
1928
2508
|
while (root.parent) root = root.parent;
|
|
@@ -1932,17 +2512,22 @@ function rootOf(box: Box): Box {
|
|
|
1932
2512
|
/**
|
|
1933
2513
|
* The box an out-of-flow box is positioned in, whose clips are the ones it
|
|
1934
2514
|
* is under (`paintPositioned`, and the hit test's `deepestAt`): its nearest
|
|
1935
|
-
* positioned or
|
|
1936
|
-
* which is under none
|
|
2515
|
+
* positioned or transformed ancestor, or the root. Null for a box fixed to
|
|
2516
|
+
* the viewport, which is under none — one in a transformed box is fixed to
|
|
2517
|
+
* that, and under what it is under.
|
|
1937
2518
|
*/
|
|
1938
2519
|
export function containingBlockOf(box: Box): Box | null {
|
|
1939
|
-
if (box.style.position === 'fixed')
|
|
2520
|
+
if (box.style.position === 'fixed') {
|
|
2521
|
+
for (let at = box.parent; at?.parent; at = at.parent) {
|
|
2522
|
+
if (holdsFixed(at)) return at;
|
|
2523
|
+
}
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
1940
2526
|
let containing = box.parent;
|
|
1941
2527
|
while (
|
|
1942
2528
|
containing?.parent &&
|
|
1943
2529
|
containing.style.position === 'static' &&
|
|
1944
|
-
!containing.style
|
|
1945
|
-
!containing.style.transformTranslate
|
|
2530
|
+
!transformed(containing.style)
|
|
1946
2531
|
) {
|
|
1947
2532
|
containing = containing.parent;
|
|
1948
2533
|
}
|
|
@@ -2026,7 +2611,7 @@ export function clipsOverflow(box: Box): boolean {
|
|
|
2026
2611
|
/** A box's `clip` region, in window coordinates. */
|
|
2027
2612
|
function clipOf(
|
|
2028
2613
|
box: Box,
|
|
2029
|
-
options:
|
|
2614
|
+
options: ClipSpace,
|
|
2030
2615
|
): { x: number; y: number; w: number; h: number } {
|
|
2031
2616
|
const clip = box.style.clip!;
|
|
2032
2617
|
const left = clip.left ?? 0;
|
|
@@ -2043,6 +2628,212 @@ function clipOf(
|
|
|
2043
2628
|
};
|
|
2044
2629
|
}
|
|
2045
2630
|
|
|
2631
|
+
/** What placing a clip reads of a paint's options: where the document's
|
|
2632
|
+
* origin is, its scale, and how far an axis left open has to reach. */
|
|
2633
|
+
type ClipSpace = Pick<PaintOptions, 'originX' | 'originY' | 'scale' | 'damage'>;
|
|
2634
|
+
|
|
2635
|
+
/** The document's own coordinates, as a space to place a clip in. */
|
|
2636
|
+
const DOCUMENT: ClipSpace = { originX: 0, originY: 0, scale: 1, damage: null };
|
|
2637
|
+
|
|
2638
|
+
/**
|
|
2639
|
+
* A box's `clip-path` in window coordinates (CSS Masking 1, 5.1): the
|
|
2640
|
+
* rectangle its shape is (CSS Shapes 1, 3.1), each edge on the pixel it
|
|
2641
|
+
* falls nearest as a box's are, and its corners where it has any — of no
|
|
2642
|
+
* area where the shape leaves none of the box to show. Measured in the box
|
|
2643
|
+
* the path names, the border box unless it says: `inset()` in from its
|
|
2644
|
+
* edges, `rect()` and `xywh()` from its top left, and the box itself, with
|
|
2645
|
+
* the element's own corners, where no shape is written.
|
|
2646
|
+
*/
|
|
2647
|
+
function clipPathOf(
|
|
2648
|
+
box: Box,
|
|
2649
|
+
options: ClipSpace,
|
|
2650
|
+
): {
|
|
2651
|
+
rect: { x: number; y: number; w: number; h: number };
|
|
2652
|
+
radii: Corners | null;
|
|
2653
|
+
} {
|
|
2654
|
+
const path = box.style.clipPath!;
|
|
2655
|
+
// the box it is measured in, in from the border box — or out from it
|
|
2656
|
+
const within = path.box;
|
|
2657
|
+
const [it, ir, ib, il] =
|
|
2658
|
+
within === 'margin-box'
|
|
2659
|
+
? [-box.marginTop, -box.marginRight, -box.marginBottom, -box.marginLeft]
|
|
2660
|
+
: edgeInsets(box, within);
|
|
2661
|
+
const width = Math.max(0, box.width - il - ir);
|
|
2662
|
+
const height = Math.max(0, frameHeight(box) - it - ib);
|
|
2663
|
+
// how far in from that box's edges the rectangle's are
|
|
2664
|
+
const [a, b, c, d] = path.lengths;
|
|
2665
|
+
let top = 0;
|
|
2666
|
+
let right = 0;
|
|
2667
|
+
let bottom = 0;
|
|
2668
|
+
let left = 0;
|
|
2669
|
+
if (path.shape === 'inset') {
|
|
2670
|
+
top = resolve(a, height);
|
|
2671
|
+
right = resolve(b, width);
|
|
2672
|
+
bottom = resolve(c, height);
|
|
2673
|
+
left = resolve(d, width);
|
|
2674
|
+
// a pair that adds up to more than the box is across is reduced to
|
|
2675
|
+
// it, each in its proportion, as overlapping radii are (3.1):
|
|
2676
|
+
// `inset(50%)` and `inset(75%)` are both the box's centre, and nothing
|
|
2677
|
+
if (left + right > width && left > 0 && right > 0) {
|
|
2678
|
+
const f = Math.max(0, width) / (left + right);
|
|
2679
|
+
left *= f;
|
|
2680
|
+
right *= f;
|
|
2681
|
+
}
|
|
2682
|
+
if (top + bottom > height && top > 0 && bottom > 0) {
|
|
2683
|
+
const f = Math.max(0, height) / (top + bottom);
|
|
2684
|
+
top *= f;
|
|
2685
|
+
bottom *= f;
|
|
2686
|
+
}
|
|
2687
|
+
} else if (path.shape === 'rect') {
|
|
2688
|
+
// four edges from the top and the left, `auto` the box's own; the
|
|
2689
|
+
// right and the bottom are no less than the left and the top
|
|
2690
|
+
top = a === AUTO ? 0 : resolve(a, height);
|
|
2691
|
+
left = d === AUTO ? 0 : resolve(d, width);
|
|
2692
|
+
right = b === AUTO ? 0 : width - Math.max(left, resolve(b, width));
|
|
2693
|
+
bottom = c === AUTO ? 0 : height - Math.max(top, resolve(c, height));
|
|
2694
|
+
} else if (path.shape === 'xywh') {
|
|
2695
|
+
left = resolve(a, width);
|
|
2696
|
+
top = resolve(b, height);
|
|
2697
|
+
right = width - left - Math.max(0, resolve(c, width));
|
|
2698
|
+
bottom = height - top - Math.max(0, resolve(d, height));
|
|
2699
|
+
}
|
|
2700
|
+
const x0 = box.x + options.originX + il;
|
|
2701
|
+
const y0 = frameY(box) + options.originY + it;
|
|
2702
|
+
const x = Math.round(x0 + left);
|
|
2703
|
+
const y = Math.round(y0 + top);
|
|
2704
|
+
const rect = {
|
|
2705
|
+
x,
|
|
2706
|
+
y,
|
|
2707
|
+
w: Math.max(0, Math.round(x0 + width - right) - x),
|
|
2708
|
+
h: Math.max(0, Math.round(y0 + height - bottom) - y),
|
|
2709
|
+
};
|
|
2710
|
+
let radii: Corners | null = null;
|
|
2711
|
+
if (path.shape === null) {
|
|
2712
|
+
// the box alone: its edge, shaped as the element's corners shape it
|
|
2713
|
+
const corners = cornersOf(box.style, box.width, frameHeight(box));
|
|
2714
|
+
if (corners) {
|
|
2715
|
+
radii =
|
|
2716
|
+
within === 'margin-box'
|
|
2717
|
+
? spreadCorners(
|
|
2718
|
+
corners,
|
|
2719
|
+
box.width,
|
|
2720
|
+
frameHeight(box),
|
|
2721
|
+
-it,
|
|
2722
|
+
-ir,
|
|
2723
|
+
-ib,
|
|
2724
|
+
-il,
|
|
2725
|
+
)
|
|
2726
|
+
: roundedOf(insetCorners(corners, it, ir, ib, il));
|
|
2727
|
+
}
|
|
2728
|
+
} else if (path.radii) {
|
|
2729
|
+
// a percentage is of the box the shape is in, and the radii are fitted
|
|
2730
|
+
// to the rectangle they round
|
|
2731
|
+
radii = cornersFor(path.radii, path.radiiY, width, height, rect.w, rect.h);
|
|
2732
|
+
}
|
|
2733
|
+
return { rect, radii };
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
/** Corners, or null where every one of them is square. */
|
|
2737
|
+
function roundedOf(c: Corners): Corners | null {
|
|
2738
|
+
for (let i = 0; i < 4; i += 1) if (c.x[i] > 0 && c.y[i] > 0) return c;
|
|
2739
|
+
return null;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
/**
|
|
2743
|
+
* Whether a point of the document is in what a box's `clip-path` leaves of
|
|
2744
|
+
* it: what a hit test asks, since nothing the path cuts away — of the box
|
|
2745
|
+
* or of anything in it — is under a pointer (`deepestAt`).
|
|
2746
|
+
*/
|
|
2747
|
+
export function inClipPath(box: Box, x: number, y: number): boolean {
|
|
2748
|
+
const { rect, radii } = clipPathOf(box, DOCUMENT);
|
|
2749
|
+
const right = rect.x + rect.w;
|
|
2750
|
+
const bottom = rect.y + rect.h;
|
|
2751
|
+
if (x < rect.x || x >= right || y < rect.y || y >= bottom) return false;
|
|
2752
|
+
if (!radii) return true;
|
|
2753
|
+
// inside the rectangle, and past a corner's curve: each corner is a
|
|
2754
|
+
// quarter of an ellipse about a centre its radii in from the two edges
|
|
2755
|
+
for (let i = 0; i < 4; i += 1) {
|
|
2756
|
+
const rx = radii.x[i];
|
|
2757
|
+
const ry = radii.y[i];
|
|
2758
|
+
if (!(rx > 0 && ry > 0)) continue;
|
|
2759
|
+
const onLeft = i === 0 || i === 3;
|
|
2760
|
+
const cx = onLeft ? rect.x + rx : right - rx;
|
|
2761
|
+
const cy = i < 2 ? rect.y + ry : bottom - ry;
|
|
2762
|
+
if (onLeft ? x >= cx : x <= cx) continue;
|
|
2763
|
+
if (i < 2 ? y >= cy : y <= cy) continue;
|
|
2764
|
+
const dx = (x - cx) / rx;
|
|
2765
|
+
const dy = (y - cy) / ry;
|
|
2766
|
+
if (dx * dx + dy * dy > 1) return false;
|
|
2767
|
+
}
|
|
2768
|
+
return true;
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
/**
|
|
2772
|
+
* Whether a point of the document is in the part of a box its `clip` shows
|
|
2773
|
+
* (CSS 2.1 11.1.2) — anywhere, for a box that is cut to none: what a hit
|
|
2774
|
+
* test asks, since what the clip cuts away is not drawn (`deepestAt`). What
|
|
2775
|
+
* it cuts away is the box and what is painted with it: all the box holds
|
|
2776
|
+
* where it is a stacking context (`stacksLayers`), and elsewhere all but a
|
|
2777
|
+
* fixed box, which the context around it paints under no clip
|
|
2778
|
+
* (`clipsFor`). Chrome cuts that one too.
|
|
2779
|
+
*/
|
|
2780
|
+
export function inClip(box: Box, x: number, y: number): boolean {
|
|
2781
|
+
if (!box.outOfFlow || !box.style.clip) return true;
|
|
2782
|
+
const rect = clipOf(box, DOCUMENT);
|
|
2783
|
+
return (
|
|
2784
|
+
x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2788
|
+
/**
|
|
2789
|
+
* The part of the document a box shows through, in the document's own
|
|
2790
|
+
* pixels, or null where nothing cuts it: the `clip` it is cut to (CSS 2.1
|
|
2791
|
+
* 11.1.2), and the clips of the boxes it is under — each that clips its
|
|
2792
|
+
* overflow or is cut to a `clip` of its own, from its containing block up
|
|
2793
|
+
* (11.1.1), so an absolute box is outside a box that clips where its
|
|
2794
|
+
* containing block is, and a fixed one outside them all. And every
|
|
2795
|
+
* `clip-path` from the box up, its own among them: a path cuts all its
|
|
2796
|
+
* element holds, whatever that is positioned from (CSS Masking 1, 5.1) —
|
|
2797
|
+
* to the rectangle a path's shape is, its corners left square. A rectangle
|
|
2798
|
+
* of no area where none of it shows. Painting cuts what it draws as it
|
|
2799
|
+
* walks down; this is for what is not painted here and has to be cut all
|
|
2800
|
+
* the same, a control's widget (`controlRectsOf`).
|
|
2801
|
+
*/
|
|
2802
|
+
export function clipAround(box: Box, scale = 1): Rect | null {
|
|
2803
|
+
const space: ClipSpace = { originX: 0, originY: 0, scale, damage: null };
|
|
2804
|
+
let x0 = -Infinity;
|
|
2805
|
+
let y0 = -Infinity;
|
|
2806
|
+
let x1 = Infinity;
|
|
2807
|
+
let y1 = Infinity;
|
|
2808
|
+
let cut = false;
|
|
2809
|
+
const under = (rect: { x: number; y: number; w: number; h: number }) => {
|
|
2810
|
+
cut = true;
|
|
2811
|
+
x0 = Math.max(x0, rect.x);
|
|
2812
|
+
y0 = Math.max(y0, rect.y);
|
|
2813
|
+
x1 = Math.min(x1, rect.x + rect.w);
|
|
2814
|
+
y1 = Math.min(y1, rect.y + rect.h);
|
|
2815
|
+
};
|
|
2816
|
+
if (box.outOfFlow && box.style.clip) under(clipOf(box, space));
|
|
2817
|
+
// what holds a box, as clips go: its parent, or out of the flow its
|
|
2818
|
+
// containing block, and the boxes between them do not clip it
|
|
2819
|
+
const holder = (inner: Box): Box | null =>
|
|
2820
|
+
inner.outOfFlow ? containingBlockOf(inner) : inner.parent;
|
|
2821
|
+
for (let outer = holder(box); outer; outer = holder(outer)) {
|
|
2822
|
+
if (outer.outOfFlow && outer.style.clip) under(clipOf(outer, space));
|
|
2823
|
+
if (clipsOverflow(outer)) under(clipEdge(outer, space).rect);
|
|
2824
|
+
}
|
|
2825
|
+
for (let at: Box | null = box; at; at = at.parent) {
|
|
2826
|
+
if (pathClips(at)) under(clipPathOf(at, space).rect);
|
|
2827
|
+
}
|
|
2828
|
+
if (!cut) return null;
|
|
2829
|
+
return {
|
|
2830
|
+
x: x0,
|
|
2831
|
+
y: y0,
|
|
2832
|
+
width: Math.max(0, x1 - x0),
|
|
2833
|
+
height: Math.max(0, y1 - y0),
|
|
2834
|
+
};
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2046
2837
|
/**
|
|
2047
2838
|
* The radii of a box's padding edge — its border radii less the borders —
|
|
2048
2839
|
* for a clip, or null where a rectangle clips the same: padding at least a
|
|
@@ -2123,6 +2914,24 @@ function cornersOf(style: ComputedStyle, w: number, h: number): Corners | null {
|
|
|
2123
2914
|
) {
|
|
2124
2915
|
return null;
|
|
2125
2916
|
}
|
|
2917
|
+
return cornersFor(across, down, w, h, w, h);
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
/**
|
|
2921
|
+
* Radii as corners in pixels: each a length or a percentage of a box `w`
|
|
2922
|
+
* by `h`, across and down, and all of them reduced together where two on a
|
|
2923
|
+
* side of the rectangle they round — `fitW` by `fitH`, which for a
|
|
2924
|
+
* `border-radius` is that box, and for a `clip-path`'s `round` is the
|
|
2925
|
+
* rectangle inside it — would overlap. Null where every corner is square.
|
|
2926
|
+
*/
|
|
2927
|
+
function cornersFor(
|
|
2928
|
+
across: readonly Len[],
|
|
2929
|
+
down: readonly Len[] | null,
|
|
2930
|
+
w: number,
|
|
2931
|
+
h: number,
|
|
2932
|
+
fitW: number,
|
|
2933
|
+
fitH: number,
|
|
2934
|
+
): Corners | null {
|
|
2126
2935
|
const vertical = down ?? across;
|
|
2127
2936
|
const x: Corners['x'] = [0, 0, 0, 0];
|
|
2128
2937
|
const y: Corners['y'] = [0, 0, 0, 0];
|
|
@@ -2140,10 +2949,10 @@ function cornersOf(style: ComputedStyle, w: number, h: number): Corners | null {
|
|
|
2140
2949
|
const fit = (side: number, sum: number): void => {
|
|
2141
2950
|
if (sum > side) f = Math.min(f, Math.max(0, side) / sum);
|
|
2142
2951
|
};
|
|
2143
|
-
fit(
|
|
2144
|
-
fit(
|
|
2145
|
-
fit(
|
|
2146
|
-
fit(
|
|
2952
|
+
fit(fitW, x[0] + x[1]);
|
|
2953
|
+
fit(fitH, y[1] + y[2]);
|
|
2954
|
+
fit(fitW, x[2] + x[3]);
|
|
2955
|
+
fit(fitH, y[3] + y[0]);
|
|
2147
2956
|
if (f < 1) {
|
|
2148
2957
|
for (let i = 0; i < 4; i += 1) {
|
|
2149
2958
|
x[i] *= f;
|
|
@@ -2295,8 +3104,7 @@ function layered(parent: Box, child: Box): boolean {
|
|
|
2295
3104
|
if (
|
|
2296
3105
|
style.position !== 'relative' &&
|
|
2297
3106
|
style.position !== 'sticky' &&
|
|
2298
|
-
!style
|
|
2299
|
-
!style.transformTranslate
|
|
3107
|
+
!transformed(style)
|
|
2300
3108
|
) {
|
|
2301
3109
|
return false;
|
|
2302
3110
|
}
|
|
@@ -2310,7 +3118,7 @@ function layered(parent: Box, child: Box): boolean {
|
|
|
2310
3118
|
* drawn twice — darker at every antialiased edge — and a translucent
|
|
2311
3119
|
* background had its alpha doubled.
|
|
2312
3120
|
*/
|
|
2313
|
-
function onLine(parent: Box, child: Box): boolean {
|
|
3121
|
+
export function onLine(parent: Box, child: Box): boolean {
|
|
2314
3122
|
if (parent.lines === null && parent.kind !== 'inline') return false;
|
|
2315
3123
|
if (child.isFloat || child.outOfFlow) return false;
|
|
2316
3124
|
return (
|
|
@@ -2384,20 +3192,26 @@ function settleLayers(box: Box, list: Box[]): void {
|
|
|
2384
3192
|
* it itself, and the ones below its flow: positioned with a `z-index`, or a
|
|
2385
3193
|
* flex item with one; fixed or sticky with none (CSS Positioned Layout 3, as
|
|
2386
3194
|
* browsers paint them — a fixed header's box set behind its content with
|
|
2387
|
-
* `z-index: -1` went behind the page);
|
|
3195
|
+
* `z-index: -1` went behind the page); transformed (CSS Transforms 1, 3);
|
|
3196
|
+
* or under full opacity (CSS Color 4
|
|
2388
3197
|
* 3.2), its own or the opacity it takes from an inline box it broke
|
|
2389
3198
|
* (`FADED_BLOCKS`), which is painted as one group — faded with it, or at
|
|
2390
3199
|
* `opacity: 0` not at all, where its root context drew a hover menu's
|
|
2391
3200
|
* absolute children.
|
|
2392
3201
|
*/
|
|
2393
|
-
function stacksLayers(box: Box): boolean {
|
|
3202
|
+
export function stacksLayers(box: Box): boolean {
|
|
2394
3203
|
const style = box.style;
|
|
2395
3204
|
if (style.position === 'fixed' || style.position === 'sticky') return true;
|
|
2396
3205
|
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
|
-
|
|
3206
|
+
// and so does a mask, which is applied to the group (CSS Masking 1, 7),
|
|
3207
|
+
// and a clip path, which cuts it (5.1)
|
|
3208
|
+
if (masked(style) || pathClips(box)) return true;
|
|
2399
3209
|
// layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
|
|
2400
3210
|
if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT)) return true;
|
|
3211
|
+
// and a transform (CSS Transforms 1, 3), on a box one applies to: what it
|
|
3212
|
+
// holds is painted with it, through its matrix, and not by a context
|
|
3213
|
+
// outside it
|
|
3214
|
+
if (transformed(style) && box.kind !== 'inline') return true;
|
|
2401
3215
|
if (typeof style.zIndex !== 'number') return false;
|
|
2402
3216
|
return style.position !== 'static' || flexItem(box);
|
|
2403
3217
|
}
|
|
@@ -2421,7 +3235,7 @@ export function clipsFor(box: Box, context: Box): Box[] {
|
|
|
2421
3235
|
const fixed = box.style.position === 'fixed';
|
|
2422
3236
|
while (from && from !== context) {
|
|
2423
3237
|
const style = from.style;
|
|
2424
|
-
if (style
|
|
3238
|
+
if (transformed(style)) break;
|
|
2425
3239
|
if (!fixed && style.position !== 'static') break;
|
|
2426
3240
|
from = from.parent;
|
|
2427
3241
|
}
|
|
@@ -2517,9 +3331,19 @@ function hoistFrom(box: Box, root: boolean): Box[] | null {
|
|
|
2517
3331
|
}
|
|
2518
3332
|
|
|
2519
3333
|
function byZIndex(a: Box, b: Box): number {
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
3334
|
+
return layerOf(a) - layerOf(b);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
/** The `z-index` a box is ordered by among its stacking context's layers:
|
|
3338
|
+
* its own where one applies — to a positioned box, and to a flex or a grid
|
|
3339
|
+
* item (CSS 2.1 9.9.1, CSS Flexbox 5.4) — and none for a box that is
|
|
3340
|
+
* among them for its transform alone, which is painted in the document's
|
|
3341
|
+
* order whatever `z-index` it was given. */
|
|
3342
|
+
function layerOf(box: Box): number {
|
|
3343
|
+
const style = box.style;
|
|
3344
|
+
const z = style.zIndex;
|
|
3345
|
+
if (z === 'auto') return 0;
|
|
3346
|
+
return style.position !== 'static' || flexItem(box) ? z : 0;
|
|
2523
3347
|
}
|
|
2524
3348
|
|
|
2525
3349
|
/** Whether anything this box or its descendants draw is in the damage. */
|
|
@@ -2619,7 +3443,7 @@ function paintBackground(
|
|
|
2619
3443
|
ctx.fillStyle = inkColor(color as string, style.color);
|
|
2620
3444
|
fill();
|
|
2621
3445
|
}
|
|
2622
|
-
if (gradient
|
|
3446
|
+
if (gradient) {
|
|
2623
3447
|
// over the colour, as the layer an image is: the size of the padding
|
|
2624
3448
|
// box, which is where it starts, and repeated under the borders — or
|
|
2625
3449
|
// the viewport's, where it is fixed; its line runs across the whole of
|
|
@@ -2641,7 +3465,7 @@ function paintBackground(
|
|
|
2641
3465
|
// one fill in the rounded shape; under the borders it carries on
|
|
2642
3466
|
// with its end colours where a browser would show the next tile
|
|
2643
3467
|
if (!(at.width > 0 && at.height > 0)) return;
|
|
2644
|
-
|
|
3468
|
+
const paint = gradientFill(
|
|
2645
3469
|
ctx,
|
|
2646
3470
|
gradient,
|
|
2647
3471
|
at.x,
|
|
@@ -2651,7 +3475,7 @@ function paintBackground(
|
|
|
2651
3475
|
style.color,
|
|
2652
3476
|
rect,
|
|
2653
3477
|
);
|
|
2654
|
-
|
|
3478
|
+
if (paint) fillGradient(ctx, paint, rect, rounded);
|
|
2655
3479
|
} else paintGradient(ctx, style, gradient, rect, at);
|
|
2656
3480
|
}
|
|
2657
3481
|
}
|
|
@@ -2681,7 +3505,7 @@ const NO_SIZE: IntrinsicSize = { width: null, height: null, ratio: 0 };
|
|
|
2681
3505
|
function paintGradient(
|
|
2682
3506
|
ctx: PaintContext,
|
|
2683
3507
|
style: ComputedStyle,
|
|
2684
|
-
gradient:
|
|
3508
|
+
gradient: Gradient,
|
|
2685
3509
|
area: { x: number; y: number; w: number; h: number },
|
|
2686
3510
|
at: Rect,
|
|
2687
3511
|
): void {
|
|
@@ -2730,17 +3554,8 @@ function paintGradient(
|
|
|
2730
3554
|
) {
|
|
2731
3555
|
// a sliver of a root repeated down a long canvas: the one tile, and
|
|
2732
3556
|
// its end colours on past it
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
gradient,
|
|
2736
|
-
x0,
|
|
2737
|
-
y0,
|
|
2738
|
-
w,
|
|
2739
|
-
h,
|
|
2740
|
-
style.color,
|
|
2741
|
-
area,
|
|
2742
|
-
);
|
|
2743
|
-
ctx.fillRect(area.x, area.y, area.w, area.h);
|
|
3557
|
+
const paint = gradientFill(ctx, gradient, x0, y0, w, h, style.color, area);
|
|
3558
|
+
if (paint) fillGradient(ctx, paint, area);
|
|
2744
3559
|
return;
|
|
2745
3560
|
}
|
|
2746
3561
|
for (let y = fromY; y < toY; y += stepY) {
|
|
@@ -2752,17 +3567,8 @@ function paintGradient(
|
|
|
2752
3567
|
const right = Math.min(x + w, area.x + area.w);
|
|
2753
3568
|
if (right <= left) continue;
|
|
2754
3569
|
const tile = { x: left, y: top, w: right - left, h: bottom - top };
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
gradient,
|
|
2758
|
-
x,
|
|
2759
|
-
y,
|
|
2760
|
-
w,
|
|
2761
|
-
h,
|
|
2762
|
-
style.color,
|
|
2763
|
-
tile,
|
|
2764
|
-
);
|
|
2765
|
-
ctx.fillRect(tile.x, tile.y, tile.w, tile.h);
|
|
3570
|
+
const paint = gradientFill(ctx, gradient, x, y, w, h, style.color, tile);
|
|
3571
|
+
if (paint) fillGradient(ctx, paint, tile);
|
|
2766
3572
|
}
|
|
2767
3573
|
}
|
|
2768
3574
|
}
|
|
@@ -2794,40 +3600,19 @@ function linearGradient(
|
|
|
2794
3600
|
? turn
|
|
2795
3601
|
: gradient.corner === 'bottom right'
|
|
2796
3602
|
? Math.PI - turn
|
|
2797
|
-
: gradient.corner === 'bottom left'
|
|
2798
|
-
? Math.PI + turn
|
|
2799
|
-
: 2 * Math.PI - turn;
|
|
2800
|
-
}
|
|
2801
|
-
const dx = Math.sin(angle);
|
|
2802
|
-
const dy = -Math.cos(angle);
|
|
2803
|
-
const length = Math.abs(w * dx) + Math.abs(h * dy);
|
|
2804
|
-
const stops = gradient.stops;
|
|
2805
|
-
const at: (number | null)[] = stops.map((stop) =>
|
|
2806
|
-
stop.at === null
|
|
2807
|
-
? null
|
|
2808
|
-
: length > 0
|
|
2809
|
-
? resolve(stop.at, length) / length
|
|
2810
|
-
: 0,
|
|
2811
|
-
);
|
|
2812
|
-
if (at[0] === null) at[0] = 0;
|
|
2813
|
-
if (at[at.length - 1] === null) at[at.length - 1] = 1;
|
|
2814
|
-
for (let i = 1; i < at.length; i += 1) {
|
|
2815
|
-
if (at[i] === null) {
|
|
2816
|
-
// spread the ones without a position between the ones with
|
|
2817
|
-
let j = i;
|
|
2818
|
-
while (at[j] === null) j += 1;
|
|
2819
|
-
const from = at[i - 1]!;
|
|
2820
|
-
const to = at[j]!;
|
|
2821
|
-
for (let k = i; k < j; k += 1) {
|
|
2822
|
-
at[k] = from + ((to - from) * (k - i + 1)) / (j - i + 1);
|
|
2823
|
-
}
|
|
2824
|
-
}
|
|
2825
|
-
at[i] = Math.max(at[i]!, at[i - 1]!);
|
|
3603
|
+
: gradient.corner === 'bottom left'
|
|
3604
|
+
? Math.PI + turn
|
|
3605
|
+
: 2 * Math.PI - turn;
|
|
2826
3606
|
}
|
|
3607
|
+
const dx = Math.sin(angle);
|
|
3608
|
+
const dy = -Math.cos(angle);
|
|
3609
|
+
const length = Math.abs(w * dx) + Math.abs(h * dy);
|
|
3610
|
+
const stops = gradient.stops;
|
|
3611
|
+
const at = stopOffsets(stops, length);
|
|
2827
3612
|
// the context's offsets are 0 to 1, so a line that stops reach past runs
|
|
2828
3613
|
// from the first to the last, and the box sees the part of it between
|
|
2829
|
-
const from = Math.min(0, at[0]
|
|
2830
|
-
const to = Math.max(1, at[at.length - 1]
|
|
3614
|
+
const from = Math.min(0, at[0]);
|
|
3615
|
+
const to = Math.max(1, at[at.length - 1]);
|
|
2831
3616
|
const startX = x + w / 2 - (dx * length) / 2;
|
|
2832
3617
|
const startY = y + h / 2 - (dy * length) / 2;
|
|
2833
3618
|
const x0 = startX + dx * length * from;
|
|
@@ -2858,11 +3643,278 @@ function linearGradient(
|
|
|
2858
3643
|
}
|
|
2859
3644
|
const g = ctx.createLinearGradient!(x0, y0, x1, y1);
|
|
2860
3645
|
for (let i = 0; i < stops.length; i += 1) {
|
|
2861
|
-
g.addColorStop((at[i]
|
|
3646
|
+
g.addColorStop((at[i] - from) / (to - from), colors[i]);
|
|
2862
3647
|
}
|
|
2863
3648
|
return g;
|
|
2864
3649
|
}
|
|
2865
3650
|
|
|
3651
|
+
/**
|
|
3652
|
+
* Where each of a gradient's stops is along a line `length` long, as a
|
|
3653
|
+
* fraction of it (CSS Images 3, 3.5.1): a stop without a position is
|
|
3654
|
+
* spread evenly between its neighbours', the first at 0 and the last at 1
|
|
3655
|
+
* where they have none, and none is before the one before it.
|
|
3656
|
+
*/
|
|
3657
|
+
function stopOffsets(stops: GradientStop[], length: number): number[] {
|
|
3658
|
+
const at: (number | null)[] = stops.map((stop) =>
|
|
3659
|
+
stop.at === null
|
|
3660
|
+
? null
|
|
3661
|
+
: length > 0
|
|
3662
|
+
? resolve(stop.at, length) / length
|
|
3663
|
+
: 0,
|
|
3664
|
+
);
|
|
3665
|
+
if (at[0] === null) at[0] = 0;
|
|
3666
|
+
if (at[at.length - 1] === null) at[at.length - 1] = 1;
|
|
3667
|
+
for (let i = 1; i < at.length; i += 1) {
|
|
3668
|
+
if (at[i] === null) {
|
|
3669
|
+
// spread the ones without a position between the ones with
|
|
3670
|
+
let j = i;
|
|
3671
|
+
while (at[j] === null) j += 1;
|
|
3672
|
+
const from = at[i - 1]!;
|
|
3673
|
+
const to = at[j]!;
|
|
3674
|
+
for (let k = i; k < j; k += 1) {
|
|
3675
|
+
at[k] = from + ((to - from) * (k - i + 1)) / (j - i + 1);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
at[i] = Math.max(at[i]!, at[i - 1]!);
|
|
3679
|
+
}
|
|
3680
|
+
return at as number[];
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
/** What a gradient fills with: the context's gradient, or the one colour
|
|
3684
|
+
* it comes to, and the matrix it is sampled through where it is an
|
|
3685
|
+
* ellipse — a context's radial gradients are circles, and an ellipse is
|
|
3686
|
+
* one stretched along its longer axis. */
|
|
3687
|
+
interface GradientFill {
|
|
3688
|
+
style: unknown;
|
|
3689
|
+
matrix: [number, number, number, number, number, number] | null;
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
/** A gradient's fill for a box at (`x`, `y`), `w` by `h`: null where the
|
|
3693
|
+
* context draws no gradient of its kind. */
|
|
3694
|
+
function gradientFill(
|
|
3695
|
+
ctx: PaintContext,
|
|
3696
|
+
gradient: Gradient,
|
|
3697
|
+
x: number,
|
|
3698
|
+
y: number,
|
|
3699
|
+
w: number,
|
|
3700
|
+
h: number,
|
|
3701
|
+
currentColor: string,
|
|
3702
|
+
visible: { x: number; y: number; w: number; h: number } | null = null,
|
|
3703
|
+
): GradientFill | null {
|
|
3704
|
+
if (gradient.kind === 'radial') {
|
|
3705
|
+
return radialGradient(ctx, gradient, x, y, w, h, currentColor, visible);
|
|
3706
|
+
}
|
|
3707
|
+
if (!ctx.createLinearGradient) return null;
|
|
3708
|
+
return {
|
|
3709
|
+
style: linearGradient(ctx, gradient, x, y, w, h, currentColor, visible),
|
|
3710
|
+
matrix: null,
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
|
|
3714
|
+
/**
|
|
3715
|
+
* A `radial-gradient()` across a box (CSS Images 3, 3.2.2): centred where
|
|
3716
|
+
* its position puts it, and as large as its radii, or as its keyword says
|
|
3717
|
+
* — the side or the corner of the box nearest the centre or furthest from
|
|
3718
|
+
* it, and for an ellipse through a corner, the ellipse with the shape the
|
|
3719
|
+
* sides would give it. The stops are spaced along the ray from the centre
|
|
3720
|
+
* to the ending shape's edge, which for an ellipse is its horizontal
|
|
3721
|
+
* radius; one before the centre, where the ray starts, gives the colour
|
|
3722
|
+
* there and no more, and one past the edge carries the gradient on.
|
|
3723
|
+
*
|
|
3724
|
+
* A shape with no width or no height is drawn as its last colour, as
|
|
3725
|
+
* Chrome draws each (crbug.com/635727): the spec draws a zero width as a
|
|
3726
|
+
* gradient mirrored about the centre line, which Chrome does not.
|
|
3727
|
+
*/
|
|
3728
|
+
function radialGradient(
|
|
3729
|
+
ctx: PaintContext,
|
|
3730
|
+
gradient: RadialGradient,
|
|
3731
|
+
x: number,
|
|
3732
|
+
y: number,
|
|
3733
|
+
w: number,
|
|
3734
|
+
h: number,
|
|
3735
|
+
currentColor: string,
|
|
3736
|
+
visible: { x: number; y: number; w: number; h: number } | null = null,
|
|
3737
|
+
): GradientFill | null {
|
|
3738
|
+
const colors = gradient.stops.map((stop) =>
|
|
3739
|
+
inkColor(stop.color, currentColor),
|
|
3740
|
+
);
|
|
3741
|
+
const cx = resolve(gradient.at[0], w);
|
|
3742
|
+
const cy = resolve(gradient.at[1], h);
|
|
3743
|
+
let rx: number;
|
|
3744
|
+
let ry: number;
|
|
3745
|
+
if (gradient.radii) {
|
|
3746
|
+
rx = resolve(gradient.radii[0], w);
|
|
3747
|
+
ry = resolve(gradient.radii[1], h);
|
|
3748
|
+
} else {
|
|
3749
|
+
const extent = gradient.extent ?? 'farthest-corner';
|
|
3750
|
+
const side = extent.startsWith('closest') ? Math.min : Math.max;
|
|
3751
|
+
const sx = side(Math.abs(cx), Math.abs(w - cx));
|
|
3752
|
+
const sy = side(Math.abs(cy), Math.abs(h - cy));
|
|
3753
|
+
const corner = extent.endsWith('corner');
|
|
3754
|
+
if (gradient.circle) {
|
|
3755
|
+
rx = ry = corner ? Math.hypot(sx, sy) : side(sx, sy);
|
|
3756
|
+
} else {
|
|
3757
|
+
// through the corner, in the shape the sides give it
|
|
3758
|
+
rx = corner ? sx * Math.SQRT2 : sx;
|
|
3759
|
+
ry = corner ? sy * Math.SQRT2 : sy;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
if (!(rx > 0 && ry > 0)) {
|
|
3763
|
+
return { style: colors[colors.length - 1], matrix: null };
|
|
3764
|
+
}
|
|
3765
|
+
if (!hasRadialGradients(ctx)) return null;
|
|
3766
|
+
let at = stopOffsets(gradient.stops, rx);
|
|
3767
|
+
let inks = colors;
|
|
3768
|
+
if (at[0] < 0) {
|
|
3769
|
+
// the colour at the centre, between the stops either side of it
|
|
3770
|
+
const first = colorAlong(at, colors, 0);
|
|
3771
|
+
const i = at.findIndex((t) => t > 0);
|
|
3772
|
+
if (i < 0) return { style: first, matrix: null };
|
|
3773
|
+
at = [0, ...at.slice(i)];
|
|
3774
|
+
inks = [first, ...colors.slice(i)];
|
|
3775
|
+
}
|
|
3776
|
+
const ax = x + cx;
|
|
3777
|
+
const ay = y + cy;
|
|
3778
|
+
const seen = visible ?? { x, y, w, h };
|
|
3779
|
+
if (!(Math.abs(ax) < RADIAL_REACH && Math.abs(ay) < RADIAL_REACH)) {
|
|
3780
|
+
return farRadial(ctx, at, inks, ax, ay, rx, ry, seen);
|
|
3781
|
+
}
|
|
3782
|
+
let to = Math.max(1, at[at.length - 1]);
|
|
3783
|
+
if (Math.max(rx, ry) * to > GRADIENT_REACH) {
|
|
3784
|
+
// as far along the ray as what is seen of it reaches, and no further:
|
|
3785
|
+
// a circle a context cannot carry, cut to the part it draws
|
|
3786
|
+
let reach = 0;
|
|
3787
|
+
for (const px of [seen.x, seen.x + seen.w]) {
|
|
3788
|
+
for (const py of [seen.y, seen.y + seen.h]) {
|
|
3789
|
+
reach = Math.max(reach, Math.hypot((px - ax) / rx, (py - ay) / ry));
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
if (reach > 0 && reach < to) {
|
|
3793
|
+
const last = colorAlong(at, inks, reach);
|
|
3794
|
+
const kept = at.filter((t) => t < reach).length;
|
|
3795
|
+
at = [...at.slice(0, kept), reach];
|
|
3796
|
+
inks = [...inks.slice(0, kept), last];
|
|
3797
|
+
to = reach;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
const circle = rx === ry;
|
|
3801
|
+
// a circle as wide as the ellipse is narrow, stretched along the other
|
|
3802
|
+
// axis: the matrix only ever widens, so its inverse, which a context
|
|
3803
|
+
// samples the gradient through, reaches no further than the centre does
|
|
3804
|
+
const r = Math.min(rx, ry);
|
|
3805
|
+
const g = circle
|
|
3806
|
+
? ctx.createRadialGradient!(ax, ay, 0, ax, ay, r * to)
|
|
3807
|
+
: ctx.createRadialGradient!(0, 0, 0, 0, 0, r * to);
|
|
3808
|
+
for (let i = 0; i < at.length; i += 1) {
|
|
3809
|
+
g.addColorStop(at[i] / to, inks[i]);
|
|
3810
|
+
}
|
|
3811
|
+
return {
|
|
3812
|
+
style: g,
|
|
3813
|
+
matrix: circle ? null : [rx / r, 0, 0, ry / r, ax, ay],
|
|
3814
|
+
};
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
/**
|
|
3818
|
+
* How far from the origin a radial gradient's centre may be and still be
|
|
3819
|
+
* the context's to draw: ntk makes a gradient past what X RENDER carries at
|
|
3820
|
+
* a scale that fits, and its 16.16 matrix holds that scale to a fraction of
|
|
3821
|
+
* a pixel to here.
|
|
3822
|
+
*/
|
|
3823
|
+
const RADIAL_REACH = 1 << 18;
|
|
3824
|
+
|
|
3825
|
+
/**
|
|
3826
|
+
* A radial gradient whose centre is further off than a context carries —
|
|
3827
|
+
* `at 0 calc(infinity * 1px)`, which comes to the largest length there is —
|
|
3828
|
+
* across the part of it that is seen: from that far its rings are as good
|
|
3829
|
+
* as straight, so it is drawn as the linear gradient that runs the way the
|
|
3830
|
+
* rings spread there, as fast as they do, cut to what is seen as a linear
|
|
3831
|
+
* gradient's line is. The rings bend away from it by the square of what is
|
|
3832
|
+
* seen over eight times the distance, under a pixel from here.
|
|
3833
|
+
*/
|
|
3834
|
+
function farRadial(
|
|
3835
|
+
ctx: PaintContext,
|
|
3836
|
+
at: number[],
|
|
3837
|
+
colors: string[],
|
|
3838
|
+
ax: number,
|
|
3839
|
+
ay: number,
|
|
3840
|
+
rx: number,
|
|
3841
|
+
ry: number,
|
|
3842
|
+
seen: { x: number; y: number; w: number; h: number },
|
|
3843
|
+
): GradientFill | null {
|
|
3844
|
+
if (!ctx.createLinearGradient) return null;
|
|
3845
|
+
// the middle of what is seen, in the circle's own space, and how fast
|
|
3846
|
+
// the gradient runs there, a pixel
|
|
3847
|
+
const px = seen.x + seen.w / 2;
|
|
3848
|
+
const py = seen.y + seen.h / 2;
|
|
3849
|
+
const ux = (px - ax) / rx;
|
|
3850
|
+
const uy = (py - ay) / ry;
|
|
3851
|
+
const t = Math.hypot(ux, uy);
|
|
3852
|
+
const gx = ux / rx / t;
|
|
3853
|
+
const gy = uy / ry / t;
|
|
3854
|
+
const g2 = gx * gx + gy * gy;
|
|
3855
|
+
if (!(t > 0 && g2 > 0 && Number.isFinite(t / g2))) {
|
|
3856
|
+
return { style: colorAlong(at, colors, t), matrix: null };
|
|
3857
|
+
}
|
|
3858
|
+
// the line it runs along, from where it would start to where it is 1
|
|
3859
|
+
const lx = gx / g2;
|
|
3860
|
+
const ly = gy / g2;
|
|
3861
|
+
return {
|
|
3862
|
+
style: clippedGradient(
|
|
3863
|
+
ctx,
|
|
3864
|
+
at,
|
|
3865
|
+
colors,
|
|
3866
|
+
px - lx * t,
|
|
3867
|
+
py - ly * t,
|
|
3868
|
+
lx,
|
|
3869
|
+
ly,
|
|
3870
|
+
seen,
|
|
3871
|
+
),
|
|
3872
|
+
matrix: null,
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
|
|
3876
|
+
/**
|
|
3877
|
+
* Whether a context draws radial gradients. react-x11's macOS and Windows
|
|
3878
|
+
* contexts have the method and paint what it returns flat, in one colour
|
|
3879
|
+
* — over whatever a vignette was meant to let through — and theirs takes
|
|
3880
|
+
* no circles, which is how it is told from one that draws them: ntk's and
|
|
3881
|
+
* the Wayland context's take the six numbers canvas gives it. There a
|
|
3882
|
+
* radial gradient is drawn as nothing, as every one was.
|
|
3883
|
+
*/
|
|
3884
|
+
function hasRadialGradients(ctx: PaintContext): boolean {
|
|
3885
|
+
return (ctx.createRadialGradient?.length ?? 0) >= 6;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
/**
|
|
3889
|
+
* Fills a rectangle, rounded where `corners` says, with a gradient. An
|
|
3890
|
+
* ellipse's shape is laid down first and filled under its matrix, which a
|
|
3891
|
+
* context applies to the fill's paint and not to a path it already has.
|
|
3892
|
+
*/
|
|
3893
|
+
function fillGradient(
|
|
3894
|
+
ctx: PaintContext,
|
|
3895
|
+
fill: GradientFill,
|
|
3896
|
+
r: { x: number; y: number; w: number; h: number },
|
|
3897
|
+
corners: Corners | null = null,
|
|
3898
|
+
): void {
|
|
3899
|
+
ctx.fillStyle = fill.style;
|
|
3900
|
+
if (!fill.matrix) {
|
|
3901
|
+
if (corners) {
|
|
3902
|
+
ctx.beginPath!();
|
|
3903
|
+
roundedRect(ctx, r.x, r.y, r.w, r.h, corners);
|
|
3904
|
+
ctx.fill!();
|
|
3905
|
+
} else ctx.fillRect(r.x, r.y, r.w, r.h);
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3908
|
+
if (!ctx.transform || !ctx.beginPath || !ctx.rect || !ctx.fill) return;
|
|
3909
|
+
ctx.save();
|
|
3910
|
+
ctx.beginPath();
|
|
3911
|
+
if (corners) roundedRect(ctx, r.x, r.y, r.w, r.h, corners);
|
|
3912
|
+
else ctx.rect(r.x, r.y, r.w, r.h);
|
|
3913
|
+
ctx.transform(...fill.matrix);
|
|
3914
|
+
ctx.fill();
|
|
3915
|
+
ctx.restore();
|
|
3916
|
+
}
|
|
3917
|
+
|
|
2866
3918
|
/**
|
|
2867
3919
|
* How far from the origin a gradient's line may end. X RENDER takes the
|
|
2868
3920
|
* ends in 16.16 fixed point, and a pair past ±32,767 pixels threw from the
|
|
@@ -2918,30 +3970,32 @@ function clippedGradient(
|
|
|
2918
3970
|
cx + (t1 - tc) * lx,
|
|
2919
3971
|
cy + (t1 - tc) * ly,
|
|
2920
3972
|
);
|
|
2921
|
-
|
|
2922
|
-
const colorAt = (t: number): string => {
|
|
2923
|
-
if (!(t > at[0])) return colors[0];
|
|
2924
|
-
for (let i = 1; i < at.length; i += 1) {
|
|
2925
|
-
if (t > at[i]) continue;
|
|
2926
|
-
const span = at[i] - at[i - 1];
|
|
2927
|
-
const f = span > 0 ? (t - at[i - 1]) / span : 1;
|
|
2928
|
-
return (
|
|
2929
|
-
blend(colors[i - 1], colors[i], f) ??
|
|
2930
|
-
(f < 0.5 ? colors[i - 1] : colors[i])
|
|
2931
|
-
);
|
|
2932
|
-
}
|
|
2933
|
-
return colors[colors.length - 1];
|
|
2934
|
-
};
|
|
2935
|
-
g.addColorStop(0, colorAt(t0));
|
|
3973
|
+
g.addColorStop(0, colorAlong(at, colors, t0));
|
|
2936
3974
|
for (let i = 0; i < at.length; i += 1) {
|
|
2937
3975
|
if (at[i] > t0 && at[i] < t1) {
|
|
2938
3976
|
g.addColorStop((at[i] - t0) / (t1 - t0), colors[i]);
|
|
2939
3977
|
}
|
|
2940
3978
|
}
|
|
2941
|
-
g.addColorStop(1,
|
|
3979
|
+
g.addColorStop(1, colorAlong(at, colors, t1));
|
|
2942
3980
|
return g;
|
|
2943
3981
|
}
|
|
2944
3982
|
|
|
3983
|
+
/** The colour at a place along a gradient whose stops are at `at`: a
|
|
3984
|
+
* stop's, or between two. */
|
|
3985
|
+
function colorAlong(at: number[], colors: string[], t: number): string {
|
|
3986
|
+
if (!(t > at[0])) return colors[0];
|
|
3987
|
+
for (let i = 1; i < at.length; i += 1) {
|
|
3988
|
+
if (t > at[i]) continue;
|
|
3989
|
+
const span = at[i] - at[i - 1];
|
|
3990
|
+
const f = span > 0 ? (t - at[i - 1]) / span : 1;
|
|
3991
|
+
return (
|
|
3992
|
+
blend(colors[i - 1], colors[i], f) ??
|
|
3993
|
+
(f < 0.5 ? colors[i - 1] : colors[i])
|
|
3994
|
+
);
|
|
3995
|
+
}
|
|
3996
|
+
return colors[colors.length - 1];
|
|
3997
|
+
}
|
|
3998
|
+
|
|
2945
3999
|
/**
|
|
2946
4000
|
* The backgrounds of a table's column groups, columns, row groups and rows
|
|
2947
4001
|
* (CSS 2.1 17.5.1), in that order: over the table's own and under its
|
|
@@ -3349,16 +4403,6 @@ function tileSize(
|
|
|
3349
4403
|
return [area.width, area.height];
|
|
3350
4404
|
}
|
|
3351
4405
|
|
|
3352
|
-
/**
|
|
3353
|
-
* Borders, as four rectangles.
|
|
3354
|
-
*
|
|
3355
|
-
* Not as a stroked path, for the reason richtext gives about its underlines:
|
|
3356
|
-
* the mock backend has no path API, and a 1px border on a pixel grid is a
|
|
3357
|
-
* rectangle rather than something an antialiased stroke improves. Corners are
|
|
3358
|
-
* mitred by drawing the top and bottom full-width and the sides between them,
|
|
3359
|
-
* which is right whenever the two sides share a colour and close enough when
|
|
3360
|
-
* they do not.
|
|
3361
|
-
*/
|
|
3362
4406
|
/**
|
|
3363
4407
|
* A box's outline (CSS 2.1 18.4, CSS UI 4 5): a border of its own width,
|
|
3364
4408
|
* style and colour round the border box grown by `outline-offset` — which
|
|
@@ -3417,6 +4461,25 @@ function paintOutline(
|
|
|
3417
4461
|
);
|
|
3418
4462
|
}
|
|
3419
4463
|
|
|
4464
|
+
/**
|
|
4465
|
+
* Borders, a side at a time.
|
|
4466
|
+
*
|
|
4467
|
+
* As rectangles where they can be, the top and the bottom full width and
|
|
4468
|
+
* the sides between them, and not as a stroked path, for the reason
|
|
4469
|
+
* richtext gives about its underlines: the mock backend has no path API, and
|
|
4470
|
+
* a 1px border on a pixel grid is a rectangle rather than something an
|
|
4471
|
+
* antialiased stroke improves. That is every border whose sides share a
|
|
4472
|
+
* colour, where a corner looks the same whichever side it is given to.
|
|
4473
|
+
*
|
|
4474
|
+
* Where two sides that meet differ in colour — one of them transparent
|
|
4475
|
+
* included — the corner is cut on the diagonal from its outer point to its
|
|
4476
|
+
* inner one and each side takes its half (CSS Backgrounds 3, 4.3), so a side
|
|
4477
|
+
* is a trapezoid, and a triangle where the box inside the borders has no
|
|
4478
|
+
* width: the CSS triangle a dropdown's caret and a tooltip's arrow are
|
|
4479
|
+
* drawn with, which came out as the rectangle around it. Only a solid side
|
|
4480
|
+
* is cut, and only against a side that is solid or paints nothing; dots,
|
|
4481
|
+
* dashes and a double border's two lines keep their rectangles.
|
|
4482
|
+
*/
|
|
3420
4483
|
function paintBorders(
|
|
3421
4484
|
ctx: PaintContext,
|
|
3422
4485
|
box: Frame,
|
|
@@ -3436,6 +4499,30 @@ function paintBorders(
|
|
|
3436
4499
|
if (roundedRing(ctx, box, x, y, w, h, options)) return;
|
|
3437
4500
|
if (sculpted(s) && paintSculpted(ctx, box, x, y, w, h, options)) return;
|
|
3438
4501
|
|
|
4502
|
+
const t = box.borderTop;
|
|
4503
|
+
const r = box.borderRight;
|
|
4504
|
+
const b = box.borderBottom;
|
|
4505
|
+
const l = box.borderLeft;
|
|
4506
|
+
/** What a side is painted in, null where it paints nothing. */
|
|
4507
|
+
const ink = (width: number, color: string): string | null =>
|
|
4508
|
+
width > 0 && !isTransparent(color) ? inkColor(color, s.color) : null;
|
|
4509
|
+
const topInk = ink(t, s.borderTopColor);
|
|
4510
|
+
const rightInk = ink(r, s.borderRightColor);
|
|
4511
|
+
const bottomInk = ink(b, s.borderBottomColor);
|
|
4512
|
+
const leftInk = ink(l, s.borderLeftColor);
|
|
4513
|
+
// the corners cut on the diagonal; none where the context draws no path,
|
|
4514
|
+
// and the sides are rectangles
|
|
4515
|
+
const paths = !!(ctx.beginPath && ctx.moveTo && ctx.lineTo && ctx.fill);
|
|
4516
|
+
const topStyle = s.borderTopStyle;
|
|
4517
|
+
const rightStyle = s.borderRightStyle;
|
|
4518
|
+
const bottomStyle = s.borderBottomStyle;
|
|
4519
|
+
const leftStyle = s.borderLeftStyle;
|
|
4520
|
+
const tl = paths && mitred(t, l, topInk, leftInk, topStyle, leftStyle);
|
|
4521
|
+
const tr = paths && mitred(t, r, topInk, rightInk, topStyle, rightStyle);
|
|
4522
|
+
const br =
|
|
4523
|
+
paths && mitred(b, r, bottomInk, rightInk, bottomStyle, rightStyle);
|
|
4524
|
+
const bl = paths && mitred(b, l, bottomInk, leftInk, bottomStyle, leftStyle);
|
|
4525
|
+
|
|
3439
4526
|
const edge = (
|
|
3440
4527
|
ex: number,
|
|
3441
4528
|
ey: number,
|
|
@@ -3447,47 +4534,130 @@ function paintBorders(
|
|
|
3447
4534
|
): void => {
|
|
3448
4535
|
const rect = clampRect(options, ex, ey, ew, eh);
|
|
3449
4536
|
if (!rect) return;
|
|
3450
|
-
ctx.fillStyle =
|
|
4537
|
+
ctx.fillStyle = color;
|
|
3451
4538
|
// The un-clamped start is the dash phase's origin, so the pattern does
|
|
3452
4539
|
// not crawl as the viewport moves along a long edge.
|
|
3453
4540
|
fillEdge(ctx, rect, horizontal ? ex : ey, style, horizontal);
|
|
3454
4541
|
};
|
|
3455
|
-
|
|
3456
|
-
|
|
4542
|
+
/** A side with a cut corner: its outer edge and then its inner one. */
|
|
4543
|
+
const side = (points: number[], color: string): void => {
|
|
4544
|
+
const area = clampRect(options, x, y, w, h);
|
|
4545
|
+
if (!area) return;
|
|
4546
|
+
ctx.fillStyle = color;
|
|
4547
|
+
fillPolygon(ctx, points, area);
|
|
4548
|
+
};
|
|
4549
|
+
// the border box's far edges, and the padding box inside it
|
|
4550
|
+
const x1 = x + w;
|
|
4551
|
+
const y1 = y + h;
|
|
4552
|
+
const px0 = x + l;
|
|
4553
|
+
const py0 = y + t;
|
|
4554
|
+
const px1 = Math.max(px0, x1 - r);
|
|
4555
|
+
const py1 = Math.max(py0, y1 - b);
|
|
4556
|
+
// The left and the right are drawn after the top and the bottom. Where
|
|
4557
|
+
// one of them is opaque it draws its half of a cut corner over the side
|
|
4558
|
+
// drawn before, which keeps the whole corner: two halves each
|
|
4559
|
+
// antialiased along the diagonal would let what is under the border
|
|
4560
|
+
// show through between them.
|
|
4561
|
+
const over = (color: string | null): boolean =>
|
|
4562
|
+
color !== null && alphaOf(color) === 1;
|
|
4563
|
+
if (topInk !== null) {
|
|
4564
|
+
const cutLeft = tl && !over(leftInk);
|
|
4565
|
+
const cutRight = tr && !over(rightInk);
|
|
4566
|
+
if (cutLeft || cutRight) {
|
|
4567
|
+
side(
|
|
4568
|
+
[x, y, x1, y, cutRight ? px1 : x1, py0, cutLeft ? px0 : x, py0],
|
|
4569
|
+
topInk,
|
|
4570
|
+
);
|
|
4571
|
+
} else edge(x, y, w, t, topStyle, topInk, true);
|
|
4572
|
+
}
|
|
4573
|
+
if (bottomInk !== null) {
|
|
4574
|
+
const cutLeft = bl && !over(leftInk);
|
|
4575
|
+
const cutRight = br && !over(rightInk);
|
|
4576
|
+
if (cutLeft || cutRight) {
|
|
4577
|
+
side(
|
|
4578
|
+
[x1, y1, x, y1, cutLeft ? px0 : x, py1, cutRight ? px1 : x1, py1],
|
|
4579
|
+
bottomInk,
|
|
4580
|
+
);
|
|
4581
|
+
} else edge(x, y1 - b, w, b, bottomStyle, bottomInk, true);
|
|
3457
4582
|
}
|
|
3458
|
-
if (
|
|
3459
|
-
|
|
3460
|
-
x,
|
|
3461
|
-
|
|
3462
|
-
w,
|
|
3463
|
-
box.borderBottom,
|
|
3464
|
-
s.borderBottomStyle,
|
|
3465
|
-
s.borderBottomColor,
|
|
3466
|
-
true,
|
|
3467
|
-
);
|
|
4583
|
+
if (leftInk !== null) {
|
|
4584
|
+
if (tl || bl) {
|
|
4585
|
+
side([x, bl ? y1 : py1, x, tl ? y : py0, px0, py0, px0, py1], leftInk);
|
|
4586
|
+
} else edge(x, y + t, l, h - t - b, leftStyle, leftInk, false);
|
|
3468
4587
|
}
|
|
3469
|
-
if (
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
box.borderLeft,
|
|
3474
|
-
h - box.borderTop - box.borderBottom,
|
|
3475
|
-
s.borderLeftStyle,
|
|
3476
|
-
s.borderLeftColor,
|
|
3477
|
-
false,
|
|
3478
|
-
);
|
|
4588
|
+
if (rightInk !== null) {
|
|
4589
|
+
if (tr || br) {
|
|
4590
|
+
side([x1, tr ? y : py0, x1, br ? y1 : py1, px1, py1, px1, py0], rightInk);
|
|
4591
|
+
} else edge(x1 - r, y + t, r, h - t - b, rightStyle, rightInk, false);
|
|
3479
4592
|
}
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
/**
|
|
4596
|
+
* Whether the corner two sides meet at is cut on its diagonal, each side
|
|
4597
|
+
* taking its half (CSS Backgrounds 3, 4.3): where both have a width, they
|
|
4598
|
+
* are painted in different colours or only one is painted at all, and each
|
|
4599
|
+
* is solid or unpainted. Sides of one colour make the same corner either
|
|
4600
|
+
* way, and so does a corner of a single pixel, which is left to the side
|
|
4601
|
+
* that has it.
|
|
4602
|
+
*/
|
|
4603
|
+
function mitred(
|
|
4604
|
+
a: number,
|
|
4605
|
+
b: number,
|
|
4606
|
+
inkA: string | null,
|
|
4607
|
+
inkB: string | null,
|
|
4608
|
+
styleA: ComputedStyle['borderTopStyle'],
|
|
4609
|
+
styleB: ComputedStyle['borderTopStyle'],
|
|
4610
|
+
): boolean {
|
|
4611
|
+
if (!(a > 0 && b > 0) || (a <= 1 && b <= 1) || inkA === inkB) return false;
|
|
4612
|
+
return (
|
|
4613
|
+
(inkA === null || styleA === 'solid') &&
|
|
4614
|
+
(inkB === null || styleB === 'solid')
|
|
4615
|
+
);
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
/**
|
|
4619
|
+
* Fill a convex polygon, its corners as `x, y` pairs, cut to `area`: the
|
|
4620
|
+
* part of it near what is painted, which is what keeps a side thousands of
|
|
4621
|
+
* pixels long inside X's coordinates. Cut edge by edge rather than by
|
|
4622
|
+
* moving its corners in, which would turn a diagonal that crosses the
|
|
4623
|
+
* painted area: a slanted divider, `border-left: 100vw solid transparent`,
|
|
4624
|
+
* has a corner far outside it.
|
|
4625
|
+
*/
|
|
4626
|
+
function fillPolygon(
|
|
4627
|
+
ctx: PaintContext,
|
|
4628
|
+
points: number[],
|
|
4629
|
+
area: { x: number; y: number; w: number; h: number },
|
|
4630
|
+
): void {
|
|
4631
|
+
const bounds = [area.x, area.y, area.x + area.w, area.y + area.h];
|
|
4632
|
+
let cut = points;
|
|
4633
|
+
// each edge of the area in turn: what is inside it is kept, and an edge
|
|
4634
|
+
// of the polygon that crosses it ends on it (Sutherland–Hodgman)
|
|
4635
|
+
for (let edge = 0; edge < 4 && cut.length; edge += 1) {
|
|
4636
|
+
const axis = edge & 1;
|
|
4637
|
+
const bound = bounds[edge];
|
|
4638
|
+
const sign = edge < 2 ? 1 : -1;
|
|
4639
|
+
const from = cut;
|
|
4640
|
+
cut = [];
|
|
4641
|
+
for (let i = 0; i < from.length; i += 2) {
|
|
4642
|
+
const j = (i + 2) % from.length;
|
|
4643
|
+
const da = sign * (from[i + axis] - bound);
|
|
4644
|
+
const db = sign * (from[j + axis] - bound);
|
|
4645
|
+
if (da >= 0) cut.push(from[i], from[i + 1]);
|
|
4646
|
+
if (da < 0 !== db < 0) {
|
|
4647
|
+
const k = da / (da - db);
|
|
4648
|
+
const other =
|
|
4649
|
+
from[i + 1 - axis] + (from[j + 1 - axis] - from[i + 1 - axis]) * k;
|
|
4650
|
+
if (axis) cut.push(other, bound);
|
|
4651
|
+
else cut.push(bound, other);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
3490
4654
|
}
|
|
4655
|
+
if (cut.length < 6) return;
|
|
4656
|
+
ctx.beginPath!();
|
|
4657
|
+
ctx.moveTo!(cut[0], cut[1]);
|
|
4658
|
+
for (let i = 2; i < cut.length; i += 2) ctx.lineTo!(cut[i], cut[i + 1]);
|
|
4659
|
+
ctx.closePath?.();
|
|
4660
|
+
ctx.fill!();
|
|
3491
4661
|
}
|
|
3492
4662
|
|
|
3493
4663
|
/**
|
|
@@ -3551,17 +4721,9 @@ function paintBorderImage(
|
|
|
3551
4721
|
h,
|
|
3552
4722
|
(sctx) => {
|
|
3553
4723
|
if (svg) svg.draw(sctx, 0, 0, w, h, scale);
|
|
3554
|
-
else if (typeof source !== 'string'
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
source,
|
|
3558
|
-
0,
|
|
3559
|
-
0,
|
|
3560
|
-
w,
|
|
3561
|
-
h,
|
|
3562
|
-
box.style.color,
|
|
3563
|
-
);
|
|
3564
|
-
sctx.fillRect(0, 0, w, h);
|
|
4724
|
+
else if (typeof source !== 'string') {
|
|
4725
|
+
const paint = gradientFill(sctx, source, 0, 0, w, h, box.style.color);
|
|
4726
|
+
if (paint) fillGradient(sctx, paint, { x: 0, y: 0, w, h });
|
|
3565
4727
|
}
|
|
3566
4728
|
},
|
|
3567
4729
|
);
|
|
@@ -3685,11 +4847,11 @@ function paintBorderImage(
|
|
|
3685
4847
|
return true;
|
|
3686
4848
|
}
|
|
3687
4849
|
|
|
3688
|
-
const GRADIENT_KEYS = new WeakMap<
|
|
4850
|
+
const GRADIENT_KEYS = new WeakMap<Gradient, string>();
|
|
3689
4851
|
|
|
3690
4852
|
/** A gradient as a key for what is drawn of it: its angle and stops, and
|
|
3691
4853
|
* the colour `currentColor` among them is. */
|
|
3692
|
-
function gradientKey(gradient:
|
|
4854
|
+
function gradientKey(gradient: Gradient, color: string): string {
|
|
3693
4855
|
let key = GRADIENT_KEYS.get(gradient);
|
|
3694
4856
|
if (key === undefined) {
|
|
3695
4857
|
key = JSON.stringify(gradient);
|
|
@@ -4263,13 +5425,20 @@ function fitted(
|
|
|
4263
5425
|
}
|
|
4264
5426
|
|
|
4265
5427
|
/** An inline `<svg>`, drawn in its content box. Its `currentColor` is the
|
|
4266
|
-
* box's `color`, as an icon's is the text's around it
|
|
5428
|
+
* box's `color`, as an icon's is the text's around it, and what it is
|
|
5429
|
+
* painted with the box's `fill` and `stroke`, where the document's style
|
|
5430
|
+
* sheets set them: an icon set's `.icon { fill: currentColor }`. */
|
|
4267
5431
|
function paintSvg(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
4268
5432
|
if (!box.el) return;
|
|
4269
5433
|
const x = Math.round(box.contentX + options.originX);
|
|
4270
5434
|
const y = Math.round(box.contentY + options.originY);
|
|
4271
5435
|
const w = Math.round(box.contentX + options.originX + box.contentWidth) - x;
|
|
4272
5436
|
const h = Math.round(box.contentY + options.originY + box.contentHeight) - y;
|
|
5437
|
+
const style = box.style;
|
|
5438
|
+
const paint = (value: string | null): string | null =>
|
|
5439
|
+
value === null || value === 'currentColor' || value === 'none'
|
|
5440
|
+
? value
|
|
5441
|
+
: inkColor(value, style.color);
|
|
4273
5442
|
inlineDrawing(box.el).draw(
|
|
4274
5443
|
ctx,
|
|
4275
5444
|
x,
|
|
@@ -4277,7 +5446,9 @@ function paintSvg(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4277
5446
|
w,
|
|
4278
5447
|
h,
|
|
4279
5448
|
options.scale ?? 1,
|
|
4280
|
-
|
|
5449
|
+
style.color,
|
|
5450
|
+
paint(style.fill),
|
|
5451
|
+
paint(style.stroke),
|
|
4281
5452
|
);
|
|
4282
5453
|
}
|
|
4283
5454
|
|
|
@@ -4314,16 +5485,29 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4314
5485
|
const bottom = top + damage.height;
|
|
4315
5486
|
let lo = 0;
|
|
4316
5487
|
let hi = lines.length;
|
|
4317
|
-
|
|
4318
|
-
|
|
5488
|
+
// and a line whose glyphs, or an atomic on it, reach past it
|
|
5489
|
+
// (`LINE_INK`) is drawn where they reach
|
|
5490
|
+
const ink = LINE_INK.get(lines);
|
|
5491
|
+
const above = ink?.above ?? 0;
|
|
5492
|
+
const below = ink?.below ?? 0;
|
|
5493
|
+
const slack = top - box.maxLineHeight - below;
|
|
5494
|
+
// the lines columns took apart are in no order down the page
|
|
5495
|
+
const apart = columned.any && COLUMN_LINES.has(lines);
|
|
5496
|
+
while (!apart && lo < hi) {
|
|
4319
5497
|
const mid = (lo + hi) >> 1;
|
|
4320
5498
|
if (lines[mid].y > slack) hi = mid;
|
|
4321
5499
|
else lo = mid + 1;
|
|
4322
5500
|
}
|
|
4323
5501
|
for (let i = lo; i < lines.length; i += 1) {
|
|
4324
5502
|
const line = lines[i];
|
|
4325
|
-
if (line.y >= bottom)
|
|
4326
|
-
|
|
5503
|
+
if (line.y - above >= bottom) {
|
|
5504
|
+
if (apart) continue;
|
|
5505
|
+
break;
|
|
5506
|
+
}
|
|
5507
|
+
if (line.y < bottom && line.y + line.height > top) visible.push(line);
|
|
5508
|
+
else if (ink && inkInto(line, top, bottom)) {
|
|
5509
|
+
visible.push(line);
|
|
5510
|
+
}
|
|
4327
5511
|
}
|
|
4328
5512
|
// and a line the damage misses whose text or inline-block `position:
|
|
4329
5513
|
// relative` moved into it: drawn by its line, it went undrawn with it
|
|
@@ -4331,7 +5515,7 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4331
5515
|
if (moved && reachedOff(visible, moved, top, bottom)) {
|
|
4332
5516
|
const drawn = new Set(visible);
|
|
4333
5517
|
for (const line of moved) {
|
|
4334
|
-
if (
|
|
5518
|
+
if (inkInto(line, top, bottom)) drawn.add(line);
|
|
4335
5519
|
}
|
|
4336
5520
|
visible.length = 0;
|
|
4337
5521
|
for (const line of lines) if (drawn.has(line)) visible.push(line);
|
|
@@ -4353,14 +5537,17 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4353
5537
|
);
|
|
4354
5538
|
for (const text of line.texts) {
|
|
4355
5539
|
const natural = text.layout.lines[text.layoutLine];
|
|
4356
|
-
if (natural)
|
|
5540
|
+
if (!natural) continue;
|
|
5541
|
+
for (const laid of [natural, trailOf(text, natural)]) {
|
|
5542
|
+
if (!laid) continue;
|
|
4357
5543
|
paintRunBackgrounds(
|
|
4358
5544
|
ctx,
|
|
4359
|
-
|
|
5545
|
+
laid,
|
|
4360
5546
|
text.drawX + dx,
|
|
4361
5547
|
text.drawY + dy,
|
|
4362
5548
|
options.scale ?? 1,
|
|
4363
5549
|
);
|
|
5550
|
+
}
|
|
4364
5551
|
}
|
|
4365
5552
|
paintSelection(ctx, line, options);
|
|
4366
5553
|
}
|
|
@@ -4377,10 +5564,16 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4377
5564
|
// drawing it once per line would be one X request per line for the same
|
|
4378
5565
|
// batch.
|
|
4379
5566
|
const drawn = new Set<unknown>();
|
|
5567
|
+
// text a `::selection` colours is drawn in that colour where selected
|
|
5568
|
+
const recolored = options.selectionStyler
|
|
5569
|
+
? recoloredBands(visible, options)
|
|
5570
|
+
: null;
|
|
4380
5571
|
for (const line of visible) {
|
|
4381
5572
|
for (const text of line.texts) {
|
|
4382
|
-
|
|
4383
|
-
|
|
5573
|
+
// once for each column its lines are in, where they are in several
|
|
5574
|
+
const rows = columned.any ? COLUMN_ROWS.get(text) : undefined;
|
|
5575
|
+
if (drawn.has(rows ?? text.layout)) continue;
|
|
5576
|
+
drawn.add(rows ?? text.layout);
|
|
4384
5577
|
const top = text.drawY + dy;
|
|
4385
5578
|
if (top < -COORD_LIMIT || top + text.layout.height > COORD_LIMIT) {
|
|
4386
5579
|
// A single layout so tall its own lines overflow the Int16 envelope
|
|
@@ -4390,7 +5583,12 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4390
5583
|
// thrown from the protocol encoder.
|
|
4391
5584
|
continue;
|
|
4392
5585
|
}
|
|
4393
|
-
text.layout
|
|
5586
|
+
const bands = recolored?.get(text.layout);
|
|
5587
|
+
const clipped =
|
|
5588
|
+
rows !== undefined && clipToRows(ctx, text, rows, text.drawX + dx, top);
|
|
5589
|
+
if (bands) drawRecolored(ctx, text.layout, text.drawX + dx, top, bands);
|
|
5590
|
+
else text.layout.draw(ctx, text.drawX + dx, top);
|
|
5591
|
+
if (clipped) ctx.restore();
|
|
4394
5592
|
}
|
|
4395
5593
|
}
|
|
4396
5594
|
|
|
@@ -4403,7 +5601,8 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4403
5601
|
for (const line of visible) {
|
|
4404
5602
|
for (const placed of line.atomics) {
|
|
4405
5603
|
if (hoisted && HOISTED.has(placed.box)) continue;
|
|
4406
|
-
|
|
5604
|
+
if (turns(placed.box)) paintTransformed(ctx, placed.box, options);
|
|
5605
|
+
else paintBox(ctx, placed.box, options);
|
|
4407
5606
|
}
|
|
4408
5607
|
}
|
|
4409
5608
|
}
|
|
@@ -4417,15 +5616,16 @@ function reachedOff(
|
|
|
4417
5616
|
bottom: number,
|
|
4418
5617
|
): boolean {
|
|
4419
5618
|
for (const line of moved) {
|
|
4420
|
-
if (!visible.includes(line) &&
|
|
5619
|
+
if (!visible.includes(line) && inkInto(line, top, bottom)) return true;
|
|
4421
5620
|
}
|
|
4422
5621
|
return false;
|
|
4423
5622
|
}
|
|
4424
5623
|
|
|
4425
|
-
/** Whether what
|
|
4426
|
-
*
|
|
4427
|
-
* between `top` and `bottom
|
|
4428
|
-
|
|
5624
|
+
/** Whether what a line draws — its inline-blocks and images, where their
|
|
5625
|
+
* ink is, and its texts, where they are drawn, their glyphs from ascent to
|
|
5626
|
+
* descent — reaches the rows between `top` and `bottom`: what was moved off
|
|
5627
|
+
* it, and what hangs past it. */
|
|
5628
|
+
function inkInto(line: LineBox, top: number, bottom: number): boolean {
|
|
4429
5629
|
for (const placed of line.atomics) {
|
|
4430
5630
|
const box = placed.box;
|
|
4431
5631
|
if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
|
|
@@ -4435,8 +5635,16 @@ function movedInto(line: LineBox, top: number, bottom: number): boolean {
|
|
|
4435
5635
|
for (const text of line.texts) {
|
|
4436
5636
|
const natural = text.layout.lines[text.layoutLine];
|
|
4437
5637
|
if (!natural) continue;
|
|
4438
|
-
const
|
|
4439
|
-
|
|
5638
|
+
const baseline = text.drawY + natural.baseline;
|
|
5639
|
+
const y1 = Math.min(
|
|
5640
|
+
text.drawY + natural.y,
|
|
5641
|
+
baseline - (natural.ascent ?? 0),
|
|
5642
|
+
);
|
|
5643
|
+
const y2 = Math.max(
|
|
5644
|
+
text.drawY + natural.y + natural.height,
|
|
5645
|
+
baseline + (natural.descent ?? 0),
|
|
5646
|
+
);
|
|
5647
|
+
if (y1 < bottom && y2 > top) return true;
|
|
4440
5648
|
}
|
|
4441
5649
|
return false;
|
|
4442
5650
|
}
|
|
@@ -4571,12 +5779,14 @@ function paintClippedText(
|
|
|
4571
5779
|
const gradient =
|
|
4572
5780
|
style.backgroundGradient ??
|
|
4573
5781
|
(style.backgroundImages?.find(
|
|
4574
|
-
(image): image is
|
|
5782
|
+
(image): image is Gradient =>
|
|
4575
5783
|
image !== null && typeof image !== 'string',
|
|
4576
5784
|
) ||
|
|
4577
5785
|
null);
|
|
4578
5786
|
let fill: unknown = null;
|
|
4579
|
-
|
|
5787
|
+
// where the fill is a picture of the gradient, the corner it starts at
|
|
5788
|
+
let origin: { x: number; y: number } | null = null;
|
|
5789
|
+
if (gradient) {
|
|
4580
5790
|
if (!(area.width > 0 && area.height > 0)) continue;
|
|
4581
5791
|
// the text it shows through: what the fill is drawn over
|
|
4582
5792
|
let x0 = Infinity;
|
|
@@ -4591,7 +5801,7 @@ function paintClippedText(
|
|
|
4591
5801
|
y1 = Math.max(y1, r.y + r.height);
|
|
4592
5802
|
}
|
|
4593
5803
|
}
|
|
4594
|
-
|
|
5804
|
+
const paint = gradientFill(
|
|
4595
5805
|
ctx,
|
|
4596
5806
|
gradient,
|
|
4597
5807
|
area.x,
|
|
@@ -4601,7 +5811,40 @@ function paintClippedText(
|
|
|
4601
5811
|
style.color,
|
|
4602
5812
|
x1 > x0 ? { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } : null,
|
|
4603
5813
|
);
|
|
4604
|
-
|
|
5814
|
+
if (paint?.matrix) {
|
|
5815
|
+
// An ellipse is a circle filled through a matrix, which would draw
|
|
5816
|
+
// the glyphs through it too: they are filled with a picture of the
|
|
5817
|
+
// gradient instead, drawn once at the area's size
|
|
5818
|
+
const left = Math.round(area.x);
|
|
5819
|
+
const top = Math.round(area.y);
|
|
5820
|
+
const w = Math.max(1, Math.round(area.x + area.width) - left);
|
|
5821
|
+
const h = Math.max(1, Math.round(area.y + area.height) - top);
|
|
5822
|
+
const picture =
|
|
5823
|
+
ctx.createPattern && ctx.translate
|
|
5824
|
+
? options.cached?.(
|
|
5825
|
+
`clip-text|${gradientKey(gradient, style.color)}|${w}x${h}`,
|
|
5826
|
+
w,
|
|
5827
|
+
h,
|
|
5828
|
+
(sctx) => {
|
|
5829
|
+
const p = gradientFill(
|
|
5830
|
+
sctx,
|
|
5831
|
+
gradient,
|
|
5832
|
+
0,
|
|
5833
|
+
0,
|
|
5834
|
+
w,
|
|
5835
|
+
h,
|
|
5836
|
+
style.color,
|
|
5837
|
+
);
|
|
5838
|
+
if (p) fillGradient(sctx, p, { x: 0, y: 0, w, h });
|
|
5839
|
+
},
|
|
5840
|
+
)
|
|
5841
|
+
: null;
|
|
5842
|
+
if (!picture) continue;
|
|
5843
|
+
fill = ctx.createPattern!(picture, 'no-repeat');
|
|
5844
|
+
origin = { x: left, y: top };
|
|
5845
|
+
} else if (paint) fill = paint.style;
|
|
5846
|
+
}
|
|
5847
|
+
if (fill === null && !isTransparent(style.backgroundColor)) {
|
|
4605
5848
|
fill = inkColor(style.backgroundColor as string, style.color);
|
|
4606
5849
|
}
|
|
4607
5850
|
if (fill === null) continue;
|
|
@@ -4613,7 +5856,11 @@ function paintClippedText(
|
|
|
4613
5856
|
for (const r of part.rects) ctx.rect(r.x, r.y, r.width, r.height);
|
|
4614
5857
|
ctx.clip();
|
|
4615
5858
|
ctx.fillStyle = fill;
|
|
4616
|
-
|
|
5859
|
+
if (origin) {
|
|
5860
|
+
// a pattern is placed from the origin of the space it fills
|
|
5861
|
+
ctx.translate!(origin.x, origin.y);
|
|
5862
|
+
inkless.draw(ctx, part.x - origin.x, part.y - origin.y);
|
|
5863
|
+
} else inkless.draw(ctx, part.x, part.y);
|
|
4617
5864
|
ctx.restore();
|
|
4618
5865
|
}
|
|
4619
5866
|
}
|
|
@@ -4653,10 +5900,15 @@ function paintTextShadows(
|
|
|
4653
5900
|
if (cast === null) continue;
|
|
4654
5901
|
const left = text.drawX + dx;
|
|
4655
5902
|
if (cast !== MIXED) {
|
|
4656
|
-
// every run casts the same: the layout's once for each shadow
|
|
4657
|
-
|
|
4658
|
-
|
|
5903
|
+
// every run casts the same: the layout's once for each shadow, in
|
|
5904
|
+
// each column its lines are in
|
|
5905
|
+
const rows = columned.any ? COLUMN_ROWS.get(text) : undefined;
|
|
5906
|
+
if (whole.has(rows ?? layout)) continue;
|
|
5907
|
+
whole.add(rows ?? layout);
|
|
5908
|
+
const clipped =
|
|
5909
|
+
rows !== undefined && clipToRows(ctx, text, rows, left, top);
|
|
4659
5910
|
castShadows(ctx, text, left, top, cast, null);
|
|
5911
|
+
if (clipped) ctx.restore();
|
|
4660
5912
|
continue;
|
|
4661
5913
|
}
|
|
4662
5914
|
const natural = layout.lines[text.layoutLine];
|
|
@@ -4788,10 +6040,12 @@ function paintRules(
|
|
|
4788
6040
|
const shifted = SHIFTED_LINES.has(line);
|
|
4789
6041
|
for (const text of line.texts) {
|
|
4790
6042
|
const natural = text.layout.lines[text.layoutLine];
|
|
4791
|
-
if (natural)
|
|
6043
|
+
if (!natural) continue;
|
|
6044
|
+
for (const laid of [natural, trailOf(text, natural)]) {
|
|
6045
|
+
if (!laid) continue;
|
|
4792
6046
|
paintRunRules(
|
|
4793
6047
|
ctx,
|
|
4794
|
-
|
|
6048
|
+
laid,
|
|
4795
6049
|
text.drawX + dx,
|
|
4796
6050
|
text.drawY + dy + (shifted ? ruleDrop(text) : 0),
|
|
4797
6051
|
scale,
|
|
@@ -4802,6 +6056,29 @@ function paintRules(
|
|
|
4802
6056
|
}
|
|
4803
6057
|
}
|
|
4804
6058
|
|
|
6059
|
+
/**
|
|
6060
|
+
* The spaces a text ends on where its line goes on after them
|
|
6061
|
+
* (`LineText.trail`), as a line of one run to draw the decorations of: the
|
|
6062
|
+
* run they are the end of, as wide as they are, after the line's content.
|
|
6063
|
+
* The engine stripped them from the piece it laid out as a line, and a
|
|
6064
|
+
* link's underline stopped at its last letter, a space short of where a
|
|
6065
|
+
* browser draws it. Null where the text has none, or reads right to left.
|
|
6066
|
+
*/
|
|
6067
|
+
function trailOf<L extends LineText['layout']['lines'][number]>(
|
|
6068
|
+
text: LineText,
|
|
6069
|
+
natural: L,
|
|
6070
|
+
): L | null {
|
|
6071
|
+
if (!text.trail || !text.trailRun) return null;
|
|
6072
|
+
const last = natural.runs[natural.runs.length - 1];
|
|
6073
|
+
if (!last || last.run?.direction === 'rtl') return null;
|
|
6074
|
+
return {
|
|
6075
|
+
...natural,
|
|
6076
|
+
runs: [
|
|
6077
|
+
{ ...last, x: natural.width, width: text.trail, span: text.trailRun },
|
|
6078
|
+
],
|
|
6079
|
+
};
|
|
6080
|
+
}
|
|
6081
|
+
|
|
4805
6082
|
/**
|
|
4806
6083
|
* How far below a raised text its rules go: to the baseline of the element
|
|
4807
6084
|
* that decorates it. `vertical-align` moves the text and not the lines an
|
|
@@ -5306,17 +6583,46 @@ export function forgetDecoratedAncestors(box: Box): void {
|
|
|
5306
6583
|
*
|
|
5307
6584
|
* Translucent under the glyphs rather than inverted over them, so the ink
|
|
5308
6585
|
* keeps its contrast on either palette — the same call `<textarea>` and
|
|
5309
|
-
* `<richtext>` both make.
|
|
6586
|
+
* `<richtext>` both make. From the top of the line to its bottom, and past
|
|
6587
|
+
* them over the glyphs where they are taller than the line (`selectionRows`).
|
|
5310
6588
|
*/
|
|
5311
6589
|
function paintSelection(
|
|
5312
6590
|
ctx: PaintContext,
|
|
5313
6591
|
line: LineBox,
|
|
5314
6592
|
options: PaintOptions,
|
|
5315
6593
|
): void {
|
|
6594
|
+
for (const band of selectedBands(line, options)) {
|
|
6595
|
+
const fill = band.style ? band.style.background : options.selectionColor;
|
|
6596
|
+
if (!fill || isTransparent(fill)) continue;
|
|
6597
|
+
ctx.fillStyle = fill;
|
|
6598
|
+
ctx.fillRect(band.x, band.y, band.width, band.height);
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
|
|
6602
|
+
/** A stretch of selected text on one line, in window pixels, and the
|
|
6603
|
+
* `::selection` of the element it is in: null for the palette's. */
|
|
6604
|
+
interface SelectedBand extends Rect {
|
|
6605
|
+
style: SelectionStyle | null;
|
|
6606
|
+
/** The layout whose text it is. */
|
|
6607
|
+
layout: unknown;
|
|
6608
|
+
}
|
|
6609
|
+
|
|
6610
|
+
const NO_BANDS: SelectedBand[] = [];
|
|
6611
|
+
|
|
6612
|
+
/**
|
|
6613
|
+
* Where the document selection covers a line's text, a band for each
|
|
6614
|
+
* stretch of it one `::selection` styles, in window pixels as the band is
|
|
6615
|
+
* filled — rounded, so a band's text is drawn to its edges and no further
|
|
6616
|
+
* (`drawRecolored`).
|
|
6617
|
+
*/
|
|
6618
|
+
function selectedBands(line: LineBox, options: PaintOptions): SelectedBand[] {
|
|
5316
6619
|
const range = options.selection;
|
|
5317
|
-
if (!range || range.end <= range.start
|
|
5318
|
-
if (line.textEnd <= range.start || line.textStart >= range.end)
|
|
5319
|
-
|
|
6620
|
+
if (!range || range.end <= range.start) return NO_BANDS;
|
|
6621
|
+
if (line.textEnd <= range.start || line.textStart >= range.end) {
|
|
6622
|
+
return NO_BANDS;
|
|
6623
|
+
}
|
|
6624
|
+
const styler = options.selectionStyler;
|
|
6625
|
+
const out: SelectedBand[] = [];
|
|
5320
6626
|
for (const text of line.texts) {
|
|
5321
6627
|
const natural = text.layout.lines[text.layoutLine];
|
|
5322
6628
|
if (!natural) continue;
|
|
@@ -5326,19 +6632,214 @@ function paintSelection(
|
|
|
5326
6632
|
const offsets = layoutOffsets(text.layout);
|
|
5327
6633
|
const layoutFrom = layoutOffsetOf(text, from);
|
|
5328
6634
|
const layoutTo = layoutOffsetOf(text, to, true);
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
layoutFrom,
|
|
5334
|
-
layoutTo,
|
|
5335
|
-
)
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
6635
|
+
const rows = selectionRows(line, text, natural);
|
|
6636
|
+
const y = Math.round(rows.y + options.originY);
|
|
6637
|
+
const height = Math.ceil(rows.height);
|
|
6638
|
+
const pieces = styler
|
|
6639
|
+
? stylePieces(text, natural, layoutFrom, layoutTo, styler)
|
|
6640
|
+
: [{ from: layoutFrom, to: layoutTo, style: null }];
|
|
6641
|
+
for (const piece of pieces) {
|
|
6642
|
+
for (const band of lineBands(
|
|
6643
|
+
text.layout,
|
|
6644
|
+
natural,
|
|
6645
|
+
offsets,
|
|
6646
|
+
piece.from,
|
|
6647
|
+
piece.to,
|
|
6648
|
+
)) {
|
|
6649
|
+
out.push({
|
|
6650
|
+
x: Math.round(band.x + text.drawX + options.originX),
|
|
6651
|
+
y,
|
|
6652
|
+
width: Math.ceil(band.width),
|
|
6653
|
+
height,
|
|
6654
|
+
style: piece.style,
|
|
6655
|
+
layout: text.layout,
|
|
6656
|
+
});
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
return out;
|
|
6661
|
+
}
|
|
6662
|
+
|
|
6663
|
+
/**
|
|
6664
|
+
* A line's selected text, `from` to `to` in the layout's offsets, cut where
|
|
6665
|
+
* the `::selection` over it changes: a run's is its element's, and a run of
|
|
6666
|
+
* no element's — an inline box's edge, laid out as a spacer — the one
|
|
6667
|
+
* beside it, so a styled band has no palette-coloured gap at a `<span>`.
|
|
6668
|
+
*/
|
|
6669
|
+
function stylePieces(
|
|
6670
|
+
text: LineText,
|
|
6671
|
+
natural: LineText['layout']['lines'][number],
|
|
6672
|
+
from: number,
|
|
6673
|
+
to: number,
|
|
6674
|
+
styler: SelectionStyler,
|
|
6675
|
+
): { from: number; to: number; style: SelectionStyle | null }[] {
|
|
6676
|
+
const runs = natural.runs
|
|
6677
|
+
.filter((run) => run.end > from && run.start < to)
|
|
6678
|
+
.sort((a, b) => a.start - b.start);
|
|
6679
|
+
const boxAt = text.spans.boxAt;
|
|
6680
|
+
const styles: (SelectionStyle | null | undefined)[] = runs.map((run) => {
|
|
6681
|
+
const box = boxAt?.call(text.spans, run.start) ?? null;
|
|
6682
|
+
return box ? styler(box) : undefined;
|
|
6683
|
+
});
|
|
6684
|
+
for (let i = 1; i < styles.length; i += 1) {
|
|
6685
|
+
if (styles[i] === undefined) styles[i] = styles[i - 1];
|
|
6686
|
+
}
|
|
6687
|
+
for (let i = styles.length - 2; i >= 0; i -= 1) {
|
|
6688
|
+
if (styles[i] === undefined) styles[i] = styles[i + 1];
|
|
6689
|
+
}
|
|
6690
|
+
const out: { from: number; to: number; style: SelectionStyle | null }[] = [];
|
|
6691
|
+
runs.forEach((run, i) => {
|
|
6692
|
+
const style = styles[i] ?? null;
|
|
6693
|
+
const last = out[out.length - 1];
|
|
6694
|
+
if (last && last.style === style) last.to = Math.min(to, run.end);
|
|
6695
|
+
else {
|
|
6696
|
+
out.push({
|
|
6697
|
+
from: Math.max(from, run.start),
|
|
6698
|
+
to: Math.min(to, run.end),
|
|
6699
|
+
style,
|
|
6700
|
+
});
|
|
6701
|
+
}
|
|
6702
|
+
});
|
|
6703
|
+
return out;
|
|
6704
|
+
}
|
|
6705
|
+
|
|
6706
|
+
/**
|
|
6707
|
+
* The layouts whose selected text a `::selection` gives a colour of its
|
|
6708
|
+
* own, each with the bands it is drawn in that colour (`drawRecolored`).
|
|
6709
|
+
* Null where none does, which is every document with no such rule.
|
|
6710
|
+
*/
|
|
6711
|
+
function recoloredBands(
|
|
6712
|
+
lines: LineBox[],
|
|
6713
|
+
options: PaintOptions,
|
|
6714
|
+
): Map<unknown, SelectedBand[]> | null {
|
|
6715
|
+
let out: Map<unknown, SelectedBand[]> | null = null;
|
|
6716
|
+
for (const line of lines) {
|
|
6717
|
+
for (const band of selectedBands(line, options)) {
|
|
6718
|
+
if (!band.style?.color || !(band.width > 0)) continue;
|
|
6719
|
+
out ??= new Map();
|
|
6720
|
+
const list = out.get(band.layout);
|
|
6721
|
+
if (list) list.push(band);
|
|
6722
|
+
else out.set(band.layout, [band]);
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
return out;
|
|
6726
|
+
}
|
|
6727
|
+
|
|
6728
|
+
/**
|
|
6729
|
+
* A layout with some of its text selected in a `::selection`'s colour
|
|
6730
|
+
* (CSS Pseudo 4, 3.2): its own colours everywhere but those bands, and in
|
|
6731
|
+
* each band the glyphs in that band's colour — cast there as a hard shadow
|
|
6732
|
+
* from the layout drawn clear of the window, the way `castShadows` draws a
|
|
6733
|
+
* text shadow, since a layout draws in its runs' own colours. Drawn over
|
|
6734
|
+
* rather than in place of the text, the old glyphs' edges showed round the
|
|
6735
|
+
* new ones. Where the context cannot clip or cast, the text keeps its own
|
|
6736
|
+
* colours.
|
|
6737
|
+
*/
|
|
6738
|
+
function drawRecolored(
|
|
6739
|
+
ctx: PaintContext,
|
|
6740
|
+
layout: LineText['layout'],
|
|
6741
|
+
left: number,
|
|
6742
|
+
top: number,
|
|
6743
|
+
bands: SelectedBand[],
|
|
6744
|
+
): void {
|
|
6745
|
+
if (
|
|
6746
|
+
!ctx.save ||
|
|
6747
|
+
!ctx.restore ||
|
|
6748
|
+
!ctx.beginPath ||
|
|
6749
|
+
!ctx.rect ||
|
|
6750
|
+
!ctx.clip ||
|
|
6751
|
+
!('shadowBlur' in ctx)
|
|
6752
|
+
) {
|
|
6753
|
+
layout.draw(ctx, left, top);
|
|
6754
|
+
return;
|
|
6755
|
+
}
|
|
6756
|
+
ctx.save();
|
|
6757
|
+
ctx.beginPath();
|
|
6758
|
+
for (const r of outsideOf(bands)) ctx.rect(r.x, r.y, r.width, r.height);
|
|
6759
|
+
ctx.clip();
|
|
6760
|
+
layout.draw(ctx, left, top);
|
|
6761
|
+
ctx.restore();
|
|
6762
|
+
let right = layout.width;
|
|
6763
|
+
for (const natural of layout.lines) {
|
|
6764
|
+
right = Math.max(right, natural.x + natural.width);
|
|
6765
|
+
}
|
|
6766
|
+
const shift = Math.ceil(left + right) + 1;
|
|
6767
|
+
const colors = new Map<string, SelectedBand[]>();
|
|
6768
|
+
for (const band of bands) {
|
|
6769
|
+
const color = band.style!.color!;
|
|
6770
|
+
const group = colors.get(color);
|
|
6771
|
+
if (group) group.push(band);
|
|
6772
|
+
else colors.set(color, [band]);
|
|
6773
|
+
}
|
|
6774
|
+
for (const [color, group] of colors) {
|
|
6775
|
+
ctx.save();
|
|
6776
|
+
ctx.beginPath();
|
|
6777
|
+
for (const r of group) ctx.rect(r.x, r.y, r.width, r.height);
|
|
6778
|
+
ctx.clip();
|
|
6779
|
+
ctx.shadowColor = color;
|
|
6780
|
+
ctx.shadowBlur = 0.01;
|
|
6781
|
+
ctx.shadowOffsetX = shift;
|
|
6782
|
+
ctx.shadowOffsetY = 0;
|
|
6783
|
+
layout.draw(ctx, left - shift, top);
|
|
6784
|
+
ctx.restore();
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
|
|
6788
|
+
/** Everything but some rectangles, as rectangles: a strip between each two
|
|
6789
|
+
* of their edges, less what of it they cover. */
|
|
6790
|
+
function outsideOf(rects: readonly Rect[]): Rect[] {
|
|
6791
|
+
const far = COORD_LIMIT;
|
|
6792
|
+
const edges = new Set<number>();
|
|
6793
|
+
for (const r of rects) {
|
|
6794
|
+
edges.add(r.y);
|
|
6795
|
+
edges.add(r.y + r.height);
|
|
6796
|
+
}
|
|
6797
|
+
const ys = [...edges].sort((a, b) => a - b);
|
|
6798
|
+
const out: Rect[] = [
|
|
6799
|
+
{ x: -far, y: -far, width: 2 * far, height: ys[0] + far },
|
|
6800
|
+
{
|
|
6801
|
+
x: -far,
|
|
6802
|
+
y: ys[ys.length - 1],
|
|
6803
|
+
width: 2 * far,
|
|
6804
|
+
height: far - ys[ys.length - 1],
|
|
6805
|
+
},
|
|
6806
|
+
];
|
|
6807
|
+
for (let i = 0; i + 1 < ys.length; i += 1) {
|
|
6808
|
+
const y0 = ys[i];
|
|
6809
|
+
const y1 = ys[i + 1];
|
|
6810
|
+
const spans = rects
|
|
6811
|
+
.filter((r) => r.y <= y0 && r.y + r.height >= y1)
|
|
6812
|
+
.sort((a, b) => a.x - b.x);
|
|
6813
|
+
let x = -far;
|
|
6814
|
+
for (const r of spans) {
|
|
6815
|
+
if (r.x > x) out.push({ x, y: y0, width: r.x - x, height: y1 - y0 });
|
|
6816
|
+
x = Math.max(x, r.x + r.width);
|
|
5342
6817
|
}
|
|
6818
|
+
if (x < far) out.push({ x, y: y0, width: far - x, height: y1 - y0 });
|
|
5343
6819
|
}
|
|
6820
|
+
return out;
|
|
6821
|
+
}
|
|
6822
|
+
|
|
6823
|
+
/**
|
|
6824
|
+
* The rows a highlight over a line's text covers: the line box's, united
|
|
6825
|
+
* with the text's content area — its face's ascent above the baseline to
|
|
6826
|
+
* its descent below (CSS 2.1 10.6.1) — which is taller wherever
|
|
6827
|
+
* `line-height` is under the face's height. Blink unites the two in the
|
|
6828
|
+
* block direction (`ExpandSelectionRectToLineHeight`), so a selection over
|
|
6829
|
+
* a tall line fills it and one over tall glyphs on a short line covers
|
|
6830
|
+
* them: Zen Garden 215's 91px title on 20px lines had a band a fifth of
|
|
6831
|
+
* its letters' height through their middle.
|
|
6832
|
+
*/
|
|
6833
|
+
export function selectionRows(
|
|
6834
|
+
line: LineBox,
|
|
6835
|
+
text: LineText,
|
|
6836
|
+
natural: { baseline: number; ascent?: number; descent?: number },
|
|
6837
|
+
): { y: number; height: number } {
|
|
6838
|
+
const baseline = text.drawY + natural.baseline;
|
|
6839
|
+
const top = Math.min(line.y, baseline - (natural.ascent ?? 0));
|
|
6840
|
+
const bottom = Math.max(
|
|
6841
|
+
line.y + line.height,
|
|
6842
|
+
baseline + (natural.descent ?? 0),
|
|
6843
|
+
);
|
|
6844
|
+
return { y: top, height: bottom - top };
|
|
5344
6845
|
}
|