@react-x11/components 0.12.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/html/controls.d.ts +12 -3
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +15 -55
- 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 +519 -122
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/parse.d.ts +29 -2
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +277 -9
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +14 -0
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +2 -3
- 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 +59 -7
- package/dist/html/css/ua.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 +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 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +5 -1
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +31 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +52 -8
- package/dist/html/layout/boxes.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/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +10 -0
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts +87 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +510 -152
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +22 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +290 -20
- package/dist/html/paint.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/package.json +2 -2
- package/src/html/controls.ts +29 -49
- package/src/html/css/cascade.ts +591 -140
- package/src/html/css/parse.ts +324 -9
- package/src/html/css/style.ts +24 -3
- package/src/html/css/ua.ts +62 -8
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +116 -6
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/block.ts +3 -0
- package/src/html/layout/boxes.ts +78 -11
- package/src/html/layout/flex.ts +4 -1
- package/src/html/layout/inline.ts +10 -0
- package/src/html/node.ts +498 -135
- package/src/html/paint.ts +330 -29
- package/src/html/widgets.ts +735 -0
- package/src/index.ts +1 -0
package/src/html/paint.ts
CHANGED
|
@@ -70,7 +70,9 @@ import type {
|
|
|
70
70
|
LineBox,
|
|
71
71
|
LineText,
|
|
72
72
|
Marker,
|
|
73
|
+
SelectionStyler,
|
|
73
74
|
} from './layout/boxes.js';
|
|
75
|
+
import type { SelectionStyle } from './css/cascade.js';
|
|
74
76
|
import {
|
|
75
77
|
CLIPPED_TEXT,
|
|
76
78
|
clipBoxOf,
|
|
@@ -192,6 +194,9 @@ export interface PaintOptions {
|
|
|
192
194
|
clips?: ClipLevel[];
|
|
193
195
|
/** @internal Whether the tree has a layer below the flow (`hoistNegative`). */
|
|
194
196
|
negative?: boolean;
|
|
197
|
+
/** @internal Each box's `::selection`, where a rule styles one
|
|
198
|
+
* (`BoxTree.selectionStyler`). */
|
|
199
|
+
selectionStyler?: SelectionStyler | null;
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
/**
|
|
@@ -210,6 +215,19 @@ export interface PaintOptions {
|
|
|
210
215
|
*/
|
|
211
216
|
export const OUT_OF_FLOW_REACH = new WeakMap<Box, number>();
|
|
212
217
|
|
|
218
|
+
/**
|
|
219
|
+
* How far above and below a block's lines its inline content draws, at
|
|
220
|
+
* most, by the array its lines are: glyphs taller than a `line-height` under
|
|
221
|
+
* their face's height, and an atomic's ink past the line it is on. The
|
|
222
|
+
* slack a paint's cull of the lines takes on top of their own rows
|
|
223
|
+
* (`paintLines`); none for the lines of almost every block, whose content
|
|
224
|
+
* is inside them.
|
|
225
|
+
*/
|
|
226
|
+
const LINE_INK = new WeakMap<
|
|
227
|
+
readonly LineBox[],
|
|
228
|
+
{ above: number; below: number }
|
|
229
|
+
>();
|
|
230
|
+
|
|
213
231
|
export function computePaintBounds(box: Box, moved = false): number {
|
|
214
232
|
// A box with no rectangle of its own gives only what it holds: nothing,
|
|
215
233
|
// until a child with bounds is met.
|
|
@@ -333,8 +351,21 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
333
351
|
// above has its bounds already: walked from its line as well, an
|
|
334
352
|
// inline-block in an inline-block was walked twice a level, and twenty of
|
|
335
353
|
// them took seventy milliseconds a layout.
|
|
354
|
+
//
|
|
355
|
+
// And a line's text is drawn from its face's ascent above the baseline to
|
|
356
|
+
// its descent below, its content area (CSS 2.1 10.6.1), which is past the
|
|
357
|
+
// line box wherever `line-height` is under the face's height: a line box
|
|
358
|
+
// is its line-height, whatever its glyphs are. Zen Garden 215's 91px title
|
|
359
|
+
// sits on the 20px lines its body's `line-height: 1.25em` gave it, as a
|
|
360
|
+
// length, and hangs 40px above them and 40px below — ink that a repaint
|
|
361
|
+
// of those rows has to reach, where the lines alone do not.
|
|
362
|
+
let above = 0;
|
|
363
|
+
let below = 0;
|
|
336
364
|
if (lines) {
|
|
365
|
+
const movedOff = MOVED_OFF_LINES.get(lines);
|
|
337
366
|
for (const line of lines) {
|
|
367
|
+
let top = line.y;
|
|
368
|
+
let bottom = line.y + line.height;
|
|
338
369
|
for (const placed of line.atomics) {
|
|
339
370
|
const atomic = placed.box;
|
|
340
371
|
if (atomic.boundsY === Infinity) continue;
|
|
@@ -342,9 +373,31 @@ export function computePaintBounds(box: Box, moved = false): number {
|
|
|
342
373
|
y1 = Math.min(y1, atomic.boundsY);
|
|
343
374
|
x2 = Math.max(x2, atomic.boundsX + atomic.boundsWidth);
|
|
344
375
|
y2 = Math.max(y2, atomic.boundsY + atomic.boundsHeight);
|
|
376
|
+
top = Math.min(top, atomic.boundsY);
|
|
377
|
+
bottom = Math.max(bottom, atomic.boundsY + atomic.boundsHeight);
|
|
378
|
+
}
|
|
379
|
+
// what `position: relative` moved off a line the cull finds on its
|
|
380
|
+
// own (`reachedOff`), wherever it went
|
|
381
|
+
if (movedOff?.has(line)) continue;
|
|
382
|
+
for (const text of line.texts) {
|
|
383
|
+
const natural = text.layout.lines[text.layoutLine];
|
|
384
|
+
if (!natural) continue;
|
|
385
|
+
const baseline = text.drawY + natural.baseline;
|
|
386
|
+
top = Math.min(top, baseline - (natural.ascent ?? 0));
|
|
387
|
+
bottom = Math.max(bottom, baseline + (natural.descent ?? 0));
|
|
388
|
+
}
|
|
389
|
+
if (top < line.y) {
|
|
390
|
+
y1 = Math.min(y1, top);
|
|
391
|
+
above = Math.max(above, line.y - top);
|
|
392
|
+
}
|
|
393
|
+
if (bottom > line.y + line.height) {
|
|
394
|
+
y2 = Math.max(y2, bottom);
|
|
395
|
+
below = Math.max(below, bottom - line.y - line.height);
|
|
345
396
|
}
|
|
346
397
|
}
|
|
347
398
|
}
|
|
399
|
+
if (lines && (above > 0 || below > 0)) LINE_INK.set(lines, { above, below });
|
|
400
|
+
else if (lines) LINE_INK.delete(lines);
|
|
348
401
|
if (x1 === Infinity) {
|
|
349
402
|
// nothing to draw: no damage meets it, and no parent takes it in
|
|
350
403
|
box.boundsX = Infinity;
|
|
@@ -526,6 +579,7 @@ export function paintDocument(
|
|
|
526
579
|
...options,
|
|
527
580
|
canvasSource: canvas?.source,
|
|
528
581
|
negative: tree.negative,
|
|
582
|
+
selectionStyler: options.selection ? tree.selectionStyler : null,
|
|
529
583
|
});
|
|
530
584
|
ctx.restore();
|
|
531
585
|
}
|
|
@@ -4314,7 +4368,12 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4314
4368
|
const bottom = top + damage.height;
|
|
4315
4369
|
let lo = 0;
|
|
4316
4370
|
let hi = lines.length;
|
|
4317
|
-
|
|
4371
|
+
// and a line whose glyphs, or an atomic on it, reach past it
|
|
4372
|
+
// (`LINE_INK`) is drawn where they reach
|
|
4373
|
+
const ink = LINE_INK.get(lines);
|
|
4374
|
+
const above = ink?.above ?? 0;
|
|
4375
|
+
const below = ink?.below ?? 0;
|
|
4376
|
+
const slack = top - box.maxLineHeight - below;
|
|
4318
4377
|
while (lo < hi) {
|
|
4319
4378
|
const mid = (lo + hi) >> 1;
|
|
4320
4379
|
if (lines[mid].y > slack) hi = mid;
|
|
@@ -4322,8 +4381,11 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4322
4381
|
}
|
|
4323
4382
|
for (let i = lo; i < lines.length; i += 1) {
|
|
4324
4383
|
const line = lines[i];
|
|
4325
|
-
if (line.y >= bottom) break;
|
|
4326
|
-
if (line.y + line.height > top) visible.push(line);
|
|
4384
|
+
if (line.y - above >= bottom) break;
|
|
4385
|
+
if (line.y < bottom && line.y + line.height > top) visible.push(line);
|
|
4386
|
+
else if (ink && inkInto(line, top, bottom)) {
|
|
4387
|
+
visible.push(line);
|
|
4388
|
+
}
|
|
4327
4389
|
}
|
|
4328
4390
|
// and a line the damage misses whose text or inline-block `position:
|
|
4329
4391
|
// relative` moved into it: drawn by its line, it went undrawn with it
|
|
@@ -4331,7 +4393,7 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4331
4393
|
if (moved && reachedOff(visible, moved, top, bottom)) {
|
|
4332
4394
|
const drawn = new Set(visible);
|
|
4333
4395
|
for (const line of moved) {
|
|
4334
|
-
if (
|
|
4396
|
+
if (inkInto(line, top, bottom)) drawn.add(line);
|
|
4335
4397
|
}
|
|
4336
4398
|
visible.length = 0;
|
|
4337
4399
|
for (const line of lines) if (drawn.has(line)) visible.push(line);
|
|
@@ -4377,6 +4439,10 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4377
4439
|
// drawing it once per line would be one X request per line for the same
|
|
4378
4440
|
// batch.
|
|
4379
4441
|
const drawn = new Set<unknown>();
|
|
4442
|
+
// text a `::selection` colours is drawn in that colour where selected
|
|
4443
|
+
const recolored = options.selectionStyler
|
|
4444
|
+
? recoloredBands(visible, options)
|
|
4445
|
+
: null;
|
|
4380
4446
|
for (const line of visible) {
|
|
4381
4447
|
for (const text of line.texts) {
|
|
4382
4448
|
if (drawn.has(text.layout)) continue;
|
|
@@ -4390,7 +4456,9 @@ function paintLines(ctx: PaintContext, box: Box, options: PaintOptions): void {
|
|
|
4390
4456
|
// thrown from the protocol encoder.
|
|
4391
4457
|
continue;
|
|
4392
4458
|
}
|
|
4393
|
-
text.layout
|
|
4459
|
+
const bands = recolored?.get(text.layout);
|
|
4460
|
+
if (bands) drawRecolored(ctx, text.layout, text.drawX + dx, top, bands);
|
|
4461
|
+
else text.layout.draw(ctx, text.drawX + dx, top);
|
|
4394
4462
|
}
|
|
4395
4463
|
}
|
|
4396
4464
|
|
|
@@ -4417,15 +4485,16 @@ function reachedOff(
|
|
|
4417
4485
|
bottom: number,
|
|
4418
4486
|
): boolean {
|
|
4419
4487
|
for (const line of moved) {
|
|
4420
|
-
if (!visible.includes(line) &&
|
|
4488
|
+
if (!visible.includes(line) && inkInto(line, top, bottom)) return true;
|
|
4421
4489
|
}
|
|
4422
4490
|
return false;
|
|
4423
4491
|
}
|
|
4424
4492
|
|
|
4425
|
-
/** Whether what
|
|
4426
|
-
*
|
|
4427
|
-
* between `top` and `bottom
|
|
4428
|
-
|
|
4493
|
+
/** Whether what a line draws — its inline-blocks and images, where their
|
|
4494
|
+
* ink is, and its texts, where they are drawn, their glyphs from ascent to
|
|
4495
|
+
* descent — reaches the rows between `top` and `bottom`: what was moved off
|
|
4496
|
+
* it, and what hangs past it. */
|
|
4497
|
+
function inkInto(line: LineBox, top: number, bottom: number): boolean {
|
|
4429
4498
|
for (const placed of line.atomics) {
|
|
4430
4499
|
const box = placed.box;
|
|
4431
4500
|
if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
|
|
@@ -4435,8 +4504,16 @@ function movedInto(line: LineBox, top: number, bottom: number): boolean {
|
|
|
4435
4504
|
for (const text of line.texts) {
|
|
4436
4505
|
const natural = text.layout.lines[text.layoutLine];
|
|
4437
4506
|
if (!natural) continue;
|
|
4438
|
-
const
|
|
4439
|
-
|
|
4507
|
+
const baseline = text.drawY + natural.baseline;
|
|
4508
|
+
const y1 = Math.min(
|
|
4509
|
+
text.drawY + natural.y,
|
|
4510
|
+
baseline - (natural.ascent ?? 0),
|
|
4511
|
+
);
|
|
4512
|
+
const y2 = Math.max(
|
|
4513
|
+
text.drawY + natural.y + natural.height,
|
|
4514
|
+
baseline + (natural.descent ?? 0),
|
|
4515
|
+
);
|
|
4516
|
+
if (y1 < bottom && y2 > top) return true;
|
|
4440
4517
|
}
|
|
4441
4518
|
return false;
|
|
4442
4519
|
}
|
|
@@ -5306,17 +5383,46 @@ export function forgetDecoratedAncestors(box: Box): void {
|
|
|
5306
5383
|
*
|
|
5307
5384
|
* Translucent under the glyphs rather than inverted over them, so the ink
|
|
5308
5385
|
* keeps its contrast on either palette — the same call `<textarea>` and
|
|
5309
|
-
* `<richtext>` both make.
|
|
5386
|
+
* `<richtext>` both make. From the top of the line to its bottom, and past
|
|
5387
|
+
* them over the glyphs where they are taller than the line (`selectionRows`).
|
|
5310
5388
|
*/
|
|
5311
5389
|
function paintSelection(
|
|
5312
5390
|
ctx: PaintContext,
|
|
5313
5391
|
line: LineBox,
|
|
5314
5392
|
options: PaintOptions,
|
|
5315
5393
|
): void {
|
|
5394
|
+
for (const band of selectedBands(line, options)) {
|
|
5395
|
+
const fill = band.style ? band.style.background : options.selectionColor;
|
|
5396
|
+
if (!fill || isTransparent(fill)) continue;
|
|
5397
|
+
ctx.fillStyle = fill;
|
|
5398
|
+
ctx.fillRect(band.x, band.y, band.width, band.height);
|
|
5399
|
+
}
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5402
|
+
/** A stretch of selected text on one line, in window pixels, and the
|
|
5403
|
+
* `::selection` of the element it is in: null for the palette's. */
|
|
5404
|
+
interface SelectedBand extends Rect {
|
|
5405
|
+
style: SelectionStyle | null;
|
|
5406
|
+
/** The layout whose text it is. */
|
|
5407
|
+
layout: unknown;
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
const NO_BANDS: SelectedBand[] = [];
|
|
5411
|
+
|
|
5412
|
+
/**
|
|
5413
|
+
* Where the document selection covers a line's text, a band for each
|
|
5414
|
+
* stretch of it one `::selection` styles, in window pixels as the band is
|
|
5415
|
+
* filled — rounded, so a band's text is drawn to its edges and no further
|
|
5416
|
+
* (`drawRecolored`).
|
|
5417
|
+
*/
|
|
5418
|
+
function selectedBands(line: LineBox, options: PaintOptions): SelectedBand[] {
|
|
5316
5419
|
const range = options.selection;
|
|
5317
|
-
if (!range || range.end <= range.start
|
|
5318
|
-
if (line.textEnd <= range.start || line.textStart >= range.end)
|
|
5319
|
-
|
|
5420
|
+
if (!range || range.end <= range.start) return NO_BANDS;
|
|
5421
|
+
if (line.textEnd <= range.start || line.textStart >= range.end) {
|
|
5422
|
+
return NO_BANDS;
|
|
5423
|
+
}
|
|
5424
|
+
const styler = options.selectionStyler;
|
|
5425
|
+
const out: SelectedBand[] = [];
|
|
5320
5426
|
for (const text of line.texts) {
|
|
5321
5427
|
const natural = text.layout.lines[text.layoutLine];
|
|
5322
5428
|
if (!natural) continue;
|
|
@@ -5326,19 +5432,214 @@ function paintSelection(
|
|
|
5326
5432
|
const offsets = layoutOffsets(text.layout);
|
|
5327
5433
|
const layoutFrom = layoutOffsetOf(text, from);
|
|
5328
5434
|
const layoutTo = layoutOffsetOf(text, to, true);
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
layoutFrom,
|
|
5334
|
-
layoutTo,
|
|
5335
|
-
)
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5435
|
+
const rows = selectionRows(line, text, natural);
|
|
5436
|
+
const y = Math.round(rows.y + options.originY);
|
|
5437
|
+
const height = Math.ceil(rows.height);
|
|
5438
|
+
const pieces = styler
|
|
5439
|
+
? stylePieces(text, natural, layoutFrom, layoutTo, styler)
|
|
5440
|
+
: [{ from: layoutFrom, to: layoutTo, style: null }];
|
|
5441
|
+
for (const piece of pieces) {
|
|
5442
|
+
for (const band of lineBands(
|
|
5443
|
+
text.layout,
|
|
5444
|
+
natural,
|
|
5445
|
+
offsets,
|
|
5446
|
+
piece.from,
|
|
5447
|
+
piece.to,
|
|
5448
|
+
)) {
|
|
5449
|
+
out.push({
|
|
5450
|
+
x: Math.round(band.x + text.drawX + options.originX),
|
|
5451
|
+
y,
|
|
5452
|
+
width: Math.ceil(band.width),
|
|
5453
|
+
height,
|
|
5454
|
+
style: piece.style,
|
|
5455
|
+
layout: text.layout,
|
|
5456
|
+
});
|
|
5457
|
+
}
|
|
5342
5458
|
}
|
|
5343
5459
|
}
|
|
5460
|
+
return out;
|
|
5461
|
+
}
|
|
5462
|
+
|
|
5463
|
+
/**
|
|
5464
|
+
* A line's selected text, `from` to `to` in the layout's offsets, cut where
|
|
5465
|
+
* the `::selection` over it changes: a run's is its element's, and a run of
|
|
5466
|
+
* no element's — an inline box's edge, laid out as a spacer — the one
|
|
5467
|
+
* beside it, so a styled band has no palette-coloured gap at a `<span>`.
|
|
5468
|
+
*/
|
|
5469
|
+
function stylePieces(
|
|
5470
|
+
text: LineText,
|
|
5471
|
+
natural: LineText['layout']['lines'][number],
|
|
5472
|
+
from: number,
|
|
5473
|
+
to: number,
|
|
5474
|
+
styler: SelectionStyler,
|
|
5475
|
+
): { from: number; to: number; style: SelectionStyle | null }[] {
|
|
5476
|
+
const runs = natural.runs
|
|
5477
|
+
.filter((run) => run.end > from && run.start < to)
|
|
5478
|
+
.sort((a, b) => a.start - b.start);
|
|
5479
|
+
const boxAt = text.spans.boxAt;
|
|
5480
|
+
const styles: (SelectionStyle | null | undefined)[] = runs.map((run) => {
|
|
5481
|
+
const box = boxAt?.call(text.spans, run.start) ?? null;
|
|
5482
|
+
return box ? styler(box) : undefined;
|
|
5483
|
+
});
|
|
5484
|
+
for (let i = 1; i < styles.length; i += 1) {
|
|
5485
|
+
if (styles[i] === undefined) styles[i] = styles[i - 1];
|
|
5486
|
+
}
|
|
5487
|
+
for (let i = styles.length - 2; i >= 0; i -= 1) {
|
|
5488
|
+
if (styles[i] === undefined) styles[i] = styles[i + 1];
|
|
5489
|
+
}
|
|
5490
|
+
const out: { from: number; to: number; style: SelectionStyle | null }[] = [];
|
|
5491
|
+
runs.forEach((run, i) => {
|
|
5492
|
+
const style = styles[i] ?? null;
|
|
5493
|
+
const last = out[out.length - 1];
|
|
5494
|
+
if (last && last.style === style) last.to = Math.min(to, run.end);
|
|
5495
|
+
else {
|
|
5496
|
+
out.push({
|
|
5497
|
+
from: Math.max(from, run.start),
|
|
5498
|
+
to: Math.min(to, run.end),
|
|
5499
|
+
style,
|
|
5500
|
+
});
|
|
5501
|
+
}
|
|
5502
|
+
});
|
|
5503
|
+
return out;
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
/**
|
|
5507
|
+
* The layouts whose selected text a `::selection` gives a colour of its
|
|
5508
|
+
* own, each with the bands it is drawn in that colour (`drawRecolored`).
|
|
5509
|
+
* Null where none does, which is every document with no such rule.
|
|
5510
|
+
*/
|
|
5511
|
+
function recoloredBands(
|
|
5512
|
+
lines: LineBox[],
|
|
5513
|
+
options: PaintOptions,
|
|
5514
|
+
): Map<unknown, SelectedBand[]> | null {
|
|
5515
|
+
let out: Map<unknown, SelectedBand[]> | null = null;
|
|
5516
|
+
for (const line of lines) {
|
|
5517
|
+
for (const band of selectedBands(line, options)) {
|
|
5518
|
+
if (!band.style?.color || !(band.width > 0)) continue;
|
|
5519
|
+
out ??= new Map();
|
|
5520
|
+
const list = out.get(band.layout);
|
|
5521
|
+
if (list) list.push(band);
|
|
5522
|
+
else out.set(band.layout, [band]);
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
5525
|
+
return out;
|
|
5526
|
+
}
|
|
5527
|
+
|
|
5528
|
+
/**
|
|
5529
|
+
* A layout with some of its text selected in a `::selection`'s colour
|
|
5530
|
+
* (CSS Pseudo 4, 3.2): its own colours everywhere but those bands, and in
|
|
5531
|
+
* each band the glyphs in that band's colour — cast there as a hard shadow
|
|
5532
|
+
* from the layout drawn clear of the window, the way `castShadows` draws a
|
|
5533
|
+
* text shadow, since a layout draws in its runs' own colours. Drawn over
|
|
5534
|
+
* rather than in place of the text, the old glyphs' edges showed round the
|
|
5535
|
+
* new ones. Where the context cannot clip or cast, the text keeps its own
|
|
5536
|
+
* colours.
|
|
5537
|
+
*/
|
|
5538
|
+
function drawRecolored(
|
|
5539
|
+
ctx: PaintContext,
|
|
5540
|
+
layout: LineText['layout'],
|
|
5541
|
+
left: number,
|
|
5542
|
+
top: number,
|
|
5543
|
+
bands: SelectedBand[],
|
|
5544
|
+
): void {
|
|
5545
|
+
if (
|
|
5546
|
+
!ctx.save ||
|
|
5547
|
+
!ctx.restore ||
|
|
5548
|
+
!ctx.beginPath ||
|
|
5549
|
+
!ctx.rect ||
|
|
5550
|
+
!ctx.clip ||
|
|
5551
|
+
!('shadowBlur' in ctx)
|
|
5552
|
+
) {
|
|
5553
|
+
layout.draw(ctx, left, top);
|
|
5554
|
+
return;
|
|
5555
|
+
}
|
|
5556
|
+
ctx.save();
|
|
5557
|
+
ctx.beginPath();
|
|
5558
|
+
for (const r of outsideOf(bands)) ctx.rect(r.x, r.y, r.width, r.height);
|
|
5559
|
+
ctx.clip();
|
|
5560
|
+
layout.draw(ctx, left, top);
|
|
5561
|
+
ctx.restore();
|
|
5562
|
+
let right = layout.width;
|
|
5563
|
+
for (const natural of layout.lines) {
|
|
5564
|
+
right = Math.max(right, natural.x + natural.width);
|
|
5565
|
+
}
|
|
5566
|
+
const shift = Math.ceil(left + right) + 1;
|
|
5567
|
+
const colors = new Map<string, SelectedBand[]>();
|
|
5568
|
+
for (const band of bands) {
|
|
5569
|
+
const color = band.style!.color!;
|
|
5570
|
+
const group = colors.get(color);
|
|
5571
|
+
if (group) group.push(band);
|
|
5572
|
+
else colors.set(color, [band]);
|
|
5573
|
+
}
|
|
5574
|
+
for (const [color, group] of colors) {
|
|
5575
|
+
ctx.save();
|
|
5576
|
+
ctx.beginPath();
|
|
5577
|
+
for (const r of group) ctx.rect(r.x, r.y, r.width, r.height);
|
|
5578
|
+
ctx.clip();
|
|
5579
|
+
ctx.shadowColor = color;
|
|
5580
|
+
ctx.shadowBlur = 0.01;
|
|
5581
|
+
ctx.shadowOffsetX = shift;
|
|
5582
|
+
ctx.shadowOffsetY = 0;
|
|
5583
|
+
layout.draw(ctx, left - shift, top);
|
|
5584
|
+
ctx.restore();
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
/** Everything but some rectangles, as rectangles: a strip between each two
|
|
5589
|
+
* of their edges, less what of it they cover. */
|
|
5590
|
+
function outsideOf(rects: readonly Rect[]): Rect[] {
|
|
5591
|
+
const far = COORD_LIMIT;
|
|
5592
|
+
const edges = new Set<number>();
|
|
5593
|
+
for (const r of rects) {
|
|
5594
|
+
edges.add(r.y);
|
|
5595
|
+
edges.add(r.y + r.height);
|
|
5596
|
+
}
|
|
5597
|
+
const ys = [...edges].sort((a, b) => a - b);
|
|
5598
|
+
const out: Rect[] = [
|
|
5599
|
+
{ x: -far, y: -far, width: 2 * far, height: ys[0] + far },
|
|
5600
|
+
{
|
|
5601
|
+
x: -far,
|
|
5602
|
+
y: ys[ys.length - 1],
|
|
5603
|
+
width: 2 * far,
|
|
5604
|
+
height: far - ys[ys.length - 1],
|
|
5605
|
+
},
|
|
5606
|
+
];
|
|
5607
|
+
for (let i = 0; i + 1 < ys.length; i += 1) {
|
|
5608
|
+
const y0 = ys[i];
|
|
5609
|
+
const y1 = ys[i + 1];
|
|
5610
|
+
const spans = rects
|
|
5611
|
+
.filter((r) => r.y <= y0 && r.y + r.height >= y1)
|
|
5612
|
+
.sort((a, b) => a.x - b.x);
|
|
5613
|
+
let x = -far;
|
|
5614
|
+
for (const r of spans) {
|
|
5615
|
+
if (r.x > x) out.push({ x, y: y0, width: r.x - x, height: y1 - y0 });
|
|
5616
|
+
x = Math.max(x, r.x + r.width);
|
|
5617
|
+
}
|
|
5618
|
+
if (x < far) out.push({ x, y: y0, width: far - x, height: y1 - y0 });
|
|
5619
|
+
}
|
|
5620
|
+
return out;
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
/**
|
|
5624
|
+
* The rows a highlight over a line's text covers: the line box's, united
|
|
5625
|
+
* with the text's content area — its face's ascent above the baseline to
|
|
5626
|
+
* its descent below (CSS 2.1 10.6.1) — which is taller wherever
|
|
5627
|
+
* `line-height` is under the face's height. Blink unites the two in the
|
|
5628
|
+
* block direction (`ExpandSelectionRectToLineHeight`), so a selection over
|
|
5629
|
+
* a tall line fills it and one over tall glyphs on a short line covers
|
|
5630
|
+
* them: Zen Garden 215's 91px title on 20px lines had a band a fifth of
|
|
5631
|
+
* its letters' height through their middle.
|
|
5632
|
+
*/
|
|
5633
|
+
export function selectionRows(
|
|
5634
|
+
line: LineBox,
|
|
5635
|
+
text: LineText,
|
|
5636
|
+
natural: { baseline: number; ascent?: number; descent?: number },
|
|
5637
|
+
): { y: number; height: number } {
|
|
5638
|
+
const baseline = text.drawY + natural.baseline;
|
|
5639
|
+
const top = Math.min(line.y, baseline - (natural.ascent ?? 0));
|
|
5640
|
+
const bottom = Math.max(
|
|
5641
|
+
line.y + line.height,
|
|
5642
|
+
baseline + (natural.descent ?? 0),
|
|
5643
|
+
);
|
|
5644
|
+
return { y: top, height: bottom - top };
|
|
5344
5645
|
}
|