@react-x11/components 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  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 +545 -126
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
@@ -42,8 +42,10 @@ import {
42
42
  scrolls,
43
43
  } from '../css/style.js';
44
44
  import { inkColor, isTransparent, resolve } from '../css/values.js';
45
+ import { tagOf } from '../dom.js';
45
46
  import {
46
47
  BOX_RAISES,
48
+ GRID_TRACKS,
47
49
  LINE_BOX_RAISES,
48
50
  PADDED_FACES,
49
51
  isOffset,
@@ -415,13 +417,21 @@ interface Cut {
415
417
  function cutOf(
416
418
  style: ComputedStyle,
417
419
  clamp: InlineOptions['clamp'],
420
+ width: number,
418
421
  ): Cut | null {
419
422
  // the ellipsis a clamp ends in is placed apart (`ellipsized`)
420
423
  if (clamp) return { maxLines: clamp.lines, overflow: 'clip' };
424
+ // `text-overflow` is how the line is drawn where it runs out of its
425
+ // box, and no part of how wide it is (CSS Overflow 3, 5.1): in a box of
426
+ // no width, which shows none of it and is what the box's content is
427
+ // measured in at its least (`MIN_CONTENT_PROBE`), the line is left
428
+ // whole. Cut there, ntk left the ellipsis alone, and a button whose
429
+ // label is a `truncate` span was measured as wide as one
421
430
  if (
422
431
  style.textOverflow === 'ellipsis' &&
423
432
  !wraps(style) &&
424
- style.overflowX !== 'visible'
433
+ style.overflowX !== 'visible' &&
434
+ width > 0
425
435
  ) {
426
436
  return { maxLines: 1, overflow: 'ellipsis', wrap: false };
427
437
  }
@@ -644,7 +654,7 @@ function linesOf(
644
654
  // line at a time, or the lines after the first were lost. A clamp is
645
655
  // the paragraph's, and stays one layout.
646
656
  const clamp = options.clamp;
647
- let cut = cutOf(style, clamp);
657
+ let cut = cutOf(style, clamp, options.width);
648
658
  const perLine = cut !== null && !clamp && hasNewline;
649
659
  // an embedding cannot be cut into chunks
650
660
  if (perLine && !hasControls(items)) {
@@ -920,6 +930,16 @@ function linesOf(
920
930
  placeDeferred();
921
931
  return;
922
932
  }
933
+ // A line with no text on it — a control, an image or an inline-block
934
+ // that wrapped on its own — stands where the text around it does: an
935
+ // empty range at the end of the line before, not at 0. What finds a
936
+ // line by its text binary-searches these ranges as sorted
937
+ // (`lineBands`, `caretAt`), and a textless line at 0 after a line of
938
+ // text sent the search past every line: a `<label>` on the line
939
+ // before a submit button that wrapped measured as no box at all.
940
+ if (!line.texts.length && lines.length) {
941
+ line.textStart = line.textEnd = lines[lines.length - 1].textEnd;
942
+ }
923
943
  lines.push(line);
924
944
  widest = Math.max(widest, line.width);
925
945
  y += line.height;
@@ -1410,6 +1430,7 @@ function linesOf(
1410
1430
  if (!text) continue;
1411
1431
  const m = hung.exec(text);
1412
1432
  if (!m) break;
1433
+ if (!trailing) placed.trailRun = segment.runs[i];
1413
1434
  trailing = m[0] + trailing;
1414
1435
  if (m[0].length < text.length) break;
1415
1436
  }
@@ -1426,6 +1447,7 @@ function linesOf(
1426
1447
  : spaceAdvance(fonts, segment.runs[segment.runs.length - 1]) *
1427
1448
  trailing.length;
1428
1449
  open.x += open.hang;
1450
+ placed.trail = open.hang;
1429
1451
  }
1430
1452
  } else if (hung.total) holdHung();
1431
1453
  index = segment.nextIndex;
@@ -2380,6 +2402,24 @@ function finishLine(
2380
2402
  if (!open.texts.length && !open.atomics.length && !open.edges.length) {
2381
2403
  return null;
2382
2404
  }
2405
+ if (open.hang > 0) {
2406
+ // The line ends on the spaces its last text ends on, and they are
2407
+ // removed (CSS Text 3, 4.1.2): the text takes no room for them, and
2408
+ // what was set after them, the end edges of the boxes they are in,
2409
+ // stands where the text's content ends. Left a space past it, a list
2410
+ // item that ended its line was a space wider than its link.
2411
+ for (let i = open.order.length - 1; i >= 0; i -= 1) {
2412
+ const placed = open.order[i];
2413
+ if (placed.kind === 'text') {
2414
+ placed.item.trail = 0;
2415
+ placed.item.trailRun = undefined;
2416
+ break;
2417
+ }
2418
+ if (placed.kind !== 'edge') break;
2419
+ placed.at -= open.hang;
2420
+ placed.item.x -= open.hang;
2421
+ }
2422
+ }
2383
2423
  const reordered = levels
2384
2424
  ? levelPieces(open, levels) ||
2385
2425
  open.order.some((p) => p.kind === 'edge' && needsOrdering(p, rtl))
@@ -3288,7 +3328,19 @@ function atomicRaise(
3288
3328
  * be after.
3289
3329
  */
3290
3330
  function atomicBaseline(box: Box): number {
3291
- const bottom = box.height + box.marginTop + box.marginBottom;
3331
+ const margin = box.height + box.marginTop + box.marginBottom;
3332
+ const button = isButton(box);
3333
+ // A `<button>` with no line in it sits on the bottom of its content box,
3334
+ // not of its margin box: where its label would have sat. No specification
3335
+ // says so and every browser does — Blink gives the element the content
3336
+ // box as the edge its baseline is synthesized from
3337
+ // (`HTMLButtonElement::AdjustStyle`). An icon button drawn of blocks, the
3338
+ // three bars of a menu toggle, stood its padding under the baseline, and
3339
+ // the line it was on was as much taller: GitHub's header, by four pixels.
3340
+ // And so one whose layout containment keeps its label's baseline in.
3341
+ const bottom = button
3342
+ ? margin - box.marginBottom - box.borderBottom - box.padBottom
3343
+ : margin;
3292
3344
  if (
3293
3345
  box.kind === 'replaced' ||
3294
3346
  // a box that clips sits on its bottom margin edge, for legacy reasons
@@ -3297,8 +3349,8 @@ function atomicBaseline(box: Box): number {
3297
3349
  // sits on its first item's, as it does unclipped. A MediaWiki button
3298
3350
  // is an `overflow: hidden` inline flex box of one icon, and stood its
3299
3351
  // whole height on the baseline, the line under it the strut's descent
3300
- // taller
3301
- (scrolls(box.style) && box.kind !== 'flex') ||
3352
+ // taller. Nor a `<button>`, which sits on its label clipped or not
3353
+ (scrolls(box.style) && box.kind !== 'flex' && !button) ||
3302
3354
  // layout containment keeps its baseline in (CSS Containment 2, 3.3)
3303
3355
  box.style.contain & CONTAIN_LAYOUT
3304
3356
  ) {
@@ -3315,17 +3367,31 @@ function atomicBaseline(box: Box): number {
3315
3367
  return baseline === null ? bottom : box.marginTop + (baseline - box.y);
3316
3368
  }
3317
3369
 
3370
+ /** Whether a box is a `<button>` element's: one drawn as the document's,
3371
+ * which a replaced control is not. */
3372
+ export function isButton(box: Box): boolean {
3373
+ return (
3374
+ box.el !== null && box.kind !== 'replaced' && tagOf(box.el) === 'button'
3375
+ );
3376
+ }
3377
+
3318
3378
  /**
3319
- * A flex container's first baseline, or its last (CSS Flexbox 8.5): that
3320
- * of the first item on its first line that is aligned by its baseline — the
3321
- * last on its last line — or where none is, of its first item, or last,
3322
- * in `order`. An item gives its own, or where it has none, its border
3323
- * box's bottom edge. Not its last child's: an `inline-flex` of a small
3324
- * item and a big one sat on the big one's baseline. Null for a box with no
3325
- * item.
3379
+ * A flex container's baseline, its first (CSS Flexbox 8.5): that of the
3380
+ * first item on its first line that is aligned by its baseline, or where
3381
+ * none is, of its first item, in `order`. An item gives its own, or where
3382
+ * it has none, its border box's bottom edge. Not its last child's: an
3383
+ * `inline-flex` of a small item and a big one sat on the big one's
3384
+ * baseline. Null for a box with no item.
3385
+ *
3386
+ * A grid's is that of its first item in row-major order, by where it was
3387
+ * placed and not where it was written (CSS Grid 1, 10.6).
3326
3388
  */
3327
- function flexBaseline(box: Box, first: boolean): number | null {
3328
- let items: Box[] = [];
3389
+ function flexBaseline(box: Box): number | null {
3390
+ if (box.style.grid) {
3391
+ const item = GRID_TRACKS.get(box)?.first;
3392
+ return item ? itemBaselineOf(item) : null;
3393
+ }
3394
+ const items: Box[] = [];
3329
3395
  let reordered = false;
3330
3396
  for (const child of box.children) {
3331
3397
  if (child.outOfFlow || child.isFloat) continue;
@@ -3335,15 +3401,11 @@ function flexBaseline(box: Box, first: boolean): number | null {
3335
3401
  }
3336
3402
  if (!items.length) return null;
3337
3403
  if (reordered) items.sort((a, b) => a.style.order - b.style.order);
3338
- if (!first) items = items.reverse();
3339
3404
  const style = box.style;
3340
- if (style.flexDirection.startsWith('row') && !style.grid) {
3341
- // a line ends where the next item does not go on along the main axis,
3342
- // which from the last item is where it goes on
3405
+ if (style.flexDirection.startsWith('row')) {
3406
+ // a line ends where the next item does not go on along the main axis
3343
3407
  const back =
3344
- ((style.flexDirection === 'row-reverse') !==
3345
- (style.direction === 'rtl')) !==
3346
- !first;
3408
+ (style.flexDirection === 'row-reverse') !== (style.direction === 'rtl');
3347
3409
  let last = NaN;
3348
3410
  for (const item of items) {
3349
3411
  if (!Number.isNaN(last) && (back ? item.x >= last : item.x <= last)) {
@@ -3354,24 +3416,20 @@ function flexBaseline(box: Box, first: boolean): number | null {
3354
3416
  item.style.alignSelf === 'auto'
3355
3417
  ? style.alignItems
3356
3418
  : item.style.alignSelf;
3357
- if (align === 'baseline') return itemBaselineOf(item, first, style);
3419
+ if (align === 'baseline') return itemBaselineOf(item);
3358
3420
  }
3359
3421
  }
3360
- return itemBaselineOf(items[0], first, style);
3422
+ return itemBaselineOf(items[0]);
3361
3423
  }
3362
3424
 
3363
- /** A flex item's baseline, in document coordinates: its first line's or
3364
- * its last's — its bottom margin edge where it clips what it holds — or
3365
- * where it has none, its border box's bottom edge, which a grid does not
3366
- * take from its item, and has none of its own then (CSS Grid 1, 9). */
3367
- function itemBaselineOf(
3368
- item: Box,
3369
- first: boolean,
3370
- container: ComputedStyle,
3371
- ): number | null {
3372
- const found = childBaseline(item, first ? firstBaselineIn : lastBaselineIn);
3373
- if (found !== null) return found;
3374
- return container.grid ? null : item.y + item.height;
3425
+ /** A flex item's baseline or a grid item's, in document coordinates: its
3426
+ * first line's, or where it has none, one synthesized from its border
3427
+ * edges, the bottom one (CSS Flexbox 8.5, CSS Grid 1, 10.6). A grid took
3428
+ * none from such an item and had none of its own: a grid of icons stood
3429
+ * its whole height on the baseline, where a browser stands it on the
3430
+ * first icon's bottom edge. */
3431
+ function itemBaselineOf(item: Box): number {
3432
+ return childBaseline(item, firstBaselineIn) ?? item.y + item.height;
3375
3433
  }
3376
3434
 
3377
3435
  /**
@@ -3399,10 +3457,18 @@ function tableBaseline(table: Box): number | null {
3399
3457
  * what overflows it gives its bottom margin edge and no deeper, whatever
3400
3458
  * text it holds (CSS Box Alignment's synthesized baseline). Null where
3401
3459
  * nothing in it has one.
3460
+ *
3461
+ * A flex box or a grid among them gives its first baseline, the one it has
3462
+ * on a line of its own, and a table gives none: what an inline-block takes
3463
+ * from a block-level child in Blink, where only a block container is asked
3464
+ * for its last (`UseLastBaselineForInlineBaseline`), and in Gecko, where a
3465
+ * child is asked for its default one and a table is passed over
3466
+ * (`nsBlockFrame::GetNaturalBaselineBOffset`). Asked for its last, a grid
3467
+ * of an icon, a label and a chevron in a button sat the button on the
3468
+ * chevron, and the line it was on came out shorter than a browser's.
3402
3469
  */
3403
3470
  function lastBaselineIn(box: Box): number | null {
3404
- // a flex box's is its items', in the order it lays them out
3405
- if (box.kind === 'flex') return flexBaseline(box, false);
3471
+ if (box.kind === 'flex') return flexBaseline(box);
3406
3472
  if (box.lines?.length) {
3407
3473
  const line = box.lines[box.lines.length - 1];
3408
3474
  return line.y + line.baseline;
@@ -3417,7 +3483,7 @@ function lastBaselineIn(box: Box): number | null {
3417
3483
  /** The same, from the first line box or child: a table's baseline is its
3418
3484
  * first row's, and so is a table cell's. */
3419
3485
  export function firstBaselineIn(box: Box): number | null {
3420
- if (box.kind === 'flex') return flexBaseline(box, true);
3486
+ if (box.kind === 'flex') return flexBaseline(box);
3421
3487
  if (box.lines?.length) return box.lines[0].y + box.lines[0].baseline;
3422
3488
  for (const child of box.children) {
3423
3489
  const found = childBaseline(child, firstBaselineIn);
@@ -3433,12 +3499,17 @@ function childBaseline(
3433
3499
  if (child.outOfFlow || child.isFloat || child.kind === 'replaced') {
3434
3500
  return null;
3435
3501
  }
3502
+ // a table is passed over for a block's last baseline (`lastBaselineIn`)
3503
+ if (inside === lastBaselineIn && child.kind === 'table') return null;
3436
3504
  // the legacy rule is a block container's last baseline alone (CSS Box
3437
- // Alignment 3, 9.2): its first is its first line's, clipped or not
3505
+ // Alignment 3, 9.2): its first is its first line's, clipped or not —
3506
+ // and a button that clips keeps its label's, here as on a line of its
3507
+ // own (`atomicBaseline`)
3438
3508
  if (
3439
3509
  inside === lastBaselineIn &&
3440
3510
  child.kind !== 'flex' &&
3441
- scrolls(child.style)
3511
+ scrolls(child.style) &&
3512
+ !isButton(child)
3442
3513
  ) {
3443
3514
  return child.y + child.height + child.marginBottom;
3444
3515
  }
@@ -4133,6 +4204,8 @@ const RUN_STYLE = [
4133
4204
  'underlineOffset',
4134
4205
  'underlineThickness',
4135
4206
  'strike',
4207
+ 'strikeStyle',
4208
+ 'strikeThickness',
4136
4209
  ] as const;
4137
4210
 
4138
4211
  /**
@@ -4700,26 +4773,57 @@ export function runFor(text: string, style: ComputedStyle): TextRun {
4700
4773
  // colours of the elements that set them (`decorate`)
4701
4774
  if (style.underline) {
4702
4775
  run.underline = style.underline;
4703
- // CSS's five rule styles and SGR 4's five are the same set under two
4704
- // names; richtext speaks SGR's, so `solid` is `single` and `wavy` is
4705
- // `curly`. The other three are spelled identically.
4706
- run.underlineStyle =
4707
- style.underlineStyle === 'wavy'
4708
- ? 'curly'
4709
- : style.underlineStyle === 'solid'
4710
- ? 'single'
4711
- : style.underlineStyle;
4712
- if (style.underlineOffset !== null) {
4713
- run.underlineOffset = style.underlineOffset;
4714
- }
4715
- if (style.underlineThickness !== null) {
4716
- run.underlineThickness = style.underlineThickness;
4776
+ run.underlineStyle = ruleStyle(style.underlineStyle);
4777
+ // The band the rule is drawn in: as thick as the box that set it made
4778
+ // it (`usedThickness`), and as far under the baseline as that box said.
4779
+ // `auto` leaves how far to the user agent, at or under the baseline
4780
+ // (CSS Text Decoration 4, 2.7 and 2.8), and it is half the rule's
4781
+ // thickness, a pixel at the least — Blink's gap, which keeps a thick
4782
+ // rule as clear of its letters as a thin one. A length is from the
4783
+ // baseline itself.
4784
+ const band = style.underlineThickness ?? 1;
4785
+ const top =
4786
+ style.underlineOffset === null
4787
+ ? Math.max(1, Math.ceil(band / 2))
4788
+ : Math.round(style.underlineOffset);
4789
+ if (run.underlineStyle === 'dotted' || run.underlineStyle === 'dashed') {
4790
+ // dots and dashes are a stroke along the band's middle, its width the
4791
+ // nearest whole pixel to the band's: 1.6 is two, where a solid rule's
4792
+ // is the one pixel it fills
4793
+ const width = Math.round(band);
4794
+ run.underlineThickness = width;
4795
+ run.underlineOffset =
4796
+ top + Math.floor(Math.max(band / 2, 0.5)) - Math.floor(width / 2);
4797
+ } else {
4798
+ run.underlineThickness = Math.floor(band);
4799
+ run.underlineOffset = top;
4717
4800
  }
4718
4801
  }
4719
- if (style.lineThrough) run.strike = style.lineThrough;
4802
+ if (style.lineThrough) {
4803
+ run.strike = style.lineThrough;
4804
+ run.strikeStyle = ruleStyle(style.lineThroughStyle);
4805
+ // as thick as the box that set it made it, on whole pixels as the
4806
+ // underline's is: a solid rule fills the ones it covers, and dots and
4807
+ // dashes are the nearest whole pixel wide. Where it is drawn is each
4808
+ // run's own face's to say (`paintRunRules`).
4809
+ const band = style.lineThroughThickness ?? 1;
4810
+ run.strikeThickness =
4811
+ run.strikeStyle === 'dotted' || run.strikeStyle === 'dashed'
4812
+ ? Math.round(band)
4813
+ : Math.floor(band);
4814
+ }
4720
4815
  return run;
4721
4816
  }
4722
4817
 
4818
+ /** A CSS rule style as richtext names it. CSS's five and SGR 4's five are
4819
+ * the same set under two names; richtext speaks SGR's, so `solid` is
4820
+ * `single` and `wavy` is `curly`. The other three are spelled identically. */
4821
+ function ruleStyle(
4822
+ style: ComputedStyle['underlineStyle'],
4823
+ ): NonNullable<TextRun['underlineStyle']> {
4824
+ return style === 'wavy' ? 'curly' : style === 'solid' ? 'single' : style;
4825
+ }
4826
+
4723
4827
  /**
4724
4828
  * Maps a code-unit offset in text handed to `TextLayout` back to its offset
4725
4829
  * in the document index. The two differ because a layout covers one segment