@react-x11/components 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flow/node.d.ts +14 -6
- package/dist/flow/node.d.ts.map +1 -1
- package/dist/flow/node.js +82 -64
- package/dist/flow/node.js.map +1 -1
- package/dist/html/controls.d.ts +38 -10
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +43 -58
- 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 +538 -128
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +19 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +86 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +40 -2
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +391 -13
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +52 -1
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +328 -11
- package/dist/html/css/style.js.map +1 -1
- 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 -7
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +10 -1
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js +27 -2
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/css/vars.d.ts +5 -0
- package/dist/html/css/vars.d.ts.map +1 -1
- package/dist/html/css/vars.js +2 -2
- package/dist/html/css/vars.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +38 -3
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +25 -1
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +99 -6
- 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 -264
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts +11 -2
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +186 -19
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +65 -7
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +87 -22
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +2 -2
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +8 -3
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/flex.js +4 -1
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/floats.d.ts +4 -0
- package/dist/html/layout/floats.d.ts.map +1 -1
- package/dist/html/layout/floats.js +26 -0
- package/dist/html/layout/floats.js.map +1 -1
- package/dist/html/layout/inline.d.ts +26 -2
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +1232 -115
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +120 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +993 -174
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +75 -2
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +836 -200
- package/dist/html/paint.js.map +1 -1
- package/dist/html/surfaces.d.ts +7 -2
- package/dist/html/surfaces.d.ts.map +1 -1
- package/dist/html/surfaces.js +16 -0
- package/dist/html/surfaces.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +620 -0
- package/dist/html/widgets.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/internal/window.d.ts +22 -0
- package/dist/internal/window.d.ts.map +1 -1
- package/dist/internal/window.js +35 -8
- package/dist/internal/window.js.map +1 -1
- package/dist/richtext/node.d.ts +7 -0
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js.map +1 -1
- package/package.json +6 -4
- package/src/flow/node.ts +80 -64
- package/src/html/controls.ts +79 -58
- package/src/html/css/cascade.ts +610 -146
- package/src/html/css/color.ts +87 -0
- package/src/html/css/parse.ts +431 -13
- package/src/html/css/style.ts +367 -11
- package/src/html/css/ua.ts +76 -8
- package/src/html/css/values.ts +31 -3
- package/src/html/css/vars.ts +2 -2
- package/src/html/dom.ts +41 -3
- package/src/html/fonts.ts +116 -6
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -300
- package/src/html/layout/block.ts +231 -16
- package/src/html/layout/boxes.ts +133 -32
- package/src/html/layout/cache.ts +8 -3
- package/src/html/layout/flex.ts +4 -1
- package/src/html/layout/floats.ts +24 -0
- package/src/html/layout/inline.ts +1445 -108
- package/src/html/node.ts +1010 -166
- package/src/html/paint.ts +980 -270
- package/src/html/surfaces.ts +21 -1
- package/src/html/widgets.ts +735 -0
- package/src/index.ts +1 -0
- package/src/internal/window.ts +36 -8
- package/src/richtext/node.ts +7 -0
package/dist/html/paint.js
CHANGED
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
import { canFill, lineBands, paintRunBackgrounds, paintRunRules, } from '../richtext/runs.js';
|
|
23
23
|
import { alphaOf, inkColor, isTransparent, resolve } from './css/values.js';
|
|
24
24
|
import { blend, borderShades } from './css/color.js';
|
|
25
|
-
import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, scrolls, } from './css/style.js';
|
|
25
|
+
import { CONTAIN_LAYOUT, CONTAIN_PAINT, CONTAIN_SIZE, copyStyle, masked, scrolls, } from './css/style.js';
|
|
26
26
|
import { contained } from './layout/block.js';
|
|
27
|
-
import { BOX_RAISES, LINE_BOX_RAISES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
27
|
+
import { BOX_RAISES, LINE_BOX_RAISES, PADDED_FACES, Box, CLAMPED, CLIPPED_CELLS, COLLAPSED_CELLS, FADED_BLOCKS, PAINT_ORDER, INLINE_OFFSETS, MOVED_OFF_LINES, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
28
28
|
import { CLIPPED_TEXT, clipBoxOf, depthOf, inklessLayout, layoutOffsetOf, layoutOffsets, } from './layout/inline.js';
|
|
29
29
|
import { tableGrid } from './layout/grid.js';
|
|
30
30
|
import { SvgDrawing, inlineDrawing } from './svg.js';
|
|
@@ -36,6 +36,22 @@ import { SvgDrawing, inlineDrawing } from './svg.js';
|
|
|
36
36
|
* document's height — handed up rather than kept on every box, where
|
|
37
37
|
* writing it and reading it back cost this walk a fifth of its time.
|
|
38
38
|
*/
|
|
39
|
+
/**
|
|
40
|
+
* How far down an out-of-flow box's scrollable overflow reaches: its border
|
|
41
|
+
* box, and what it holds where it does not clip it (`computePaintBounds`),
|
|
42
|
+
* which the box it is in does not take in and the document does
|
|
43
|
+
* (`layoutDocument`).
|
|
44
|
+
*/
|
|
45
|
+
export const OUT_OF_FLOW_REACH = new WeakMap();
|
|
46
|
+
/**
|
|
47
|
+
* How far above and below a block's lines its inline content draws, at
|
|
48
|
+
* most, by the array its lines are: glyphs taller than a `line-height` under
|
|
49
|
+
* their face's height, and an atomic's ink past the line it is on. The
|
|
50
|
+
* slack a paint's cull of the lines takes on top of their own rows
|
|
51
|
+
* (`paintLines`); none for the lines of almost every block, whose content
|
|
52
|
+
* is inside them.
|
|
53
|
+
*/
|
|
54
|
+
const LINE_INK = new WeakMap();
|
|
39
55
|
export function computePaintBounds(box, moved = false) {
|
|
40
56
|
// A box with no rectangle of its own gives only what it holds: nothing,
|
|
41
57
|
// until a child with bounds is met.
|
|
@@ -47,8 +63,12 @@ export function computePaintBounds(box, moved = false) {
|
|
|
47
63
|
// How far down the content reaches, for the document's height: the
|
|
48
64
|
// border box, and every box and line under it, but not past a box that
|
|
49
65
|
// clips what it holds — where the scrollable overflow ends. Out-of-flow
|
|
50
|
-
// boxes are the layout's to count.
|
|
51
|
-
|
|
66
|
+
// boxes are the layout's to count. A border box of no area reaches
|
|
67
|
+
// nowhere, as a browser has it (Blink's `ScrollableOverflowCalculator`
|
|
68
|
+
// adds no empty rect): the empty blocks a design leaves at its end,
|
|
69
|
+
// below the last one's bottom margin, made the page that margin taller
|
|
70
|
+
const empty = own && !(box.width > 0 && box.height > 0);
|
|
71
|
+
let bottom = empty ? -Infinity : y2;
|
|
52
72
|
// A shadow is ink past the box, and no overflow: a repaint of the strip
|
|
53
73
|
// under a card has to reach the card, and the document is no taller.
|
|
54
74
|
const shadows = own ? box.style.boxShadow : null;
|
|
@@ -71,6 +91,25 @@ export function computePaintBounds(box, moved = false) {
|
|
|
71
91
|
x2 = Math.max(x2, box.x + box.width + reach);
|
|
72
92
|
y2 = Math.max(y2, box.y + box.height + reach);
|
|
73
93
|
}
|
|
94
|
+
// An inline box's fragments are boxes on the lines they are on, and its
|
|
95
|
+
// padding and border reach above and below those lines: they count in
|
|
96
|
+
// the scrollable overflow of the block it is in (CSS Overflow 3, 2.2) —
|
|
97
|
+
// Blink adds each inline box fragment's border box
|
|
98
|
+
// (`ScrollableOverflowCalculator::AddItemsInternal`). Design 150's footer
|
|
99
|
+
// links, 50px of padding under their text, made Chrome's page 19px
|
|
100
|
+
// taller than the box they end. Only a box with padding or a border
|
|
101
|
+
// above or below its text can reach past its lines.
|
|
102
|
+
if (box.kind === 'inline' &&
|
|
103
|
+
box.padTop + box.padBottom + box.borderTop + box.borderBottom > 0) {
|
|
104
|
+
const reach = inlineFragmentsReach(box);
|
|
105
|
+
if (reach) {
|
|
106
|
+
x1 = Math.min(x1, reach.x1);
|
|
107
|
+
y1 = Math.min(y1, reach.y1);
|
|
108
|
+
x2 = Math.max(x2, reach.x2);
|
|
109
|
+
y2 = Math.max(y2, reach.y2);
|
|
110
|
+
bottom = Math.max(bottom, reach.y2);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
74
113
|
const lines = box.lines;
|
|
75
114
|
if (lines) {
|
|
76
115
|
let tallest = 0;
|
|
@@ -127,6 +166,8 @@ export function computePaintBounds(box, moved = false) {
|
|
|
127
166
|
const reach = computePaintBounds(child, moved);
|
|
128
167
|
if (!child.outOfFlow)
|
|
129
168
|
bottom = Math.max(bottom, reach);
|
|
169
|
+
else
|
|
170
|
+
OUT_OF_FLOW_REACH.set(child, reach);
|
|
130
171
|
if (child.boundsY === Infinity)
|
|
131
172
|
continue;
|
|
132
173
|
x1 = Math.min(x1, child.boundsX);
|
|
@@ -138,8 +179,21 @@ export function computePaintBounds(box, moved = false) {
|
|
|
138
179
|
// above has its bounds already: walked from its line as well, an
|
|
139
180
|
// inline-block in an inline-block was walked twice a level, and twenty of
|
|
140
181
|
// them took seventy milliseconds a layout.
|
|
182
|
+
//
|
|
183
|
+
// And a line's text is drawn from its face's ascent above the baseline to
|
|
184
|
+
// its descent below, its content area (CSS 2.1 10.6.1), which is past the
|
|
185
|
+
// line box wherever `line-height` is under the face's height: a line box
|
|
186
|
+
// is its line-height, whatever its glyphs are. Zen Garden 215's 91px title
|
|
187
|
+
// sits on the 20px lines its body's `line-height: 1.25em` gave it, as a
|
|
188
|
+
// length, and hangs 40px above them and 40px below — ink that a repaint
|
|
189
|
+
// of those rows has to reach, where the lines alone do not.
|
|
190
|
+
let above = 0;
|
|
191
|
+
let below = 0;
|
|
141
192
|
if (lines) {
|
|
193
|
+
const movedOff = MOVED_OFF_LINES.get(lines);
|
|
142
194
|
for (const line of lines) {
|
|
195
|
+
let top = line.y;
|
|
196
|
+
let bottom = line.y + line.height;
|
|
143
197
|
for (const placed of line.atomics) {
|
|
144
198
|
const atomic = placed.box;
|
|
145
199
|
if (atomic.boundsY === Infinity)
|
|
@@ -148,9 +202,35 @@ export function computePaintBounds(box, moved = false) {
|
|
|
148
202
|
y1 = Math.min(y1, atomic.boundsY);
|
|
149
203
|
x2 = Math.max(x2, atomic.boundsX + atomic.boundsWidth);
|
|
150
204
|
y2 = Math.max(y2, atomic.boundsY + atomic.boundsHeight);
|
|
205
|
+
top = Math.min(top, atomic.boundsY);
|
|
206
|
+
bottom = Math.max(bottom, atomic.boundsY + atomic.boundsHeight);
|
|
207
|
+
}
|
|
208
|
+
// what `position: relative` moved off a line the cull finds on its
|
|
209
|
+
// own (`reachedOff`), wherever it went
|
|
210
|
+
if (movedOff?.has(line))
|
|
211
|
+
continue;
|
|
212
|
+
for (const text of line.texts) {
|
|
213
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
214
|
+
if (!natural)
|
|
215
|
+
continue;
|
|
216
|
+
const baseline = text.drawY + natural.baseline;
|
|
217
|
+
top = Math.min(top, baseline - (natural.ascent ?? 0));
|
|
218
|
+
bottom = Math.max(bottom, baseline + (natural.descent ?? 0));
|
|
219
|
+
}
|
|
220
|
+
if (top < line.y) {
|
|
221
|
+
y1 = Math.min(y1, top);
|
|
222
|
+
above = Math.max(above, line.y - top);
|
|
223
|
+
}
|
|
224
|
+
if (bottom > line.y + line.height) {
|
|
225
|
+
y2 = Math.max(y2, bottom);
|
|
226
|
+
below = Math.max(below, bottom - line.y - line.height);
|
|
151
227
|
}
|
|
152
228
|
}
|
|
153
229
|
}
|
|
230
|
+
if (lines && (above > 0 || below > 0))
|
|
231
|
+
LINE_INK.set(lines, { above, below });
|
|
232
|
+
else if (lines)
|
|
233
|
+
LINE_INK.delete(lines);
|
|
154
234
|
if (x1 === Infinity) {
|
|
155
235
|
// nothing to draw: no damage meets it, and no parent takes it in
|
|
156
236
|
box.boundsX = Infinity;
|
|
@@ -174,7 +254,7 @@ export function computePaintBounds(box, moved = false) {
|
|
|
174
254
|
return bottom;
|
|
175
255
|
// whether the box clips only matters where its content reaches past it,
|
|
176
256
|
// and its style is one more object a walk of every box would read
|
|
177
|
-
const end = box.y + box.height;
|
|
257
|
+
const end = empty ? -Infinity : box.y + box.height;
|
|
178
258
|
if (bottom <= end)
|
|
179
259
|
return end;
|
|
180
260
|
const style = box.style;
|
|
@@ -200,6 +280,56 @@ export function hasRect(box) {
|
|
|
200
280
|
const display = box.style.display;
|
|
201
281
|
return display !== 'table-column' && display !== 'table-column-group';
|
|
202
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Where an inline box's fragments are, over the lines of the block it is
|
|
285
|
+
* in that hold its text: from its face's ascent and its top padding and
|
|
286
|
+
* border above each line's baseline to its descent and its bottom ones
|
|
287
|
+
* below, as `paintInlineBoxes` draws them. Null for a box with no text or
|
|
288
|
+
* no face.
|
|
289
|
+
*/
|
|
290
|
+
function inlineFragmentsReach(box) {
|
|
291
|
+
const face = box.decoration ?? PADDED_FACES.get(box);
|
|
292
|
+
if (!face || box.subtreeTextEnd <= box.subtreeTextStart)
|
|
293
|
+
return null;
|
|
294
|
+
let block = box.parent;
|
|
295
|
+
while (block && block.kind === 'inline')
|
|
296
|
+
block = block.parent;
|
|
297
|
+
const lines = block?.lines;
|
|
298
|
+
if (!lines?.length)
|
|
299
|
+
return null;
|
|
300
|
+
const from = box.subtreeTextStart;
|
|
301
|
+
const to = box.subtreeTextEnd;
|
|
302
|
+
let lo = 0;
|
|
303
|
+
let hi = lines.length;
|
|
304
|
+
while (lo < hi) {
|
|
305
|
+
const mid = (lo + hi) >> 1;
|
|
306
|
+
if (lines[mid].textEnd > from)
|
|
307
|
+
hi = mid;
|
|
308
|
+
else
|
|
309
|
+
lo = mid + 1;
|
|
310
|
+
}
|
|
311
|
+
const moved = offsetOf(box);
|
|
312
|
+
let x1 = Infinity;
|
|
313
|
+
let y1 = Infinity;
|
|
314
|
+
let x2 = -Infinity;
|
|
315
|
+
let y2 = -Infinity;
|
|
316
|
+
for (let i = lo; i < lines.length; i += 1) {
|
|
317
|
+
const line = lines[i];
|
|
318
|
+
if (line.textStart >= to)
|
|
319
|
+
break;
|
|
320
|
+
if (line.textEnd <= from)
|
|
321
|
+
continue;
|
|
322
|
+
const raise = SHIFTED_LINES.has(line)
|
|
323
|
+
? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
|
|
324
|
+
: 0;
|
|
325
|
+
const baseline = line.y + line.baseline - raise + (moved?.y ?? 0);
|
|
326
|
+
x1 = Math.min(x1, line.x);
|
|
327
|
+
x2 = Math.max(x2, line.x + line.width);
|
|
328
|
+
y1 = Math.min(y1, baseline - face.ascent - box.padTop - box.borderTop);
|
|
329
|
+
y2 = Math.max(y2, baseline + face.descent + box.padBottom + box.borderBottom);
|
|
330
|
+
}
|
|
331
|
+
return x1 === Infinity ? null : { x1, y1, x2, y2 };
|
|
332
|
+
}
|
|
203
333
|
/** Children lists past this size get the sorted viewport index; below it a
|
|
204
334
|
* linear scan is cheaper than keeping one. */
|
|
205
335
|
const PAINT_INDEX_MIN = 64;
|
|
@@ -287,6 +417,7 @@ export function paintDocument(ctx, tree, options) {
|
|
|
287
417
|
...options,
|
|
288
418
|
canvasSource: canvas?.source,
|
|
289
419
|
negative: tree.negative,
|
|
420
|
+
selectionStyler: options.selection ? tree.selectionStyler : null,
|
|
290
421
|
});
|
|
291
422
|
ctx.restore();
|
|
292
423
|
}
|
|
@@ -370,7 +501,7 @@ function paintCanvas(ctx, { style: source, anchor }, root, options) {
|
|
|
370
501
|
// browser does — the stripes a short page with a gradient on its
|
|
371
502
|
// body shows — or by the viewport, where it is fixed
|
|
372
503
|
const box = style.backgroundAttachment === 'fixed' && options.canvas
|
|
373
|
-
? options.canvas
|
|
504
|
+
? (options.viewport ?? options.canvas)
|
|
374
505
|
: originBox(anchor, options, style);
|
|
375
506
|
paintGradient(ctx, style, style.backgroundGradient, area, snapped(box.x, box.y, box.width, box.height));
|
|
376
507
|
}
|
|
@@ -458,8 +589,10 @@ function paddingBox(box, options) {
|
|
|
458
589
|
};
|
|
459
590
|
}
|
|
460
591
|
/** Where a layer of a box's background is placed, in window coordinates:
|
|
461
|
-
* the box its `background-origin` names (CSS Backgrounds 3, 3.8)
|
|
462
|
-
|
|
592
|
+
* the box its `background-origin` names (CSS Backgrounds 3, 3.8) — of the
|
|
593
|
+
* whole strip, for a fragment of an inline box that is sliced. */
|
|
594
|
+
function originBox(frame, options, style) {
|
|
595
|
+
const box = frame.strip ?? frame;
|
|
463
596
|
const [t, r, b, l] = edgeInsets(box, style.backgroundOrigin);
|
|
464
597
|
return {
|
|
465
598
|
x: box.x + l + options.originX,
|
|
@@ -525,8 +658,14 @@ function paintBox(ctx, box, options) {
|
|
|
525
658
|
ctx.save();
|
|
526
659
|
ctx.globalAlpha = ctx.globalAlpha * opacity;
|
|
527
660
|
}
|
|
661
|
+
if (masked(box.style) && paintMasked(ctx, box, options)) {
|
|
662
|
+
if (fade)
|
|
663
|
+
ctx.restore();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
528
666
|
// `clip` shows the part of an absolutely positioned box it names, its own
|
|
529
|
-
// background and borders among it (CSS 2.1 11.1.2)
|
|
667
|
+
// background and borders among it (CSS 2.1 11.1.2) — written out here,
|
|
668
|
+
// not called: a frame more a level and a deep document ran out of stack
|
|
530
669
|
const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
|
|
531
670
|
if (!clip || (clip.w > 0 && clip.h > 0)) {
|
|
532
671
|
const clipped = !!clip && pushClip(ctx, clip, null);
|
|
@@ -537,6 +676,135 @@ function paintBox(ctx, box, options) {
|
|
|
537
676
|
if (fade)
|
|
538
677
|
ctx.restore();
|
|
539
678
|
}
|
|
679
|
+
/** A box and what it holds, cut to its `clip` as `paintBox` cuts it. */
|
|
680
|
+
function paintClipped(ctx, box, options) {
|
|
681
|
+
const clip = box.outOfFlow && box.style.clip ? clipOf(box, options) : null;
|
|
682
|
+
if (clip && !(clip.w > 0 && clip.h > 0))
|
|
683
|
+
return;
|
|
684
|
+
const clipped = !!clip && pushClip(ctx, clip, null);
|
|
685
|
+
paintContent(ctx, box, options);
|
|
686
|
+
if (clipped)
|
|
687
|
+
ctx.restore();
|
|
688
|
+
}
|
|
689
|
+
const MASK_LAYERS = new WeakMap();
|
|
690
|
+
/** No corners: a mask layer is placed in the box's rectangle, which its
|
|
691
|
+
* radii do not round (CSS Masking 1, 7). */
|
|
692
|
+
const SQUARE_RADII = [0, 0, 0, 0];
|
|
693
|
+
/**
|
|
694
|
+
* A style for each of a box's mask layers, top first, as `layersOf` makes
|
|
695
|
+
* a background's: the box's own style with the layer's image, repeat, size,
|
|
696
|
+
* position, origin and clip in its background's place, and no colour — so
|
|
697
|
+
* the background painter places and repeats a mask layer as it does a
|
|
698
|
+
* background layer, which is what CSS Masking 1 says a mask layer is (7).
|
|
699
|
+
* Made once a style.
|
|
700
|
+
*/
|
|
701
|
+
function maskLayersOf(style) {
|
|
702
|
+
let layers = MASK_LAYERS.get(style);
|
|
703
|
+
if (layers)
|
|
704
|
+
return layers;
|
|
705
|
+
const mask = style.mask;
|
|
706
|
+
const nth = (list, i) => list[i % list.length];
|
|
707
|
+
layers = mask.images.map((image, i) => {
|
|
708
|
+
const layer = Object.create(style);
|
|
709
|
+
layer.backgroundColor = null;
|
|
710
|
+
layer.backgroundImages = null;
|
|
711
|
+
layer.backgroundImage = typeof image === 'string' ? image : null;
|
|
712
|
+
layer.backgroundGradient =
|
|
713
|
+
image !== null && typeof image !== 'string' ? image : null;
|
|
714
|
+
layer.backgroundRepeat = nth(mask.repeats, i);
|
|
715
|
+
layer.backgroundSize = nth(mask.sizes, i);
|
|
716
|
+
layer.backgroundAttachment = 'scroll';
|
|
717
|
+
[layer.backgroundPositionX, layer.backgroundPositionY] = nth(mask.positions, i);
|
|
718
|
+
layer.backgroundOrigin = nth(mask.origins, i);
|
|
719
|
+
layer.backgroundClip = nth(mask.clips, i);
|
|
720
|
+
layer.backgroundClipText = false;
|
|
721
|
+
layer.borderRadius = SQUARE_RADII;
|
|
722
|
+
layer.borderRadiusY = null;
|
|
723
|
+
return layer;
|
|
724
|
+
});
|
|
725
|
+
MASK_LAYERS.set(style, layers);
|
|
726
|
+
return layers;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* A masked box (CSS Masking 1, 7): the box and what it holds drawn on a
|
|
730
|
+
* surface of their own, which the alpha of its mask layers — painted as a
|
|
731
|
+
* background's are, on a second surface, each added over the ones below
|
|
732
|
+
* it — then cuts with `destination-in`, and the result drawn in its place,
|
|
733
|
+
* as the group a mask makes. Only the mask painting area, the border box,
|
|
734
|
+
* can show, and only its part in the damage is drawn.
|
|
735
|
+
*
|
|
736
|
+
* A layer whose image has not arrived is transparent black (7.2), so a box
|
|
737
|
+
* none of whose layers can be drawn yet draws nothing: an icon masked out
|
|
738
|
+
* of its `background-color` is not a solid square while its image loads.
|
|
739
|
+
* False where there is no surface to draw on, or no `destination-in` to
|
|
740
|
+
* draw with, and the caller paints the box unmasked.
|
|
741
|
+
*/
|
|
742
|
+
function paintMasked(ctx, box, options) {
|
|
743
|
+
const layers = maskLayersOf(box.style);
|
|
744
|
+
const drawable = layers.some((layer) => !!layer.backgroundGradient ||
|
|
745
|
+
(!!layer.backgroundImage &&
|
|
746
|
+
!!options.backgroundImageFor?.(layer.backgroundImage)));
|
|
747
|
+
if (!drawable)
|
|
748
|
+
return true;
|
|
749
|
+
if (!options.surface || !ctx.drawImage)
|
|
750
|
+
return false;
|
|
751
|
+
let x0 = Math.floor(box.x + options.originX);
|
|
752
|
+
let y0 = Math.floor(frameY(box) + options.originY);
|
|
753
|
+
let x1 = Math.ceil(box.x + box.width + options.originX);
|
|
754
|
+
let y1 = Math.ceil(frameY(box) + frameHeight(box) + options.originY);
|
|
755
|
+
const damage = options.damage;
|
|
756
|
+
if (damage) {
|
|
757
|
+
x0 = Math.max(x0, Math.floor(damage.x));
|
|
758
|
+
y0 = Math.max(y0, Math.floor(damage.y));
|
|
759
|
+
x1 = Math.min(x1, Math.ceil(damage.x + damage.width));
|
|
760
|
+
y1 = Math.min(y1, Math.ceil(damage.y + damage.height));
|
|
761
|
+
}
|
|
762
|
+
const w = x1 - x0;
|
|
763
|
+
const h = y1 - y0;
|
|
764
|
+
if (!(w > 0 && h > 0))
|
|
765
|
+
return true;
|
|
766
|
+
const content = options.surface(w, h);
|
|
767
|
+
const mask = content && options.surface(w, h);
|
|
768
|
+
try {
|
|
769
|
+
if (!content || !mask)
|
|
770
|
+
return false;
|
|
771
|
+
const cctx = content.getContext('2d');
|
|
772
|
+
const mctx = mask.getContext('2d');
|
|
773
|
+
// the surfaces' own coordinates, the window's moved to their corner
|
|
774
|
+
const on = {
|
|
775
|
+
...options,
|
|
776
|
+
originX: options.originX - x0,
|
|
777
|
+
originY: options.originY - y0,
|
|
778
|
+
damage: { x: 0, y: 0, width: w, height: h },
|
|
779
|
+
canvas: options.canvas && {
|
|
780
|
+
...options.canvas,
|
|
781
|
+
x: options.canvas.x - x0,
|
|
782
|
+
y: options.canvas.y - y0,
|
|
783
|
+
},
|
|
784
|
+
viewport: options.viewport && {
|
|
785
|
+
...options.viewport,
|
|
786
|
+
x: options.viewport.x - x0,
|
|
787
|
+
y: options.viewport.y - y0,
|
|
788
|
+
},
|
|
789
|
+
clips: [],
|
|
790
|
+
};
|
|
791
|
+
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
792
|
+
paintLayer(mctx, box, on, layers[i], frameImages(mctx, box, on));
|
|
793
|
+
}
|
|
794
|
+
paintClipped(cctx, box, on);
|
|
795
|
+
cctx.globalCompositeOperation = 'destination-in';
|
|
796
|
+
if (cctx.globalCompositeOperation !== 'destination-in')
|
|
797
|
+
return false;
|
|
798
|
+
cctx.drawImage(mask, 0, 0);
|
|
799
|
+
cctx.globalCompositeOperation = 'source-over';
|
|
800
|
+
ctx.drawImage(content, x0, y0);
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
finally {
|
|
804
|
+
content?.destroy?.();
|
|
805
|
+
mask?.destroy?.();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
540
808
|
function paintContent(ctx, box, options) {
|
|
541
809
|
const visible = box.style.visibility === 'visible';
|
|
542
810
|
if (visible)
|
|
@@ -779,9 +1047,6 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
779
1047
|
}, 0);
|
|
780
1048
|
continue;
|
|
781
1049
|
}
|
|
782
|
-
if (bakedOuter(ctx, options, s, color, shape, around, covered ? null : { rect: own, corners })) {
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
1050
|
// what of the shadow falls under the box is not drawn
|
|
786
1051
|
const clipped = !covered && ctx.clip && ctx.rect && ctx.save;
|
|
787
1052
|
if (clipped) {
|
|
@@ -792,7 +1057,7 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
792
1057
|
ctx.clip();
|
|
793
1058
|
}
|
|
794
1059
|
fillShadow(ctx, s, color, (dx) => {
|
|
795
|
-
|
|
1060
|
+
shadowShape(ctx, shape.x - dx, shape.y, shape.width, shape.height, around);
|
|
796
1061
|
}, shape.x + shape.width + reach);
|
|
797
1062
|
if (clipped)
|
|
798
1063
|
ctx.restore();
|
|
@@ -842,8 +1107,6 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
842
1107
|
fillRing(ctx, pad, inner, hole, within);
|
|
843
1108
|
continue;
|
|
844
1109
|
}
|
|
845
|
-
if (bakedInset(ctx, options, s, color, pad, inner, hole, within))
|
|
846
|
-
continue;
|
|
847
1110
|
if (!ctx.clip || !ctx.save || !ctx.rect)
|
|
848
1111
|
continue;
|
|
849
1112
|
ctx.save();
|
|
@@ -861,128 +1124,24 @@ function paintShadows(ctx, box, options, inset) {
|
|
|
861
1124
|
fillShadow(ctx, s, color, (dx) => {
|
|
862
1125
|
ctx.rect(frame.x - dx, frame.y, frame.width, frame.height);
|
|
863
1126
|
if (hole.width > 0 && hole.height > 0) {
|
|
864
|
-
|
|
1127
|
+
shadowShape(ctx, hole.x - dx, hole.y, hole.width, hole.height, within);
|
|
865
1128
|
}
|
|
866
|
-
}, frame.x + frame.width + shadowReach(s.blur));
|
|
1129
|
+
}, frame.x + frame.width + shadowReach(s.blur), 'evenodd');
|
|
867
1130
|
ctx.restore();
|
|
868
1131
|
}
|
|
869
1132
|
}
|
|
870
|
-
/** Corners as part of a key. */
|
|
871
|
-
function cornerKey(c) {
|
|
872
|
-
return `${c.x.join(',')}/${c.y.join(',')}`;
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* An outer shadow drawn once on a surface of its own and composited: the
|
|
876
|
-
* shape's shadow, and where `box` is given the box cut out of it — so a box
|
|
877
|
-
* that shows what is behind it shows no shadow there, with the clip on the
|
|
878
|
-
* small surface rather than the window. False where there is no surface.
|
|
879
|
-
*/
|
|
880
|
-
function bakedOuter(ctx, options, s, color, shape, around, box) {
|
|
881
|
-
if (!options.cached || !ctx.drawImage)
|
|
882
|
-
return false;
|
|
883
|
-
const reach = shadowReach(s.blur) + 1;
|
|
884
|
-
const x0 = Math.floor(shape.x) - reach;
|
|
885
|
-
const y0 = Math.floor(shape.y) - reach;
|
|
886
|
-
const w = Math.ceil(shape.x + shape.width) + reach - x0;
|
|
887
|
-
const h = Math.ceil(shape.y + shape.height) + reach - y0;
|
|
888
|
-
const sx = shape.x - x0;
|
|
889
|
-
const sy = shape.y - y0;
|
|
890
|
-
const cut = box && {
|
|
891
|
-
x: box.rect.x - x0,
|
|
892
|
-
y: box.rect.y - y0,
|
|
893
|
-
width: box.rect.width,
|
|
894
|
-
height: box.rect.height,
|
|
895
|
-
};
|
|
896
|
-
const key = [
|
|
897
|
-
'shadow',
|
|
898
|
-
w,
|
|
899
|
-
h,
|
|
900
|
-
sx,
|
|
901
|
-
sy,
|
|
902
|
-
shape.width,
|
|
903
|
-
shape.height,
|
|
904
|
-
cornerKey(around),
|
|
905
|
-
s.blur,
|
|
906
|
-
color,
|
|
907
|
-
cut
|
|
908
|
-
? `${cut.x},${cut.y},${cut.width},${cut.height},${cornerKey(box.corners)}`
|
|
909
|
-
: '',
|
|
910
|
-
].join('|');
|
|
911
|
-
const image = options.cached(key, w, h, (sctx) => {
|
|
912
|
-
if (cut) {
|
|
913
|
-
sctx.save();
|
|
914
|
-
sctx.beginPath();
|
|
915
|
-
sctx.rect(0, 0, w, h);
|
|
916
|
-
roundedRect(sctx, cut.x, cut.y, cut.width, cut.height, box.corners, true, true);
|
|
917
|
-
sctx.clip();
|
|
918
|
-
}
|
|
919
|
-
fillShadow(sctx, s, color, (dx) => {
|
|
920
|
-
roundedRect(sctx, sx - dx, sy, shape.width, shape.height, around);
|
|
921
|
-
}, sx + shape.width + reach);
|
|
922
|
-
if (cut)
|
|
923
|
-
sctx.restore();
|
|
924
|
-
});
|
|
925
|
-
if (!image)
|
|
926
|
-
return false;
|
|
927
|
-
ctx.drawImage(image, x0, y0);
|
|
928
|
-
return true;
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* An inset shadow drawn once on a surface the size of the padding box and
|
|
932
|
-
* composited: a frame around the hole, whose shadow falls inside, clipped
|
|
933
|
-
* to the padding box's corners on the surface. False where there is none.
|
|
934
|
-
*/
|
|
935
|
-
function bakedInset(ctx, options, s, color, pad, inner, hole, within) {
|
|
936
|
-
if (!options.cached || !ctx.drawImage)
|
|
937
|
-
return false;
|
|
938
|
-
const w = Math.round(pad.width);
|
|
939
|
-
const h = Math.round(pad.height);
|
|
940
|
-
const hx = hole.x - pad.x;
|
|
941
|
-
const hy = hole.y - pad.y;
|
|
942
|
-
const reach = shadowReach(s.blur) + Math.abs(s.x) + Math.abs(s.y) + 1;
|
|
943
|
-
const key = [
|
|
944
|
-
'inset',
|
|
945
|
-
w,
|
|
946
|
-
h,
|
|
947
|
-
cornerKey(inner),
|
|
948
|
-
hx,
|
|
949
|
-
hy,
|
|
950
|
-
hole.width,
|
|
951
|
-
hole.height,
|
|
952
|
-
cornerKey(within),
|
|
953
|
-
s.blur,
|
|
954
|
-
color,
|
|
955
|
-
].join('|');
|
|
956
|
-
const image = options.cached(key, w, h, (sctx) => {
|
|
957
|
-
sctx.save();
|
|
958
|
-
sctx.beginPath();
|
|
959
|
-
roundedRect(sctx, 0, 0, w, h, inner);
|
|
960
|
-
sctx.clip();
|
|
961
|
-
fillShadow(sctx, s, color, (dx) => {
|
|
962
|
-
sctx.rect(-reach - dx, -reach, w + 2 * reach, h + 2 * reach);
|
|
963
|
-
if (hole.width > 0 && hole.height > 0) {
|
|
964
|
-
roundedRect(sctx, hx - dx, hy, hole.width, hole.height, within, true, true);
|
|
965
|
-
}
|
|
966
|
-
}, w + reach + shadowReach(s.blur));
|
|
967
|
-
sctx.restore();
|
|
968
|
-
});
|
|
969
|
-
if (!image)
|
|
970
|
-
return false;
|
|
971
|
-
ctx.drawImage(image, pad.x, pad.y);
|
|
972
|
-
return true;
|
|
973
|
-
}
|
|
974
1133
|
/**
|
|
975
1134
|
* Fill a shadow's shape: in its colour where it has no blur, and otherwise
|
|
976
1135
|
* as the context's shadow of the shape drawn `dx` to the left — clear of
|
|
977
1136
|
* the window, its right edge at `right` before the move — with the shadow
|
|
978
1137
|
* offset back by as much, so that only the shadow lands.
|
|
979
1138
|
*/
|
|
980
|
-
function fillShadow(ctx, s, color, shape, right) {
|
|
1139
|
+
function fillShadow(ctx, s, color, shape, right, rule = 'nonzero') {
|
|
981
1140
|
if (!(s.blur > 0)) {
|
|
982
1141
|
ctx.fillStyle = color;
|
|
983
1142
|
ctx.beginPath();
|
|
984
1143
|
shape(0);
|
|
985
|
-
ctx.fill();
|
|
1144
|
+
ctx.fill(rule);
|
|
986
1145
|
return;
|
|
987
1146
|
}
|
|
988
1147
|
const dx = Math.ceil(right) + 1;
|
|
@@ -994,9 +1153,26 @@ function fillShadow(ctx, s, color, shape, right) {
|
|
|
994
1153
|
ctx.fillStyle = '#000000';
|
|
995
1154
|
ctx.beginPath();
|
|
996
1155
|
shape(dx);
|
|
997
|
-
ctx.fill();
|
|
1156
|
+
ctx.fill(rule);
|
|
998
1157
|
ctx.restore();
|
|
999
1158
|
}
|
|
1159
|
+
/**
|
|
1160
|
+
* A shadow's shape, spelled the way a 2d context recognises one: a `rect`,
|
|
1161
|
+
* or a `roundRect` with each corner's own radii, elliptical where the
|
|
1162
|
+
* corner is. react-x11's contexts draw the blurred shadow of such a shape —
|
|
1163
|
+
* and of a `rect` less such a shape, filled `evenodd` — from a tile they
|
|
1164
|
+
* keep, where one built of curves they blur afresh on every fill.
|
|
1165
|
+
*/
|
|
1166
|
+
function shadowShape(ctx, x, y, w, h, c) {
|
|
1167
|
+
if (c.x.every((r) => r === 0) || c.y.every((r) => r === 0)) {
|
|
1168
|
+
ctx.rect(x, y, w, h);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
// a circular corner as the number every context has always taken; only
|
|
1172
|
+
// an elliptical one as the point
|
|
1173
|
+
const corner = (i) => c.x[i] === c.y[i] ? c.x[i] : { x: c.x[i], y: c.y[i] };
|
|
1174
|
+
ctx.roundRect(x, y, w, h, [corner(0), corner(1), corner(2), corner(3)]);
|
|
1175
|
+
}
|
|
1000
1176
|
/**
|
|
1001
1177
|
* The band between two rounded rectangles, the inner inside the outer, in
|
|
1002
1178
|
* the colour already set. Where a side has no band the two edges meet, and
|
|
@@ -1043,6 +1219,7 @@ function inFlow(parent, child, options) {
|
|
|
1043
1219
|
return (style.overflowX === 'visible' &&
|
|
1044
1220
|
style.overflowY === 'visible' &&
|
|
1045
1221
|
opacityOf(child) >= 1 &&
|
|
1222
|
+
!masked(style) &&
|
|
1046
1223
|
// containment makes it a stacking context, painted whole
|
|
1047
1224
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
|
|
1048
1225
|
}
|
|
@@ -1068,6 +1245,7 @@ function flowFlex(parent, child, options) {
|
|
|
1068
1245
|
return (style.overflowX === 'visible' &&
|
|
1069
1246
|
style.overflowY === 'visible' &&
|
|
1070
1247
|
!(child.outOfFlow && style.clip) &&
|
|
1248
|
+
!masked(style) &&
|
|
1071
1249
|
!contained(child, CONTAIN_LAYOUT | CONTAIN_PAINT));
|
|
1072
1250
|
}
|
|
1073
1251
|
/**
|
|
@@ -1294,7 +1472,55 @@ function paintPositioned(ctx, box, options) {
|
|
|
1294
1472
|
return;
|
|
1295
1473
|
}
|
|
1296
1474
|
}
|
|
1297
|
-
paintBox(ctx, box, options);
|
|
1475
|
+
paintBox(ctx, box, atViewport(box, options));
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* The options a box is painted with where it is fixed to the viewport
|
|
1479
|
+
* (CSS 2.1 9.6.1): laid out against the viewport at the document's top,
|
|
1480
|
+
* and drawn where the viewport is now, however far the pane that scrolls
|
|
1481
|
+
* the element has moved it — a fixed header scrolled away with the text.
|
|
1482
|
+
* Once for a box and all it holds.
|
|
1483
|
+
*/
|
|
1484
|
+
function atViewport(box, options) {
|
|
1485
|
+
const viewport = options.viewport;
|
|
1486
|
+
if (!viewport || options.atViewport || !fixedToViewport(box)) {
|
|
1487
|
+
return options;
|
|
1488
|
+
}
|
|
1489
|
+
// where the document's top left is from the viewport's, less where the
|
|
1490
|
+
// viewport is in the document: the scroll
|
|
1491
|
+
const root = rootOf(box);
|
|
1492
|
+
const dx = viewport.x - options.originX - root.x;
|
|
1493
|
+
const dy = viewport.y - options.originY - root.y;
|
|
1494
|
+
return {
|
|
1495
|
+
...options,
|
|
1496
|
+
originX: options.originX + dx,
|
|
1497
|
+
originY: options.originY + dy,
|
|
1498
|
+
atViewport: true,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
/** Per tree, its boxes fixed to the viewport (`fixedToViewport`), found
|
|
1502
|
+
* as the layout positions them: what the element answers a scroll pane's
|
|
1503
|
+
* blit with (`HtmlViewNode.viewportFixedRects`). */
|
|
1504
|
+
export const FIXED_BOXES = new WeakMap();
|
|
1505
|
+
/** Whether a box is fixed to the viewport: `position: fixed` with no
|
|
1506
|
+
* transformed or contained box around it, which would be its containing
|
|
1507
|
+
* block instead (CSS Transforms 1, CSS Containment 2, 3.3). */
|
|
1508
|
+
export function fixedToViewport(box) {
|
|
1509
|
+
if (box.style.position !== 'fixed')
|
|
1510
|
+
return false;
|
|
1511
|
+
for (let at = box.parent; at?.parent; at = at.parent) {
|
|
1512
|
+
if (at.style.translate || at.style.transformTranslate)
|
|
1513
|
+
return false;
|
|
1514
|
+
if (contained(at, CONTAIN_LAYOUT | CONTAIN_PAINT))
|
|
1515
|
+
return false;
|
|
1516
|
+
}
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
function rootOf(box) {
|
|
1520
|
+
let root = box;
|
|
1521
|
+
while (root.parent)
|
|
1522
|
+
root = root.parent;
|
|
1523
|
+
return root;
|
|
1298
1524
|
}
|
|
1299
1525
|
/**
|
|
1300
1526
|
* The box an out-of-flow box is positioned in, whose clips are the ones it
|
|
@@ -1713,6 +1939,9 @@ function stacksLayers(box) {
|
|
|
1713
1939
|
return true;
|
|
1714
1940
|
if (style.opacity < 1 || FADED_BLOCKS.has(box))
|
|
1715
1941
|
return true;
|
|
1942
|
+
// and so does a mask, which is applied to the group (CSS Masking 1, 7)
|
|
1943
|
+
if (masked(style))
|
|
1944
|
+
return true;
|
|
1716
1945
|
// layout and paint containment make one (CSS Containment 2, 3.3, 3.5)
|
|
1717
1946
|
if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT))
|
|
1718
1947
|
return true;
|
|
@@ -1732,7 +1961,7 @@ function flexItem(box) {
|
|
|
1732
1961
|
* outside a box that clips where its containing block is, a fixed one
|
|
1733
1962
|
* outside every one, and a relative one inside every one.
|
|
1734
1963
|
*/
|
|
1735
|
-
function clipsFor(box, context) {
|
|
1964
|
+
export function clipsFor(box, context) {
|
|
1736
1965
|
let from = box.parent;
|
|
1737
1966
|
if (box.outOfFlow) {
|
|
1738
1967
|
const fixed = box.style.position === 'fixed';
|
|
@@ -1759,6 +1988,7 @@ function clipsFor(box, context) {
|
|
|
1759
1988
|
* parent painted it, it was. A clip of no area leaves none of it to paint.
|
|
1760
1989
|
*/
|
|
1761
1990
|
function paintStacked(ctx, box, options) {
|
|
1991
|
+
options = atViewport(box, options);
|
|
1762
1992
|
if (!intersects(box, options))
|
|
1763
1993
|
return;
|
|
1764
1994
|
const between = CLIPS_BETWEEN.get(box);
|
|
@@ -1922,7 +2152,8 @@ function paintBackground(ctx, box, options, style) {
|
|
|
1922
2152
|
// box, which is where it starts, and repeated under the borders — or
|
|
1923
2153
|
// the viewport's, where it is fixed; its line runs across the whole of
|
|
1924
2154
|
// it, not the part this paint reaches
|
|
1925
|
-
const fixed = style.backgroundAttachment === 'fixed' &&
|
|
2155
|
+
const fixed = style.backgroundAttachment === 'fixed' &&
|
|
2156
|
+
(options.viewport ?? options.canvas);
|
|
1926
2157
|
const origin = fixed || originBox(box, options, style);
|
|
1927
2158
|
const at = snapped(origin.x, origin.y, origin.width, origin.height);
|
|
1928
2159
|
if (rounded && style.backgroundSize !== 'auto' && ctx.clip) {
|
|
@@ -2298,7 +2529,7 @@ const MAX_TILES = 4096;
|
|
|
2298
2529
|
*/
|
|
2299
2530
|
function paintBackgroundImage(ctx, style, area, at, options, corners = null) {
|
|
2300
2531
|
if (style.backgroundAttachment === 'fixed' && options.canvas) {
|
|
2301
|
-
at = options.canvas;
|
|
2532
|
+
at = options.viewport ?? options.canvas;
|
|
2302
2533
|
}
|
|
2303
2534
|
const url = style.backgroundImage;
|
|
2304
2535
|
const loaded = url ? options.backgroundImageFor?.(url) : null;
|
|
@@ -3320,7 +3551,12 @@ function paintLines(ctx, box, options) {
|
|
|
3320
3551
|
const bottom = top + damage.height;
|
|
3321
3552
|
let lo = 0;
|
|
3322
3553
|
let hi = lines.length;
|
|
3323
|
-
|
|
3554
|
+
// and a line whose glyphs, or an atomic on it, reach past it
|
|
3555
|
+
// (`LINE_INK`) is drawn where they reach
|
|
3556
|
+
const ink = LINE_INK.get(lines);
|
|
3557
|
+
const above = ink?.above ?? 0;
|
|
3558
|
+
const below = ink?.below ?? 0;
|
|
3559
|
+
const slack = top - box.maxLineHeight - below;
|
|
3324
3560
|
while (lo < hi) {
|
|
3325
3561
|
const mid = (lo + hi) >> 1;
|
|
3326
3562
|
if (lines[mid].y > slack)
|
|
@@ -3330,10 +3566,13 @@ function paintLines(ctx, box, options) {
|
|
|
3330
3566
|
}
|
|
3331
3567
|
for (let i = lo; i < lines.length; i += 1) {
|
|
3332
3568
|
const line = lines[i];
|
|
3333
|
-
if (line.y >= bottom)
|
|
3569
|
+
if (line.y - above >= bottom)
|
|
3334
3570
|
break;
|
|
3335
|
-
if (line.y + line.height > top)
|
|
3571
|
+
if (line.y < bottom && line.y + line.height > top)
|
|
3336
3572
|
visible.push(line);
|
|
3573
|
+
else if (ink && inkInto(line, top, bottom)) {
|
|
3574
|
+
visible.push(line);
|
|
3575
|
+
}
|
|
3337
3576
|
}
|
|
3338
3577
|
// and a line the damage misses whose text or inline-block `position:
|
|
3339
3578
|
// relative` moved into it: drawn by its line, it went undrawn with it
|
|
@@ -3341,7 +3580,7 @@ function paintLines(ctx, box, options) {
|
|
|
3341
3580
|
if (moved && reachedOff(visible, moved, top, bottom)) {
|
|
3342
3581
|
const drawn = new Set(visible);
|
|
3343
3582
|
for (const line of moved) {
|
|
3344
|
-
if (
|
|
3583
|
+
if (inkInto(line, top, bottom))
|
|
3345
3584
|
drawn.add(line);
|
|
3346
3585
|
}
|
|
3347
3586
|
visible.length = 0;
|
|
@@ -3359,7 +3598,7 @@ function paintLines(ctx, box, options) {
|
|
|
3359
3598
|
for (const line of visible) {
|
|
3360
3599
|
if (line.background)
|
|
3361
3600
|
paintLineBackground(ctx, line, options);
|
|
3362
|
-
paintInlineBoxes(ctx, line, options, line === lines[0] ? null : bleeds);
|
|
3601
|
+
paintInlineBoxes(ctx, line, lines, options, line === lines[0] ? null : bleeds);
|
|
3363
3602
|
for (const text of line.texts) {
|
|
3364
3603
|
const natural = text.layout.lines[text.layoutLine];
|
|
3365
3604
|
if (natural)
|
|
@@ -3378,6 +3617,10 @@ function paintLines(ctx, box, options) {
|
|
|
3378
3617
|
// drawing it once per line would be one X request per line for the same
|
|
3379
3618
|
// batch.
|
|
3380
3619
|
const drawn = new Set();
|
|
3620
|
+
// text a `::selection` colours is drawn in that colour where selected
|
|
3621
|
+
const recolored = options.selectionStyler
|
|
3622
|
+
? recoloredBands(visible, options)
|
|
3623
|
+
: null;
|
|
3381
3624
|
for (const line of visible) {
|
|
3382
3625
|
for (const text of line.texts) {
|
|
3383
3626
|
if (drawn.has(text.layout))
|
|
@@ -3392,7 +3635,11 @@ function paintLines(ctx, box, options) {
|
|
|
3392
3635
|
// thrown from the protocol encoder.
|
|
3393
3636
|
continue;
|
|
3394
3637
|
}
|
|
3395
|
-
text.layout
|
|
3638
|
+
const bands = recolored?.get(text.layout);
|
|
3639
|
+
if (bands)
|
|
3640
|
+
drawRecolored(ctx, text.layout, text.drawX + dx, top, bands);
|
|
3641
|
+
else
|
|
3642
|
+
text.layout.draw(ctx, text.drawX + dx, top);
|
|
3396
3643
|
}
|
|
3397
3644
|
}
|
|
3398
3645
|
paintRules(ctx, visible, dx, dy, options.scale ?? 1, 'over');
|
|
@@ -3414,15 +3661,16 @@ function paintLines(ctx, box, options) {
|
|
|
3414
3661
|
* does not already draw, has it within the damage's rows. */
|
|
3415
3662
|
function reachedOff(visible, moved, top, bottom) {
|
|
3416
3663
|
for (const line of moved) {
|
|
3417
|
-
if (!visible.includes(line) &&
|
|
3664
|
+
if (!visible.includes(line) && inkInto(line, top, bottom))
|
|
3418
3665
|
return true;
|
|
3419
3666
|
}
|
|
3420
3667
|
return false;
|
|
3421
3668
|
}
|
|
3422
|
-
/** Whether what
|
|
3423
|
-
*
|
|
3424
|
-
* between `top` and `bottom
|
|
3425
|
-
|
|
3669
|
+
/** Whether what a line draws — its inline-blocks and images, where their
|
|
3670
|
+
* ink is, and its texts, where they are drawn, their glyphs from ascent to
|
|
3671
|
+
* descent — reaches the rows between `top` and `bottom`: what was moved off
|
|
3672
|
+
* it, and what hangs past it. */
|
|
3673
|
+
function inkInto(line, top, bottom) {
|
|
3426
3674
|
for (const placed of line.atomics) {
|
|
3427
3675
|
const box = placed.box;
|
|
3428
3676
|
if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
|
|
@@ -3433,8 +3681,10 @@ function movedInto(line, top, bottom) {
|
|
|
3433
3681
|
const natural = text.layout.lines[text.layoutLine];
|
|
3434
3682
|
if (!natural)
|
|
3435
3683
|
continue;
|
|
3436
|
-
const
|
|
3437
|
-
|
|
3684
|
+
const baseline = text.drawY + natural.baseline;
|
|
3685
|
+
const y1 = Math.min(text.drawY + natural.y, baseline - (natural.ascent ?? 0));
|
|
3686
|
+
const y2 = Math.max(text.drawY + natural.y + natural.height, baseline + (natural.descent ?? 0));
|
|
3687
|
+
if (y1 < bottom && y2 > top)
|
|
3438
3688
|
return true;
|
|
3439
3689
|
}
|
|
3440
3690
|
return false;
|
|
@@ -3803,27 +4053,22 @@ function paintLineBackground(ctx, line, options) {
|
|
|
3803
4053
|
style: background.style,
|
|
3804
4054
|
}, options);
|
|
3805
4055
|
}
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
*
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
// Every text on a line is drawn on one baseline (`finishLine`), and an
|
|
3823
|
-
// engine's is from the top of its layout rather than of the line. Taken
|
|
3824
|
-
// where the line has each text: a box `position: relative` moves is
|
|
3825
|
-
// moved by its own offset below, and the boxes around it are not.
|
|
3826
|
-
let baseline = line.y + line.baseline;
|
|
4056
|
+
/**
|
|
4057
|
+
* The fragments of the decorated inline boxes on a line: how far across it
|
|
4058
|
+
* each reaches, its border box, and whether the box opens or closes on it.
|
|
4059
|
+
* Where the line holds text `position: relative` moved, taken where the
|
|
4060
|
+
* text was: a fragment is moved by its own box's offset.
|
|
4061
|
+
*
|
|
4062
|
+
* A box is more than one fragment on a line where bidi reordering puts
|
|
4063
|
+
* what is not the box's between parts of it (CSS 2.1 9.10): a letter of
|
|
4064
|
+
* another element, an atomic, another element's edge. Its parts are one
|
|
4065
|
+
* fragment wherever nothing else comes between them, whatever room is
|
|
4066
|
+
* between them — a space it hangs, a box of its own inside it — so a line
|
|
4067
|
+
* that reads one way has one fragment of each box, as it always had.
|
|
4068
|
+
*/
|
|
4069
|
+
function fragmentsOn(line) {
|
|
4070
|
+
/** What is on the line, where, and which decorated boxes it is in. */
|
|
4071
|
+
const marks = [];
|
|
3827
4072
|
const shifted = SHIFTED_LINES.has(line);
|
|
3828
4073
|
for (const text of line.texts) {
|
|
3829
4074
|
const natural = text.layout.lines[text.layoutLine];
|
|
@@ -3831,8 +4076,6 @@ bleeds = null) {
|
|
|
3831
4076
|
if (!natural || !boxAt)
|
|
3832
4077
|
continue;
|
|
3833
4078
|
const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
|
|
3834
|
-
const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
|
|
3835
|
-
baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
|
|
3836
4079
|
const x = text.drawX - (shift?.x ?? 0) + natural.x;
|
|
3837
4080
|
for (const run of natural.runs) {
|
|
3838
4081
|
const owner = boxAt.call(text.spans, run.start);
|
|
@@ -3844,45 +4087,147 @@ bleeds = null) {
|
|
|
3844
4087
|
const right = Math.min(x + natural.width, x + run.x + run.width);
|
|
3845
4088
|
if (right <= left)
|
|
3846
4089
|
continue;
|
|
3847
|
-
|
|
3848
|
-
|
|
4090
|
+
marks.push({ left, right, boxes: decoratedAncestors(owner) });
|
|
4091
|
+
}
|
|
4092
|
+
// and the spaces `pre-wrap` keeps that the line ends on, which hang
|
|
4093
|
+
// past it and are their box's all the same (`LineText.hung`)
|
|
4094
|
+
for (const space of text.hung ?? []) {
|
|
4095
|
+
const owner = boxAt.call(text.spans, space.at);
|
|
4096
|
+
if (!owner)
|
|
4097
|
+
continue;
|
|
4098
|
+
const left = text.drawX - (shift?.x ?? 0) + space.x;
|
|
4099
|
+
marks.push({
|
|
4100
|
+
left,
|
|
4101
|
+
right: left + space.width,
|
|
4102
|
+
boxes: decoratedAncestors(owner),
|
|
4103
|
+
});
|
|
3849
4104
|
}
|
|
3850
4105
|
}
|
|
3851
4106
|
for (const placed of line.atomics) {
|
|
3852
4107
|
const atomic = placed.box;
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4108
|
+
marks.push({
|
|
4109
|
+
left: placed.x - atomic.marginLeft,
|
|
4110
|
+
right: placed.x + atomic.width + atomic.marginRight,
|
|
4111
|
+
boxes: decoratedAncestors(atomic),
|
|
4112
|
+
});
|
|
3856
4113
|
}
|
|
3857
4114
|
for (const edge of line.edges ?? []) {
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
4115
|
+
marks.push({
|
|
4116
|
+
left: edge.x,
|
|
4117
|
+
right: edge.x + edge.width,
|
|
4118
|
+
boxes: decoratedAncestors(edge.box),
|
|
4119
|
+
edge,
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
let fragments = null;
|
|
4123
|
+
// each box's parts, left to right
|
|
4124
|
+
const parts = new Map();
|
|
4125
|
+
for (const mark of marks) {
|
|
4126
|
+
for (const box of mark.boxes) {
|
|
4127
|
+
let list = parts.get(box);
|
|
4128
|
+
if (!list)
|
|
4129
|
+
parts.set(box, (list = []));
|
|
4130
|
+
list.push(mark);
|
|
3868
4131
|
}
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
4132
|
+
const own = mark.edge?.box;
|
|
4133
|
+
if (own?.decoration) {
|
|
4134
|
+
let list = parts.get(own);
|
|
4135
|
+
if (!list)
|
|
4136
|
+
parts.set(own, (list = []));
|
|
4137
|
+
list.push(mark);
|
|
3872
4138
|
}
|
|
3873
|
-
const f = fragments.get(box);
|
|
3874
|
-
if (edge.side === 'start')
|
|
3875
|
-
f.start = true;
|
|
3876
|
-
else
|
|
3877
|
-
f.end = true;
|
|
3878
4139
|
}
|
|
4140
|
+
for (const [box, list] of parts) {
|
|
4141
|
+
list.sort((a, b) => a.left - b.left || a.right - b.right);
|
|
4142
|
+
const out = [];
|
|
4143
|
+
let open = null;
|
|
4144
|
+
let reach = -Infinity;
|
|
4145
|
+
for (const mark of list) {
|
|
4146
|
+
// The element's `direction` says which side its start is on, whatever
|
|
4147
|
+
// its text reads as (CSS 2.1 8.6); its own edge's margin is outside
|
|
4148
|
+
// the box, its border and padding inside.
|
|
4149
|
+
let left = mark.left;
|
|
4150
|
+
let right = mark.right;
|
|
4151
|
+
const own = mark.edge?.box === box ? mark.edge : null;
|
|
4152
|
+
if (own) {
|
|
4153
|
+
const onLeft = (own.side === 'start') !== (box.style.direction === 'rtl');
|
|
4154
|
+
if (onLeft)
|
|
4155
|
+
left = right = own.x + box.marginLeft;
|
|
4156
|
+
else
|
|
4157
|
+
left = right = own.x + own.width - box.marginRight;
|
|
4158
|
+
}
|
|
4159
|
+
if (!open || between(marks, box, reach, mark.left)) {
|
|
4160
|
+
open = { left, right, start: false, end: false };
|
|
4161
|
+
out.push(open);
|
|
4162
|
+
}
|
|
4163
|
+
else {
|
|
4164
|
+
open.left = Math.min(open.left, left);
|
|
4165
|
+
open.right = Math.max(open.right, right);
|
|
4166
|
+
}
|
|
4167
|
+
reach = Math.max(reach, mark.right);
|
|
4168
|
+
if (own) {
|
|
4169
|
+
if (own.side === 'start')
|
|
4170
|
+
open.start = true;
|
|
4171
|
+
else
|
|
4172
|
+
open.end = true;
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
(fragments ??= new Map()).set(box, out);
|
|
4176
|
+
}
|
|
4177
|
+
return fragments;
|
|
4178
|
+
}
|
|
4179
|
+
/** Whether anything on a line that is not in a box lies between two of its
|
|
4180
|
+
* parts, from `left` to `right`: what splits it into two fragments. */
|
|
4181
|
+
function between(marks, box, left, right) {
|
|
4182
|
+
if (right - left < 0.5)
|
|
4183
|
+
return false;
|
|
4184
|
+
for (const mark of marks) {
|
|
4185
|
+
if (mark.right - mark.left < 0.5)
|
|
4186
|
+
continue;
|
|
4187
|
+
if (mark.boxes.includes(box) || mark.edge?.box === box)
|
|
4188
|
+
continue;
|
|
4189
|
+
const middle = (mark.left + mark.right) / 2;
|
|
4190
|
+
if (middle > left && middle < right)
|
|
4191
|
+
return true;
|
|
4192
|
+
}
|
|
4193
|
+
return false;
|
|
4194
|
+
}
|
|
4195
|
+
function paintInlineBoxes(ctx, line,
|
|
4196
|
+
/** The block's lines, which `line` is one of. */
|
|
4197
|
+
lines, options,
|
|
4198
|
+
/** Where a fragment that reaches up over the lines before goes, to be
|
|
4199
|
+
* drawn again over their text; null for a block's first line. */
|
|
4200
|
+
bleeds = null) {
|
|
4201
|
+
const fragments = fragmentsOn(line);
|
|
3879
4202
|
if (!fragments)
|
|
3880
4203
|
return;
|
|
4204
|
+
// Every text on a line is drawn on one baseline (`finishLine`), and an
|
|
4205
|
+
// engine's is from the top of its layout rather than of the line. Taken
|
|
4206
|
+
// where the line has each text: a box `position: relative` moves is
|
|
4207
|
+
// moved by its own offset below, and the boxes around it are not.
|
|
4208
|
+
let baseline = line.y + line.baseline;
|
|
4209
|
+
const shifted = SHIFTED_LINES.has(line);
|
|
4210
|
+
for (const text of line.texts) {
|
|
4211
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
4212
|
+
if (!natural || !text.spans.boxAt)
|
|
4213
|
+
continue;
|
|
4214
|
+
const shift = shifted ? TEXT_SHIFTS.get(text) : undefined;
|
|
4215
|
+
const raise = shifted ? (TEXT_RAISES.get(text) ?? 0) : 0;
|
|
4216
|
+
baseline = text.drawY - (shift?.y ?? 0) + raise + natural.baseline;
|
|
4217
|
+
}
|
|
3881
4218
|
const boxes = [...fragments.keys()].sort((a, b) => depthOf(a) - depthOf(b));
|
|
3882
4219
|
for (const box of boxes) {
|
|
3883
4220
|
if (box.style.visibility !== 'visible')
|
|
3884
4221
|
continue;
|
|
3885
|
-
const f
|
|
4222
|
+
for (const f of fragments.get(box)) {
|
|
4223
|
+
paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
/** One fragment of an inline box on a line (`paintInlineBoxes`). */
|
|
4228
|
+
function paintInlineFragment(ctx, line, lines, options, bleeds, box, f, baseline) {
|
|
4229
|
+
const shifted = SHIFTED_LINES.has(line);
|
|
4230
|
+
{
|
|
3886
4231
|
const face = box.decoration;
|
|
3887
4232
|
// on its own baseline, which `vertical-align` may raise off the line's
|
|
3888
4233
|
const own = shifted
|
|
@@ -3917,10 +4262,15 @@ bleeds = null) {
|
|
|
3917
4262
|
style,
|
|
3918
4263
|
};
|
|
3919
4264
|
if (fragment.width <= 0)
|
|
3920
|
-
|
|
3921
|
-
// its images
|
|
3922
|
-
//
|
|
3923
|
-
//
|
|
4265
|
+
return;
|
|
4266
|
+
// its images and gradients from the strip its fragments make, where it
|
|
4267
|
+
// goes on to another line, and from its own padding box where it is
|
|
4268
|
+
// on this one alone or `clone` says each fragment is its own
|
|
4269
|
+
if (!(f.start && f.end) && sliced(box.style)) {
|
|
4270
|
+
const strip = stripFor(lines, line, box, fragment);
|
|
4271
|
+
if (strip)
|
|
4272
|
+
fragment.strip = strip;
|
|
4273
|
+
}
|
|
3924
4274
|
paintLayers(ctx, fragment, options, frameImages(ctx, fragment, options));
|
|
3925
4275
|
paintBorders(ctx, fragment, options);
|
|
3926
4276
|
if (box.style.outlineStyle !== 'none')
|
|
@@ -3930,6 +4280,76 @@ bleeds = null) {
|
|
|
3930
4280
|
}
|
|
3931
4281
|
}
|
|
3932
4282
|
}
|
|
4283
|
+
/** Whether an inline box places background images or gradients, which
|
|
4284
|
+
* `slice` places across its fragments, as one box end to end. */
|
|
4285
|
+
function sliced(style) {
|
|
4286
|
+
return (style.boxDecorationBreak === 'slice' &&
|
|
4287
|
+
(style.backgroundImage !== null ||
|
|
4288
|
+
style.backgroundGradient !== null ||
|
|
4289
|
+
style.backgroundImages !== null));
|
|
4290
|
+
}
|
|
4291
|
+
/** Of a block's lines, by the array they are, the strip of each inline box
|
|
4292
|
+
* on them that `sliced` says places images across its fragments: made
|
|
4293
|
+
* once a layout, as `MOVED_OFF_LINES` is, so a repaint of one line reads
|
|
4294
|
+
* the lines before it without going over them again. What `sliced` reads
|
|
4295
|
+
* is never restyled in place (`HtmlViewNode`), which lays the lines out
|
|
4296
|
+
* again instead. */
|
|
4297
|
+
const STRIPS = new WeakMap();
|
|
4298
|
+
function stripsOf(lines) {
|
|
4299
|
+
const strips = new Map();
|
|
4300
|
+
for (const line of lines) {
|
|
4301
|
+
const fragments = fragmentsOn(line);
|
|
4302
|
+
if (!fragments)
|
|
4303
|
+
continue;
|
|
4304
|
+
for (const [box, list] of fragments) {
|
|
4305
|
+
if (!sliced(box.style))
|
|
4306
|
+
continue;
|
|
4307
|
+
let strip = strips.get(box);
|
|
4308
|
+
if (!strip) {
|
|
4309
|
+
strip = { width: 0, lines: 0, before: new Map() };
|
|
4310
|
+
strips.set(box, strip);
|
|
4311
|
+
}
|
|
4312
|
+
strip.before.set(line, strip.width);
|
|
4313
|
+
for (const f of list)
|
|
4314
|
+
strip.width += Math.max(0, f.right - f.left);
|
|
4315
|
+
strip.lines += 1;
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
4318
|
+
return strips;
|
|
4319
|
+
}
|
|
4320
|
+
/**
|
|
4321
|
+
* The box an inline box's fragment on `line` is a slice of (CSS
|
|
4322
|
+
* Fragmentation 3, 5.4): the box's fragments laid end to end in its
|
|
4323
|
+
* direction, as though it had not wrapped — as wide as
|
|
4324
|
+
* they are together, with the box's own borders and padding at its two
|
|
4325
|
+
* ends, placed so that this fragment is where it falls along it. So an
|
|
4326
|
+
* image `no-repeat` at the start is on the first fragment alone, and a
|
|
4327
|
+
* gradient runs once across them all. Undefined where the box is on one
|
|
4328
|
+
* line.
|
|
4329
|
+
*/
|
|
4330
|
+
function stripFor(lines, line, box, fragment) {
|
|
4331
|
+
let strips = STRIPS.get(lines);
|
|
4332
|
+
if (!strips) {
|
|
4333
|
+
strips = stripsOf(lines);
|
|
4334
|
+
STRIPS.set(lines, strips);
|
|
4335
|
+
}
|
|
4336
|
+
const strip = strips.get(box);
|
|
4337
|
+
const before = strip?.before.get(line);
|
|
4338
|
+
if (!strip || strip.lines < 2 || before === undefined)
|
|
4339
|
+
return undefined;
|
|
4340
|
+
const x = box.style.direction === 'rtl'
|
|
4341
|
+
? fragment.x + fragment.width + before - strip.width
|
|
4342
|
+
: fragment.x - before;
|
|
4343
|
+
return {
|
|
4344
|
+
...fragment,
|
|
4345
|
+
x,
|
|
4346
|
+
width: strip.width,
|
|
4347
|
+
borderLeft: box.borderLeft,
|
|
4348
|
+
borderRight: box.borderRight,
|
|
4349
|
+
padLeft: box.padLeft,
|
|
4350
|
+
padRight: box.padRight,
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
3933
4353
|
/** An inline box's style on a line it does not both start and end on: no
|
|
3934
4354
|
* rounded corner on a side it goes on from. Made once a style and a pair
|
|
3935
4355
|
* of ends, so a repaint copies no style and the layers made of it are
|
|
@@ -3990,20 +4410,53 @@ function decoratedAncestors(box) {
|
|
|
3990
4410
|
DECORATED.set(box, found);
|
|
3991
4411
|
return found;
|
|
3992
4412
|
}
|
|
4413
|
+
/** A restyle in place (a `:hover`) gave an inline box a decoration where it
|
|
4414
|
+
* had none, or took it away, and the lists kept above do not know: every
|
|
4415
|
+
* box whose list could name it — inside it, through the inline boxes the
|
|
4416
|
+
* walk above climbs — looks again. A document built again has new boxes,
|
|
4417
|
+
* and so no lists. */
|
|
4418
|
+
export function forgetDecoratedAncestors(box) {
|
|
4419
|
+
const stack = [...box.children];
|
|
4420
|
+
while (stack.length) {
|
|
4421
|
+
const at = stack.pop();
|
|
4422
|
+
DECORATED.delete(at);
|
|
4423
|
+
if (at.kind === 'inline')
|
|
4424
|
+
stack.push(...at.children);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
3993
4427
|
/**
|
|
3994
4428
|
* The band the document selection covers on one line.
|
|
3995
4429
|
*
|
|
3996
4430
|
* Translucent under the glyphs rather than inverted over them, so the ink
|
|
3997
4431
|
* keeps its contrast on either palette — the same call `<textarea>` and
|
|
3998
|
-
* `<richtext>` both make.
|
|
4432
|
+
* `<richtext>` both make. From the top of the line to its bottom, and past
|
|
4433
|
+
* them over the glyphs where they are taller than the line (`selectionRows`).
|
|
3999
4434
|
*/
|
|
4000
4435
|
function paintSelection(ctx, line, options) {
|
|
4436
|
+
for (const band of selectedBands(line, options)) {
|
|
4437
|
+
const fill = band.style ? band.style.background : options.selectionColor;
|
|
4438
|
+
if (!fill || isTransparent(fill))
|
|
4439
|
+
continue;
|
|
4440
|
+
ctx.fillStyle = fill;
|
|
4441
|
+
ctx.fillRect(band.x, band.y, band.width, band.height);
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
const NO_BANDS = [];
|
|
4445
|
+
/**
|
|
4446
|
+
* Where the document selection covers a line's text, a band for each
|
|
4447
|
+
* stretch of it one `::selection` styles, in window pixels as the band is
|
|
4448
|
+
* filled — rounded, so a band's text is drawn to its edges and no further
|
|
4449
|
+
* (`drawRecolored`).
|
|
4450
|
+
*/
|
|
4451
|
+
function selectedBands(line, options) {
|
|
4001
4452
|
const range = options.selection;
|
|
4002
|
-
if (!range || range.end <= range.start
|
|
4003
|
-
return;
|
|
4004
|
-
if (line.textEnd <= range.start || line.textStart >= range.end)
|
|
4005
|
-
return;
|
|
4006
|
-
|
|
4453
|
+
if (!range || range.end <= range.start)
|
|
4454
|
+
return NO_BANDS;
|
|
4455
|
+
if (line.textEnd <= range.start || line.textStart >= range.end) {
|
|
4456
|
+
return NO_BANDS;
|
|
4457
|
+
}
|
|
4458
|
+
const styler = options.selectionStyler;
|
|
4459
|
+
const out = [];
|
|
4007
4460
|
for (const text of line.texts) {
|
|
4008
4461
|
const natural = text.layout.lines[text.layoutLine];
|
|
4009
4462
|
if (!natural)
|
|
@@ -4015,9 +4468,192 @@ function paintSelection(ctx, line, options) {
|
|
|
4015
4468
|
const offsets = layoutOffsets(text.layout);
|
|
4016
4469
|
const layoutFrom = layoutOffsetOf(text, from);
|
|
4017
4470
|
const layoutTo = layoutOffsetOf(text, to, true);
|
|
4018
|
-
|
|
4019
|
-
|
|
4471
|
+
const rows = selectionRows(line, text, natural);
|
|
4472
|
+
const y = Math.round(rows.y + options.originY);
|
|
4473
|
+
const height = Math.ceil(rows.height);
|
|
4474
|
+
const pieces = styler
|
|
4475
|
+
? stylePieces(text, natural, layoutFrom, layoutTo, styler)
|
|
4476
|
+
: [{ from: layoutFrom, to: layoutTo, style: null }];
|
|
4477
|
+
for (const piece of pieces) {
|
|
4478
|
+
for (const band of lineBands(text.layout, natural, offsets, piece.from, piece.to)) {
|
|
4479
|
+
out.push({
|
|
4480
|
+
x: Math.round(band.x + text.drawX + options.originX),
|
|
4481
|
+
y,
|
|
4482
|
+
width: Math.ceil(band.width),
|
|
4483
|
+
height,
|
|
4484
|
+
style: piece.style,
|
|
4485
|
+
layout: text.layout,
|
|
4486
|
+
});
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
return out;
|
|
4491
|
+
}
|
|
4492
|
+
/**
|
|
4493
|
+
* A line's selected text, `from` to `to` in the layout's offsets, cut where
|
|
4494
|
+
* the `::selection` over it changes: a run's is its element's, and a run of
|
|
4495
|
+
* no element's — an inline box's edge, laid out as a spacer — the one
|
|
4496
|
+
* beside it, so a styled band has no palette-coloured gap at a `<span>`.
|
|
4497
|
+
*/
|
|
4498
|
+
function stylePieces(text, natural, from, to, styler) {
|
|
4499
|
+
const runs = natural.runs
|
|
4500
|
+
.filter((run) => run.end > from && run.start < to)
|
|
4501
|
+
.sort((a, b) => a.start - b.start);
|
|
4502
|
+
const boxAt = text.spans.boxAt;
|
|
4503
|
+
const styles = runs.map((run) => {
|
|
4504
|
+
const box = boxAt?.call(text.spans, run.start) ?? null;
|
|
4505
|
+
return box ? styler(box) : undefined;
|
|
4506
|
+
});
|
|
4507
|
+
for (let i = 1; i < styles.length; i += 1) {
|
|
4508
|
+
if (styles[i] === undefined)
|
|
4509
|
+
styles[i] = styles[i - 1];
|
|
4510
|
+
}
|
|
4511
|
+
for (let i = styles.length - 2; i >= 0; i -= 1) {
|
|
4512
|
+
if (styles[i] === undefined)
|
|
4513
|
+
styles[i] = styles[i + 1];
|
|
4514
|
+
}
|
|
4515
|
+
const out = [];
|
|
4516
|
+
runs.forEach((run, i) => {
|
|
4517
|
+
const style = styles[i] ?? null;
|
|
4518
|
+
const last = out[out.length - 1];
|
|
4519
|
+
if (last && last.style === style)
|
|
4520
|
+
last.to = Math.min(to, run.end);
|
|
4521
|
+
else {
|
|
4522
|
+
out.push({
|
|
4523
|
+
from: Math.max(from, run.start),
|
|
4524
|
+
to: Math.min(to, run.end),
|
|
4525
|
+
style,
|
|
4526
|
+
});
|
|
4527
|
+
}
|
|
4528
|
+
});
|
|
4529
|
+
return out;
|
|
4530
|
+
}
|
|
4531
|
+
/**
|
|
4532
|
+
* The layouts whose selected text a `::selection` gives a colour of its
|
|
4533
|
+
* own, each with the bands it is drawn in that colour (`drawRecolored`).
|
|
4534
|
+
* Null where none does, which is every document with no such rule.
|
|
4535
|
+
*/
|
|
4536
|
+
function recoloredBands(lines, options) {
|
|
4537
|
+
let out = null;
|
|
4538
|
+
for (const line of lines) {
|
|
4539
|
+
for (const band of selectedBands(line, options)) {
|
|
4540
|
+
if (!band.style?.color || !(band.width > 0))
|
|
4541
|
+
continue;
|
|
4542
|
+
out ??= new Map();
|
|
4543
|
+
const list = out.get(band.layout);
|
|
4544
|
+
if (list)
|
|
4545
|
+
list.push(band);
|
|
4546
|
+
else
|
|
4547
|
+
out.set(band.layout, [band]);
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
return out;
|
|
4551
|
+
}
|
|
4552
|
+
/**
|
|
4553
|
+
* A layout with some of its text selected in a `::selection`'s colour
|
|
4554
|
+
* (CSS Pseudo 4, 3.2): its own colours everywhere but those bands, and in
|
|
4555
|
+
* each band the glyphs in that band's colour — cast there as a hard shadow
|
|
4556
|
+
* from the layout drawn clear of the window, the way `castShadows` draws a
|
|
4557
|
+
* text shadow, since a layout draws in its runs' own colours. Drawn over
|
|
4558
|
+
* rather than in place of the text, the old glyphs' edges showed round the
|
|
4559
|
+
* new ones. Where the context cannot clip or cast, the text keeps its own
|
|
4560
|
+
* colours.
|
|
4561
|
+
*/
|
|
4562
|
+
function drawRecolored(ctx, layout, left, top, bands) {
|
|
4563
|
+
if (!ctx.save ||
|
|
4564
|
+
!ctx.restore ||
|
|
4565
|
+
!ctx.beginPath ||
|
|
4566
|
+
!ctx.rect ||
|
|
4567
|
+
!ctx.clip ||
|
|
4568
|
+
!('shadowBlur' in ctx)) {
|
|
4569
|
+
layout.draw(ctx, left, top);
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
ctx.save();
|
|
4573
|
+
ctx.beginPath();
|
|
4574
|
+
for (const r of outsideOf(bands))
|
|
4575
|
+
ctx.rect(r.x, r.y, r.width, r.height);
|
|
4576
|
+
ctx.clip();
|
|
4577
|
+
layout.draw(ctx, left, top);
|
|
4578
|
+
ctx.restore();
|
|
4579
|
+
let right = layout.width;
|
|
4580
|
+
for (const natural of layout.lines) {
|
|
4581
|
+
right = Math.max(right, natural.x + natural.width);
|
|
4582
|
+
}
|
|
4583
|
+
const shift = Math.ceil(left + right) + 1;
|
|
4584
|
+
const colors = new Map();
|
|
4585
|
+
for (const band of bands) {
|
|
4586
|
+
const color = band.style.color;
|
|
4587
|
+
const group = colors.get(color);
|
|
4588
|
+
if (group)
|
|
4589
|
+
group.push(band);
|
|
4590
|
+
else
|
|
4591
|
+
colors.set(color, [band]);
|
|
4592
|
+
}
|
|
4593
|
+
for (const [color, group] of colors) {
|
|
4594
|
+
ctx.save();
|
|
4595
|
+
ctx.beginPath();
|
|
4596
|
+
for (const r of group)
|
|
4597
|
+
ctx.rect(r.x, r.y, r.width, r.height);
|
|
4598
|
+
ctx.clip();
|
|
4599
|
+
ctx.shadowColor = color;
|
|
4600
|
+
ctx.shadowBlur = 0.01;
|
|
4601
|
+
ctx.shadowOffsetX = shift;
|
|
4602
|
+
ctx.shadowOffsetY = 0;
|
|
4603
|
+
layout.draw(ctx, left - shift, top);
|
|
4604
|
+
ctx.restore();
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
/** Everything but some rectangles, as rectangles: a strip between each two
|
|
4608
|
+
* of their edges, less what of it they cover. */
|
|
4609
|
+
function outsideOf(rects) {
|
|
4610
|
+
const far = COORD_LIMIT;
|
|
4611
|
+
const edges = new Set();
|
|
4612
|
+
for (const r of rects) {
|
|
4613
|
+
edges.add(r.y);
|
|
4614
|
+
edges.add(r.y + r.height);
|
|
4615
|
+
}
|
|
4616
|
+
const ys = [...edges].sort((a, b) => a - b);
|
|
4617
|
+
const out = [
|
|
4618
|
+
{ x: -far, y: -far, width: 2 * far, height: ys[0] + far },
|
|
4619
|
+
{
|
|
4620
|
+
x: -far,
|
|
4621
|
+
y: ys[ys.length - 1],
|
|
4622
|
+
width: 2 * far,
|
|
4623
|
+
height: far - ys[ys.length - 1],
|
|
4624
|
+
},
|
|
4625
|
+
];
|
|
4626
|
+
for (let i = 0; i + 1 < ys.length; i += 1) {
|
|
4627
|
+
const y0 = ys[i];
|
|
4628
|
+
const y1 = ys[i + 1];
|
|
4629
|
+
const spans = rects
|
|
4630
|
+
.filter((r) => r.y <= y0 && r.y + r.height >= y1)
|
|
4631
|
+
.sort((a, b) => a.x - b.x);
|
|
4632
|
+
let x = -far;
|
|
4633
|
+
for (const r of spans) {
|
|
4634
|
+
if (r.x > x)
|
|
4635
|
+
out.push({ x, y: y0, width: r.x - x, height: y1 - y0 });
|
|
4636
|
+
x = Math.max(x, r.x + r.width);
|
|
4020
4637
|
}
|
|
4638
|
+
if (x < far)
|
|
4639
|
+
out.push({ x, y: y0, width: far - x, height: y1 - y0 });
|
|
4021
4640
|
}
|
|
4641
|
+
return out;
|
|
4642
|
+
}
|
|
4643
|
+
/**
|
|
4644
|
+
* The rows a highlight over a line's text covers: the line box's, united
|
|
4645
|
+
* with the text's content area — its face's ascent above the baseline to
|
|
4646
|
+
* its descent below (CSS 2.1 10.6.1) — which is taller wherever
|
|
4647
|
+
* `line-height` is under the face's height. Blink unites the two in the
|
|
4648
|
+
* block direction (`ExpandSelectionRectToLineHeight`), so a selection over
|
|
4649
|
+
* a tall line fills it and one over tall glyphs on a short line covers
|
|
4650
|
+
* them: Zen Garden 215's 91px title on 20px lines had a band a fifth of
|
|
4651
|
+
* its letters' height through their middle.
|
|
4652
|
+
*/
|
|
4653
|
+
export function selectionRows(line, text, natural) {
|
|
4654
|
+
const baseline = text.drawY + natural.baseline;
|
|
4655
|
+
const top = Math.min(line.y, baseline - (natural.ascent ?? 0));
|
|
4656
|
+
const bottom = Math.max(line.y + line.height, baseline + (natural.descent ?? 0));
|
|
4657
|
+
return { y: top, height: bottom - top };
|
|
4022
4658
|
}
|
|
4023
4659
|
//# sourceMappingURL=paint.js.map
|