@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.
Files changed (80) hide show
  1. package/dist/html/controls.d.ts +12 -3
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +15 -55
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +519 -122
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/parse.d.ts +29 -2
  10. package/dist/html/css/parse.d.ts.map +1 -1
  11. package/dist/html/css/parse.js +277 -9
  12. package/dist/html/css/parse.js.map +1 -1
  13. package/dist/html/css/style.d.ts +14 -0
  14. package/dist/html/css/style.d.ts.map +1 -1
  15. package/dist/html/css/style.js +2 -3
  16. package/dist/html/css/style.js.map +1 -1
  17. package/dist/html/css/ua.d.ts +11 -1
  18. package/dist/html/css/ua.d.ts.map +1 -1
  19. package/dist/html/css/ua.js +59 -7
  20. package/dist/html/css/ua.js.map +1 -1
  21. package/dist/html/dom.d.ts.map +1 -1
  22. package/dist/html/dom.js +3 -1
  23. package/dist/html/dom.js.map +1 -1
  24. package/dist/html/fonts.d.ts +25 -1
  25. package/dist/html/fonts.d.ts.map +1 -1
  26. package/dist/html/fonts.js +99 -6
  27. package/dist/html/fonts.js.map +1 -1
  28. package/dist/html/form.d.ts +200 -0
  29. package/dist/html/form.d.ts.map +1 -0
  30. package/dist/html/form.js +801 -0
  31. package/dist/html/form.js.map +1 -0
  32. package/dist/html/index.d.ts +12 -0
  33. package/dist/html/index.d.ts.map +1 -1
  34. package/dist/html/index.js +30 -301
  35. package/dist/html/index.js.map +1 -1
  36. package/dist/html/layout/block.d.ts.map +1 -1
  37. package/dist/html/layout/block.js +5 -1
  38. package/dist/html/layout/block.js.map +1 -1
  39. package/dist/html/layout/boxes.d.ts +31 -1
  40. package/dist/html/layout/boxes.d.ts.map +1 -1
  41. package/dist/html/layout/boxes.js +52 -8
  42. package/dist/html/layout/boxes.js.map +1 -1
  43. package/dist/html/layout/flex.js +4 -1
  44. package/dist/html/layout/flex.js.map +1 -1
  45. package/dist/html/layout/inline.d.ts.map +1 -1
  46. package/dist/html/layout/inline.js +10 -0
  47. package/dist/html/layout/inline.js.map +1 -1
  48. package/dist/html/node.d.ts +87 -15
  49. package/dist/html/node.d.ts.map +1 -1
  50. package/dist/html/node.js +510 -152
  51. package/dist/html/node.js.map +1 -1
  52. package/dist/html/paint.d.ts +22 -1
  53. package/dist/html/paint.d.ts.map +1 -1
  54. package/dist/html/paint.js +290 -20
  55. package/dist/html/paint.js.map +1 -1
  56. package/dist/html/widgets.d.ts +29 -0
  57. package/dist/html/widgets.d.ts.map +1 -0
  58. package/dist/html/widgets.js +620 -0
  59. package/dist/html/widgets.js.map +1 -0
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js.map +1 -1
  63. package/package.json +2 -2
  64. package/src/html/controls.ts +29 -49
  65. package/src/html/css/cascade.ts +591 -140
  66. package/src/html/css/parse.ts +324 -9
  67. package/src/html/css/style.ts +24 -3
  68. package/src/html/css/ua.ts +62 -8
  69. package/src/html/dom.ts +3 -1
  70. package/src/html/fonts.ts +116 -6
  71. package/src/html/form.ts +962 -0
  72. package/src/html/index.ts +49 -338
  73. package/src/html/layout/block.ts +3 -0
  74. package/src/html/layout/boxes.ts +78 -11
  75. package/src/html/layout/flex.ts +4 -1
  76. package/src/html/layout/inline.ts +10 -0
  77. package/src/html/node.ts +498 -135
  78. package/src/html/paint.ts +330 -29
  79. package/src/html/widgets.ts +735 -0
  80. 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
- const slack = top - box.maxLineHeight;
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 (movedInto(line, top, bottom)) drawn.add(line);
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.draw(ctx, text.drawX + dx, top);
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) && movedInto(line, top, bottom)) return true;
4488
+ if (!visible.includes(line) && inkInto(line, top, bottom)) return true;
4421
4489
  }
4422
4490
  return false;
4423
4491
  }
4424
4492
 
4425
- /** Whether what was moved off a line — its inline-blocks and images, where
4426
- * their ink is, and its texts, where they are drawn — reaches the rows
4427
- * between `top` and `bottom`. */
4428
- function movedInto(line: LineBox, top: number, bottom: number): boolean {
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 y = text.drawY + natural.y;
4439
- if (y < bottom && y + natural.height > top) return true;
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 || !options.selectionColor) return;
5318
- if (line.textEnd <= range.start || line.textStart >= range.end) return;
5319
- ctx.fillStyle = options.selectionColor;
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
- for (const band of lineBands(
5330
- text.layout,
5331
- natural,
5332
- offsets,
5333
- layoutFrom,
5334
- layoutTo,
5335
- )) {
5336
- ctx.fillRect(
5337
- Math.round(band.x + text.drawX + options.originX),
5338
- Math.round(line.y + options.originY),
5339
- Math.ceil(band.width),
5340
- Math.ceil(line.height),
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
  }