@react-x11/components 0.11.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 (100) hide show
  1. package/dist/flow/node.d.ts +14 -6
  2. package/dist/flow/node.d.ts.map +1 -1
  3. package/dist/flow/node.js +82 -64
  4. package/dist/flow/node.js.map +1 -1
  5. package/dist/html/controls.d.ts +26 -7
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +28 -3
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts.map +1 -1
  10. package/dist/html/css/cascade.js +19 -6
  11. package/dist/html/css/cascade.js.map +1 -1
  12. package/dist/html/css/color.d.ts +19 -0
  13. package/dist/html/css/color.d.ts.map +1 -1
  14. package/dist/html/css/color.js +86 -0
  15. package/dist/html/css/color.js.map +1 -1
  16. package/dist/html/css/parse.d.ts +11 -0
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +114 -4
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/style.d.ts +38 -1
  21. package/dist/html/css/style.d.ts.map +1 -1
  22. package/dist/html/css/style.js +326 -8
  23. package/dist/html/css/style.js.map +1 -1
  24. package/dist/html/css/ua.js +14 -0
  25. package/dist/html/css/ua.js.map +1 -1
  26. package/dist/html/css/values.d.ts +10 -1
  27. package/dist/html/css/values.d.ts.map +1 -1
  28. package/dist/html/css/values.js +27 -2
  29. package/dist/html/css/values.js.map +1 -1
  30. package/dist/html/css/vars.d.ts +5 -0
  31. package/dist/html/css/vars.d.ts.map +1 -1
  32. package/dist/html/css/vars.js +2 -2
  33. package/dist/html/css/vars.js.map +1 -1
  34. package/dist/html/dom.d.ts.map +1 -1
  35. package/dist/html/dom.js +35 -2
  36. package/dist/html/dom.js.map +1 -1
  37. package/dist/html/index.d.ts.map +1 -1
  38. package/dist/html/index.js +38 -1
  39. package/dist/html/index.js.map +1 -1
  40. package/dist/html/layout/block.d.ts +11 -2
  41. package/dist/html/layout/block.d.ts.map +1 -1
  42. package/dist/html/layout/block.js +181 -18
  43. package/dist/html/layout/block.js.map +1 -1
  44. package/dist/html/layout/boxes.d.ts +34 -6
  45. package/dist/html/layout/boxes.d.ts.map +1 -1
  46. package/dist/html/layout/boxes.js +35 -14
  47. package/dist/html/layout/boxes.js.map +1 -1
  48. package/dist/html/layout/cache.d.ts +2 -2
  49. package/dist/html/layout/cache.d.ts.map +1 -1
  50. package/dist/html/layout/cache.js +8 -3
  51. package/dist/html/layout/cache.js.map +1 -1
  52. package/dist/html/layout/floats.d.ts +4 -0
  53. package/dist/html/layout/floats.d.ts.map +1 -1
  54. package/dist/html/layout/floats.js +26 -0
  55. package/dist/html/layout/floats.js.map +1 -1
  56. package/dist/html/layout/inline.d.ts +26 -2
  57. package/dist/html/layout/inline.d.ts.map +1 -1
  58. package/dist/html/layout/inline.js +1222 -115
  59. package/dist/html/layout/inline.js.map +1 -1
  60. package/dist/html/node.d.ts +33 -0
  61. package/dist/html/node.d.ts.map +1 -1
  62. package/dist/html/node.js +512 -51
  63. package/dist/html/node.js.map +1 -1
  64. package/dist/html/paint.d.ts +53 -1
  65. package/dist/html/paint.d.ts.map +1 -1
  66. package/dist/html/paint.js +546 -180
  67. package/dist/html/paint.js.map +1 -1
  68. package/dist/html/surfaces.d.ts +7 -2
  69. package/dist/html/surfaces.d.ts.map +1 -1
  70. package/dist/html/surfaces.js +16 -0
  71. package/dist/html/surfaces.js.map +1 -1
  72. package/dist/internal/window.d.ts +22 -0
  73. package/dist/internal/window.d.ts.map +1 -1
  74. package/dist/internal/window.js +35 -8
  75. package/dist/internal/window.js.map +1 -1
  76. package/dist/richtext/node.d.ts +7 -0
  77. package/dist/richtext/node.d.ts.map +1 -1
  78. package/dist/richtext/node.js.map +1 -1
  79. package/package.json +5 -3
  80. package/src/flow/node.ts +80 -64
  81. package/src/html/controls.ts +50 -9
  82. package/src/html/css/cascade.ts +19 -6
  83. package/src/html/css/color.ts +87 -0
  84. package/src/html/css/parse.ts +107 -4
  85. package/src/html/css/style.ts +343 -8
  86. package/src/html/css/ua.ts +14 -0
  87. package/src/html/css/values.ts +31 -3
  88. package/src/html/css/vars.ts +2 -2
  89. package/src/html/dom.ts +38 -2
  90. package/src/html/index.ts +39 -1
  91. package/src/html/layout/block.ts +228 -16
  92. package/src/html/layout/boxes.ts +55 -21
  93. package/src/html/layout/cache.ts +8 -3
  94. package/src/html/layout/floats.ts +24 -0
  95. package/src/html/layout/inline.ts +1435 -108
  96. package/src/html/node.ts +535 -54
  97. package/src/html/paint.ts +650 -241
  98. package/src/html/surfaces.ts +21 -1
  99. package/src/internal/window.ts +36 -8
  100. package/src/richtext/node.ts +7 -0
package/src/html/index.ts CHANGED
@@ -505,6 +505,9 @@ function renderControl(
505
505
  top: Math.round(at.y),
506
506
  width: Math.round(at.width),
507
507
  height: Math.round(at.height),
508
+ // core's `opacity` is CSS's: the widget faded as a group, and at 0 not
509
+ // drawn and still hit
510
+ ...(rect.opacity !== undefined && { opacity: rect.opacity }),
508
511
  };
509
512
  const field = rect.bare ? bareField(rect.bare) : fieldChrome(look);
510
513
  const report = (value: string | boolean): void => {
@@ -528,6 +531,9 @@ function renderControl(
528
531
  widget = h(Checkbox, {
529
532
  checked: attr(el, 'checked') !== undefined,
530
533
  disabled,
534
+ // the element's whole box takes the press, as it does in a browser:
535
+ // a page that sizes one over its label, invisible, means the label
536
+ style: { width: '100%', height: '100%' },
531
537
  onChange: (ev) => {
532
538
  const next = ev.value;
533
539
  if (next) el.attribs.checked = '';
@@ -570,7 +576,21 @@ function renderControl(
570
576
  options: options.map((o) => ({ value: o.value, label: o.label })),
571
577
  value: selectedOption(el) ?? undefined,
572
578
  disabled,
573
- style: { width: '100%', height: '100%' },
579
+ style: rect.bare
580
+ ? [BARE_TRIGGER, { width: '100%', height: '100%' }]
581
+ : { width: '100%', height: '100%' },
582
+ // the slots choose the drawn trigger on every backend, and put the
583
+ // caption and the arrow in the page's ink
584
+ ...(rect.bare && {
585
+ labelStyle: {
586
+ color: rect.bare.color,
587
+ fontFamily: rect.bare.fontFamily,
588
+ fontSize: rect.bare.fontSize,
589
+ },
590
+ chevronStyle: rect.bare.chevron
591
+ ? { color: rect.bare.color }
592
+ : { display: 'none' },
593
+ }),
574
594
  onChange: (ev) => {
575
595
  const next = String(ev.value ?? '');
576
596
  setSelectedOption(el, next);
@@ -655,6 +675,24 @@ function bareField(bare: BareField): Style {
655
675
  };
656
676
  }
657
677
 
678
+ /**
679
+ * The trigger of a `<select>` whose box the page styled: no frame, no fill
680
+ * and none of its own insets, and no wash under the pointer, since the box
681
+ * it would tint is the document's. Core's focus ring still marks it for the
682
+ * keyboard.
683
+ */
684
+ const BARE_TRIGGER: Style = {
685
+ paddingTop: 0,
686
+ paddingBottom: 0,
687
+ paddingLeft: 0,
688
+ paddingRight: 0,
689
+ borderWidth: 0,
690
+ borderRadius: 0,
691
+ backgroundColor: 'transparent',
692
+ ':hover': { backgroundColor: 'transparent' },
693
+ ':active': { backgroundColor: 'transparent' },
694
+ };
695
+
658
696
  function clearRadioGroup(el: Element): void {
659
697
  const name = attr(el, 'name');
660
698
  if (!name) return;
@@ -60,6 +60,7 @@ import type {
60
60
  import { FloatContext } from './floats.js';
61
61
  import {
62
62
  faceExtentOf,
63
+ joinsBefore,
63
64
  layoutInline,
64
65
  lineHeightMultiplier,
65
66
  spaceBreaksOnly,
@@ -71,7 +72,15 @@ import type { TextRun } from '../../richtext/index.js';
71
72
  import { layoutFlex } from './flex.js';
72
73
  import { finishCaptions, layoutTable } from './table.js';
73
74
  import { collapseEdges } from './collapse.js';
74
- import { computePaintBounds, hoistNegative, stackLayers } from '../paint.js';
75
+ import {
76
+ clipsFor,
77
+ computePaintBounds,
78
+ FIXED_BOXES,
79
+ fixedToViewport,
80
+ hoistNegative,
81
+ OUT_OF_FLOW_REACH,
82
+ stackLayers,
83
+ } from '../paint.js';
75
84
 
76
85
  export interface LayoutContext {
77
86
  fonts: FontsLike | null;
@@ -238,19 +247,35 @@ export function layoutDocument(
238
247
  // against where it *was* — which is the whole of what makes it relative —
239
248
  // and the ink bounds are computed after that, so culling sees where boxes
240
249
  // ended up rather than where they were laid out.
241
- if (tree.relative) applyRelativeOffsets(root);
250
+ if (tree.relative) applyRelativeOffsets(root, viewportWidth, viewportHeight);
242
251
  const reach = computePaintBounds(root, tree.movedInline);
243
252
  if (tree.negative) hoistNegative(root);
244
253
  stackLayers(root);
245
254
 
246
255
  // The document is as tall as what overflows the root, not the root: an
247
256
  // `html, body { height: 100% }` a window tall holds a message longer than
248
- // the window, and the element sizes to all of it.
257
+ // the window, and the element sizes to all of it. Not what a box that
258
+ // clips its overflow holds: that is its own to scroll, and the root's
259
+ // scrollable overflow takes in a positioned box only where no box on the
260
+ // way to it clips it (CSS Overflow 3, 2.2): an absolute box 900px down
261
+ // an `overflow: hidden` card made a page a browser shows 400px tall run
262
+ // on to 980, blank
249
263
  let bottom = Math.max(root.height, reach);
264
+ let fixed: Box[] | null = null;
250
265
  for (const { box } of ctx.positioned) {
251
- if (box.style.position !== 'fixed')
252
- bottom = Math.max(bottom, box.y + box.height);
266
+ if (box.style.position === 'fixed') {
267
+ if (fixedToViewport(box)) (fixed ??= []).push(box);
268
+ continue;
269
+ }
270
+ if (clipsFor(box, root).length) continue;
271
+ // and what it holds, where it does not clip it: a page set in an
272
+ // absolute wrapper 497px tall runs on below it, and the document with
273
+ // it — design 094 was a window tall and no taller, its text past the
274
+ // end of the scroll
275
+ bottom = Math.max(bottom, OUT_OF_FLOW_REACH.get(box) ?? box.y + box.height);
253
276
  }
277
+ if (fixed) FIXED_BOXES.set(tree, fixed);
278
+ else FIXED_BOXES.delete(tree);
254
279
  return {
255
280
  width: viewportWidth,
256
281
  height: bottom,
@@ -768,8 +793,12 @@ function layoutChildren(
768
793
  // this box's first formatted line is its first child's in flow (CSS 2.1
769
794
  // 5.12.1), which its `::first-line` is handed to
770
795
  let firstLine = ctx.firstLine ? firstLineOf(box) : null;
771
- // and the room a list item's marker takes on it (`MARKER_ROOM`)
772
- let marked = MARKER_ROOM.get(box) ?? null;
796
+ // and the room a list item's marker takes on it (`MARKER_ROOM`): an
797
+ // inside marker's is handed down to the line it is on, and an outside
798
+ // one's is room above the baseline of the first child that has a line,
799
+ // which goes lower where the marker reaches higher (`MARKER_ASCENT`)
800
+ let pushed = MARKER_ASCENT.get(box) ?? null;
801
+ let marked = pushed === null ? (MARKER_ROOM.get(box) ?? null) : null;
773
802
  const clamp = ctx.clamp ?? null;
774
803
 
775
804
  for (const child of box.children) {
@@ -863,6 +892,7 @@ function layoutChildren(
863
892
  }
864
893
 
865
894
  const counted = clamp?.fit?.lines ?? 0;
895
+ const floatMark = floats.count;
866
896
  if (
867
897
  !floats.isEmpty &&
868
898
  (child.kind === 'replaced' || establishesBFC(child))
@@ -871,6 +901,17 @@ function layoutChildren(
871
901
  } else {
872
902
  layoutBlockLevel(child, ctx, floats, contentLeft, childY, contentWidth);
873
903
  }
904
+ if (pushed !== null) {
905
+ const line = firstLineIn(child);
906
+ if (line) {
907
+ const push = pushed - (line.y + line.baseline - child.y);
908
+ if (push > 0) {
909
+ translate(child, 0, push);
910
+ floats.moveSince(floatMark, push);
911
+ }
912
+ pushed = null;
913
+ }
914
+ }
874
915
  // a block with none of the flow's lines in it — an image, a box of a
875
916
  // formatting context of its own — puts the clamp point of an `auto`
876
917
  // clamp before it where it ends below the room
@@ -1030,11 +1071,20 @@ function layoutBesideFloats(
1030
1071
  band = over;
1031
1072
  continue;
1032
1073
  }
1074
+ // Where no float narrows the room on either side, the box is where
1075
+ // it would be were there none, and one too wide for its containing
1076
+ // block overflows it here: below the floats it would be the same box
1077
+ // in the same room. Blink tests a fit only against a side a float is
1078
+ // on (`can_expand_outside_opportunity`, BlockLayoutAlgorithm::
1079
+ // LayoutNewFormattingContext). Design 209's 247px heading, which
1080
+ // clips its overflow, in a 240px column beside a float that ends
1081
+ // left of the column, went under the float, 555px down.
1082
+ if (lo === -Infinity && hi === Infinity) fits = true;
1033
1083
  // What is too wide overflows at the end of the line, as far as a
1034
1084
  // negative margin there takes it, but no further into a float or
1035
1085
  // past the room; at the start it would run into the floats, and
1036
1086
  // waits below them (the suite's floats-wrap-bfc-with-margin tests)
1037
- if (inRoom) {
1087
+ else if (inRoom) {
1038
1088
  fits = box.marginLeft + box.width + box.marginRight <= room + 0.5;
1039
1089
  } else if (rtl) {
1040
1090
  const limit = Math.max(contentLeft + Math.min(0, box.marginLeft), lo);
@@ -1744,6 +1794,8 @@ function layoutBox(
1744
1794
  const room = markerRoom(box, box.marker, ctx.fonts);
1745
1795
  if (room) MARKER_ROOM.set(box, room);
1746
1796
  else MARKER_ROOM.delete(box);
1797
+ if (box.style.listStylePosition === 'inside') MARKER_ASCENT.delete(box);
1798
+ else MARKER_ASCENT.set(box, markerAscent(box, box.marker, ctx.fonts));
1747
1799
  }
1748
1800
  const flow = layoutChildren(box, ctx, floats, box.contentY, contentWidth);
1749
1801
  // A list item with a marker and no line holds one, the marker's, a line
@@ -1830,6 +1882,28 @@ function markerRoom(
1830
1882
  : null;
1831
1883
  }
1832
1884
 
1885
+ /**
1886
+ * How far an outside marker reaches above the baseline it sits on. The
1887
+ * marker is a line of its own face, the item's or a `::marker` rule's, so a
1888
+ * bullet or a number reaches its face's ascent on that line, and an image,
1889
+ * set on the line bottom on the baseline, the higher of that and its own
1890
+ * height. Where the item's first line is in a block inside it — a link set
1891
+ * `display: block`, a paragraph — and the block's first baseline is nearer
1892
+ * its top than that, the block goes lower by the difference, and its line
1893
+ * stays the height it was: CSS 2.1 (12.5.1) leaves this to the user agent,
1894
+ * and Blink aligns the marker's baseline with the block's and pushes the
1895
+ * block down (`UnpositionedListMarker::AddToBox`). Its first line grew
1896
+ * instead, and design 196's links, under 18px bullets, were 21px tall where
1897
+ * Chrome has them 14px, 7px lower. On the item's own line the two come to
1898
+ * the same: the line grows by what the push would have moved it.
1899
+ */
1900
+ const MARKER_ASCENT = new WeakMap<Box, number>();
1901
+
1902
+ function markerAscent(box: Box, marker: Marker, fonts: FontsLike): number {
1903
+ const ascent = strutOf(fonts, marker.style ?? box.style).ascent;
1904
+ return marker.image ? Math.max(marker.image.height, ascent) : ascent;
1905
+ }
1906
+
1833
1907
  /** The first line box anywhere under a box, in layout order. */
1834
1908
  function firstLineIn(box: Box): LineBox | null {
1835
1909
  if (box.lines?.length) return box.lines[0];
@@ -2827,10 +2901,19 @@ function wordBound(box: Box, fonts: FontsLike, measured: boolean): number {
2827
2901
  let last: object | null = null;
2828
2902
  for (const line of box.lines) {
2829
2903
  if (unbroken) widest = Math.max(widest, line.width);
2904
+ // a word across inline boxes' edges is in two fragments of text, and
2905
+ // as wide as its parts in both and the edges between: bound by the
2906
+ // widest word of each, a float in a narrow room took a letter of a
2907
+ // word of letters in bordered spans for its least width, and was cut
2908
+ // to the room (`joinsBefore`)
2909
+ let chain = 0;
2830
2910
  for (const text of line.texts) {
2831
- if (text.layout === last) continue;
2911
+ const joins = joinsBefore(text);
2912
+ if (joins === undefined && text.layout === last) continue;
2832
2913
  last = text.layout;
2833
- widest = Math.max(widest, widestWord(fonts, text.layout, measured));
2914
+ const own = widestWord(fonts, text.layout, measured);
2915
+ chain = joins === undefined ? own : chain + joins + own;
2916
+ widest = Math.max(widest, chain);
2834
2917
  }
2835
2918
  for (const placed of line.atomics) {
2836
2919
  const b = placed.box;
@@ -4103,9 +4186,15 @@ export function establishesBFC(box: Box): boolean {
4103
4186
 
4104
4187
  /** What a box's `position: relative` offset moves it by, applied after
4105
4188
  * layout so it does not affect anything else's position — which is the
4106
- * whole of what makes it *relative* — and its translation with it. */
4107
- export function applyRelativeOffsets(box: Box): void {
4108
- for (const child of box.children) applyRelativeOffsets(child);
4189
+ * whole of what makes it *relative* — and its translation with it, and
4190
+ * where `position: sticky` puts one at rest (`stickyOffset`). */
4191
+ export function applyRelativeOffsets(
4192
+ box: Box,
4193
+ viewportWidth: number,
4194
+ viewportHeight: number,
4195
+ ): void {
4196
+ for (const child of box.children)
4197
+ applyRelativeOffsets(child, viewportWidth, viewportHeight);
4109
4198
  const style = box.style;
4110
4199
  if (transformed(style) && box.kind !== 'inline') {
4111
4200
  // a transform moves the box it is on, and an inline box is none
@@ -4116,7 +4205,10 @@ export function applyRelativeOffsets(box: Box): void {
4116
4205
  }
4117
4206
  }
4118
4207
  if (style.position !== 'relative' && style.position !== 'sticky') return;
4119
- const [dx, dy] = relativeOffset(box);
4208
+ const [dx, dy] =
4209
+ style.position === 'sticky'
4210
+ ? stickyOffset(box, viewportWidth, viewportHeight)
4211
+ : relativeOffset(box);
4120
4212
  if (!(dx || dy)) return;
4121
4213
  translate(box, dx, dy);
4122
4214
  if (box.kind !== 'inline') {
@@ -4137,10 +4229,10 @@ export function transformed(style: ComputedStyle): boolean {
4137
4229
 
4138
4230
  /** How far `translate` and a `transform` move a box: a percentage is of
4139
4231
  * its own border box (CSS Transforms 1, 7). */
4140
- function translationOf(box: Box): [number, number] {
4232
+ function translationOf(box: Box, style = box.style): [number, number] {
4141
4233
  let dx = 0;
4142
4234
  let dy = 0;
4143
- for (const moved of [box.style.translate, box.style.transformTranslate]) {
4235
+ for (const moved of [style.translate, style.transformTranslate]) {
4144
4236
  if (!moved) continue;
4145
4237
  dx += resolve(moved[0], box.width, 0);
4146
4238
  dy += resolve(moved[1], box.height, 0);
@@ -4148,6 +4240,21 @@ function translationOf(box: Box): [number, number] {
4148
4240
  return [dx, dy];
4149
4241
  }
4150
4242
 
4243
+ /**
4244
+ * Move a laid-out box, and everything in it, by how far its translation
4245
+ * changed when its style went from `was` to the one it has — what
4246
+ * `applyRelativeOffsets` moves it by now, less what it moved it by then.
4247
+ * For a style changed in place, where nothing else moves (a `transform` is
4248
+ * what makes that true). Not for an inline box, whose text is what moves.
4249
+ */
4250
+ export function retranslate(box: Box, was: ComputedStyle): void {
4251
+ const [x0, y0] = translationOf(box, was);
4252
+ const [x1, y1] = translationOf(box);
4253
+ if (x1 === x0 && y1 === y0) return;
4254
+ translate(box, x1 - x0, y1 - y0);
4255
+ movedOffLine(box);
4256
+ }
4257
+
4151
4258
  /** How far `position: relative` moves a box. */
4152
4259
  function relativeOffset(box: Box): [number, number] {
4153
4260
  const style = box.style;
@@ -4166,6 +4273,111 @@ function relativeOffset(box: Box): [number, number] {
4166
4273
  return [dx, dy];
4167
4274
  }
4168
4275
 
4276
+ /**
4277
+ * Where `position: sticky` puts a box at rest (CSS Positioned Layout 3,
4278
+ * 3.4): moved only as far as keeps its border box inside the sticky view
4279
+ * rectangle — its nearest scroll container's scrollport, scrolled to its
4280
+ * start, less its insets — and no further than keeps its margin box in its
4281
+ * containing block, `top` winning over `bottom` and the start side over the
4282
+ * end. Nothing here scrolls a box the document holds, so for one of those
4283
+ * this is where the box stays. The viewport does scroll, and the document
4284
+ * with it, and a sticky box does not follow: against the viewport only its
4285
+ * `top` and its start side are kept, which at rest put a box as a browser
4286
+ * does, while a `bottom: 0` would pin a footer to the middle of the page
4287
+ * once it was scrolled. Taken as `relative`, a box stood its `top` below
4288
+ * where a browser starts it: Wikipedia's contents, 24px low.
4289
+ */
4290
+ function stickyOffset(
4291
+ box: Box,
4292
+ viewportWidth: number,
4293
+ viewportHeight: number,
4294
+ ): [number, number] {
4295
+ const style = box.style;
4296
+ // the scroll container a sticky box keeps to: the nearest box that
4297
+ // scrolls, or the viewport, whose are the root's and the body's overflow
4298
+ let port = box.parent;
4299
+ while (port && !scrolls(port.style)) port = port.parent;
4300
+ const tag = port?.el?.name;
4301
+ const inside = !!port && tag !== 'html' && tag !== 'body';
4302
+ let cb = box.parent;
4303
+ while (cb && cb.kind === 'inline') cb = cb.parent;
4304
+ if (!cb) return [0, 0];
4305
+ // the scrollport, at its start: a scroll container's padding box, or
4306
+ // the viewport, which the document's top is at
4307
+ const px = port && inside ? port.x + port.borderLeft : 0;
4308
+ const py = port && inside ? port.y + port.borderTop : 0;
4309
+ const pw =
4310
+ port && inside
4311
+ ? port.width - port.borderLeft - port.borderRight
4312
+ : viewportWidth;
4313
+ const ph =
4314
+ port && inside
4315
+ ? port.height - port.borderTop - port.borderBottom
4316
+ : viewportHeight;
4317
+ const dx = stuck(
4318
+ box.x,
4319
+ box.width,
4320
+ box.marginLeft,
4321
+ box.marginRight,
4322
+ cb.contentX,
4323
+ cb.contentWidth,
4324
+ px,
4325
+ pw,
4326
+ resolveOrNull(style.left, pw),
4327
+ inside ? resolveOrNull(style.right, pw) : null,
4328
+ // `left` wins where both hold, or `right` in a right-to-left block
4329
+ cb.style.direction !== 'rtl',
4330
+ );
4331
+ const dy = stuck(
4332
+ box.y,
4333
+ box.height,
4334
+ box.marginTop,
4335
+ box.marginBottom,
4336
+ cb.contentY,
4337
+ cb.contentHeight,
4338
+ py,
4339
+ ph,
4340
+ resolveOrNull(style.top, ph),
4341
+ inside ? resolveOrNull(style.bottom, ph) : null,
4342
+ true,
4343
+ );
4344
+ return [dx, dy];
4345
+ }
4346
+
4347
+ /**
4348
+ * How far a sticky box moves along one axis: toward the end until its
4349
+ * start edge is `start` inside the scrollport's, or toward the start until
4350
+ * its end edge is `end` inside the scrollport's, each no further than its
4351
+ * margin box stays in its containing block — and where both would move
4352
+ * it, `startWins` says which does.
4353
+ */
4354
+ function stuck(
4355
+ at: number,
4356
+ size: number,
4357
+ marginStart: number,
4358
+ marginEnd: number,
4359
+ cbAt: number,
4360
+ cbSize: number,
4361
+ portAt: number,
4362
+ portSize: number,
4363
+ start: number | null,
4364
+ end: number | null,
4365
+ startWins: boolean,
4366
+ ): number {
4367
+ let byStart = 0;
4368
+ if (start !== null && Number.isFinite(start) && at < portAt + start) {
4369
+ const room = cbAt + cbSize - (at + size + marginEnd);
4370
+ byStart = Math.max(0, Math.min(portAt + start - at, room));
4371
+ }
4372
+ let byEnd = 0;
4373
+ if (end !== null && Number.isFinite(end)) {
4374
+ const over = at + size - (portAt + portSize - end);
4375
+ const room = at - marginStart - cbAt;
4376
+ if (over > 0) byEnd = -Math.max(0, Math.min(over, room));
4377
+ }
4378
+ return startWins ? byStart || byEnd : byEnd || byStart;
4379
+ }
4380
+
4169
4381
  /**
4170
4382
  * Move an inline box's text where `position: relative` puts the box. Its
4171
4383
  * text is on its block's lines, laid out apart from the text around it
@@ -36,6 +36,7 @@ import type { ContentItem } from '../css/content.js';
36
36
  import {
37
37
  CONTAIN_STYLE,
38
38
  FIRST_LINE_INHERITED,
39
+ NO_MASK,
39
40
  copyStyle,
40
41
  firstLineParent,
41
42
  inherit,
@@ -187,6 +188,15 @@ export interface LineText {
187
188
  * spacer: a unit of the layout that is no text of the document's
188
189
  * (`documentOffsetOf`, `layoutOffsetOf`). */
189
190
  gaps?: number[];
191
+ /**
192
+ * The spaces `pre-wrap` keeps that this fragment's line ends on, which an
193
+ * engine strips from a line's end as though CSS removed them there: each
194
+ * one's offset in the layout's text and where it is, from the layout's
195
+ * origin, beside the line's end in logical order (`hangPreserved`). They
196
+ * are their inline box's, whose background and border cover them, and
197
+ * the ones before a forced break that fit take room on the line.
198
+ */
199
+ hung?: { at: number; x: number; width: number }[];
190
200
  /** Any offset in the layout's own text as a document index — how a run
191
201
  * under the pointer finds the element whose text it is. Per pass: the
192
202
  * layout may be one an earlier pass made (`TextLayoutCache`), and the
@@ -233,6 +243,10 @@ export interface TextLayoutLike {
233
243
  height: number;
234
244
  baseline: number;
235
245
  width: number;
246
+ /** Its width with the white space it ends on kept, in the item it
247
+ * ends: how far it moves the pen where text goes on after it on the
248
+ * same line. ntk's, from 8.17.0; an engine without it leaves it out. */
249
+ advance?: number;
236
250
  ascent: number;
237
251
  descent: number;
238
252
  start: number;
@@ -581,6 +595,12 @@ export const BOX_RAISES = new WeakMap<Box, number>();
581
595
  * background, drawn from its baseline, is put there by line. */
582
596
  export const LINE_BOX_RAISES = new WeakMap<LineBox, Map<Box, number>>();
583
597
 
598
+ /** The face of an inline box with padding above or below its text and
599
+ * nothing to paint behind it, which has no `decoration`: its fragments
600
+ * still reach past its lines by the padding, and count in the scrollable
601
+ * overflow of the block they are in (`computePaintBounds`). */
602
+ export const PADDED_FACES = new WeakMap<Box, InlineDecoration>();
603
+
584
604
  /** The table cells wholly in columns `visibility: collapse` took out of
585
605
  * their table (CSS 2.1 17.5.5), which the paint pass leaves out: a cell is
586
606
  * no descendant of its column and inherits nothing from it, so its own
@@ -615,11 +635,17 @@ export const GRID_TRACKS = new WeakMap<Box, GridTracks>();
615
635
  * `order` moves any: an absolutely positioned child's is 0. */
616
636
  export const PAINT_ORDER = new WeakMap<Box, Box[]>();
617
637
 
618
- /** The blocks that broke a relatively positioned inline box in pieces,
619
- * under its first piece: its offset moves them too (CSS 2.1 9.2.1.1),
620
- * though they stand outside it (`breakAround`). */
638
+ /** The blocks that broke an inline box in pieces, under its first piece
639
+ * (`breakAround`): a relative offset of the box moves them too (CSS 2.1
640
+ * 9.2.1.1), though they stand outside it, and its rect takes them in, as
641
+ * a browser's does (`elementRect`). */
621
642
  export const CUT_BLOCKS = new WeakMap<Box, Box[]>();
622
643
 
644
+ /** The element a pseudo-element's box is generated from, which has no
645
+ * element of its own (`Box.pseudo`): whose the images its styles name are,
646
+ * when the host is asked for them. */
647
+ export const GENERATED_FROM = new WeakMap<Box, Element>();
648
+
623
649
  /** The blocks that broke an inline box under full opacity in pieces, and
624
650
  * the opacity they take from it — from each such inline box around them,
625
651
  * multiplied. They stand outside it (CSS 2.1 9.2.1.1) and are still its
@@ -647,9 +673,10 @@ export interface BoxTree {
647
673
  controls: Box[];
648
674
  /** Every box carrying an `href`, for click and hover. */
649
675
  links: Box[];
650
- /** Every element's box with a `background-image` or a border image,
651
- * for the host to be asked for: a document has a handful, and finding
652
- * them was a walk over every box after every build. */
676
+ /** Every box with a `background-image`, a border image or a mask image,
677
+ * an element's or a pseudo-element's, for the host to be asked for: a
678
+ * document has a handful, and finding them was a walk over every box
679
+ * after every build. */
653
680
  backgrounds: Box[];
654
681
  /** Every image generated content names, the element whose
655
682
  * pseudo-element names it, for the host to be asked for, and whether its
@@ -1025,13 +1052,7 @@ class Builder {
1025
1052
  this._firstLine = true;
1026
1053
  }
1027
1054
  }
1028
- if (
1029
- style.backgroundImage ||
1030
- style.backgroundImages ||
1031
- typeof style.borderImage.source === 'string'
1032
- ) {
1033
- this._backgrounds.push(box);
1034
- }
1055
+ if (namesImages(style)) this._backgrounds.push(box);
1035
1056
  if (style.position === 'absolute' || style.position === 'fixed')
1036
1057
  box.outOfFlow = true;
1037
1058
  else if (style.float !== 'none') box.isFloat = true;
@@ -1158,13 +1179,7 @@ class Builder {
1158
1179
  const box = new Box('replaced', el, style);
1159
1180
  box.replaced = replaced;
1160
1181
  into.append(box);
1161
- if (
1162
- style.backgroundImage ||
1163
- style.backgroundImages ||
1164
- typeof style.borderImage.source === 'string'
1165
- ) {
1166
- this._backgrounds.push(box);
1167
- }
1182
+ if (namesImages(style)) this._backgrounds.push(box);
1168
1183
  if (style.position === 'absolute' || style.position === 'fixed')
1169
1184
  box.outOfFlow = true;
1170
1185
  else if (style.float !== 'none') box.isFloat = true;
@@ -1295,6 +1310,10 @@ class Builder {
1295
1310
  const box = new Box(boxKindFor(style.display), null, style);
1296
1311
  box.pseudo = which;
1297
1312
  into.append(box);
1313
+ // its images are its element's to ask for: a `::after` with no content
1314
+ // but a background, which designs hang a picture on, drew nothing
1315
+ GENERATED_FROM.set(box, el);
1316
+ if (namesImages(style)) this._backgrounds.push(box);
1298
1317
  // a list item it generates has a marker as an element's has, of the
1299
1318
  // `list-item` counter it has just counted (CSS 2.1 12.5): outside it,
1300
1319
  // or at the start of its content
@@ -1714,6 +1733,10 @@ class Builder {
1714
1733
  const letterStyle = cascade.firstLetterStyle(search.rules, style);
1715
1734
  const box = new Box(boxKindFor(letterStyle.display), null, letterStyle);
1716
1735
  box.pseudo = 'first-letter';
1736
+ if (owner) {
1737
+ GENERATED_FROM.set(box, owner);
1738
+ if (namesImages(letterStyle)) this._backgrounds.push(box);
1739
+ }
1717
1740
  if (letterStyle.float !== 'none') {
1718
1741
  box.isFloat = true;
1719
1742
  if (into.kind === 'inline') this._nestedOutOfLine = true;
@@ -2608,7 +2631,7 @@ function breakAround(inline: Box): Box[] | null {
2608
2631
  for (let i = 0; i < pieces.length; i += 1) {
2609
2632
  pieces[i].cut = (i > 0 ? 1 : 0) | (i < pieces.length - 1 ? 2 : 0);
2610
2633
  }
2611
- if (isRelative(inline.style)) CUT_BLOCKS.set(pieces[0], blocks);
2634
+ CUT_BLOCKS.set(pieces[0], blocks);
2612
2635
  const fade = inline.style.opacity;
2613
2636
  if (fade < 1) {
2614
2637
  for (const block of blocks) {
@@ -2618,6 +2641,17 @@ function breakAround(inline: Box): Box[] | null {
2618
2641
  return out;
2619
2642
  }
2620
2643
 
2644
+ /** Whether a style names an image for its box to be drawn with: a
2645
+ * background's, a border's, or a mask's. */
2646
+ function namesImages(style: ComputedStyle): boolean {
2647
+ return (
2648
+ !!style.backgroundImage ||
2649
+ !!style.backgroundImages ||
2650
+ typeof style.borderImage.source === 'string' ||
2651
+ style.mask !== NO_MASK
2652
+ );
2653
+ }
2654
+
2621
2655
  /**
2622
2656
  * The style of an anonymous box inside a parent: what the parent passes on
2623
2657
  * by inheritance, and every other property at its initial value, as CSS 2.1
@@ -190,6 +190,7 @@ export const RUN_FIELDS = [
190
190
  'features',
191
191
  'nowrap',
192
192
  'shapeApart',
193
+ 'kernAcross',
193
194
  ] as const satisfies readonly (keyof TextRun)[];
194
195
 
195
196
  /** A field of `TextRun` missing from `RUN_FIELDS` names itself here. */
@@ -223,7 +224,8 @@ function sameRun(a: TextRun, b: TextRun): boolean {
223
224
  a.href === b.href &&
224
225
  a.features === b.features &&
225
226
  a.nowrap === b.nowrap &&
226
- a.shapeApart === b.shapeApart
227
+ a.shapeApart === b.shapeApart &&
228
+ a.kernAcross === b.kernAcross
227
229
  );
228
230
  }
229
231
 
@@ -250,7 +252,8 @@ function sameStyle(
250
252
  a.href === b.href &&
251
253
  a.features === b.features &&
252
254
  a.nowrap === b.nowrap &&
253
- a.shapeApart === b.shapeApart
255
+ a.shapeApart === b.shapeApart &&
256
+ a.kernAcross === b.kernAcross
254
257
  );
255
258
  }
256
259
 
@@ -265,6 +268,7 @@ export const OPTION_FIELDS = [
265
268
  'overflow',
266
269
  'overflowWrap',
267
270
  'wrap',
271
+ 'fit',
268
272
  ] as const satisfies readonly (keyof Options)[];
269
273
 
270
274
  /** An option missing from `OPTION_FIELDS` names itself here. */
@@ -284,6 +288,7 @@ function sameOptions(a: Options, b: Options): boolean {
284
288
  a.maxLines === b.maxLines &&
285
289
  a.overflow === b.overflow &&
286
290
  a.overflowWrap === b.overflowWrap &&
287
- a.wrap === b.wrap
291
+ a.wrap === b.wrap &&
292
+ a.fit === b.fit
288
293
  );
289
294
  }
@@ -153,6 +153,30 @@ export class FloatContext {
153
153
  return lowest;
154
154
  }
155
155
 
156
+ /** Move the floats placed since `mark` down by `dy`, with the block they
157
+ * were placed in: a list item's first block goes lower to make room
158
+ * for its marker once it is laid out (`layoutChildren`). */
159
+ moveSince(mark: number, dy: number): void {
160
+ if (!dy || mark >= this._boxes.length) return;
161
+ for (let i = mark; i < this._boxes.length; i += 1) {
162
+ const float = this._boxes[i];
163
+ this._boxes[i] = {
164
+ ...float,
165
+ top: float.top + dy,
166
+ bottom: float.bottom + dy,
167
+ };
168
+ }
169
+ this._lowestLeft = -Infinity;
170
+ this._lowestRight = -Infinity;
171
+ this._lastTop = -Infinity;
172
+ for (const float of this._boxes) {
173
+ if (float.top > this._lastTop) this._lastTop = float.top;
174
+ if (float.side === 'left')
175
+ this._lowestLeft = Math.max(this._lowestLeft, float.bottom);
176
+ else this._lowestRight = Math.max(this._lowestRight, float.bottom);
177
+ }
178
+ }
179
+
156
180
  /** How far down the floats reach — what a container that establishes a
157
181
  * BFC has to grow to, so it does not end above its own floats. */
158
182
  get bottom(): number {