@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
@@ -43,6 +43,15 @@ import { SvgDrawing, inlineDrawing } from './svg.js';
43
43
  * (`layoutDocument`).
44
44
  */
45
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();
46
55
  export function computePaintBounds(box, moved = false) {
47
56
  // A box with no rectangle of its own gives only what it holds: nothing,
48
57
  // until a child with bounds is met.
@@ -170,8 +179,21 @@ export function computePaintBounds(box, moved = false) {
170
179
  // above has its bounds already: walked from its line as well, an
171
180
  // inline-block in an inline-block was walked twice a level, and twenty of
172
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;
173
192
  if (lines) {
193
+ const movedOff = MOVED_OFF_LINES.get(lines);
174
194
  for (const line of lines) {
195
+ let top = line.y;
196
+ let bottom = line.y + line.height;
175
197
  for (const placed of line.atomics) {
176
198
  const atomic = placed.box;
177
199
  if (atomic.boundsY === Infinity)
@@ -180,9 +202,35 @@ export function computePaintBounds(box, moved = false) {
180
202
  y1 = Math.min(y1, atomic.boundsY);
181
203
  x2 = Math.max(x2, atomic.boundsX + atomic.boundsWidth);
182
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);
183
227
  }
184
228
  }
185
229
  }
230
+ if (lines && (above > 0 || below > 0))
231
+ LINE_INK.set(lines, { above, below });
232
+ else if (lines)
233
+ LINE_INK.delete(lines);
186
234
  if (x1 === Infinity) {
187
235
  // nothing to draw: no damage meets it, and no parent takes it in
188
236
  box.boundsX = Infinity;
@@ -369,6 +417,7 @@ export function paintDocument(ctx, tree, options) {
369
417
  ...options,
370
418
  canvasSource: canvas?.source,
371
419
  negative: tree.negative,
420
+ selectionStyler: options.selection ? tree.selectionStyler : null,
372
421
  });
373
422
  ctx.restore();
374
423
  }
@@ -3502,7 +3551,12 @@ function paintLines(ctx, box, options) {
3502
3551
  const bottom = top + damage.height;
3503
3552
  let lo = 0;
3504
3553
  let hi = lines.length;
3505
- const slack = top - box.maxLineHeight;
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;
3506
3560
  while (lo < hi) {
3507
3561
  const mid = (lo + hi) >> 1;
3508
3562
  if (lines[mid].y > slack)
@@ -3512,10 +3566,13 @@ function paintLines(ctx, box, options) {
3512
3566
  }
3513
3567
  for (let i = lo; i < lines.length; i += 1) {
3514
3568
  const line = lines[i];
3515
- if (line.y >= bottom)
3569
+ if (line.y - above >= bottom)
3516
3570
  break;
3517
- if (line.y + line.height > top)
3571
+ if (line.y < bottom && line.y + line.height > top)
3572
+ visible.push(line);
3573
+ else if (ink && inkInto(line, top, bottom)) {
3518
3574
  visible.push(line);
3575
+ }
3519
3576
  }
3520
3577
  // and a line the damage misses whose text or inline-block `position:
3521
3578
  // relative` moved into it: drawn by its line, it went undrawn with it
@@ -3523,7 +3580,7 @@ function paintLines(ctx, box, options) {
3523
3580
  if (moved && reachedOff(visible, moved, top, bottom)) {
3524
3581
  const drawn = new Set(visible);
3525
3582
  for (const line of moved) {
3526
- if (movedInto(line, top, bottom))
3583
+ if (inkInto(line, top, bottom))
3527
3584
  drawn.add(line);
3528
3585
  }
3529
3586
  visible.length = 0;
@@ -3560,6 +3617,10 @@ function paintLines(ctx, box, options) {
3560
3617
  // drawing it once per line would be one X request per line for the same
3561
3618
  // batch.
3562
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;
3563
3624
  for (const line of visible) {
3564
3625
  for (const text of line.texts) {
3565
3626
  if (drawn.has(text.layout))
@@ -3574,7 +3635,11 @@ function paintLines(ctx, box, options) {
3574
3635
  // thrown from the protocol encoder.
3575
3636
  continue;
3576
3637
  }
3577
- text.layout.draw(ctx, text.drawX + dx, top);
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);
3578
3643
  }
3579
3644
  }
3580
3645
  paintRules(ctx, visible, dx, dy, options.scale ?? 1, 'over');
@@ -3596,15 +3661,16 @@ function paintLines(ctx, box, options) {
3596
3661
  * does not already draw, has it within the damage's rows. */
3597
3662
  function reachedOff(visible, moved, top, bottom) {
3598
3663
  for (const line of moved) {
3599
- if (!visible.includes(line) && movedInto(line, top, bottom))
3664
+ if (!visible.includes(line) && inkInto(line, top, bottom))
3600
3665
  return true;
3601
3666
  }
3602
3667
  return false;
3603
3668
  }
3604
- /** Whether what was moved off a line — its inline-blocks and images, where
3605
- * their ink is, and its texts, where they are drawn — reaches the rows
3606
- * between `top` and `bottom`. */
3607
- function movedInto(line, top, bottom) {
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) {
3608
3674
  for (const placed of line.atomics) {
3609
3675
  const box = placed.box;
3610
3676
  if (box.boundsY < bottom && box.boundsY + box.boundsHeight > top) {
@@ -3615,8 +3681,10 @@ function movedInto(line, top, bottom) {
3615
3681
  const natural = text.layout.lines[text.layoutLine];
3616
3682
  if (!natural)
3617
3683
  continue;
3618
- const y = text.drawY + natural.y;
3619
- if (y < bottom && y + natural.height > top)
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)
3620
3688
  return true;
3621
3689
  }
3622
3690
  return false;
@@ -4361,15 +4429,34 @@ export function forgetDecoratedAncestors(box) {
4361
4429
  *
4362
4430
  * Translucent under the glyphs rather than inverted over them, so the ink
4363
4431
  * keeps its contrast on either palette — the same call `<textarea>` and
4364
- * `<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`).
4365
4434
  */
4366
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) {
4367
4452
  const range = options.selection;
4368
- if (!range || range.end <= range.start || !options.selectionColor)
4369
- return;
4370
- if (line.textEnd <= range.start || line.textStart >= range.end)
4371
- return;
4372
- ctx.fillStyle = options.selectionColor;
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 = [];
4373
4460
  for (const text of line.texts) {
4374
4461
  const natural = text.layout.lines[text.layoutLine];
4375
4462
  if (!natural)
@@ -4381,9 +4468,192 @@ function paintSelection(ctx, line, options) {
4381
4468
  const offsets = layoutOffsets(text.layout);
4382
4469
  const layoutFrom = layoutOffsetOf(text, from);
4383
4470
  const layoutTo = layoutOffsetOf(text, to, true);
4384
- for (const band of lineBands(text.layout, natural, offsets, layoutFrom, layoutTo)) {
4385
- ctx.fillRect(Math.round(band.x + text.drawX + options.originX), Math.round(line.y + options.originY), Math.ceil(band.width), Math.ceil(line.height));
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);
4386
4637
  }
4638
+ if (x < far)
4639
+ out.push({ x, y: y0, width: far - x, height: y1 - y0 });
4387
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 };
4388
4658
  }
4389
4659
  //# sourceMappingURL=paint.js.map