@react-x11/components 0.10.0 → 0.12.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 (155) hide show
  1. package/dist/code/index.d.ts.map +1 -1
  2. package/dist/code/index.js +25 -24
  3. package/dist/code/index.js.map +1 -1
  4. package/dist/code-language/index.d.ts +1 -1
  5. package/dist/code-language/index.d.ts.map +1 -1
  6. package/dist/code-language/index.js +1 -1
  7. package/dist/code-language/index.js.map +1 -1
  8. package/dist/code-language/runs.d.ts +31 -0
  9. package/dist/code-language/runs.d.ts.map +1 -1
  10. package/dist/code-language/runs.js +105 -0
  11. package/dist/code-language/runs.js.map +1 -1
  12. package/dist/codeblock/index.d.ts +13 -0
  13. package/dist/codeblock/index.d.ts.map +1 -1
  14. package/dist/codeblock/index.js +37 -1
  15. package/dist/codeblock/index.js.map +1 -1
  16. package/dist/flow/node.d.ts +14 -6
  17. package/dist/flow/node.d.ts.map +1 -1
  18. package/dist/flow/node.js +82 -64
  19. package/dist/flow/node.js.map +1 -1
  20. package/dist/html/controls.d.ts +26 -7
  21. package/dist/html/controls.d.ts.map +1 -1
  22. package/dist/html/controls.js +28 -3
  23. package/dist/html/controls.js.map +1 -1
  24. package/dist/html/css/cascade.d.ts +32 -10
  25. package/dist/html/css/cascade.d.ts.map +1 -1
  26. package/dist/html/css/cascade.js +89 -31
  27. package/dist/html/css/cascade.js.map +1 -1
  28. package/dist/html/css/color.d.ts +19 -0
  29. package/dist/html/css/color.d.ts.map +1 -1
  30. package/dist/html/css/color.js +86 -0
  31. package/dist/html/css/color.js.map +1 -1
  32. package/dist/html/css/parse.d.ts +11 -0
  33. package/dist/html/css/parse.d.ts.map +1 -1
  34. package/dist/html/css/parse.js +114 -4
  35. package/dist/html/css/parse.js.map +1 -1
  36. package/dist/html/css/style.d.ts +45 -5
  37. package/dist/html/css/style.d.ts.map +1 -1
  38. package/dist/html/css/style.js +363 -14
  39. package/dist/html/css/style.js.map +1 -1
  40. package/dist/html/css/ua.js +22 -5
  41. package/dist/html/css/ua.js.map +1 -1
  42. package/dist/html/css/values.d.ts +10 -1
  43. package/dist/html/css/values.d.ts.map +1 -1
  44. package/dist/html/css/values.js +27 -2
  45. package/dist/html/css/values.js.map +1 -1
  46. package/dist/html/css/vars.d.ts +5 -0
  47. package/dist/html/css/vars.d.ts.map +1 -1
  48. package/dist/html/css/vars.js +2 -2
  49. package/dist/html/css/vars.js.map +1 -1
  50. package/dist/html/dom.d.ts.map +1 -1
  51. package/dist/html/dom.js +35 -2
  52. package/dist/html/dom.js.map +1 -1
  53. package/dist/html/index.d.ts.map +1 -1
  54. package/dist/html/index.js +38 -1
  55. package/dist/html/index.js.map +1 -1
  56. package/dist/html/layout/block.d.ts +11 -2
  57. package/dist/html/layout/block.d.ts.map +1 -1
  58. package/dist/html/layout/block.js +306 -25
  59. package/dist/html/layout/block.js.map +1 -1
  60. package/dist/html/layout/boxes.d.ts +61 -6
  61. package/dist/html/layout/boxes.d.ts.map +1 -1
  62. package/dist/html/layout/boxes.js +65 -16
  63. package/dist/html/layout/boxes.js.map +1 -1
  64. package/dist/html/layout/cache.d.ts +2 -2
  65. package/dist/html/layout/cache.d.ts.map +1 -1
  66. package/dist/html/layout/cache.js +8 -3
  67. package/dist/html/layout/cache.js.map +1 -1
  68. package/dist/html/layout/floats.d.ts +4 -0
  69. package/dist/html/layout/floats.d.ts.map +1 -1
  70. package/dist/html/layout/floats.js +26 -0
  71. package/dist/html/layout/floats.js.map +1 -1
  72. package/dist/html/layout/inline.d.ts +29 -2
  73. package/dist/html/layout/inline.d.ts.map +1 -1
  74. package/dist/html/layout/inline.js +1454 -125
  75. package/dist/html/layout/inline.js.map +1 -1
  76. package/dist/html/node.d.ts +33 -0
  77. package/dist/html/node.d.ts.map +1 -1
  78. package/dist/html/node.js +608 -86
  79. package/dist/html/node.js.map +1 -1
  80. package/dist/html/paint.d.ts +53 -1
  81. package/dist/html/paint.d.ts.map +1 -1
  82. package/dist/html/paint.js +746 -214
  83. package/dist/html/paint.js.map +1 -1
  84. package/dist/html/surfaces.d.ts +7 -2
  85. package/dist/html/surfaces.d.ts.map +1 -1
  86. package/dist/html/surfaces.js +16 -0
  87. package/dist/html/surfaces.js.map +1 -1
  88. package/dist/html/svg.d.ts.map +1 -1
  89. package/dist/html/svg.js +27 -20
  90. package/dist/html/svg.js.map +1 -1
  91. package/dist/internal/codelines.d.ts +42 -0
  92. package/dist/internal/codelines.d.ts.map +1 -0
  93. package/dist/internal/codelines.js +170 -0
  94. package/dist/internal/codelines.js.map +1 -0
  95. package/dist/internal/window.d.ts +22 -0
  96. package/dist/internal/window.d.ts.map +1 -1
  97. package/dist/internal/window.js +35 -8
  98. package/dist/internal/window.js.map +1 -1
  99. package/dist/markdown/index.d.ts.map +1 -1
  100. package/dist/markdown/index.js +98 -26
  101. package/dist/markdown/index.js.map +1 -1
  102. package/dist/rich-text-editor/index.d.ts.map +1 -1
  103. package/dist/rich-text-editor/index.js +3 -8
  104. package/dist/rich-text-editor/index.js.map +1 -1
  105. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  106. package/dist/rich-text-editor/markdown.js +30 -1
  107. package/dist/rich-text-editor/markdown.js.map +1 -1
  108. package/dist/rich-text-editor/replace.d.ts +13 -0
  109. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  110. package/dist/rich-text-editor/replace.js +64 -0
  111. package/dist/rich-text-editor/replace.js.map +1 -0
  112. package/dist/richtext/node.d.ts +7 -0
  113. package/dist/richtext/node.d.ts.map +1 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/terminal-output/index.d.ts.map +1 -1
  116. package/dist/terminal-output/index.js +100 -25
  117. package/dist/terminal-output/index.js.map +1 -1
  118. package/dist/terminal-output/runs.d.ts +4 -1
  119. package/dist/terminal-output/runs.d.ts.map +1 -1
  120. package/dist/terminal-output/runs.js +5 -1
  121. package/dist/terminal-output/runs.js.map +1 -1
  122. package/package.json +5 -3
  123. package/src/code/index.ts +34 -32
  124. package/src/code-language/index.ts +1 -1
  125. package/src/code-language/runs.ts +111 -1
  126. package/src/codeblock/index.ts +48 -1
  127. package/src/flow/node.ts +80 -64
  128. package/src/html/controls.ts +50 -9
  129. package/src/html/css/cascade.ts +134 -43
  130. package/src/html/css/color.ts +87 -0
  131. package/src/html/css/parse.ts +107 -4
  132. package/src/html/css/style.ts +386 -23
  133. package/src/html/css/ua.ts +22 -5
  134. package/src/html/css/values.ts +31 -3
  135. package/src/html/css/vars.ts +2 -2
  136. package/src/html/dom.ts +38 -2
  137. package/src/html/index.ts +39 -1
  138. package/src/html/layout/block.ts +360 -22
  139. package/src/html/layout/boxes.ts +98 -23
  140. package/src/html/layout/cache.ts +8 -3
  141. package/src/html/layout/floats.ts +24 -0
  142. package/src/html/layout/inline.ts +1690 -119
  143. package/src/html/node.ts +648 -101
  144. package/src/html/paint.ts +883 -283
  145. package/src/html/surfaces.ts +21 -1
  146. package/src/html/svg.ts +30 -19
  147. package/src/internal/codelines.ts +200 -0
  148. package/src/internal/window.ts +36 -8
  149. package/src/markdown/index.ts +145 -26
  150. package/src/rich-text-editor/index.ts +3 -11
  151. package/src/rich-text-editor/markdown.ts +36 -1
  152. package/src/rich-text-editor/replace.ts +85 -0
  153. package/src/richtext/node.ts +7 -0
  154. package/src/terminal-output/index.ts +127 -34
  155. package/src/terminal-output/runs.ts +16 -2
package/dist/html/node.js CHANGED
@@ -43,17 +43,17 @@ import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
43
43
  import { Cascade } from './css/cascade.js';
44
44
  import { mediaMatches, parseStylesheet } from './css/parse.js';
45
45
  import { uaStylesheet } from './css/ua.js';
46
- import { buildBoxes, CONTENT_IMAGES } from './layout/boxes.js';
47
- import { layoutDocument } from './layout/block.js';
46
+ import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, } from './layout/boxes.js';
47
+ import { layoutDocument, retranslate, transformed } from './layout/block.js';
48
48
  import { TextLayoutCache } from './layout/cache.js';
49
49
  import { shapingSafe } from './layout/shaping.js';
50
- import { SurfaceCache } from './surfaces.js';
51
- import { inlineDecoration, runFor } from './layout/inline.js';
50
+ import { SurfaceCache, newSurface } from './surfaces.js';
51
+ import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
52
52
  // Through the inline module rather than a second cache: the offsets table for
53
53
  // a layout is built once, on the first selection that needs it.
54
54
  import { documentOffsetOf, layoutOffsetOf, layoutOffsets as layoutOffsetsOf, } from './layout/inline.js';
55
55
  import { lineBands as bandsFor } from '../richtext/runs.js';
56
- import { clipsOverflow, containingBlockOf, hasRect, holds, holdsAbsolute, paintDocument, queryChildIndex, } from './paint.js';
56
+ import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, paintDocument, queryChildIndex, stackLayers, } from './paint.js';
57
57
  import { controlRectsOf, measureControl } from './controls.js';
58
58
  import { ResourceStore } from './resources.js';
59
59
  import { WebFonts } from './fonts.js';
@@ -118,6 +118,8 @@ export class HtmlViewNode extends Node {
118
118
  _sheetsRead = null;
119
119
  /** Blurred shadows, drawn once each. */
120
120
  _shadowCache = null;
121
+ /** Whether the backend was found to have no offscreen surface. */
122
+ _noSurface = false;
121
123
  _cascade = null;
122
124
  _tree = null;
123
125
  /** The faces `_warmFaces` has asked the font matcher for, and of which
@@ -315,14 +317,16 @@ export class HtmlViewNode extends Node {
315
317
  */
316
318
  _requestBackgrounds(tree) {
317
319
  for (const box of tree.backgrounds) {
318
- if (!box.el)
320
+ // a pseudo-element's are its element's
321
+ const element = box.el ?? GENERATED_FROM.get(box);
322
+ if (!element)
319
323
  continue;
320
324
  // each layer's, where there is more than one
321
325
  for (const url of box.style.backgroundImages ?? [
322
326
  box.style.backgroundImage,
323
327
  ]) {
324
328
  if (typeof url === 'string') {
325
- this._resources.request({ url, kind: 'image', element: box.el });
329
+ this._resources.request({ url, kind: 'image', element });
326
330
  }
327
331
  }
328
332
  const border = box.style.borderImage.source;
@@ -330,9 +334,15 @@ export class HtmlViewNode extends Node {
330
334
  this._resources.request({
331
335
  url: border,
332
336
  kind: 'image',
333
- element: box.el,
337
+ element,
334
338
  });
335
339
  }
340
+ // and each mask layer's, which is an image as a background's is
341
+ for (const url of box.style.mask.images) {
342
+ if (typeof url === 'string') {
343
+ this._resources.request({ url, kind: 'image', element });
344
+ }
345
+ }
336
346
  }
337
347
  // and every image generated content names, which is only known there
338
348
  // too
@@ -463,7 +473,7 @@ export class HtmlViewNode extends Node {
463
473
  order += sheet.rules.length + 1;
464
474
  sheets.push(sheet);
465
475
  }
466
- this._cascade = new Cascade(sheets, look, width, this._viewportHeight(), this._scale, fonts ? (family, size) => xHeightOf(fonts, family, size) : null, fonts ? (family, size) => zeroWidthOf(fonts, family, size) : null, fonts ? (family, size) => normalLineOf(fonts, family, size) : null, faces.length ? this._webFonts : null);
476
+ this._cascade = new Cascade(sheets, look, width, this._viewportHeight(), this._scale, fonts ? (face) => xHeightOf(fonts, face) : null, fonts ? (face) => zeroWidthOf(fonts, face) : null, fonts ? (face) => normalLineOf(fonts, face) : null, faces.length ? this._webFonts : null);
467
477
  this._sheetsRead = {
468
478
  look,
469
479
  scale: this._scale,
@@ -548,6 +558,58 @@ export class HtmlViewNode extends Node {
548
558
  }
549
559
  return true;
550
560
  }
561
+ /**
562
+ * The viewport the document is seen through, in window coordinates: the
563
+ * content box of the pane that scrolls the element, where one does —
564
+ * what `position: fixed` and a fixed background are placed against, as
565
+ * a browser places them against its page area. Null where nothing
566
+ * scrolls it, and the element is its own.
567
+ */
568
+ _viewport() {
569
+ for (let node = this.parent; node; node = node.parent) {
570
+ // `Scrollable`'s, and not every node's: asked the way core asks it
571
+ const scroller = node;
572
+ if (!scroller.isScroller?.())
573
+ continue;
574
+ const box = node.contentBox();
575
+ return box.width > 0 && box.height > 0 ? box : null;
576
+ }
577
+ return null;
578
+ }
579
+ /**
580
+ * What the document draws fixed to the viewport of the pane that scrolls
581
+ * it, for that pane's scroll blit to repaint rather than copy with the
582
+ * text (react-x11's `viewportFixedRects`): the whole viewport where a
583
+ * fixed background shows through it, which makes a scroll a repaint, and
584
+ * where each fixed box is drawn. Null for a document with neither, and
585
+ * where nothing scrolls the element.
586
+ */
587
+ viewportFixedRects() {
588
+ const tree = this._tree;
589
+ const viewport = tree && this._viewport();
590
+ if (!tree || !viewport)
591
+ return null;
592
+ if (hasFixedBackground(tree))
593
+ return [viewport];
594
+ const boxes = FIXED_BOXES.get(tree);
595
+ if (!boxes)
596
+ return null;
597
+ // laid out against the viewport at the document's top, and drawn at the
598
+ // viewport (`atViewport`)
599
+ const root = tree.root;
600
+ const out = [];
601
+ for (const box of boxes) {
602
+ if (!(box.boundsWidth > 0 && box.boundsHeight > 0))
603
+ continue;
604
+ out.push({
605
+ x: viewport.x + box.boundsX - root.x,
606
+ y: viewport.y + box.boundsY - root.y,
607
+ width: box.boundsWidth,
608
+ height: box.boundsHeight,
609
+ });
610
+ }
611
+ return out.length ? out : null;
612
+ }
551
613
  _viewportHeight() {
552
614
  // The viewport a `vh` resolves against is the one the document is seen
553
615
  // through, not the document — a document taller than it does not make
@@ -1002,13 +1064,52 @@ export class HtmlViewNode extends Node {
1002
1064
  }
1003
1065
  else {
1004
1066
  const bands = [];
1005
- if (box.subtreeTextEnd > box.subtreeTextStart) {
1006
- collectBands(tree.root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
1067
+ const fonts = this._layouts?.fonts ?? null;
1068
+ // an inline box broken around the blocks in it is its pieces, a box
1069
+ // of the element's each (CSS 2.1 9.2.1.1), and its fragments are
1070
+ // theirs; and on the lines between them it has a fragment around each
1071
+ // block, across the box the block is in and as tall as its border
1072
+ // box, as a browser's has — Blink's block-in-inline, Gecko's split
1073
+ // inline's anonymous block. A list item made inline around a block
1074
+ // link measured as the empty edge after it
1075
+ for (const piece of piecesOf(tree.root, box)) {
1076
+ inlineBands(tree.root, piece, fonts, bands);
1077
+ }
1078
+ for (const block of (box.cut ? CUT_BLOCKS.get(box) : null) ?? []) {
1079
+ const around = block.parent;
1080
+ if (!around)
1081
+ continue;
1082
+ bands.push({
1083
+ x: around.contentX,
1084
+ y: block.y,
1085
+ width: around.contentWidth,
1086
+ height: block.height,
1087
+ });
1007
1088
  }
1008
- if (box.kind === 'inline')
1009
- fragmentReach(box, bands);
1010
1089
  for (const band of bands)
1011
1090
  rect = rect ? unionRect(rect, band) : band;
1091
+ if (!rect) {
1092
+ // on no line, where its block has none: text set at no size takes
1093
+ // no room and gets no box, and CSS puts it on lines of no height
1094
+ // at the block's top (9.4.2), as a browser measures it — nothing,
1095
+ // where the block's content starts. Where the text after it
1096
+ // starts was the next text that took room, elsewhere in the
1097
+ // document: the Zen Garden's 170 hides a heading at `font-size:
1098
+ // 0`, and an `<abbr>` in it measured as the end of the paragraph
1099
+ // above it
1100
+ let block = box.parent;
1101
+ while (block && block.kind === 'inline')
1102
+ block = block.parent;
1103
+ if (block && !block.lines?.length) {
1104
+ const rtl = block.style.direction === 'rtl';
1105
+ rect = {
1106
+ x: rtl ? block.contentX + block.contentWidth : block.contentX,
1107
+ y: block.contentY,
1108
+ width: 0,
1109
+ height: 0,
1110
+ };
1111
+ }
1112
+ }
1012
1113
  if (!rect) {
1013
1114
  // an empty subtree's range is `[0, 0)`, wherever it stands: where
1014
1115
  // its text would be is where the text after it starts
@@ -1032,7 +1133,20 @@ export class HtmlViewNode extends Node {
1032
1133
  if (!tree)
1033
1134
  return null;
1034
1135
  const local = this._toDocument(x, y);
1035
- return deepestAt(tree, local.x, local.y);
1136
+ return deepestAt(tree, local.x, local.y, undefined, this._fixedShift());
1137
+ }
1138
+ /** How far a box fixed to the viewport is drawn from where it was laid
1139
+ * out, in the document's pixels: the scroll of the pane the element is
1140
+ * in (`atViewport`). Null where nothing scrolls it. */
1141
+ _fixedShift() {
1142
+ const tree = this._tree;
1143
+ const viewport = tree && FIXED_BOXES.has(tree) && this._viewport();
1144
+ if (!tree || !viewport)
1145
+ return null;
1146
+ return {
1147
+ x: viewport.x - this.abs.x - tree.root.x,
1148
+ y: viewport.y - this.abs.y - tree.root.y,
1149
+ };
1036
1150
  }
1037
1151
  /**
1038
1152
  * The cursor for a point of this element's, in device pixels: what core
@@ -1049,7 +1163,7 @@ export class HtmlViewNode extends Node {
1049
1163
  if (!tree)
1050
1164
  return null;
1051
1165
  const hit = { text: false };
1052
- const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit);
1166
+ const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit, this._fixedShift());
1053
1167
  const cursor = el ? tree.styles.get(el)?.style.cursor : null;
1054
1168
  // a keyword; a `url()` this cannot load falls back, as its list would
1055
1169
  if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor))
@@ -1091,10 +1205,8 @@ export class HtmlViewNode extends Node {
1091
1205
  }
1092
1206
  /** The hovered chain moved: restyle where it did, or the document. */
1093
1207
  _restyleHover(was, now) {
1094
- const done = this._hoverInPlace(was, now);
1095
- if (done === 'painted')
1096
- this.invalidate(false, this, 'props');
1097
- else if (done === false)
1208
+ // a restyle in place names what it repainted (`_hoverInPlace`)
1209
+ if (this._hoverInPlace(was, now) === false)
1098
1210
  this._invalidate(Stale.Boxes);
1099
1211
  }
1100
1212
  /**
@@ -1189,6 +1301,11 @@ export class HtmlViewNode extends Node {
1189
1301
  const fresh = new Map();
1190
1302
  const changed = new Map();
1191
1303
  let refused = false;
1304
+ // what changed beyond ink (`hoverChange`): how far some box's ink
1305
+ // reaches, the order its layer paints in, where one is
1306
+ let reaches = false;
1307
+ let reorders = false;
1308
+ const moving = new Set();
1192
1309
  const styleOf = (el) => {
1193
1310
  const kept = tree.styles.get(el);
1194
1311
  if (!kept)
@@ -1208,11 +1325,18 @@ export class HtmlViewNode extends Node {
1208
1325
  }
1209
1326
  const style = cascade.styleFor(el, parentStyle, kept.inFlex);
1210
1327
  fresh.set(el, style);
1211
- const diff = inkOnly(kept.style, style);
1328
+ const diff = hoverChange(kept.style, style);
1212
1329
  if (diff === false)
1213
1330
  refused = true;
1214
- else if (diff)
1331
+ else if (diff) {
1215
1332
  changed.set(el, style);
1333
+ if (diff.reach)
1334
+ reaches = true;
1335
+ if (diff.order)
1336
+ reorders = true;
1337
+ if (diff.move)
1338
+ moving.add(el);
1339
+ }
1216
1340
  return style;
1217
1341
  };
1218
1342
  for (const el of reach) {
@@ -1246,10 +1370,14 @@ export class HtmlViewNode extends Node {
1246
1370
  continue;
1247
1371
  const kept = tree.styles.get(owner);
1248
1372
  // an anonymous box, a pseudo-element, a marker, a control: a style
1249
- // derived from the element's, or drawn from it somewhere else
1373
+ // derived from the element's, or drawn from it somewhere else — a
1374
+ // widget mounted beside the document takes its look from it. An
1375
+ // image, a drawing or a rule is drawn here, from its box, like any.
1250
1376
  if (!box.el || box.style !== kept.style)
1251
1377
  return false;
1252
- if (box.marker || box.replaced !== 'none')
1378
+ if (box.marker || WIDGETS.has(box.replaced))
1379
+ return false;
1380
+ if (moving.has(owner) && !movable(box, kept.style, style))
1253
1381
  return false;
1254
1382
  restyled.push([box, style]);
1255
1383
  if (box.kind === 'inline') {
@@ -1286,11 +1414,29 @@ export class HtmlViewNode extends Node {
1286
1414
  }
1287
1415
  }
1288
1416
  }
1417
+ // what each box drew before, to be repainted with what it draws after
1418
+ const inks = [];
1419
+ for (const [box] of restyled) {
1420
+ const ink = inkOf(box);
1421
+ if (ink)
1422
+ inks.push(ink);
1423
+ }
1289
1424
  // and only now, all of it
1290
- for (const [box, style] of restyled)
1425
+ const moved = [];
1426
+ for (const [box, style] of restyled) {
1427
+ const was = box.style;
1291
1428
  box.style = style;
1292
- for (const [box, decoration] of redecorated)
1429
+ if (moving.has(box.el))
1430
+ moved.push([box, was]);
1431
+ }
1432
+ for (const [box, decoration] of redecorated) {
1433
+ // what is inside it keeps which boxes around it paint, and this is
1434
+ // one more or one fewer of them
1435
+ if ((decoration === null) !== (box.decoration === null)) {
1436
+ forgetDecoratedAncestors(box);
1437
+ }
1293
1438
  box.decoration = decoration;
1439
+ }
1294
1440
  for (const text of texts) {
1295
1441
  const next = relaid.get(text.layout);
1296
1442
  if (next)
@@ -1299,8 +1445,72 @@ export class HtmlViewNode extends Node {
1299
1445
  for (const [el, style] of changed) {
1300
1446
  tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
1301
1447
  }
1448
+ // A box that moved moves what is in it, as layout moved it the first
1449
+ // time (`applyRelativeOffsets`), and a later layout moves it the same
1450
+ // way; where it reached the document's end, before or after, the
1451
+ // document's height may have changed with it, which only a layout says.
1452
+ let relayout = false;
1453
+ for (const [box, was] of moved) {
1454
+ const before = box.boundsY + box.boundsHeight;
1455
+ retranslate(box, was);
1456
+ tree.relative = true;
1457
+ if (before >= this._documentHeight)
1458
+ relayout = true;
1459
+ }
1460
+ // how far the boxes' ink reaches, and the order their layers paint in,
1461
+ // as layout leaves them (`layoutDocument`)
1462
+ if (reaches || reorders || moved.length) {
1463
+ computePaintBounds(tree.root, tree.movedInline);
1464
+ if (tree.negative)
1465
+ hoistNegative(tree.root);
1466
+ stackLayers(tree.root);
1467
+ }
1468
+ for (const [box] of moved) {
1469
+ if (box.boundsY + box.boundsHeight >= this._documentHeight) {
1470
+ relayout = true;
1471
+ }
1472
+ }
1473
+ if (moved.length)
1474
+ this._reportControls();
1475
+ if (relayout) {
1476
+ this._invalidate(Stale.Layout);
1477
+ return 'painted';
1478
+ }
1479
+ for (const [box] of restyled) {
1480
+ const ink = inkOf(box);
1481
+ if (ink)
1482
+ inks.push(ink);
1483
+ }
1484
+ this._repaintInk(inks);
1302
1485
  return 'painted';
1303
1486
  }
1487
+ /**
1488
+ * Repaint the document where `inks` are, in its own coordinates: the ink
1489
+ * of the boxes a hover restyled, before and after. Past a handful of rects
1490
+ * the damage is one around them all, which is what core would make of
1491
+ * them anyway.
1492
+ */
1493
+ _repaintInk(inks) {
1494
+ if (!inks.length)
1495
+ return;
1496
+ const x = this.abs.x;
1497
+ const y = this.abs.y;
1498
+ const place = (r) => ({
1499
+ x: Math.floor(x + r.x),
1500
+ y: Math.floor(y + r.y),
1501
+ width: Math.ceil(r.width) + 1,
1502
+ height: Math.ceil(r.height) + 1,
1503
+ });
1504
+ if (inks.length > 6) {
1505
+ let all = inks[0];
1506
+ for (const r of inks)
1507
+ all = unionRect(all, r);
1508
+ this.invalidate(false, place(all), 'props');
1509
+ return;
1510
+ }
1511
+ for (const r of inks)
1512
+ this.invalidate(false, place(r), 'props');
1513
+ }
1304
1514
  /** The document, for an application that wants to read or change it. */
1305
1515
  get document() {
1306
1516
  return this._source.document;
@@ -1372,6 +1582,16 @@ export class HtmlViewNode extends Node {
1372
1582
  this._fail(error, this._laidOutWidth);
1373
1583
  }
1374
1584
  }
1585
+ /** A surface for the painter to draw a masked element on, asked of the
1586
+ * backend until it is found to have none. */
1587
+ _surface(width, height) {
1588
+ if (this._noSurface)
1589
+ return null;
1590
+ const surface = newSurface(this.app, width, height);
1591
+ if (!surface)
1592
+ this._noSurface = true;
1593
+ return surface;
1594
+ }
1375
1595
  _paint(ctx, tree, range, damage) {
1376
1596
  paintDocument(ctx, tree, {
1377
1597
  originX: this.abs.x,
@@ -1379,6 +1599,7 @@ export class HtmlViewNode extends Node {
1379
1599
  // the root's background covers the whole element, not only the
1380
1600
  // document: an element grown past its content is canvas too
1381
1601
  canvas: this.abs,
1602
+ viewport: this._viewport() ?? undefined,
1382
1603
  scale: this._scale,
1383
1604
  damage,
1384
1605
  selection: range
@@ -1394,11 +1615,34 @@ export class HtmlViewNode extends Node {
1394
1615
  const size = image ? this._resources.imageSize(url) : null;
1395
1616
  return image && size ? { image, ...size } : null;
1396
1617
  },
1618
+ surface: (width, height) => this._surface(width, height),
1397
1619
  cached: (key, width, height, draw) => (this._shadowCache ??= new SurfaceCache(this.app)).get(key, width, height, draw),
1398
1620
  });
1399
1621
  }
1400
1622
  }
1401
1623
  const EMPTY_SET = new Set();
1624
+ /** Whether a tree has a background fixed to the viewport: an image a box
1625
+ * names (`tree.backgrounds`), or the canvas's image or gradient — the
1626
+ * root's, or the body's it paints, which the root box stands in for where
1627
+ * the markup has neither element. */
1628
+ function hasFixedBackground(tree) {
1629
+ for (const box of tree.backgrounds) {
1630
+ if (box.style.backgroundAttachment === 'fixed')
1631
+ return true;
1632
+ }
1633
+ const root = tree.root;
1634
+ const body = root.children.find((child) => child.el?.name === 'body');
1635
+ for (const box of body ? [root, body] : [root]) {
1636
+ const style = box.style;
1637
+ if (style.backgroundAttachment === 'fixed' &&
1638
+ (style.backgroundImage ||
1639
+ style.backgroundImages ||
1640
+ style.backgroundGradient)) {
1641
+ return true;
1642
+ }
1643
+ }
1644
+ return false;
1645
+ }
1402
1646
  /** The first box an element made, depth first. */
1403
1647
  function boxFor(root, element) {
1404
1648
  const stack = [root];
@@ -1412,6 +1656,83 @@ function boxFor(root, element) {
1412
1656
  }
1413
1657
  return null;
1414
1658
  }
1659
+ /**
1660
+ * An inline element's boxes: the one it has, or where a block in it broke
1661
+ * it (`breakAround`), each piece, in document order.
1662
+ */
1663
+ function piecesOf(root, first) {
1664
+ // the first piece is cut at its end where there are more
1665
+ if (first.kind !== 'inline' || !(first.cut & 2))
1666
+ return [first];
1667
+ const pieces = [];
1668
+ const stack = [root];
1669
+ while (stack.length) {
1670
+ const box = stack.pop();
1671
+ if (box.el === first.el && box.kind === 'inline')
1672
+ pieces.push(box);
1673
+ for (let i = box.children.length - 1; i >= 0; i -= 1) {
1674
+ stack.push(box.children[i]);
1675
+ }
1676
+ }
1677
+ return pieces;
1678
+ }
1679
+ /**
1680
+ * The bands of one inline box's fragments, or a text's: on the lines of
1681
+ * the block it is laid out in, and no text of a float or a positioned box
1682
+ * inside it — on lines of their own — is one of them (CSSOM View 6.1): a
1683
+ * list item made inline around an absolute link measured as the link.
1684
+ * Each across its text, the inline-blocks and inline boxes' edges it holds
1685
+ * and its own padding and border where it starts or ends there, and as
1686
+ * tall as its border box.
1687
+ */
1688
+ function inlineBands(root, box, fonts, out) {
1689
+ // its own, apart from any other piece's: `fragmentHeights` makes each of
1690
+ // the bands it is handed as tall as this box on that band's line
1691
+ const bands = [];
1692
+ let block = box.parent;
1693
+ while (block && !block.lines)
1694
+ block = block.parent;
1695
+ if (box.subtreeTextEnd > box.subtreeTextStart) {
1696
+ if (box.kind === 'inline' && block?.lines) {
1697
+ lineBands(block.lines, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands, true);
1698
+ }
1699
+ else {
1700
+ collectBands(root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
1701
+ }
1702
+ }
1703
+ if (box.kind === 'inline') {
1704
+ fragmentReach(box, bands);
1705
+ if (fonts)
1706
+ fragmentHeights(box, bands, fonts);
1707
+ // where `position: relative` on it or an inline box around it moved
1708
+ // it, all of it (CSS 2.1 9.4.3), as `getBoundingClientRect` has it
1709
+ // (CSSOM View 6.1): the bands are where the lines put it, and a link
1710
+ // moved 120px down measured on its line, as wide as from where it was
1711
+ // laid out to where it was drawn
1712
+ const { x, y } = relativeOffsetOf(box);
1713
+ if (x || y) {
1714
+ for (const band of bands) {
1715
+ band.x += x;
1716
+ band.y += y;
1717
+ }
1718
+ }
1719
+ }
1720
+ out.push(...bands);
1721
+ }
1722
+ /** How far an inline box and the inline boxes around it moved it by their
1723
+ * relative offsets (`INLINE_OFFSETS`). */
1724
+ function relativeOffsetOf(box) {
1725
+ let x = 0;
1726
+ let y = 0;
1727
+ for (let at = box; at?.kind === 'inline'; at = at.parent) {
1728
+ const offset = INLINE_OFFSETS.get(at);
1729
+ if (offset) {
1730
+ x += offset.x;
1731
+ y += offset.y;
1732
+ }
1733
+ }
1734
+ return { x, y };
1735
+ }
1415
1736
  /** Where the document's text goes on after a box with none: the start of
1416
1737
  * the next text in document order, or the end of the last before it. */
1417
1738
  function textAfter(root, target) {
@@ -1472,6 +1793,7 @@ function sameRects(a, b) {
1472
1793
  p.y !== q.y ||
1473
1794
  p.width !== q.width ||
1474
1795
  p.height !== q.height ||
1796
+ p.opacity !== q.opacity ||
1475
1797
  !sameBare(p.bare, q.bare)) {
1476
1798
  return false;
1477
1799
  }
@@ -1487,7 +1809,8 @@ function sameBare(a, b) {
1487
1809
  a.height === b.height &&
1488
1810
  a.color === b.color &&
1489
1811
  a.fontFamily === b.fontFamily &&
1490
- a.fontSize === b.fontSize);
1812
+ a.fontSize === b.fontSize &&
1813
+ a.chevron === b.chevron);
1491
1814
  }
1492
1815
  // --- walks over the laid-out tree -------------------------------------------
1493
1816
  /** Distance from a coordinate to an interval, zero inside it. */
@@ -1717,50 +2040,127 @@ function fragmentReach(box, out) {
1717
2040
  }
1718
2041
  }
1719
2042
  }
1720
- function collectBands(box, from, to, dx, dy, out) {
1721
- if (box.subtreeTextEnd <= box.subtreeTextStart)
1722
- return;
1723
- if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
2043
+ /**
2044
+ * An inline element's bands made as tall as its fragments' border boxes,
2045
+ * as CSSOM View takes an inline box's client rects (6.1): its font's
2046
+ * content area about its own baseline, with the padding and the border
2047
+ * above and below it, which take no room on the line (CSS 2.1 10.6.1) and
2048
+ * which paint draws the background over (`paintInlineBoxes`). A band is a
2049
+ * line's height across until then: a link padded 10px below measured as
2050
+ * tall as its line and no taller.
2051
+ */
2052
+ function fragmentHeights(box, bands, fonts) {
2053
+ let block = box.parent;
2054
+ while (block && !block.lines)
2055
+ block = block.parent;
2056
+ if (!block?.lines?.length)
1724
2057
  return;
1725
- if (box.lines) {
1726
- const lines = box.lines;
1727
- // First line whose text can reach `from`, by binary search over the
1728
- // sorted text starts; stop at the first line past `to`.
1729
- let lo = 0;
1730
- let hi = lines.length;
1731
- while (lo < hi) {
1732
- const mid = (lo + hi) >> 1;
1733
- if (lines[mid].textEnd > from)
1734
- hi = mid;
1735
- else
1736
- lo = mid + 1;
1737
- }
1738
- for (let i = lo; i < lines.length; i += 1) {
1739
- const line = lines[i];
1740
- if (line.textStart >= to)
1741
- break;
1742
- if (line.textEnd > from && line.textStart < to) {
1743
- for (const text of line.texts) {
1744
- const natural = text.layout.lines[text.layoutLine];
1745
- if (!natural)
1746
- continue;
1747
- const a = Math.max(from, text.textStart);
1748
- const b = Math.min(to, text.textEnd);
1749
- if (b <= a)
1750
- continue;
1751
- const offsets = layoutOffsetsOf(text.layout);
1752
- for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
1753
- out.push({
1754
- x: dx + band.x + text.drawX,
1755
- y: dy + line.y,
1756
- width: band.width,
1757
- height: line.height,
1758
- });
2058
+ const face = faceExtentOf(fonts, box.style);
2059
+ const byTop = new Map();
2060
+ for (const line of block.lines)
2061
+ byTop.set(line.y, line);
2062
+ for (let i = 0; i < bands.length; i += 1) {
2063
+ const band = bands[i];
2064
+ const line = byTop.get(band.y);
2065
+ if (!line)
2066
+ continue;
2067
+ // on its own baseline, which `vertical-align` may raise off the line's
2068
+ const raise = SHIFTED_LINES.has(line)
2069
+ ? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
2070
+ : 0;
2071
+ const baseline = line.y + line.baseline - raise;
2072
+ const top = baseline - face.ascent - box.padTop - box.borderTop;
2073
+ const bottom = baseline + face.descent + box.padBottom + box.borderBottom;
2074
+ bands[i] = { x: band.x, y: top, width: band.width, height: bottom - top };
2075
+ }
2076
+ }
2077
+ /** The bands of a text range on one block's own lines. */
2078
+ function lineBands(lines, from, to, dx, dy, out,
2079
+ /** Where the lines put the text, not where a relative offset moved it
2080
+ * (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
2081
+ laidOut = false) {
2082
+ // First line whose text can reach `from`, by binary search over the
2083
+ // sorted text starts; stop at the first line past `to`.
2084
+ let lo = 0;
2085
+ let hi = lines.length;
2086
+ while (lo < hi) {
2087
+ const mid = (lo + hi) >> 1;
2088
+ if (lines[mid].textEnd > from)
2089
+ hi = mid;
2090
+ else
2091
+ lo = mid + 1;
2092
+ }
2093
+ for (let i = lo; i < lines.length; i += 1) {
2094
+ const line = lines[i];
2095
+ if (line.textStart >= to)
2096
+ break;
2097
+ if (line.textEnd > from && line.textStart < to) {
2098
+ for (const text of line.texts) {
2099
+ const natural = text.layout.lines[text.layoutLine];
2100
+ if (!natural)
2101
+ continue;
2102
+ const a = Math.max(from, text.textStart);
2103
+ const b = Math.min(to, text.textEnd);
2104
+ if (b <= a)
2105
+ continue;
2106
+ const offsets = layoutOffsetsOf(text.layout);
2107
+ const moved = laidOut ? (TEXT_SHIFTS.get(text)?.x ?? 0) : 0;
2108
+ // The spaces a line ends on are removed (CSS Text 3, 4.1.2), and
2109
+ // take no room on it: the engine sets them past its line's content,
2110
+ // where a caret after them goes, and a band that took them in made
2111
+ // a link that ends a line a space wider than Chrome's. So the text
2112
+ // a line ends with is measured no further than its content.
2113
+ const ends = endsLine(line, text, natural);
2114
+ for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
2115
+ let left = band.x;
2116
+ let right = band.x + band.width;
2117
+ if (ends) {
2118
+ left = Math.max(left, natural.x);
2119
+ right = Math.max(left, Math.min(right, natural.x + natural.width));
1759
2120
  }
2121
+ out.push({
2122
+ x: dx + left + text.drawX - moved,
2123
+ y: dy + line.y,
2124
+ width: right - left,
2125
+ height: line.height,
2126
+ });
2127
+ }
2128
+ // the spaces `pre-wrap` keeps that the line ends on, which have no
2129
+ // run: the engine hung them past the line (`LineText.hung`)
2130
+ for (const space of text.hung ?? []) {
2131
+ const at = documentOffsetOf(text, space.at);
2132
+ if (at < a || at >= b)
2133
+ continue;
2134
+ out.push({
2135
+ x: dx + space.x + text.drawX - moved,
2136
+ y: dy + line.y,
2137
+ width: space.width,
2138
+ height: line.height,
2139
+ });
1760
2140
  }
1761
2141
  }
1762
2142
  }
1763
2143
  }
2144
+ }
2145
+ /** Whether a line's text is where the line ends: its last, with nothing
2146
+ * placed after it — an inline-block or an image past its content. */
2147
+ function endsLine(line, text, natural) {
2148
+ if (line.texts[line.texts.length - 1] !== text)
2149
+ return false;
2150
+ const right = text.drawX + natural.x + natural.width;
2151
+ for (const placed of line.atomics) {
2152
+ if (placed.x >= right - 0.01)
2153
+ return false;
2154
+ }
2155
+ return true;
2156
+ }
2157
+ function collectBands(box, from, to, dx, dy, out) {
2158
+ if (box.subtreeTextEnd <= box.subtreeTextStart)
2159
+ return;
2160
+ if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
2161
+ return;
2162
+ if (box.lines)
2163
+ lineBands(box.lines, from, to, dx, dy, out);
1764
2164
  // Atomics are ordinary children, reached below.
1765
2165
  for (const child of box.children) {
1766
2166
  if (child.kind === 'text' || child.kind === 'break')
@@ -1779,7 +2179,11 @@ const HIT_POSITIONED = 3;
1779
2179
  function deepestAt(tree, x, y,
1780
2180
  /** Told whether what was found was found under text: a run's, rather
1781
2181
  * than a box's. */
1782
- hit) {
2182
+ hit,
2183
+ /** How far a box fixed to the viewport is drawn from where it was laid
2184
+ * out (`HtmlViewNode._fixedShift`): the point is taken back by it in
2185
+ * there, as paint moved the box by it (`atViewport`). */
2186
+ fixedShift = null) {
1783
2187
  const box = tree.root;
1784
2188
  let found = box.el;
1785
2189
  let foundKey = [];
@@ -1820,7 +2224,25 @@ hit) {
1820
2224
  // Zen Garden's archive links are absolute `<li>`s in an `overflow:
1821
2225
  // hidden` list with no height of its own, and not one of them could be
1822
2226
  // hovered or pressed.
2227
+ let atViewport = false;
1823
2228
  const enter = (child, context, clipped) => {
2229
+ if (!fixedShift || atViewport || !fixedToViewport(child)) {
2230
+ enterAt(child, context, clipped);
2231
+ return;
2232
+ }
2233
+ x -= fixedShift.x;
2234
+ y -= fixedShift.y;
2235
+ atViewport = true;
2236
+ try {
2237
+ enterAt(child, context, clipped);
2238
+ }
2239
+ finally {
2240
+ x += fixedShift.x;
2241
+ y += fixedShift.y;
2242
+ atViewport = false;
2243
+ }
2244
+ };
2245
+ const enterAt = (child, context, clipped) => {
1824
2246
  const style = child.style;
1825
2247
  if (style.position !== 'static' ||
1826
2248
  (child.parent?.kind === 'flex' && typeof style.zIndex === 'number')) {
@@ -1978,21 +2400,112 @@ const FACE_FIELDS = [
1978
2400
  'letterSpacing',
1979
2401
  'features',
1980
2402
  ];
1981
- /** Whether two styles differ in ink alone: true where they do, null where
1982
- * they do not differ, false where something else does. Custom properties
1983
- * are read through the properties that use them. */
1984
- function inkOnly(was, now) {
2403
+ /** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
2404
+ * a widget mounted beside the document, or a button whose press it reports. */
2405
+ const WIDGETS = new Set([
2406
+ 'input',
2407
+ 'textarea',
2408
+ 'select',
2409
+ 'button',
2410
+ 'checkbox',
2411
+ 'radio',
2412
+ ]);
2413
+ /** What only moves how far a box's ink reaches: a shadow, and an outline's
2414
+ * size, which take no room (CSS Backgrounds 3, 7.1; CSS UI 4, 3). */
2415
+ const INK_REACH = new Set([
2416
+ 'boxShadow',
2417
+ 'outlineStyle',
2418
+ 'outlineWidth',
2419
+ 'outlineOffset',
2420
+ ]);
2421
+ /**
2422
+ * How two styles of one element differ, for a restyle in place: null where
2423
+ * they do not, a `HoverChange` where every difference is ink, how far ink
2424
+ * reaches, a `z-index` that stays a stacking context's, or a translation —
2425
+ * none of which moves anything but the box and what it holds (CSS
2426
+ * Transforms 1: a transform does not affect layout) — and false where
2427
+ * anything else differs.
2428
+ */
2429
+ function hoverChange(was, now) {
1985
2430
  const a = was;
1986
2431
  const b = now;
1987
- let ink = false;
2432
+ let change = null;
1988
2433
  for (const key in b) {
1989
2434
  if (key === 'custom' || sameValue(a[key], b[key]))
1990
2435
  continue;
1991
- if (!PAINT_ONLY.has(key))
2436
+ change ??= { reach: false, order: false, move: false };
2437
+ if (PAINT_ONLY.has(key))
2438
+ continue;
2439
+ if (INK_REACH.has(key)) {
2440
+ change.reach = true;
2441
+ continue;
2442
+ }
2443
+ if (key === 'zIndex') {
2444
+ // one stacking context either way, with nothing hoisted below the
2445
+ // flow (`hoistNegative`): only its place among its layers changes
2446
+ const z0 = a[key];
2447
+ const z1 = b[key];
2448
+ if (typeof z0 !== 'number' || typeof z1 !== 'number')
2449
+ return false;
2450
+ if (z0 < 0 || z1 < 0)
2451
+ return false;
2452
+ change.order = true;
2453
+ continue;
2454
+ }
2455
+ if (key === 'transformTranslate' || key === 'translate') {
2456
+ change.move = true;
2457
+ continue;
2458
+ }
2459
+ return false;
2460
+ }
2461
+ return change;
2462
+ }
2463
+ /**
2464
+ * Whether a box can take a new translation where it is: a box of its own
2465
+ * rather than an inline one, whose text is what moves; and the same
2466
+ * containing block and stacking context for what is in it, which a
2467
+ * transform makes a box (CSS Transforms 1, 2) — so either transformed
2468
+ * before and after, or already positioned with a `z-index`, and holding
2469
+ * nothing fixed, which only a transform takes in.
2470
+ */
2471
+ function movable(box, was, now) {
2472
+ if (box.kind === 'inline')
2473
+ return false;
2474
+ if (transformed(was) === transformed(now))
2475
+ return true;
2476
+ if (was.position === 'static')
2477
+ return false;
2478
+ if (typeof was.zIndex !== 'number' || typeof now.zIndex !== 'number') {
2479
+ return false;
2480
+ }
2481
+ const stack = [...box.children];
2482
+ while (stack.length) {
2483
+ const at = stack.pop();
2484
+ if (at.style.position === 'fixed')
1992
2485
  return false;
1993
- ink = true;
2486
+ for (const child of at.children)
2487
+ stack.push(child);
1994
2488
  }
1995
- return ink ? true : null;
2489
+ return true;
2490
+ }
2491
+ /** Where a box draws, in document coordinates: its ink bounds, or for an
2492
+ * inline box or a run of text, which are drawn on their block's lines and
2493
+ * have none of their own, the block's. */
2494
+ function inkOf(box) {
2495
+ let at = box;
2496
+ while (at && (at.kind === 'text' || at.kind === 'break' || !hasRect(at))) {
2497
+ at = at.parent;
2498
+ }
2499
+ if (!at || at.boundsY === Infinity)
2500
+ return null;
2501
+ if (!(at.boundsWidth > 0 && at.boundsHeight > 0))
2502
+ return null;
2503
+ return {
2504
+ x: at.boundsX,
2505
+ y: at.boundsY,
2506
+ width: at.boundsWidth,
2507
+ height: at.boundsHeight,
2508
+ };
1996
2509
  }
1997
2510
  /** Structural equality for a computed value: a number, a string, or the
1998
2511
  * plain objects and arrays a length or a shadow list is. */
@@ -2138,33 +2651,42 @@ function ownerOf(boxes, index) {
2138
2651
  const box = boxes[lo];
2139
2652
  return box && index >= box.textStart && index < box.textEnd ? box.el : null;
2140
2653
  }
2141
- /** The advance of a font's "0" at a size, laid out: null where the engine
2142
- * cannot lay it out. */
2143
- function zeroWidthOf(fonts, family, size) {
2654
+ /** The advance of a face's "0", laid out: null where the engine cannot
2655
+ * lay it out. */
2656
+ function zeroWidthOf(fonts, face) {
2657
+ const { family, size, weight } = face;
2658
+ // a run is upright or slanted, as `inline.ts` sets one
2659
+ const style = face.style === 'normal' ? 'normal' : 'italic';
2144
2660
  try {
2145
- return fonts.layout([{ text: '0', family, size }], { family, size }, {})
2146
- .width;
2661
+ const run = { family, size, weight, style };
2662
+ return fonts.layout([{ text: '0', ...run }], run, {}).width;
2147
2663
  }
2148
2664
  catch {
2149
2665
  return null;
2150
2666
  }
2151
2667
  }
2152
- /** A font's own line height at a size, as the engine reports it: null
2153
- * where it cannot say. */
2154
- function normalLineOf(fonts, family, size) {
2668
+ /** A face's own line height, as the engine reports it: null where it
2669
+ * cannot say. */
2670
+ function normalLineOf(fonts, face) {
2671
+ const { family, size, weight, style } = face;
2155
2672
  try {
2156
- const line = fonts.match(family, { size }).metrics(size).lineHeight;
2673
+ const line = fonts
2674
+ .match(family, { size, weight, style })
2675
+ .metrics(size).lineHeight;
2157
2676
  return line > 0 ? line : null;
2158
2677
  }
2159
2678
  catch {
2160
2679
  return null;
2161
2680
  }
2162
2681
  }
2163
- /** A font's x-height at a size, as the engine reports it: null where the
2164
- * face states none, or the engine does not say. */
2165
- function xHeightOf(fonts, family, size) {
2682
+ /** A face's x-height, as the engine reports it: null where the face
2683
+ * states none, or the engine does not say. */
2684
+ function xHeightOf(fonts, face) {
2685
+ const { family, size, weight, style } = face;
2166
2686
  try {
2167
- const metrics = fonts.match(family, { size }).metrics(size);
2687
+ const metrics = fonts
2688
+ .match(family, { size, weight, style })
2689
+ .metrics(size);
2168
2690
  const x = metrics.xHeight;
2169
2691
  // NaN where the face states none, which is a number
2170
2692
  return typeof x === 'number' && x > 0 ? x : null;