@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/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 { BOX_RAISES, buildBoxes, CONTENT_IMAGES, LINE_BOX_RAISES, SHIFTED_LINES, } 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';
50
+ import { SurfaceCache, newSurface } from './surfaces.js';
51
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
@@ -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,29 +1064,52 @@ export class HtmlViewNode extends Node {
1002
1064
  }
1003
1065
  else {
1004
1066
  const bands = [];
1005
- // an inline box's fragments are on the lines of the block it is laid
1006
- // out in, and no text of a float or a positioned box inside it — on
1007
- // lines of their own — is one of them (CSSOM View 6.1): a list item
1008
- // made inline around an absolute link measured as the link
1009
- let block = box.parent;
1010
- while (block && !block.lines)
1011
- block = block.parent;
1012
- if (box.subtreeTextEnd > box.subtreeTextStart) {
1013
- if (box.kind === 'inline' && block?.lines) {
1014
- lineBands(block.lines, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
1015
- }
1016
- else {
1017
- collectBands(tree.root, box.subtreeTextStart, box.subtreeTextEnd, 0, 0, bands);
1018
- }
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);
1019
1077
  }
1020
- if (box.kind === 'inline') {
1021
- fragmentReach(box, bands);
1022
- const fonts = this._layouts?.fonts;
1023
- if (fonts)
1024
- fragmentHeights(box, bands, fonts);
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
+ });
1025
1088
  }
1026
1089
  for (const band of bands)
1027
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
+ }
1028
1113
  if (!rect) {
1029
1114
  // an empty subtree's range is `[0, 0)`, wherever it stands: where
1030
1115
  // its text would be is where the text after it starts
@@ -1048,7 +1133,20 @@ export class HtmlViewNode extends Node {
1048
1133
  if (!tree)
1049
1134
  return null;
1050
1135
  const local = this._toDocument(x, y);
1051
- 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
+ };
1052
1150
  }
1053
1151
  /**
1054
1152
  * The cursor for a point of this element's, in device pixels: what core
@@ -1065,7 +1163,7 @@ export class HtmlViewNode extends Node {
1065
1163
  if (!tree)
1066
1164
  return null;
1067
1165
  const hit = { text: false };
1068
- 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());
1069
1167
  const cursor = el ? tree.styles.get(el)?.style.cursor : null;
1070
1168
  // a keyword; a `url()` this cannot load falls back, as its list would
1071
1169
  if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor))
@@ -1107,10 +1205,8 @@ export class HtmlViewNode extends Node {
1107
1205
  }
1108
1206
  /** The hovered chain moved: restyle where it did, or the document. */
1109
1207
  _restyleHover(was, now) {
1110
- const done = this._hoverInPlace(was, now);
1111
- if (done === 'painted')
1112
- this.invalidate(false, this, 'props');
1113
- else if (done === false)
1208
+ // a restyle in place names what it repainted (`_hoverInPlace`)
1209
+ if (this._hoverInPlace(was, now) === false)
1114
1210
  this._invalidate(Stale.Boxes);
1115
1211
  }
1116
1212
  /**
@@ -1205,6 +1301,11 @@ export class HtmlViewNode extends Node {
1205
1301
  const fresh = new Map();
1206
1302
  const changed = new Map();
1207
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();
1208
1309
  const styleOf = (el) => {
1209
1310
  const kept = tree.styles.get(el);
1210
1311
  if (!kept)
@@ -1224,11 +1325,18 @@ export class HtmlViewNode extends Node {
1224
1325
  }
1225
1326
  const style = cascade.styleFor(el, parentStyle, kept.inFlex);
1226
1327
  fresh.set(el, style);
1227
- const diff = inkOnly(kept.style, style);
1328
+ const diff = hoverChange(kept.style, style);
1228
1329
  if (diff === false)
1229
1330
  refused = true;
1230
- else if (diff)
1331
+ else if (diff) {
1231
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
+ }
1232
1340
  return style;
1233
1341
  };
1234
1342
  for (const el of reach) {
@@ -1262,10 +1370,14 @@ export class HtmlViewNode extends Node {
1262
1370
  continue;
1263
1371
  const kept = tree.styles.get(owner);
1264
1372
  // an anonymous box, a pseudo-element, a marker, a control: a style
1265
- // 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.
1266
1376
  if (!box.el || box.style !== kept.style)
1267
1377
  return false;
1268
- 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))
1269
1381
  return false;
1270
1382
  restyled.push([box, style]);
1271
1383
  if (box.kind === 'inline') {
@@ -1302,11 +1414,29 @@ export class HtmlViewNode extends Node {
1302
1414
  }
1303
1415
  }
1304
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
+ }
1305
1424
  // and only now, all of it
1306
- for (const [box, style] of restyled)
1425
+ const moved = [];
1426
+ for (const [box, style] of restyled) {
1427
+ const was = box.style;
1307
1428
  box.style = style;
1308
- 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
+ }
1309
1438
  box.decoration = decoration;
1439
+ }
1310
1440
  for (const text of texts) {
1311
1441
  const next = relaid.get(text.layout);
1312
1442
  if (next)
@@ -1315,8 +1445,72 @@ export class HtmlViewNode extends Node {
1315
1445
  for (const [el, style] of changed) {
1316
1446
  tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
1317
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);
1318
1485
  return 'painted';
1319
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
+ }
1320
1514
  /** The document, for an application that wants to read or change it. */
1321
1515
  get document() {
1322
1516
  return this._source.document;
@@ -1388,6 +1582,16 @@ export class HtmlViewNode extends Node {
1388
1582
  this._fail(error, this._laidOutWidth);
1389
1583
  }
1390
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
+ }
1391
1595
  _paint(ctx, tree, range, damage) {
1392
1596
  paintDocument(ctx, tree, {
1393
1597
  originX: this.abs.x,
@@ -1395,6 +1599,7 @@ export class HtmlViewNode extends Node {
1395
1599
  // the root's background covers the whole element, not only the
1396
1600
  // document: an element grown past its content is canvas too
1397
1601
  canvas: this.abs,
1602
+ viewport: this._viewport() ?? undefined,
1398
1603
  scale: this._scale,
1399
1604
  damage,
1400
1605
  selection: range
@@ -1410,11 +1615,34 @@ export class HtmlViewNode extends Node {
1410
1615
  const size = image ? this._resources.imageSize(url) : null;
1411
1616
  return image && size ? { image, ...size } : null;
1412
1617
  },
1618
+ surface: (width, height) => this._surface(width, height),
1413
1619
  cached: (key, width, height, draw) => (this._shadowCache ??= new SurfaceCache(this.app)).get(key, width, height, draw),
1414
1620
  });
1415
1621
  }
1416
1622
  }
1417
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
+ }
1418
1646
  /** The first box an element made, depth first. */
1419
1647
  function boxFor(root, element) {
1420
1648
  const stack = [root];
@@ -1428,6 +1656,83 @@ function boxFor(root, element) {
1428
1656
  }
1429
1657
  return null;
1430
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
+ }
1431
1736
  /** Where the document's text goes on after a box with none: the start of
1432
1737
  * the next text in document order, or the end of the last before it. */
1433
1738
  function textAfter(root, target) {
@@ -1488,6 +1793,7 @@ function sameRects(a, b) {
1488
1793
  p.y !== q.y ||
1489
1794
  p.width !== q.width ||
1490
1795
  p.height !== q.height ||
1796
+ p.opacity !== q.opacity ||
1491
1797
  !sameBare(p.bare, q.bare)) {
1492
1798
  return false;
1493
1799
  }
@@ -1503,7 +1809,8 @@ function sameBare(a, b) {
1503
1809
  a.height === b.height &&
1504
1810
  a.color === b.color &&
1505
1811
  a.fontFamily === b.fontFamily &&
1506
- a.fontSize === b.fontSize);
1812
+ a.fontSize === b.fontSize &&
1813
+ a.chevron === b.chevron);
1507
1814
  }
1508
1815
  // --- walks over the laid-out tree -------------------------------------------
1509
1816
  /** Distance from a coordinate to an interval, zero inside it. */
@@ -1768,7 +2075,10 @@ function fragmentHeights(box, bands, fonts) {
1768
2075
  }
1769
2076
  }
1770
2077
  /** The bands of a text range on one block's own lines. */
1771
- function lineBands(lines, from, to, dx, dy, out) {
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) {
1772
2082
  // First line whose text can reach `from`, by binary search over the
1773
2083
  // sorted text starts; stop at the first line past `to`.
1774
2084
  let lo = 0;
@@ -1794,11 +2104,37 @@ function lineBands(lines, from, to, dx, dy, out) {
1794
2104
  if (b <= a)
1795
2105
  continue;
1796
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);
1797
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));
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;
1798
2134
  out.push({
1799
- x: dx + band.x + text.drawX,
2135
+ x: dx + space.x + text.drawX - moved,
1800
2136
  y: dy + line.y,
1801
- width: band.width,
2137
+ width: space.width,
1802
2138
  height: line.height,
1803
2139
  });
1804
2140
  }
@@ -1806,6 +2142,18 @@ function lineBands(lines, from, to, dx, dy, out) {
1806
2142
  }
1807
2143
  }
1808
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
+ }
1809
2157
  function collectBands(box, from, to, dx, dy, out) {
1810
2158
  if (box.subtreeTextEnd <= box.subtreeTextStart)
1811
2159
  return;
@@ -1831,7 +2179,11 @@ const HIT_POSITIONED = 3;
1831
2179
  function deepestAt(tree, x, y,
1832
2180
  /** Told whether what was found was found under text: a run's, rather
1833
2181
  * than a box's. */
1834
- 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) {
1835
2187
  const box = tree.root;
1836
2188
  let found = box.el;
1837
2189
  let foundKey = [];
@@ -1872,7 +2224,25 @@ hit) {
1872
2224
  // Zen Garden's archive links are absolute `<li>`s in an `overflow:
1873
2225
  // hidden` list with no height of its own, and not one of them could be
1874
2226
  // hovered or pressed.
2227
+ let atViewport = false;
1875
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) => {
1876
2246
  const style = child.style;
1877
2247
  if (style.position !== 'static' ||
1878
2248
  (child.parent?.kind === 'flex' && typeof style.zIndex === 'number')) {
@@ -2030,21 +2400,112 @@ const FACE_FIELDS = [
2030
2400
  'letterSpacing',
2031
2401
  'features',
2032
2402
  ];
2033
- /** Whether two styles differ in ink alone: true where they do, null where
2034
- * they do not differ, false where something else does. Custom properties
2035
- * are read through the properties that use them. */
2036
- 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) {
2037
2430
  const a = was;
2038
2431
  const b = now;
2039
- let ink = false;
2432
+ let change = null;
2040
2433
  for (const key in b) {
2041
2434
  if (key === 'custom' || sameValue(a[key], b[key]))
2042
2435
  continue;
2043
- 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')
2044
2485
  return false;
2045
- ink = true;
2486
+ for (const child of at.children)
2487
+ stack.push(child);
2046
2488
  }
2047
- 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
+ };
2048
2509
  }
2049
2510
  /** Structural equality for a computed value: a number, a string, or the
2050
2511
  * plain objects and arrays a length or a shadow list is. */