@react-x11/components 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/html/controls.d.ts +41 -5
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +85 -57
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +134 -20
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +545 -126
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +47 -6
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +336 -30
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/style.d.ts +142 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +563 -147
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts +11 -1
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +73 -9
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/dom.d.ts.map +1 -1
- package/dist/html/dom.js +3 -1
- package/dist/html/dom.js.map +1 -1
- package/dist/html/fonts.d.ts +78 -4
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +373 -19
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/form.d.ts +200 -0
- package/dist/html/form.d.ts.map +1 -0
- package/dist/html/form.js +801 -0
- package/dist/html/form.js.map +1 -0
- package/dist/html/index.d.ts +12 -0
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +30 -301
- package/dist/html/index.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +93 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +393 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +90 -2
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +74 -10
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +90 -18
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +574 -84
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +154 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +62 -18
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +90 -15
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +771 -186
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +101 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1502 -160
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +15 -2
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +249 -17
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.d.ts +29 -0
- package/dist/html/widgets.d.ts.map +1 -0
- package/dist/html/widgets.js +700 -0
- package/dist/html/widgets.js.map +1 -0
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +2 -2
- package/src/html/controls.ts +114 -51
- package/src/html/css/cascade.ts +614 -140
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +388 -33
- package/src/html/css/style.ts +688 -150
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +77 -10
- package/src/html/css/values.ts +4 -0
- package/src/html/dom.ts +3 -1
- package/src/html/fonts.ts +417 -24
- package/src/html/form.ts +962 -0
- package/src/html/index.ts +49 -338
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +447 -37
- package/src/html/layout/boxes.ts +143 -14
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +95 -22
- package/src/html/layout/flex.ts +675 -78
- package/src/html/layout/inline.ts +159 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +62 -19
- package/src/html/node.ts +798 -172
- package/src/html/paint.ts +1742 -241
- package/src/html/svg.ts +277 -19
- package/src/html/widgets.ts +821 -0
- package/src/html/woff2.ts +612 -0
- package/src/index.ts +1 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/dist/html/node.js
CHANGED
|
@@ -40,12 +40,14 @@ import { registerElement, registeredElements } from 'react-x11/host';
|
|
|
40
40
|
import { Node } from 'react-x11/node';
|
|
41
41
|
import { codePointAtOffset, codeUnitOffsets } from '../internal/text.js';
|
|
42
42
|
import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
|
|
43
|
-
import { Cascade } from './css/cascade.js';
|
|
43
|
+
import { Cascade, HOVER_FOLLOWED, HOVER_PSEUDO_NONE, HOVER_PSEUDO_OTHER, HOVER_UNTOUCHED, } 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, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, } from './layout/boxes.js';
|
|
47
|
-
import { layoutDocument,
|
|
46
|
+
import { BOX_RAISES, buildBoxes, CONTENT_IMAGES, CLEARED_FROM, COLUMN_LINES, COLUMN_PIECES, COLUMN_ROWS, CUT_BLOCKS, GENERATED_FROM, INLINE_OFFSETS, LINE_BOX_RAISES, SHIFTED_LINES, TEXT_SHIFTS, columned, } from './layout/boxes.js';
|
|
47
|
+
import { layoutDocument, placedMatrix, retranslate } from './layout/block.js';
|
|
48
|
+
import { invert, mapPoint, mapRect, transformed } from './css/transform.js';
|
|
48
49
|
import { TextLayoutCache } from './layout/cache.js';
|
|
50
|
+
import { weightAxes } from './layout/axes.js';
|
|
49
51
|
import { shapingSafe } from './layout/shaping.js';
|
|
50
52
|
import { SurfaceCache, newSurface } from './surfaces.js';
|
|
51
53
|
import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
@@ -53,7 +55,7 @@ import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
|
|
|
53
55
|
// a layout is built once, on the first selection that needs it.
|
|
54
56
|
import { documentOffsetOf, layoutOffsetOf, layoutOffsets as layoutOffsetsOf, } from './layout/inline.js';
|
|
55
57
|
import { lineBands as bandsFor } from '../richtext/runs.js';
|
|
56
|
-
import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, paintDocument, queryChildIndex, stackLayers, } from './paint.js';
|
|
58
|
+
import { clipsOverflow, computePaintBounds, containingBlockOf, FIXED_BOXES, fixedToViewport, forgetDecoratedAncestors, hasRect, hoistNegative, holds, holdsAbsolute, inClip, inClipPath, onLine, ownBounds, paintDocument, pathClips, queryChildIndex, selectionRows, stackLayers, stacksLayers, } from './paint.js';
|
|
57
59
|
import { controlRectsOf, measureControl } from './controls.js';
|
|
58
60
|
import { ResourceStore } from './resources.js';
|
|
59
61
|
import { WebFonts } from './fonts.js';
|
|
@@ -135,6 +137,8 @@ export class HtmlViewNode extends Node {
|
|
|
135
137
|
* that read it (`vw`, `vh`). */
|
|
136
138
|
_styledWidth = -1;
|
|
137
139
|
_styledHeight = -1;
|
|
140
|
+
/** The cascade the box tree's styles were computed by. */
|
|
141
|
+
_styledWith = null;
|
|
138
142
|
_documentHeight = 0;
|
|
139
143
|
_documentWidth = 0;
|
|
140
144
|
/** The viewport height the document was last laid out under, and whether
|
|
@@ -152,6 +156,20 @@ export class HtmlViewNode extends Node {
|
|
|
152
156
|
* the text carries surrogate pairs. null until checked. */
|
|
153
157
|
_pointsAreUnits = null;
|
|
154
158
|
_hovered = [];
|
|
159
|
+
/** Where the pointer last moved to, in logical window pixels, while it
|
|
160
|
+
* is over the element (`defaultMouseMove`). */
|
|
161
|
+
_pointerAt = null;
|
|
162
|
+
/** A hover held until the content under a still pointer stops moving:
|
|
163
|
+
* the timer, and when the content last moved (`_holdHover`). */
|
|
164
|
+
_heldHover = null;
|
|
165
|
+
_heldSince = 0;
|
|
166
|
+
/**
|
|
167
|
+
* The elements the build of the boxes that is due has to style again,
|
|
168
|
+
* where every change asking for it said which (`_invalidate`), and null
|
|
169
|
+
* where any element's style may have changed — a new cascade, a viewport
|
|
170
|
+
* a `vw` reads, a face that arrived.
|
|
171
|
+
*/
|
|
172
|
+
_restyleOnly = null;
|
|
155
173
|
_scriptsSeen = new WeakSet();
|
|
156
174
|
_controls = [];
|
|
157
175
|
_reportedDomRevision = -1;
|
|
@@ -230,6 +248,7 @@ export class HtmlViewNode extends Node {
|
|
|
230
248
|
controlPadY: look.controlPadY * s,
|
|
231
249
|
controlBorder: look.controlBorder * s,
|
|
232
250
|
controlRadius: look.controlRadius * s,
|
|
251
|
+
controlFontSize: (look.controlFontSize ?? look.fontSize) * s,
|
|
233
252
|
};
|
|
234
253
|
this._deviceLookFor = look;
|
|
235
254
|
this._deviceLookAt = s;
|
|
@@ -237,7 +256,26 @@ export class HtmlViewNode extends Node {
|
|
|
237
256
|
return scaled;
|
|
238
257
|
}
|
|
239
258
|
// --- the pipeline ---------------------------------------------------------
|
|
240
|
-
|
|
259
|
+
/**
|
|
260
|
+
* `restyle` with `Stale.Boxes`: the elements whose styles the change
|
|
261
|
+
* could have touched, where that is all of it — a pointer move's reach
|
|
262
|
+
* (`_hoverReach`). The build it asks for keeps every other element's
|
|
263
|
+
* (`_restyleOnly`).
|
|
264
|
+
*/
|
|
265
|
+
_invalidate(stale, restyle = null) {
|
|
266
|
+
if (stale >= Stale.Boxes) {
|
|
267
|
+
const only = stale === Stale.Boxes ? restyle : null;
|
|
268
|
+
if (this._stale < Stale.Boxes) {
|
|
269
|
+
this._restyleOnly = only ? new Set(only) : null;
|
|
270
|
+
}
|
|
271
|
+
else if (this._restyleOnly && only) {
|
|
272
|
+
for (const el of only)
|
|
273
|
+
this._restyleOnly.add(el);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
this._restyleOnly = null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
241
279
|
if (stale > this._stale)
|
|
242
280
|
this._stale = stale;
|
|
243
281
|
if (stale >= Stale.Layout)
|
|
@@ -443,7 +481,10 @@ export class HtmlViewNode extends Node {
|
|
|
443
481
|
faces = kept.faces;
|
|
444
482
|
}
|
|
445
483
|
else {
|
|
446
|
-
|
|
484
|
+
// the UA sheet's `px` are CSS pixels, which the cascade scales like an
|
|
485
|
+
// author's, so it is written from the look in CSS pixels: from the
|
|
486
|
+
// device one, a control's size and a button's chrome were doubled at 2x
|
|
487
|
+
const sheets = [uaStylesheet(props.look)];
|
|
447
488
|
const imports = [];
|
|
448
489
|
faces = [];
|
|
449
490
|
let order = 0;
|
|
@@ -490,8 +531,9 @@ export class HtmlViewNode extends Node {
|
|
|
490
531
|
// The faces this width and scheme declare: a `@font-face` may sit in a
|
|
491
532
|
// `@media` block like any rule.
|
|
492
533
|
const cssWidth = width / this._scale;
|
|
534
|
+
const cssHeight = this._viewportHeight() / this._scale;
|
|
493
535
|
this._webFonts.setFallback(look.fontFamily);
|
|
494
|
-
this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme)));
|
|
536
|
+
this._webFonts.setFaces(faces.filter((f) => mediaMatches(f.rule.media, cssWidth, look.colorScheme, cssHeight, this._scale)));
|
|
495
537
|
this._cascade.setPointer({
|
|
496
538
|
hovered: new Set(this._hovered),
|
|
497
539
|
active: EMPTY_SET,
|
|
@@ -505,8 +547,16 @@ export class HtmlViewNode extends Node {
|
|
|
505
547
|
* next restyle whether the sheets read the same (`_sameSheets`). A sheet
|
|
506
548
|
* that imports itself, or one of the sheets importing it, is read once.
|
|
507
549
|
*/
|
|
508
|
-
_placeImports(sheet, encoding, element, layers, seen, place, depth = 0, chain = new Set()
|
|
509
|
-
|
|
550
|
+
_placeImports(sheet, encoding, element, layers, seen, place, depth = 0, chain = new Set(),
|
|
551
|
+
/** The media queries `sheet` is itself under, where it was imported
|
|
552
|
+
* under some. */
|
|
553
|
+
under = null) {
|
|
554
|
+
for (let i = 0; i < sheet.imports.length; i += 1) {
|
|
555
|
+
const url = sheet.imports[i];
|
|
556
|
+
// what it imports is under its import's media queries, and under
|
|
557
|
+
// the ones the sheet importing it is under
|
|
558
|
+
const conditions = sheet.importConditions[i];
|
|
559
|
+
const media = conditions ? [...(under ?? []), conditions] : under;
|
|
510
560
|
this._resources.request({ url, kind: 'stylesheet', element });
|
|
511
561
|
const fetched = this._resources.stylesheet(url, [encoding]);
|
|
512
562
|
const base = this._resources.sheetBase(url);
|
|
@@ -514,9 +564,9 @@ export class HtmlViewNode extends Node {
|
|
|
514
564
|
const key = base ?? url;
|
|
515
565
|
if (!fetched || depth >= MAX_IMPORT_DEPTH || chain.has(key))
|
|
516
566
|
continue;
|
|
517
|
-
const imported = parseStylesheet(fetched.text, 0, layers, base);
|
|
567
|
+
const imported = parseStylesheet(fetched.text, 0, layers, base, media);
|
|
518
568
|
chain.add(key);
|
|
519
|
-
this._placeImports(imported, fetched.encoding, element, layers, seen, place, depth + 1, chain);
|
|
569
|
+
this._placeImports(imported, fetched.encoding, element, layers, seen, place, depth + 1, chain, media);
|
|
520
570
|
chain.delete(key);
|
|
521
571
|
place(imported, element);
|
|
522
572
|
}
|
|
@@ -631,11 +681,22 @@ export class HtmlViewNode extends Node {
|
|
|
631
681
|
const height = root?.abs?.height;
|
|
632
682
|
return height && height > 0 ? height : 600;
|
|
633
683
|
}
|
|
684
|
+
/** The engine this document lays text out with, and the one it is over:
|
|
685
|
+
* one object while the engine is, since caches are kept by it. */
|
|
686
|
+
_engine = null;
|
|
634
687
|
_fonts() {
|
|
635
688
|
const fonts = this.app?.fonts;
|
|
689
|
+
if (!fonts)
|
|
690
|
+
return null;
|
|
636
691
|
// a face the engine cannot shape from costs its characters, not the
|
|
637
692
|
// document (`shaping.ts`)
|
|
638
|
-
|
|
693
|
+
const safe = shapingSafe(fonts);
|
|
694
|
+
if (this._engine?.over !== safe) {
|
|
695
|
+
// and a variable face of the document's is set at the weight its
|
|
696
|
+
// rule has for a style's (`axes.ts`)
|
|
697
|
+
this._engine = { over: safe, fonts: weightAxes(safe, this._webFonts) };
|
|
698
|
+
}
|
|
699
|
+
return this._engine.fonts;
|
|
639
700
|
}
|
|
640
701
|
/** The text layouts the last pass made, for this one to reuse. */
|
|
641
702
|
_layouts = null;
|
|
@@ -720,9 +781,14 @@ export class HtmlViewNode extends Node {
|
|
|
720
781
|
const onlyViewport = this._stale === Stale.Nothing && this._laidOutWidth === target;
|
|
721
782
|
const wasWidth = this._documentWidth;
|
|
722
783
|
const wasHeight = this._documentHeight;
|
|
784
|
+
// the styles a build may keep (`_restyleOnly`), unless something
|
|
785
|
+
// found below changes them all
|
|
786
|
+
let restyleOnly = this._stale === Stale.Boxes ? this._restyleOnly : null;
|
|
787
|
+
this._restyleOnly = null;
|
|
723
788
|
if (this._stale >= Stale.Style || !this._cascade) {
|
|
724
789
|
this._restyle(target);
|
|
725
790
|
this._stale = Math.max(this._stale, Stale.Boxes);
|
|
791
|
+
restyleOnly = null;
|
|
726
792
|
}
|
|
727
793
|
else if (this._cascade.mediaBand(target) !== this._mediaBand) {
|
|
728
794
|
// A resize that crossed a `@media` breakpoint is the one resize that
|
|
@@ -730,6 +796,7 @@ export class HtmlViewNode extends Node {
|
|
|
730
796
|
// breakpoints are collected at parse time.
|
|
731
797
|
this._restyle(target);
|
|
732
798
|
this._stale = Math.max(this._stale, Stale.Boxes);
|
|
799
|
+
restyleOnly = null;
|
|
733
800
|
}
|
|
734
801
|
const cascade = this._cascade;
|
|
735
802
|
if (!cascade)
|
|
@@ -740,21 +807,32 @@ export class HtmlViewNode extends Node {
|
|
|
740
807
|
// styles computed for another viewport are wrong for this one: built
|
|
741
808
|
// again, where some style reads the side that moved. A document that
|
|
742
809
|
// reads neither — most — goes on skipping the cascade on a resize.
|
|
743
|
-
if (
|
|
744
|
-
(
|
|
745
|
-
|
|
746
|
-
|
|
810
|
+
if ((cascade.readsViewportWidth && this._styledWidth !== target) ||
|
|
811
|
+
(cascade.readsViewportHeight && this._styledHeight !== viewport)) {
|
|
812
|
+
if (this._stale < Stale.Boxes)
|
|
813
|
+
this._stale = Stale.Boxes;
|
|
814
|
+
restyleOnly = null;
|
|
747
815
|
}
|
|
748
816
|
if (this._stale >= Stale.Boxes || !this._tree) {
|
|
749
817
|
const look = this._deviceLook();
|
|
818
|
+
// A build a pointer move asked for styles the elements the move
|
|
819
|
+
// reached, and takes every other element's from the tree it replaces
|
|
820
|
+
// (`Cascade.beginSharing`): the first build only, since a second is
|
|
821
|
+
// one the first found a reason for — a face, an image's size.
|
|
822
|
+
let kept = restyleOnly && this._tree && this._styledWith === cascade
|
|
823
|
+
? { styles: this._tree.styles, restyle: restyleOnly }
|
|
824
|
+
: null;
|
|
750
825
|
const build = () => buildBoxes(this._source.document, {
|
|
751
826
|
cascade,
|
|
827
|
+
kept,
|
|
752
828
|
scale: this._scale,
|
|
753
829
|
imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
|
|
754
830
|
urlSize: (url) => this._resources.imageSize(url),
|
|
755
831
|
controlSize: (el, kind, style) => measureControl(el, kind, style, this._fonts(), look),
|
|
756
832
|
});
|
|
757
833
|
this._tree = build();
|
|
834
|
+
kept = null;
|
|
835
|
+
this._styledWith = cascade;
|
|
758
836
|
this._styledWidth = target;
|
|
759
837
|
this._styledHeight = viewport;
|
|
760
838
|
this._requestBackgrounds(this._tree);
|
|
@@ -812,7 +890,7 @@ export class HtmlViewNode extends Node {
|
|
|
812
890
|
// The boxes are device pixels; each rect becomes the style of a widget
|
|
813
891
|
// mounted beside this element, and a style is logical.
|
|
814
892
|
const s = this._scale;
|
|
815
|
-
const rects = controlRectsOf(tree).map((r) => s === 1
|
|
893
|
+
const rects = controlRectsOf(tree, s).map((r) => s === 1
|
|
816
894
|
? r
|
|
817
895
|
: {
|
|
818
896
|
...r,
|
|
@@ -820,6 +898,15 @@ export class HtmlViewNode extends Node {
|
|
|
820
898
|
y: r.y / s,
|
|
821
899
|
width: r.width / s,
|
|
822
900
|
height: r.height / s,
|
|
901
|
+
fontSize: r.fontSize / s,
|
|
902
|
+
...(r.clip && {
|
|
903
|
+
clip: {
|
|
904
|
+
x: r.clip.x / s,
|
|
905
|
+
y: r.clip.y / s,
|
|
906
|
+
width: r.clip.width / s,
|
|
907
|
+
height: r.clip.height / s,
|
|
908
|
+
},
|
|
909
|
+
}),
|
|
823
910
|
...(r.bare && {
|
|
824
911
|
bare: {
|
|
825
912
|
...r.bare,
|
|
@@ -914,6 +1001,7 @@ export class HtmlViewNode extends Node {
|
|
|
914
1001
|
}
|
|
915
1002
|
}
|
|
916
1003
|
destroySubtree() {
|
|
1004
|
+
this._dropHeldHover();
|
|
917
1005
|
this._resources.destroy();
|
|
918
1006
|
this._webFonts.destroy();
|
|
919
1007
|
this._source.destroy();
|
|
@@ -1008,7 +1096,8 @@ export class HtmlViewNode extends Node {
|
|
|
1008
1096
|
if (to <= from)
|
|
1009
1097
|
return [];
|
|
1010
1098
|
const out = [];
|
|
1011
|
-
|
|
1099
|
+
// the rows the highlight paints, over glyphs taller than their line
|
|
1100
|
+
collectBands(tree.root, from, to, this.abs.x, this.abs.y, out, true);
|
|
1012
1101
|
return out;
|
|
1013
1102
|
}
|
|
1014
1103
|
// --- pointer --------------------------------------------------------------
|
|
@@ -1079,11 +1168,16 @@ export class HtmlViewNode extends Node {
|
|
|
1079
1168
|
const around = block.parent;
|
|
1080
1169
|
if (!around)
|
|
1081
1170
|
continue;
|
|
1171
|
+
// from where clearance moved it down from, as Blink's line around
|
|
1172
|
+
// a block in an inline box starts there: the Zen Garden's 214
|
|
1173
|
+
// makes its content an inline box of floats and a footer that
|
|
1174
|
+
// clears them, and Chrome measures it from the floats' top
|
|
1175
|
+
const top = Math.min(block.y, CLEARED_FROM.get(block) ?? block.y);
|
|
1082
1176
|
bands.push({
|
|
1083
1177
|
x: around.contentX,
|
|
1084
|
-
y:
|
|
1178
|
+
y: top,
|
|
1085
1179
|
width: around.contentWidth,
|
|
1086
|
-
height: block.height,
|
|
1180
|
+
height: block.y + block.height - top,
|
|
1087
1181
|
});
|
|
1088
1182
|
}
|
|
1089
1183
|
for (const band of bands)
|
|
@@ -1119,6 +1213,9 @@ export class HtmlViewNode extends Node {
|
|
|
1119
1213
|
rect = { x: caret.x, y: caret.y, width: 0, height: caret.height };
|
|
1120
1214
|
}
|
|
1121
1215
|
}
|
|
1216
|
+
// where it is drawn: the rectangle around it, turned and scaled as the
|
|
1217
|
+
// boxes it is in are, and itself (`getBoundingClientRect`, CSSOM View)
|
|
1218
|
+
rect = throughTransforms(rect, box);
|
|
1122
1219
|
const s = this._scale;
|
|
1123
1220
|
return {
|
|
1124
1221
|
x: rect.x / s,
|
|
@@ -1177,6 +1274,7 @@ export class HtmlViewNode extends Node {
|
|
|
1177
1274
|
* contains a `:hover` rule.
|
|
1178
1275
|
*/
|
|
1179
1276
|
setHover(x, y) {
|
|
1277
|
+
this._dropHeldHover();
|
|
1180
1278
|
const cascade = this._cascade;
|
|
1181
1279
|
if (!cascade || !cascade.hoverSensitive)
|
|
1182
1280
|
return false;
|
|
@@ -1195,6 +1293,7 @@ export class HtmlViewNode extends Node {
|
|
|
1195
1293
|
return true;
|
|
1196
1294
|
}
|
|
1197
1295
|
clearHover() {
|
|
1296
|
+
this._dropHeldHover();
|
|
1198
1297
|
if (!this._hovered.length)
|
|
1199
1298
|
return false;
|
|
1200
1299
|
const was = this._hovered;
|
|
@@ -1203,62 +1302,53 @@ export class HtmlViewNode extends Node {
|
|
|
1203
1302
|
this._restyleHover(was, []);
|
|
1204
1303
|
return true;
|
|
1205
1304
|
}
|
|
1206
|
-
/**
|
|
1305
|
+
/**
|
|
1306
|
+
* The hovered chain moved: nothing, where no rule that tests the pointer
|
|
1307
|
+
* names an element it moved over; a restyle where they are, where all it
|
|
1308
|
+
* changed is ink (`_hoverInPlace`); and otherwise the boxes built again,
|
|
1309
|
+
* with the styles of every element the move did not reach kept.
|
|
1310
|
+
*/
|
|
1207
1311
|
_restyleHover(was, now) {
|
|
1208
|
-
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
1312
|
+
const reach = this._hoverReach(was, now);
|
|
1313
|
+
if (reach !== null) {
|
|
1314
|
+
if (!reach.size)
|
|
1315
|
+
return;
|
|
1316
|
+
if (this._hoverInPlace(reach, was))
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
this._invalidate(Stale.Boxes, reach);
|
|
1211
1320
|
}
|
|
1212
1321
|
/**
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
* A move to another element is a change to the cascade, and the pipeline
|
|
1216
|
-
* answers one by building every box again and laying the document out:
|
|
1217
|
-
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1218
|
-
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1219
|
-
* nearly all change a colour, an underline, a background or a border's
|
|
1220
|
-
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1322
|
+
* The elements whose styles a move of the hovered chain can change, or
|
|
1323
|
+
* null where that cannot be said (`Cascade.hoverLocal`).
|
|
1221
1324
|
*
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
* anonymous or a pseudo-element box, a marker, a control — this is
|
|
1229
|
-
* not the move to take.
|
|
1230
|
-
* - The boxes take their new styles, and each layout of their text is
|
|
1231
|
-
* made again from the runs it was made from (`TextLayoutCache.inputsOf`)
|
|
1232
|
-
* with the new ink (`runFor`), where its geometry comes out the same.
|
|
1233
|
-
*
|
|
1234
|
-
* Everything is checked before anything is changed. 'none' where nothing
|
|
1235
|
-
* could change, 'painted' where it was restyled here, false where the
|
|
1236
|
-
* document has to be built again.
|
|
1325
|
+
* Only an element whose hover state flipped and that a compound testing
|
|
1326
|
+
* the pointer could match can change (`Cascade.hoverTouches`), with its
|
|
1327
|
+
* subtree, and its later siblings' where a sibling combinator is in play;
|
|
1328
|
+
* and the elements a `:has()` testing the pointer may flip, which are
|
|
1329
|
+
* around the ones that changed. A move between two paragraphs under
|
|
1330
|
+
* `a:hover` reaches nothing.
|
|
1237
1331
|
*/
|
|
1238
|
-
|
|
1332
|
+
_hoverReach(was, now) {
|
|
1239
1333
|
const cascade = this._cascade;
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
if (!cascade || !tree || !layouts || !cascade.hoverLocal)
|
|
1243
|
-
return false;
|
|
1244
|
-
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
|
|
1245
|
-
return false;
|
|
1334
|
+
if (!cascade || !cascade.hoverLocal)
|
|
1335
|
+
return null;
|
|
1246
1336
|
const before = new Set(was);
|
|
1247
1337
|
const after = new Set(now);
|
|
1248
|
-
const roots =
|
|
1249
|
-
// and the elements a `:has()` testing the pointer may flip, which
|
|
1250
|
-
// are around the ones that changed; the chains are ancestor chains,
|
|
1251
|
-
// so the deepest that changed on each side is where to look from
|
|
1338
|
+
const roots = new Map();
|
|
1252
1339
|
const flipped = (el, other) => {
|
|
1253
1340
|
if (other.has(el))
|
|
1254
1341
|
return;
|
|
1255
|
-
|
|
1256
|
-
|
|
1342
|
+
const touch = cascade.hoverTouches(el);
|
|
1343
|
+
if (touch > (roots.get(el) ?? HOVER_UNTOUCHED))
|
|
1344
|
+
roots.set(el, touch);
|
|
1257
1345
|
};
|
|
1258
1346
|
for (const el of was)
|
|
1259
1347
|
flipped(el, after);
|
|
1260
1348
|
for (const el of now)
|
|
1261
1349
|
flipped(el, before);
|
|
1350
|
+
// the chains are ancestor chains, so the deepest that changed on each
|
|
1351
|
+
// side is where a `:has()` is looked for from
|
|
1262
1352
|
const deepest = (chain, other) => {
|
|
1263
1353
|
const el = chain[0];
|
|
1264
1354
|
if (el && !other.has(el))
|
|
@@ -1266,11 +1356,9 @@ export class HtmlViewNode extends Node {
|
|
|
1266
1356
|
};
|
|
1267
1357
|
deepest(was, after);
|
|
1268
1358
|
deepest(now, before);
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
//
|
|
1272
|
-
// descendant combinator reaches, and their later siblings' too where a
|
|
1273
|
-
// sibling combinator follows a compound that tests the pointer
|
|
1359
|
+
// the roots' subtrees, which inherit from them and a descendant
|
|
1360
|
+
// combinator reaches, and their later siblings' too where a sibling
|
|
1361
|
+
// combinator follows the compound that tests the pointer
|
|
1274
1362
|
const reach = new Set();
|
|
1275
1363
|
const collect = (el) => {
|
|
1276
1364
|
const stack = [el];
|
|
@@ -1279,27 +1367,85 @@ export class HtmlViewNode extends Node {
|
|
|
1279
1367
|
if (reach.has(at))
|
|
1280
1368
|
continue;
|
|
1281
1369
|
reach.add(at);
|
|
1282
|
-
if (reach.size > HOVER_RESTYLE_LIMIT)
|
|
1283
|
-
return false;
|
|
1284
1370
|
for (const child of at.children)
|
|
1285
1371
|
if (isElement(child))
|
|
1286
1372
|
stack.push(child);
|
|
1287
1373
|
}
|
|
1288
|
-
return true;
|
|
1289
1374
|
};
|
|
1290
|
-
for (const root of roots) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
if (!cascade.hoverSiblings)
|
|
1375
|
+
for (const [root, touch] of roots) {
|
|
1376
|
+
collect(root);
|
|
1377
|
+
if (touch !== HOVER_FOLLOWED)
|
|
1294
1378
|
continue;
|
|
1295
1379
|
for (let s = root.nextSibling; s; s = s.nextSibling) {
|
|
1296
|
-
if (isElement(s)
|
|
1297
|
-
|
|
1380
|
+
if (isElement(s))
|
|
1381
|
+
collect(s);
|
|
1298
1382
|
}
|
|
1299
1383
|
}
|
|
1384
|
+
return reach;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* A pointer move restyled where it happened, when all it changed is ink.
|
|
1388
|
+
*
|
|
1389
|
+
* A move to another element is a change to the cascade, and the pipeline
|
|
1390
|
+
* answers one by building every box again and laying the document out:
|
|
1391
|
+
* 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
|
|
1392
|
+
* link the pointer crossed. But the rules a page writes for `:hover`
|
|
1393
|
+
* nearly all change a colour, an underline, a background or a border's
|
|
1394
|
+
* colour — 77 of that article's 79 — and those move nothing. So:
|
|
1395
|
+
*
|
|
1396
|
+
* - The elements the move reached (`_hoverReach`) are styled again from
|
|
1397
|
+
* their parents — the ones a rule testing the pointer answers
|
|
1398
|
+
* differently for, or whose parent's style changed; the rest, most of
|
|
1399
|
+
* a hovered card or table row, have the styles they had
|
|
1400
|
+
* (`Cascade.pointerChanged`). Where anything but ink differs
|
|
1401
|
+
* (`PAINT_ONLY`), this is not the move to take.
|
|
1402
|
+
* - The boxes take their new styles: an element's own, the text in it,
|
|
1403
|
+
* the anonymous boxes the fix-up made around what is in it, and its
|
|
1404
|
+
* `::before` and `::after`, styled again by their own rules. A box
|
|
1405
|
+
* whose style is derived some other way — a marker, a first letter, a
|
|
1406
|
+
* control — is not one to restyle here.
|
|
1407
|
+
* - Each layout of their text is made again from the runs it was made
|
|
1408
|
+
* from (`TextLayoutCache.inputsOf`) with the new ink (`runFor`), where
|
|
1409
|
+
* its geometry comes out the same.
|
|
1410
|
+
*
|
|
1411
|
+
* Everything is checked before anything is changed, and nothing is looked
|
|
1412
|
+
* at but the blocks the restyled elements are in (`_blocksOf`): a move
|
|
1413
|
+
* costs what it changed, in a document of any length. True where it was
|
|
1414
|
+
* restyled here, or had nothing to restyle; false where the boxes have to
|
|
1415
|
+
* be built again.
|
|
1416
|
+
*/
|
|
1417
|
+
_hoverInPlace(reach, was) {
|
|
1418
|
+
const cascade = this._cascade;
|
|
1419
|
+
const tree = this._tree;
|
|
1420
|
+
const layouts = this._layouts;
|
|
1421
|
+
if (!cascade || !tree || !layouts)
|
|
1422
|
+
return false;
|
|
1423
|
+
if (this._stale !== Stale.Nothing || this._laidOutWidth < 0)
|
|
1424
|
+
return false;
|
|
1425
|
+
if (reach.size > HOVER_RESTYLE_LIMIT)
|
|
1426
|
+
return false;
|
|
1427
|
+
// a first line's, a first letter's and a marker's styles are layout's
|
|
1428
|
+
const pseudoRules = cascade.hoverPseudo;
|
|
1429
|
+
if (pseudoRules === HOVER_PSEUDO_OTHER)
|
|
1430
|
+
return false;
|
|
1431
|
+
// whether the rules that test the pointer answer differently for an
|
|
1432
|
+
// element than they did before the move
|
|
1433
|
+
const before = { hovered: new Set(was), active: EMPTY_SET };
|
|
1434
|
+
const flips = new Map();
|
|
1435
|
+
const flipped = (el) => {
|
|
1436
|
+
let flip = flips.get(el);
|
|
1437
|
+
if (flip === undefined) {
|
|
1438
|
+
flip = cascade.pointerChanged(el, before);
|
|
1439
|
+
flips.set(el, flip);
|
|
1440
|
+
}
|
|
1441
|
+
return flip;
|
|
1442
|
+
};
|
|
1300
1443
|
// styled again from their parents, a parent first
|
|
1301
1444
|
const fresh = new Map();
|
|
1302
1445
|
const changed = new Map();
|
|
1446
|
+
// the ones whose ink is what it was: only their custom properties
|
|
1447
|
+
// changed, which the elements under them read
|
|
1448
|
+
const quiet = new Set();
|
|
1303
1449
|
let refused = false;
|
|
1304
1450
|
// what changed beyond ink (`hoverChange`): how far some box's ink
|
|
1305
1451
|
// reaches, the order its layer paints in, where one is
|
|
@@ -1323,20 +1469,27 @@ export class HtmlViewNode extends Node {
|
|
|
1323
1469
|
refused = true;
|
|
1324
1470
|
return null;
|
|
1325
1471
|
}
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
if (diff
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1472
|
+
const parentWas = parent ? tree.styles.get(parent).style : parentStyle;
|
|
1473
|
+
let style = kept.style;
|
|
1474
|
+
if (parentStyle !== parentWas || flipped(el)) {
|
|
1475
|
+
const made = cascade.styleFor(el, parentStyle, kept.inFlex);
|
|
1476
|
+
const diff = hoverChange(kept.style, made);
|
|
1477
|
+
if (diff === false)
|
|
1478
|
+
refused = true;
|
|
1479
|
+
else if (diff) {
|
|
1480
|
+
style = made;
|
|
1481
|
+
changed.set(el, made);
|
|
1482
|
+
if (!diff.ink)
|
|
1483
|
+
quiet.add(el);
|
|
1484
|
+
if (diff.reach)
|
|
1485
|
+
reaches = true;
|
|
1486
|
+
if (diff.order)
|
|
1487
|
+
reorders = true;
|
|
1488
|
+
if (diff.move)
|
|
1489
|
+
moving.add(el);
|
|
1490
|
+
}
|
|
1339
1491
|
}
|
|
1492
|
+
fresh.set(el, style);
|
|
1340
1493
|
return style;
|
|
1341
1494
|
};
|
|
1342
1495
|
for (const el of reach) {
|
|
@@ -1344,42 +1497,122 @@ export class HtmlViewNode extends Node {
|
|
|
1344
1497
|
if (refused)
|
|
1345
1498
|
return false;
|
|
1346
1499
|
}
|
|
1347
|
-
if (!changed.size)
|
|
1348
|
-
return
|
|
1500
|
+
if (!changed.size && pseudoRules === HOVER_PSEUDO_NONE)
|
|
1501
|
+
return true;
|
|
1349
1502
|
for (const el of changed.keys()) {
|
|
1350
1503
|
const tag = tagOf(el);
|
|
1351
1504
|
// their backgrounds are the canvas's
|
|
1352
1505
|
if (tag === 'html' || tag === 'body')
|
|
1353
1506
|
return false;
|
|
1354
1507
|
}
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1508
|
+
// The boxes, each under its parent: what a box takes depends on what
|
|
1509
|
+
// the box it is in took.
|
|
1357
1510
|
const fonts = layouts.fonts;
|
|
1511
|
+
const blocks = this._blocksOf(tree, pseudoRules === HOVER_PSEUDO_NONE ? changed.keys() : reach);
|
|
1358
1512
|
const restyled = [];
|
|
1513
|
+
const next = new Map();
|
|
1514
|
+
/** The restyled boxes whose ink changed, to repaint. */
|
|
1515
|
+
const inked = new Set();
|
|
1516
|
+
/** The `::before` and `::after` boxes found, by element: 1 and 2. */
|
|
1517
|
+
const generated = new Map();
|
|
1359
1518
|
const redecorated = [];
|
|
1360
|
-
const walk =
|
|
1519
|
+
const walk = blocks.slice();
|
|
1361
1520
|
while (walk.length) {
|
|
1362
1521
|
const box = walk.pop();
|
|
1363
1522
|
for (const child of box.children)
|
|
1364
1523
|
walk.push(child);
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1524
|
+
const parent = box.parent;
|
|
1525
|
+
const above = parent ? next.get(parent) : undefined;
|
|
1526
|
+
let style;
|
|
1527
|
+
let ink = !!parent && inked.has(parent);
|
|
1528
|
+
if (box.kind === 'text' || box.kind === 'break') {
|
|
1529
|
+
// Text is set in the style of the box it is in — an element's, a
|
|
1530
|
+
// pseudo-element's — or of its element, where the fix-up put an
|
|
1531
|
+
// anonymous box around it.
|
|
1532
|
+
const el = box.el;
|
|
1533
|
+
if (parent && box.style === parent.style)
|
|
1534
|
+
style = above;
|
|
1535
|
+
else if (el && box.style === tree.styles.get(el)?.style) {
|
|
1536
|
+
style = changed.get(el);
|
|
1537
|
+
ink = !quiet.has(el);
|
|
1538
|
+
}
|
|
1539
|
+
else if (above || (el && changed.has(el)))
|
|
1540
|
+
return false;
|
|
1541
|
+
}
|
|
1542
|
+
else if (box.pseudo === 'before' || box.pseudo === 'after') {
|
|
1543
|
+
const from = GENERATED_FROM.get(box);
|
|
1544
|
+
if (!from) {
|
|
1545
|
+
// an inside marker, made of its item's style and its own rules
|
|
1546
|
+
if (above)
|
|
1547
|
+
return false;
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
if (!reach.has(from))
|
|
1551
|
+
continue;
|
|
1552
|
+
generated.set(from, (generated.get(from) ?? 0) | (box.pseudo === 'before' ? 1 : 2));
|
|
1553
|
+
const inherits = fresh.get(from);
|
|
1554
|
+
const kept = tree.styles.get(from)?.style;
|
|
1555
|
+
if (!inherits || !kept)
|
|
1556
|
+
return false;
|
|
1557
|
+
if (inherits === kept && !flipped(from))
|
|
1558
|
+
continue;
|
|
1559
|
+
// styled by its own rules over its element's style, as the builder
|
|
1560
|
+
// styled it, in its element's box
|
|
1561
|
+
if (!parent || parent.style !== kept || box.marker)
|
|
1562
|
+
return false;
|
|
1563
|
+
const made = cascade.pseudoStyleFor(from, box.pseudo, inherits);
|
|
1564
|
+
if (!made || made.display === 'none')
|
|
1565
|
+
return false;
|
|
1566
|
+
const diff = hoverChange(box.style, made);
|
|
1567
|
+
if (diff === false)
|
|
1568
|
+
return false;
|
|
1569
|
+
if (!diff)
|
|
1570
|
+
continue;
|
|
1571
|
+
if (diff.reach || diff.order || diff.move)
|
|
1572
|
+
return false;
|
|
1573
|
+
style = made;
|
|
1574
|
+
ink = diff.ink;
|
|
1575
|
+
}
|
|
1576
|
+
else if (box.pseudo) {
|
|
1577
|
+
// a first letter, styled from the box its letter is in
|
|
1578
|
+
if (above)
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
else if (box.el) {
|
|
1582
|
+
const el = box.el;
|
|
1583
|
+
style = changed.get(el);
|
|
1584
|
+
if (!style)
|
|
1585
|
+
continue;
|
|
1586
|
+
const kept = tree.styles.get(el).style;
|
|
1587
|
+
// a style derived from the element's, or drawn from it somewhere
|
|
1588
|
+
// else — a widget mounted beside the document takes its look from
|
|
1589
|
+
// it. An image, a drawing or a rule is drawn here, from its box,
|
|
1590
|
+
// like any.
|
|
1591
|
+
if (box.style !== kept)
|
|
1592
|
+
return false;
|
|
1593
|
+
if (box.marker || WIDGETS.has(box.replaced))
|
|
1594
|
+
return false;
|
|
1595
|
+
if (moving.has(el) && !movable(box, kept, style))
|
|
1596
|
+
return false;
|
|
1597
|
+
ink = !quiet.has(el);
|
|
1598
|
+
}
|
|
1599
|
+
else if (above) {
|
|
1600
|
+
// An anonymous box takes what its parent's style passes on
|
|
1601
|
+
// (`anonymousStyles`), so it takes that of the new one: the text of
|
|
1602
|
+
// a link made a flex row is in one, and takes the link's colour.
|
|
1603
|
+
if (GENERATED_FROM.has(box))
|
|
1604
|
+
return false;
|
|
1605
|
+
const display = box.style.display;
|
|
1606
|
+
if (tree.anonymous(parent.style, display) !== box.style)
|
|
1607
|
+
return false;
|
|
1608
|
+
style = tree.anonymous(above, display);
|
|
1609
|
+
}
|
|
1369
1610
|
if (!style)
|
|
1370
1611
|
continue;
|
|
1371
|
-
|
|
1372
|
-
// an anonymous box, a pseudo-element, a marker, a control: a style
|
|
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.
|
|
1376
|
-
if (!box.el || box.style !== kept.style)
|
|
1377
|
-
return false;
|
|
1378
|
-
if (box.marker || WIDGETS.has(box.replaced))
|
|
1379
|
-
return false;
|
|
1380
|
-
if (moving.has(owner) && !movable(box, kept.style, style))
|
|
1381
|
-
return false;
|
|
1612
|
+
next.set(box, style);
|
|
1382
1613
|
restyled.push([box, style]);
|
|
1614
|
+
if (ink)
|
|
1615
|
+
inked.add(box);
|
|
1383
1616
|
if (box.kind === 'inline') {
|
|
1384
1617
|
const decoration = inlineDecoration(fonts, box, style);
|
|
1385
1618
|
if ((decoration === null) !== (box.decoration === null)) {
|
|
@@ -1391,11 +1624,34 @@ export class HtmlViewNode extends Node {
|
|
|
1391
1624
|
redecorated.push([box, decoration]);
|
|
1392
1625
|
}
|
|
1393
1626
|
}
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1627
|
+
// a `::before` or an `::after` a rule that tests the pointer now gives
|
|
1628
|
+
// an element is a box to build
|
|
1629
|
+
if (pseudoRules !== HOVER_PSEUDO_NONE) {
|
|
1630
|
+
for (const el of reach) {
|
|
1631
|
+
if (!flipped(el))
|
|
1632
|
+
continue;
|
|
1633
|
+
const style = fresh.get(el);
|
|
1634
|
+
if (!style)
|
|
1635
|
+
continue;
|
|
1636
|
+
const has = generated.get(el) ?? 0;
|
|
1637
|
+
for (const [which, bit] of GENERATED_BITS) {
|
|
1638
|
+
if (has & bit)
|
|
1639
|
+
continue;
|
|
1640
|
+
const made = cascade.pseudoStyleFor(el, which, style);
|
|
1641
|
+
if (made && made.display !== 'none')
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (!restyled.length)
|
|
1647
|
+
return true;
|
|
1648
|
+
// the text: each layout holding a run of a restyled box's, made again
|
|
1649
|
+
// with its new ink, where it comes out the same shape
|
|
1396
1650
|
const relaid = new Map();
|
|
1397
1651
|
const texts = [];
|
|
1398
|
-
|
|
1652
|
+
if (inked.size)
|
|
1653
|
+
for (const block of blocks)
|
|
1654
|
+
walk.push(block);
|
|
1399
1655
|
while (walk.length) {
|
|
1400
1656
|
const box = walk.pop();
|
|
1401
1657
|
for (const child of box.children)
|
|
@@ -1407,16 +1663,16 @@ export class HtmlViewNode extends Node {
|
|
|
1407
1663
|
texts.push(text);
|
|
1408
1664
|
if (relaid.has(text.layout))
|
|
1409
1665
|
continue;
|
|
1410
|
-
const
|
|
1411
|
-
if (
|
|
1666
|
+
const again = reinked(text, layouts, next);
|
|
1667
|
+
if (again === false)
|
|
1412
1668
|
return false;
|
|
1413
|
-
relaid.set(text.layout,
|
|
1669
|
+
relaid.set(text.layout, again);
|
|
1414
1670
|
}
|
|
1415
1671
|
}
|
|
1416
1672
|
}
|
|
1417
1673
|
// what each box drew before, to be repainted with what it draws after
|
|
1418
1674
|
const inks = [];
|
|
1419
|
-
for (const
|
|
1675
|
+
for (const box of inked) {
|
|
1420
1676
|
const ink = inkOf(box);
|
|
1421
1677
|
if (ink)
|
|
1422
1678
|
inks.push(ink);
|
|
@@ -1426,8 +1682,9 @@ export class HtmlViewNode extends Node {
|
|
|
1426
1682
|
for (const [box, style] of restyled) {
|
|
1427
1683
|
const was = box.style;
|
|
1428
1684
|
box.style = style;
|
|
1429
|
-
if (moving.has(box.el))
|
|
1685
|
+
if (box.el && moving.has(box.el) && box.kind !== 'text') {
|
|
1430
1686
|
moved.push([box, was]);
|
|
1687
|
+
}
|
|
1431
1688
|
}
|
|
1432
1689
|
for (const [box, decoration] of redecorated) {
|
|
1433
1690
|
// what is inside it keeps which boxes around it paint, and this is
|
|
@@ -1438,9 +1695,9 @@ export class HtmlViewNode extends Node {
|
|
|
1438
1695
|
box.decoration = decoration;
|
|
1439
1696
|
}
|
|
1440
1697
|
for (const text of texts) {
|
|
1441
|
-
const
|
|
1442
|
-
if (
|
|
1443
|
-
text.layout =
|
|
1698
|
+
const again = relaid.get(text.layout);
|
|
1699
|
+
if (again)
|
|
1700
|
+
text.layout = again;
|
|
1444
1701
|
}
|
|
1445
1702
|
for (const [el, style] of changed) {
|
|
1446
1703
|
tree.styles.set(el, { style, inFlex: tree.styles.get(el).inFlex });
|
|
@@ -1474,15 +1731,61 @@ export class HtmlViewNode extends Node {
|
|
|
1474
1731
|
this._reportControls();
|
|
1475
1732
|
if (relayout) {
|
|
1476
1733
|
this._invalidate(Stale.Layout);
|
|
1477
|
-
return
|
|
1734
|
+
return true;
|
|
1478
1735
|
}
|
|
1479
|
-
for (const
|
|
1736
|
+
for (const box of inked) {
|
|
1480
1737
|
const ink = inkOf(box);
|
|
1481
1738
|
if (ink)
|
|
1482
1739
|
inks.push(ink);
|
|
1483
1740
|
}
|
|
1484
1741
|
this._repaintInk(inks);
|
|
1485
|
-
return
|
|
1742
|
+
return true;
|
|
1743
|
+
}
|
|
1744
|
+
/** An element's first box in document order, for the tree it was asked
|
|
1745
|
+
* of: made on the first move over a tree, which is one walk of it. */
|
|
1746
|
+
_firstBoxes = null;
|
|
1747
|
+
/**
|
|
1748
|
+
* The blocks a restyle of `changed` has to look in: for each element, the
|
|
1749
|
+
* box its text is set in the lines of — the nearest box around its first
|
|
1750
|
+
* that is not an inline one — which holds every box that takes its style
|
|
1751
|
+
* from it, the pieces a block in it broke it into among them
|
|
1752
|
+
* (`breakAround`). One that is inside another is left to it.
|
|
1753
|
+
*/
|
|
1754
|
+
_blocksOf(tree, changed) {
|
|
1755
|
+
let index = this._firstBoxes;
|
|
1756
|
+
if (index?.tree !== tree) {
|
|
1757
|
+
const of = new Map();
|
|
1758
|
+
const stack = [tree.root];
|
|
1759
|
+
while (stack.length) {
|
|
1760
|
+
const box = stack.pop();
|
|
1761
|
+
if (box.el && !of.has(box.el))
|
|
1762
|
+
of.set(box.el, box);
|
|
1763
|
+
for (let i = box.children.length - 1; i >= 0; i -= 1) {
|
|
1764
|
+
stack.push(box.children[i]);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
index = this._firstBoxes = { tree, of };
|
|
1768
|
+
}
|
|
1769
|
+
const blocks = new Set();
|
|
1770
|
+
for (const el of changed) {
|
|
1771
|
+
let box = index.of.get(el) ?? null;
|
|
1772
|
+
while (box?.parent &&
|
|
1773
|
+
(box.kind === 'inline' || box.kind === 'text' || box.kind === 'break')) {
|
|
1774
|
+
box = box.parent;
|
|
1775
|
+
}
|
|
1776
|
+
if (box)
|
|
1777
|
+
blocks.add(box);
|
|
1778
|
+
}
|
|
1779
|
+
const out = [];
|
|
1780
|
+
for (const block of blocks) {
|
|
1781
|
+
let inside = false;
|
|
1782
|
+
for (let at = block.parent; at && !inside; at = at.parent) {
|
|
1783
|
+
inside = blocks.has(at);
|
|
1784
|
+
}
|
|
1785
|
+
if (!inside)
|
|
1786
|
+
out.push(block);
|
|
1787
|
+
}
|
|
1788
|
+
return out;
|
|
1486
1789
|
}
|
|
1487
1790
|
/**
|
|
1488
1791
|
* Repaint the document where `inks` are, in its own coordinates: the ink
|
|
@@ -1542,17 +1845,64 @@ export class HtmlViewNode extends Node {
|
|
|
1542
1845
|
// `:hover` is wired at the element rather than through React: the cascade
|
|
1543
1846
|
// already knows whether any rule in the document tests it, `setHover`
|
|
1544
1847
|
// returns without work when none does, and a document that does use it
|
|
1545
|
-
// re-styles only when the hovered chain actually changed
|
|
1546
|
-
//
|
|
1547
|
-
//
|
|
1848
|
+
// re-styles only when the hovered chain actually changed, and then only
|
|
1849
|
+
// the elements the change reached (`_restyleHover`).
|
|
1850
|
+
//
|
|
1851
|
+
// **A hover waits for a scroll to stop.** Core asks again what is under a
|
|
1852
|
+
// pointer that stayed where it was after every frame that laid out —
|
|
1853
|
+
// hover follows content, react-x11#793 — and says so with a move to the
|
|
1854
|
+
// point the pointer is already at. While a document scrolls that is every
|
|
1855
|
+
// frame, each with something else under the pointer: a restyle a frame
|
|
1856
|
+
// at best, and where a hover moves something, the boxes built again a
|
|
1857
|
+
// frame, which took a page scrolled under a parked pointer from 52 frames
|
|
1858
|
+
// a second to 4. A browser holds its hover until the scroll is over, for
|
|
1859
|
+
// the same reason; so does this, for `HOVER_REST_MS` after the content
|
|
1860
|
+
// last moved. What was hovered stays hovered as it scrolls away, as it
|
|
1861
|
+
// does there. A move of the pointer itself is answered at once.
|
|
1548
1862
|
defaultMouseMove(ev) {
|
|
1549
1863
|
super.defaultMouseMove?.(ev);
|
|
1864
|
+
const at = this._pointerAt;
|
|
1865
|
+
if (at !== null && at.x === ev.x && at.y === ev.y) {
|
|
1866
|
+
this._holdHover();
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
this._pointerAt = { x: ev.x, y: ev.y };
|
|
1550
1870
|
this.setHover(ev.x, ev.y);
|
|
1551
1871
|
}
|
|
1552
1872
|
defaultMouseLeave(ev) {
|
|
1553
1873
|
super.defaultMouseLeave?.(ev);
|
|
1874
|
+
this._pointerAt = null;
|
|
1554
1875
|
this.clearHover();
|
|
1555
1876
|
}
|
|
1877
|
+
/** The content moved under the pointer: the hover is asked for once it
|
|
1878
|
+
* has been still for `HOVER_REST_MS`. One timer a wait, put off by
|
|
1879
|
+
* being armed again when it finds the content moved since. */
|
|
1880
|
+
_holdHover() {
|
|
1881
|
+
if (!this._cascade?.hoverSensitive)
|
|
1882
|
+
return;
|
|
1883
|
+
this._heldSince = hoverClock.now();
|
|
1884
|
+
if (this._heldHover !== null)
|
|
1885
|
+
return;
|
|
1886
|
+
const rest = () => {
|
|
1887
|
+
this._heldHover = null;
|
|
1888
|
+
const at = this._pointerAt;
|
|
1889
|
+
if (!at || this.destroyed)
|
|
1890
|
+
return;
|
|
1891
|
+
const left = this._heldSince + HOVER_REST_MS - hoverClock.now();
|
|
1892
|
+
if (left > 0) {
|
|
1893
|
+
this._heldHover = hoverClock.arm(rest, left);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
this.setHover(at.x, at.y);
|
|
1897
|
+
};
|
|
1898
|
+
this._heldHover = hoverClock.arm(rest, HOVER_REST_MS);
|
|
1899
|
+
}
|
|
1900
|
+
_dropHeldHover() {
|
|
1901
|
+
if (this._heldHover === null)
|
|
1902
|
+
return;
|
|
1903
|
+
hoverClock.disarm(this._heldHover);
|
|
1904
|
+
this._heldHover = null;
|
|
1905
|
+
}
|
|
1556
1906
|
// --- paint ----------------------------------------------------------------
|
|
1557
1907
|
paint(ctx) {
|
|
1558
1908
|
super.paint(ctx); // background, border, clip to `abs`
|
|
@@ -1793,13 +2143,21 @@ function sameRects(a, b) {
|
|
|
1793
2143
|
p.y !== q.y ||
|
|
1794
2144
|
p.width !== q.width ||
|
|
1795
2145
|
p.height !== q.height ||
|
|
2146
|
+
p.fontFamily !== q.fontFamily ||
|
|
2147
|
+
p.fontSize !== q.fontSize ||
|
|
1796
2148
|
p.opacity !== q.opacity ||
|
|
2149
|
+
!sameClip(p.clip, q.clip) ||
|
|
1797
2150
|
!sameBare(p.bare, q.bare)) {
|
|
1798
2151
|
return false;
|
|
1799
2152
|
}
|
|
1800
2153
|
}
|
|
1801
2154
|
return true;
|
|
1802
2155
|
}
|
|
2156
|
+
function sameClip(a, b) {
|
|
2157
|
+
if (!a || !b)
|
|
2158
|
+
return a === b;
|
|
2159
|
+
return (a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height);
|
|
2160
|
+
}
|
|
1803
2161
|
function sameBare(a, b) {
|
|
1804
2162
|
if (!a || !b)
|
|
1805
2163
|
return a === b;
|
|
@@ -1846,7 +2204,13 @@ function nearestText(box, x, y) {
|
|
|
1846
2204
|
if (distance >= bestDistance)
|
|
1847
2205
|
continue;
|
|
1848
2206
|
bestDistance = distance;
|
|
1849
|
-
|
|
2207
|
+
// in the row that is this line's, where the layout's other lines are
|
|
2208
|
+
// in another column and a point past this one is not over them
|
|
2209
|
+
let row = y - text.drawY;
|
|
2210
|
+
if (columned.any && COLUMN_ROWS.has(text)) {
|
|
2211
|
+
row = Math.max(natural.y, Math.min(row, natural.y + natural.height - 0.01));
|
|
2212
|
+
}
|
|
2213
|
+
const local = text.layout.indexAt(x - text.drawX, row);
|
|
1850
2214
|
const offsets = layoutOffsetsOf(text.layout);
|
|
1851
2215
|
const units = offsets.length
|
|
1852
2216
|
? offsets[Math.max(0, Math.min(local, offsets.length - 1))]
|
|
@@ -1859,13 +2223,38 @@ function nearestText(box, x, y) {
|
|
|
1859
2223
|
axisDistance(x, node.boundsX, node.boundsWidth);
|
|
1860
2224
|
if (boundsDistance >= bestDistance)
|
|
1861
2225
|
return;
|
|
2226
|
+
// in a box painted through a matrix, the point taken back through it
|
|
2227
|
+
// (`deepestAt`): the text is where it was laid out
|
|
2228
|
+
const matrix = placedMatrix(node);
|
|
2229
|
+
if (!matrix) {
|
|
2230
|
+
visitIn(node);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const back = invert(matrix);
|
|
2234
|
+
if (!back)
|
|
2235
|
+
return;
|
|
2236
|
+
const px = x;
|
|
2237
|
+
const py = y;
|
|
2238
|
+
[x, y] = mapPoint(back, px, py);
|
|
2239
|
+
try {
|
|
2240
|
+
visitIn(node);
|
|
2241
|
+
}
|
|
2242
|
+
finally {
|
|
2243
|
+
x = px;
|
|
2244
|
+
y = py;
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
const visitIn = (node) => {
|
|
1862
2248
|
const lines = node.lines;
|
|
1863
2249
|
if (lines?.length) {
|
|
1864
2250
|
// The line nearest in y, by binary search; then outward both ways
|
|
1865
2251
|
// while vertical distance alone could still beat the best.
|
|
1866
2252
|
let lo = 0;
|
|
1867
2253
|
let hi = lines.length;
|
|
1868
|
-
|
|
2254
|
+
// the lines columns took apart are in no order down the page: each
|
|
2255
|
+
// is tried
|
|
2256
|
+
const apart = columned.any && COLUMN_LINES.has(lines);
|
|
2257
|
+
while (!apart && lo < hi) {
|
|
1869
2258
|
const mid = (lo + hi) >> 1;
|
|
1870
2259
|
if (lines[mid].y > y)
|
|
1871
2260
|
hi = mid;
|
|
@@ -1878,8 +2267,11 @@ function nearestText(box, x, y) {
|
|
|
1878
2267
|
tryLine(lines[i]);
|
|
1879
2268
|
}
|
|
1880
2269
|
for (let i = lo; i < lines.length; i += 1) {
|
|
1881
|
-
if (axisDistance(y, lines[i].y, lines[i].height) * 4 >= bestDistance)
|
|
2270
|
+
if (axisDistance(y, lines[i].y, lines[i].height) * 4 >= bestDistance) {
|
|
2271
|
+
if (apart)
|
|
2272
|
+
continue;
|
|
1882
2273
|
break;
|
|
2274
|
+
}
|
|
1883
2275
|
tryLine(lines[i]);
|
|
1884
2276
|
}
|
|
1885
2277
|
}
|
|
@@ -1967,7 +2359,13 @@ function caretAt(box, units) {
|
|
|
1967
2359
|
const offsets = layoutOffsetsOf(text.layout);
|
|
1968
2360
|
const layoutUnits = layoutOffsetOf(text, units);
|
|
1969
2361
|
const caret = text.layout.caretPosition(codePointAtOffset(offsets, layoutUnits));
|
|
1970
|
-
|
|
2362
|
+
const at = throughTransforms({
|
|
2363
|
+
x: text.drawX + caret.x,
|
|
2364
|
+
y: line.y,
|
|
2365
|
+
width: 0,
|
|
2366
|
+
height: line.height,
|
|
2367
|
+
}, node);
|
|
2368
|
+
found = { x: at.x, y: at.y, height: at.height };
|
|
1971
2369
|
return;
|
|
1972
2370
|
}
|
|
1973
2371
|
}
|
|
@@ -1983,6 +2381,21 @@ function caretAt(box, units) {
|
|
|
1983
2381
|
visit(box);
|
|
1984
2382
|
return found;
|
|
1985
2383
|
}
|
|
2384
|
+
/**
|
|
2385
|
+
* A rectangle in a box's own coordinates — the ones it and what it holds
|
|
2386
|
+
* were laid out in — as the rectangle around where it is drawn: through
|
|
2387
|
+
* the matrix of each box from `from` up that is painted through one
|
|
2388
|
+
* (`placedMatrix`). The rectangle itself, in a document that transforms
|
|
2389
|
+
* nothing but by moving it.
|
|
2390
|
+
*/
|
|
2391
|
+
function throughTransforms(rect, from) {
|
|
2392
|
+
for (let at = from; at; at = at.parent) {
|
|
2393
|
+
const matrix = placedMatrix(at);
|
|
2394
|
+
if (matrix)
|
|
2395
|
+
rect = mapRect(matrix, rect.x, rect.y, rect.width, rect.height);
|
|
2396
|
+
}
|
|
2397
|
+
return rect;
|
|
2398
|
+
}
|
|
1986
2399
|
/** Every band a document range covers, in window coordinates. The subtree
|
|
1987
2400
|
* text ranges prune the walk to the boxes the range actually crosses, so a
|
|
1988
2401
|
* drag's small range costs its own paragraphs; only Ctrl+A pays for all. */
|
|
@@ -2078,7 +2491,10 @@ function fragmentHeights(box, bands, fonts) {
|
|
|
2078
2491
|
function lineBands(lines, from, to, dx, dy, out,
|
|
2079
2492
|
/** Where the lines put the text, not where a relative offset moved it
|
|
2080
2493
|
* (`TEXT_SHIFTS`): what an inline box's own offset is added to. */
|
|
2081
|
-
laidOut = false
|
|
2494
|
+
laidOut = false,
|
|
2495
|
+
/** The rows a highlight covers (`selectionRows`) rather than the line's:
|
|
2496
|
+
* past it where the glyphs are taller than the line. */
|
|
2497
|
+
ink = false) {
|
|
2082
2498
|
// First line whose text can reach `from`, by binary search over the
|
|
2083
2499
|
// sorted text starts; stop at the first line past `to`.
|
|
2084
2500
|
let lo = 0;
|
|
@@ -2111,6 +2527,9 @@ laidOut = false) {
|
|
|
2111
2527
|
// a link that ends a line a space wider than Chrome's. So the text
|
|
2112
2528
|
// a line ends with is measured no further than its content.
|
|
2113
2529
|
const ends = endsLine(line, text, natural);
|
|
2530
|
+
const rows = ink
|
|
2531
|
+
? selectionRows(line, text, natural)
|
|
2532
|
+
: { y: line.y, height: line.height };
|
|
2114
2533
|
for (const band of bandsFor(text.layout, natural, offsets, layoutOffsetOf(text, a), layoutOffsetOf(text, b, true))) {
|
|
2115
2534
|
let left = band.x;
|
|
2116
2535
|
let right = band.x + band.width;
|
|
@@ -2120,9 +2539,22 @@ laidOut = false) {
|
|
|
2120
2539
|
}
|
|
2121
2540
|
out.push({
|
|
2122
2541
|
x: dx + left + text.drawX - moved,
|
|
2123
|
-
y: dy +
|
|
2542
|
+
y: dy + rows.y,
|
|
2124
2543
|
width: right - left,
|
|
2125
|
-
height:
|
|
2544
|
+
height: rows.height,
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
// The spaces a piece of a line ends on, where the line goes on
|
|
2548
|
+
// after them: the engine stripped them from the piece it laid out
|
|
2549
|
+
// as a line, and they are the text's all the same, in the elements
|
|
2550
|
+
// that hold its end — a link whose text ends in a space, before
|
|
2551
|
+
// the next link, is that space wider than its letters
|
|
2552
|
+
if (text.trail && b >= text.textEnd && !rtlLine(natural)) {
|
|
2553
|
+
out.push({
|
|
2554
|
+
x: dx + natural.x + natural.width + text.drawX - moved,
|
|
2555
|
+
y: dy + rows.y,
|
|
2556
|
+
width: text.trail,
|
|
2557
|
+
height: rows.height,
|
|
2126
2558
|
});
|
|
2127
2559
|
}
|
|
2128
2560
|
// the spaces `pre-wrap` keeps that the line ends on, which have no
|
|
@@ -2133,15 +2565,20 @@ laidOut = false) {
|
|
|
2133
2565
|
continue;
|
|
2134
2566
|
out.push({
|
|
2135
2567
|
x: dx + space.x + text.drawX - moved,
|
|
2136
|
-
y: dy +
|
|
2568
|
+
y: dy + rows.y,
|
|
2137
2569
|
width: space.width,
|
|
2138
|
-
height:
|
|
2570
|
+
height: rows.height,
|
|
2139
2571
|
});
|
|
2140
2572
|
}
|
|
2141
2573
|
}
|
|
2142
2574
|
}
|
|
2143
2575
|
}
|
|
2144
2576
|
}
|
|
2577
|
+
/** Whether a laid-out line's last run reads right to left, its end at its
|
|
2578
|
+
* left. */
|
|
2579
|
+
function rtlLine(natural) {
|
|
2580
|
+
return natural.runs[natural.runs.length - 1]?.run?.direction === 'rtl';
|
|
2581
|
+
}
|
|
2145
2582
|
/** Whether a line's text is where the line ends: its last, with nothing
|
|
2146
2583
|
* placed after it — an inline-block or an image past its content. */
|
|
2147
2584
|
function endsLine(line, text, natural) {
|
|
@@ -2154,18 +2591,28 @@ function endsLine(line, text, natural) {
|
|
|
2154
2591
|
}
|
|
2155
2592
|
return true;
|
|
2156
2593
|
}
|
|
2157
|
-
function collectBands(box, from, to, dx, dy, out) {
|
|
2594
|
+
function collectBands(box, from, to, dx, dy, out, ink = false) {
|
|
2158
2595
|
if (box.subtreeTextEnd <= box.subtreeTextStart)
|
|
2159
2596
|
return;
|
|
2160
2597
|
if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to)
|
|
2161
2598
|
return;
|
|
2599
|
+
const first = out.length;
|
|
2162
2600
|
if (box.lines)
|
|
2163
|
-
lineBands(box.lines, from, to, dx, dy, out);
|
|
2601
|
+
lineBands(box.lines, from, to, dx, dy, out, false, ink);
|
|
2164
2602
|
// Atomics are ordinary children, reached below.
|
|
2165
2603
|
for (const child of box.children) {
|
|
2166
2604
|
if (child.kind === 'text' || child.kind === 'break')
|
|
2167
2605
|
continue;
|
|
2168
|
-
collectBands(child, from, to, dx, dy, out);
|
|
2606
|
+
collectBands(child, from, to, dx, dy, out, ink);
|
|
2607
|
+
}
|
|
2608
|
+
// where a box painted through a matrix draws them
|
|
2609
|
+
const matrix = out.length > first ? placedMatrix(box) : null;
|
|
2610
|
+
if (!matrix)
|
|
2611
|
+
return;
|
|
2612
|
+
for (let i = first; i < out.length; i += 1) {
|
|
2613
|
+
const band = out[i];
|
|
2614
|
+
const to = mapRect(matrix, band.x - dx, band.y - dy, band.width, band.height);
|
|
2615
|
+
out[i] = { ...band, ...to, x: to.x + dx, y: to.y + dy };
|
|
2169
2616
|
}
|
|
2170
2617
|
}
|
|
2171
2618
|
/** The layers of a hit test, in the order CSS paints them within a
|
|
@@ -2200,7 +2647,17 @@ fixedShift = null) {
|
|
|
2200
2647
|
// are painted in `z-index` order and then the document's (`byZIndex`),
|
|
2201
2648
|
// the negative ones under the flow: the Zen Garden's `›` has
|
|
2202
2649
|
// `z-index: 3` over the bar the "View All Designs" link fills after it.
|
|
2203
|
-
|
|
2650
|
+
// Nor is a box the pointer passes through under it: one that is not
|
|
2651
|
+
// visible, which is no target for the pointer in any browser (CSS 2.1
|
|
2652
|
+
// 11.2 has it invisible, and a hit test finds what is seen), or one with
|
|
2653
|
+
// `pointer-events: none` (CSS UI 4, 5.2). Both are inherited, and what is
|
|
2654
|
+
// in such a box may say otherwise, so the walk goes on through it. A
|
|
2655
|
+
// site's closed menu is laid out over its page, hidden: GitHub's covered
|
|
2656
|
+
// its repository's tabs, and the pointer over a tab was over the menu.
|
|
2657
|
+
const take = (el, style, key, text) => {
|
|
2658
|
+
if (style.visibility !== 'visible' || style.pointerEvents === 'none') {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2204
2661
|
if (compareKeys(key, foundKey) < 0)
|
|
2205
2662
|
return;
|
|
2206
2663
|
found = el;
|
|
@@ -2242,14 +2699,79 @@ fixedShift = null) {
|
|
|
2242
2699
|
atViewport = false;
|
|
2243
2700
|
}
|
|
2244
2701
|
};
|
|
2702
|
+
// A box painted through a matrix (`placedMatrix`) is under the point
|
|
2703
|
+
// where the matrix puts it: inside the rectangle around what it draws
|
|
2704
|
+
// there, the point is taken back through the matrix, into the
|
|
2705
|
+
// coordinates the box and all it holds were laid out in, and the walk
|
|
2706
|
+
// goes on with that. An icon turned a quarter is pressed where it is
|
|
2707
|
+
// drawn; a box flattened to nothing is nowhere.
|
|
2245
2708
|
const enterAt = (child, context, clipped) => {
|
|
2709
|
+
const matrix = placedMatrix(child);
|
|
2710
|
+
if (!matrix) {
|
|
2711
|
+
enterIn(child, context, clipped, null);
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
if (x < child.boundsX ||
|
|
2715
|
+
x >= child.boundsX + child.boundsWidth ||
|
|
2716
|
+
y < child.boundsY ||
|
|
2717
|
+
y >= child.boundsY + child.boundsHeight) {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
const back = invert(matrix);
|
|
2721
|
+
if (!back)
|
|
2722
|
+
return;
|
|
2723
|
+
const px = x;
|
|
2724
|
+
const py = y;
|
|
2725
|
+
[x, y] = mapPoint(back, px, py);
|
|
2726
|
+
try {
|
|
2727
|
+
enterIn(child, context, clipped, ownBounds(child));
|
|
2728
|
+
}
|
|
2729
|
+
finally {
|
|
2730
|
+
x = px;
|
|
2731
|
+
y = py;
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
const enterIn = (child, context, clipped,
|
|
2735
|
+
/** What the box draws in its own coordinates, where those are not the
|
|
2736
|
+
* ones its bounds are in: a box painted through a matrix. */
|
|
2737
|
+
bounds) => {
|
|
2246
2738
|
const style = child.style;
|
|
2739
|
+
// What a `clip-path` cuts away of a box it cuts away of all the box
|
|
2740
|
+
// holds, whatever that is positioned from (CSS Masking 1, 5.1), and
|
|
2741
|
+
// nothing not drawn is under a pointer: a label a page hides for a
|
|
2742
|
+
// screen reader alone under `clip-path: inset(50%)` took the hover and
|
|
2743
|
+
// the press of what was drawn where it lay.
|
|
2744
|
+
if (pathClips(child) && !inClipPath(child, x, y))
|
|
2745
|
+
return;
|
|
2746
|
+
// `clip` is not that rule as paint has it. It shows the part of an
|
|
2747
|
+
// absolute box it names (CSS 2.1 11.1.2), and past that part nothing is
|
|
2748
|
+
// drawn of the box, nor of what is painted with it. A stacking context
|
|
2749
|
+
// paints all it holds, inside its clip. A box that is none leaves its
|
|
2750
|
+
// positioned boxes to the context around it, which cuts each to the
|
|
2751
|
+
// clips its containing block is under (`clipsFor`): this one for all
|
|
2752
|
+
// but a fixed box, which is under none and is drawn past it. So past
|
|
2753
|
+
// its clip the first is left, and the second is walked as a box that
|
|
2754
|
+
// clips its overflow is past its edge — whether or not the point is in
|
|
2755
|
+
// its own rectangle, which a clip may show only a part of. Both were
|
|
2756
|
+
// walked as if they showed whole: a label hidden for a screen reader
|
|
2757
|
+
// alone under `clip: rect(0, 0, 0, 0)` took the hover and the press of
|
|
2758
|
+
// the link drawn where it lay.
|
|
2759
|
+
let cut = !inClip(child, x, y);
|
|
2760
|
+
if (cut && stacksLayers(child))
|
|
2761
|
+
return;
|
|
2247
2762
|
if (style.position !== 'static' ||
|
|
2248
2763
|
(child.parent?.kind === 'flex' && typeof style.zIndex === 'number')) {
|
|
2249
2764
|
// a flex item with a `z-index` is layered unpositioned (`layered`)
|
|
2250
2765
|
const z = style.zIndex === 'auto' ? 0 : style.zIndex;
|
|
2251
2766
|
context = [...context, z < 0 ? HIT_NEGATIVE : HIT_POSITIONED, z];
|
|
2252
2767
|
}
|
|
2768
|
+
else if (transformed(style) &&
|
|
2769
|
+
child.kind !== 'inline' &&
|
|
2770
|
+
child.parent &&
|
|
2771
|
+
!onLine(child.parent, child)) {
|
|
2772
|
+
// a transformed box is painted with the positioned ones (`layered`)
|
|
2773
|
+
context = [...context, HIT_POSITIONED, 0];
|
|
2774
|
+
}
|
|
2253
2775
|
else if (style.float !== 'none')
|
|
2254
2776
|
context = [...context, HIT_FLOAT];
|
|
2255
2777
|
if (clipped.length !== 0 && child.outOfFlow) {
|
|
@@ -2258,33 +2780,45 @@ fixedShift = null) {
|
|
|
2258
2780
|
? clipped.filter((clip) => holds(clip, containing))
|
|
2259
2781
|
: [];
|
|
2260
2782
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2783
|
+
// in one of its pieces, where columns broke it: its rect takes in the
|
|
2784
|
+
// columns between them, which are other boxes'
|
|
2785
|
+
const pieces = columned.any ? COLUMN_PIECES.get(child) : undefined;
|
|
2786
|
+
const inside = pieces
|
|
2787
|
+
? pieces.some((piece) => x >= piece.x &&
|
|
2788
|
+
x < piece.x + piece.width &&
|
|
2789
|
+
y >= piece.y &&
|
|
2790
|
+
y < piece.y + piece.height)
|
|
2791
|
+
: x >= child.x &&
|
|
2792
|
+
x < child.x + child.width &&
|
|
2793
|
+
y >= child.y &&
|
|
2794
|
+
y < child.y + child.height;
|
|
2265
2795
|
const own = inside && hasRect(child);
|
|
2266
2796
|
if (!own) {
|
|
2267
|
-
const
|
|
2268
|
-
const
|
|
2269
|
-
|
|
2797
|
+
const left = bounds ? bounds.x : child.boundsX;
|
|
2798
|
+
const reach = bounds ? bounds.y : child.boundsY;
|
|
2799
|
+
const across = bounds ? bounds.width : child.boundsWidth;
|
|
2800
|
+
const down = bounds ? bounds.height : child.boundsHeight;
|
|
2801
|
+
const known = Number.isFinite(reach) && (across > 0 || down > 0);
|
|
2270
2802
|
if (!known) {
|
|
2271
2803
|
if (!inside)
|
|
2272
2804
|
return;
|
|
2273
2805
|
}
|
|
2274
|
-
else if (x <
|
|
2275
|
-
x >=
|
|
2806
|
+
else if (x < left ||
|
|
2807
|
+
x >= left + across ||
|
|
2276
2808
|
y < reach ||
|
|
2277
|
-
y >= reach +
|
|
2809
|
+
y >= reach + down) {
|
|
2278
2810
|
return;
|
|
2279
2811
|
}
|
|
2280
|
-
if (clipsOverflow(child))
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2812
|
+
if (clipsOverflow(child))
|
|
2813
|
+
cut = true;
|
|
2814
|
+
}
|
|
2815
|
+
if (cut) {
|
|
2816
|
+
if (!holdsAbsolute(child))
|
|
2817
|
+
return;
|
|
2818
|
+
clipped = [...clipped, child];
|
|
2285
2819
|
}
|
|
2286
2820
|
if (own && child.el && clipped.length === 0) {
|
|
2287
|
-
take(child.el, [...context, HIT_BLOCK], false);
|
|
2821
|
+
take(child.el, style, [...context, HIT_BLOCK], false);
|
|
2288
2822
|
}
|
|
2289
2823
|
visit(child, context, clipped);
|
|
2290
2824
|
};
|
|
@@ -2326,8 +2860,9 @@ fixedShift = null) {
|
|
|
2326
2860
|
const left = text.drawX + natural.x + run.x;
|
|
2327
2861
|
if (x >= left && x < left + run.width) {
|
|
2328
2862
|
const owner = ownerOf(tree.textBoxes, text.spans.documentAt(run.start));
|
|
2329
|
-
|
|
2330
|
-
|
|
2863
|
+
const style = owner && tree.styles.get(owner)?.style;
|
|
2864
|
+
if (style)
|
|
2865
|
+
take(owner, style, [...context, HIT_INLINE], true);
|
|
2331
2866
|
}
|
|
2332
2867
|
}
|
|
2333
2868
|
}
|
|
@@ -2354,6 +2889,32 @@ function compareKeys(a, b) {
|
|
|
2354
2889
|
* is built again instead: a link's subtree is a handful, and a compound
|
|
2355
2890
|
* that matches a container reaches everything in it. */
|
|
2356
2891
|
const HOVER_RESTYLE_LIMIT = 300;
|
|
2892
|
+
/** How long the content under a still pointer has to have stopped moving
|
|
2893
|
+
* before the hover is asked for again (`HtmlViewNode._holdHover`): a
|
|
2894
|
+
* tenth of a second, which is what WebKit waits after a scroll before it
|
|
2895
|
+
* sends the mouse move that updates its own. */
|
|
2896
|
+
const HOVER_REST_MS = 100;
|
|
2897
|
+
const timers = globalThis;
|
|
2898
|
+
/**
|
|
2899
|
+
* The clock a held hover waits on. Through `globalThis` because `src/`
|
|
2900
|
+
* compiles with `types: []`, and unref'd where the runtime allows it: a
|
|
2901
|
+
* timer of a document's must not keep a process alive that is otherwise
|
|
2902
|
+
* done. One object, exported from this module though not from the package,
|
|
2903
|
+
* so a test can hold it and say when the rest is over (`test/held-clock.ts`).
|
|
2904
|
+
*/
|
|
2905
|
+
export const hoverClock = {
|
|
2906
|
+
now() {
|
|
2907
|
+
return Date.now();
|
|
2908
|
+
},
|
|
2909
|
+
arm(step, ms) {
|
|
2910
|
+
const handle = timers.setTimeout?.(step, ms) ?? null;
|
|
2911
|
+
handle?.unref?.();
|
|
2912
|
+
return handle;
|
|
2913
|
+
},
|
|
2914
|
+
disarm(handle) {
|
|
2915
|
+
timers.clearTimeout?.(handle);
|
|
2916
|
+
},
|
|
2917
|
+
};
|
|
2357
2918
|
/**
|
|
2358
2919
|
* The computed properties a pointer move may change in place: ink, which
|
|
2359
2920
|
* moves nothing. A background colour is drawn inside the box it colours
|
|
@@ -2365,6 +2926,9 @@ const PAINT_ONLY = new Set([
|
|
|
2365
2926
|
'color',
|
|
2366
2927
|
'textFillColor',
|
|
2367
2928
|
'cursor',
|
|
2929
|
+
'pointerEvents',
|
|
2930
|
+
'fill',
|
|
2931
|
+
'stroke',
|
|
2368
2932
|
'backgroundColor',
|
|
2369
2933
|
'borderTopColor',
|
|
2370
2934
|
'borderRightColor',
|
|
@@ -2381,6 +2945,8 @@ const PAINT_ONLY = new Set([
|
|
|
2381
2945
|
'underlineThickness',
|
|
2382
2946
|
'underlineOffset',
|
|
2383
2947
|
'lineThrough',
|
|
2948
|
+
'lineThroughStyle',
|
|
2949
|
+
'lineThroughThickness',
|
|
2384
2950
|
]);
|
|
2385
2951
|
/** The fields of a run its ink is: what `runFor` takes from `PAINT_ONLY`. */
|
|
2386
2952
|
const INK_FIELDS = [
|
|
@@ -2390,6 +2956,8 @@ const INK_FIELDS = [
|
|
|
2390
2956
|
'underlineOffset',
|
|
2391
2957
|
'underlineThickness',
|
|
2392
2958
|
'strike',
|
|
2959
|
+
'strikeStyle',
|
|
2960
|
+
'strikeThickness',
|
|
2393
2961
|
];
|
|
2394
2962
|
/** The fields of a run its shape is, which ink never changes. */
|
|
2395
2963
|
const FACE_FIELDS = [
|
|
@@ -2400,6 +2968,12 @@ const FACE_FIELDS = [
|
|
|
2400
2968
|
'letterSpacing',
|
|
2401
2969
|
'features',
|
|
2402
2970
|
];
|
|
2971
|
+
/** An element's `::before` and `::after`, each with its bit in the set
|
|
2972
|
+
* of the ones a restyle found boxes of (`_hoverInPlace`). */
|
|
2973
|
+
const GENERATED_BITS = [
|
|
2974
|
+
['before', 1],
|
|
2975
|
+
['after', 2],
|
|
2976
|
+
];
|
|
2403
2977
|
/** The replaced boxes that are controls (`BoxTree.controls`, `controls.ts`):
|
|
2404
2978
|
* a widget mounted beside the document, or a button whose press it reports. */
|
|
2405
2979
|
const WIDGETS = new Set([
|
|
@@ -2418,10 +2992,19 @@ const INK_REACH = new Set([
|
|
|
2418
2992
|
'outlineWidth',
|
|
2419
2993
|
'outlineOffset',
|
|
2420
2994
|
]);
|
|
2995
|
+
/** What transforms a box: it moves, turns or scales where it is, and
|
|
2996
|
+
* nothing around it does (CSS Transforms 1, 3). */
|
|
2997
|
+
const TRANSFORM_FIELDS = new Set([
|
|
2998
|
+
'translate',
|
|
2999
|
+
'rotate',
|
|
3000
|
+
'scale',
|
|
3001
|
+
'transform',
|
|
3002
|
+
'transformOrigin',
|
|
3003
|
+
]);
|
|
2421
3004
|
/**
|
|
2422
3005
|
* How two styles of one element differ, for a restyle in place: null where
|
|
2423
3006
|
* 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
|
|
3007
|
+
* reaches, a `z-index` that stays a stacking context's, or a transform —
|
|
2425
3008
|
* none of which moves anything but the box and what it holds (CSS
|
|
2426
3009
|
* Transforms 1: a transform does not affect layout) — and false where
|
|
2427
3010
|
* anything else differs.
|
|
@@ -2431,9 +3014,18 @@ function hoverChange(was, now) {
|
|
|
2431
3014
|
const b = now;
|
|
2432
3015
|
let change = null;
|
|
2433
3016
|
for (const key in b) {
|
|
2434
|
-
|
|
3017
|
+
// the sets are shared by what made them (`Cascade._customFor`): one
|
|
3018
|
+
// object where they are the same
|
|
3019
|
+
if (key === 'custom') {
|
|
3020
|
+
if (a[key] !== b[key]) {
|
|
3021
|
+
change ??= { ink: false, reach: false, order: false, move: false };
|
|
3022
|
+
}
|
|
2435
3023
|
continue;
|
|
2436
|
-
|
|
3024
|
+
}
|
|
3025
|
+
if (sameValue(a[key], b[key]))
|
|
3026
|
+
continue;
|
|
3027
|
+
change ??= { ink: true, reach: false, order: false, move: false };
|
|
3028
|
+
change.ink = true;
|
|
2437
3029
|
if (PAINT_ONLY.has(key))
|
|
2438
3030
|
continue;
|
|
2439
3031
|
if (INK_REACH.has(key)) {
|
|
@@ -2452,7 +3044,7 @@ function hoverChange(was, now) {
|
|
|
2452
3044
|
change.order = true;
|
|
2453
3045
|
continue;
|
|
2454
3046
|
}
|
|
2455
|
-
if (key
|
|
3047
|
+
if (TRANSFORM_FIELDS.has(key)) {
|
|
2456
3048
|
change.move = true;
|
|
2457
3049
|
continue;
|
|
2458
3050
|
}
|
|
@@ -2500,12 +3092,14 @@ function inkOf(box) {
|
|
|
2500
3092
|
return null;
|
|
2501
3093
|
if (!(at.boundsWidth > 0 && at.boundsHeight > 0))
|
|
2502
3094
|
return null;
|
|
2503
|
-
|
|
3095
|
+
// its bounds are in the coordinates of the box it is in, which a box
|
|
3096
|
+
// around it painted through a matrix draws somewhere else
|
|
3097
|
+
return throughTransforms({
|
|
2504
3098
|
x: at.boundsX,
|
|
2505
3099
|
y: at.boundsY,
|
|
2506
3100
|
width: at.boundsWidth,
|
|
2507
3101
|
height: at.boundsHeight,
|
|
2508
|
-
};
|
|
3102
|
+
}, at.parent);
|
|
2509
3103
|
}
|
|
2510
3104
|
/** Structural equality for a computed value: a number, a string, or the
|
|
2511
3105
|
* plain objects and arrays a length or a shadow list is. */
|
|
@@ -2529,32 +3123,23 @@ function sameValue(x, y) {
|
|
|
2529
3123
|
return false;
|
|
2530
3124
|
return true;
|
|
2531
3125
|
}
|
|
2532
|
-
/** The element an anonymous box takes its style from: its nearest
|
|
2533
|
-
* ancestor's that has one. */
|
|
2534
|
-
function nearestElement(box) {
|
|
2535
|
-
for (let at = box.parent; at; at = at.parent)
|
|
2536
|
-
if (at.el)
|
|
2537
|
-
return at.el;
|
|
2538
|
-
return null;
|
|
2539
|
-
}
|
|
2540
3126
|
/**
|
|
2541
|
-
* `text`'s layout made again with the ink of the
|
|
2542
|
-
* restyled: from the runs it was made from, each of a
|
|
3127
|
+
* `text`'s layout made again with the ink of the boxes a pointer move
|
|
3128
|
+
* restyled: from the runs it was made from, each of a restyled text box's
|
|
2543
3129
|
* given the ink its new style makes (`runFor`) and nothing else. Null where
|
|
2544
3130
|
* none of its runs is theirs; false where that cannot be told — a run this
|
|
2545
3131
|
* cannot place in the document, one some other pass inked (a first line's),
|
|
2546
3132
|
* or a layout that comes out another shape.
|
|
2547
3133
|
*/
|
|
2548
|
-
function reinked(text, layouts,
|
|
3134
|
+
function reinked(text, layouts, next) {
|
|
2549
3135
|
const spans = text.spans;
|
|
2550
|
-
const ownerAt = (offset) => spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
|
|
2551
3136
|
const inputs = layouts.inputsOf(text.layout);
|
|
2552
3137
|
if (!inputs || !spans.boxAt) {
|
|
2553
3138
|
// not one this could make again: none of its text may be theirs
|
|
2554
3139
|
for (const line of text.layout.lines) {
|
|
2555
3140
|
for (const run of line.runs) {
|
|
2556
|
-
const
|
|
2557
|
-
if (!spans.boxAt || (
|
|
3141
|
+
const box = spans.boxAt?.(run.start);
|
|
3142
|
+
if (!spans.boxAt || (box && next.has(box)))
|
|
2558
3143
|
return false;
|
|
2559
3144
|
}
|
|
2560
3145
|
}
|
|
@@ -2566,32 +3151,32 @@ function reinked(text, layouts, changed, styles) {
|
|
|
2566
3151
|
for (let i = 0; i < content.length; i += 1) {
|
|
2567
3152
|
const run = content[i];
|
|
2568
3153
|
const length = run.text.length;
|
|
2569
|
-
const
|
|
2570
|
-
const style =
|
|
2571
|
-
if (
|
|
2572
|
-
if (
|
|
3154
|
+
const box = length ? spans.boxAt(offset) : null;
|
|
3155
|
+
const style = box ? next.get(box) : undefined;
|
|
3156
|
+
if (box && style) {
|
|
3157
|
+
if (spans.boxAt(offset + length - 1) !== box)
|
|
2573
3158
|
return false;
|
|
2574
|
-
const was = runFor(run.text,
|
|
3159
|
+
const was = runFor(run.text, box.style);
|
|
2575
3160
|
const now = runFor(run.text, style);
|
|
2576
3161
|
for (const f of FACE_FIELDS)
|
|
2577
3162
|
if (!sameValue(was[f], now[f]))
|
|
2578
3163
|
return false;
|
|
2579
|
-
let
|
|
3164
|
+
let inked = null;
|
|
2580
3165
|
for (const f of INK_FIELDS) {
|
|
2581
3166
|
// a run that is not what its style made — a first line's colour
|
|
2582
3167
|
if (!sameValue(run[f], was[f]))
|
|
2583
3168
|
return false;
|
|
2584
3169
|
if (sameValue(was[f], now[f]))
|
|
2585
3170
|
continue;
|
|
2586
|
-
|
|
3171
|
+
inked ??= { ...run };
|
|
2587
3172
|
if (now[f] === undefined)
|
|
2588
|
-
delete
|
|
3173
|
+
delete inked[f];
|
|
2589
3174
|
else
|
|
2590
|
-
|
|
3175
|
+
inked[f] = now[f];
|
|
2591
3176
|
}
|
|
2592
|
-
if (
|
|
3177
|
+
if (inked) {
|
|
2593
3178
|
runs ??= content.slice();
|
|
2594
|
-
runs[i] =
|
|
3179
|
+
runs[i] = inked;
|
|
2595
3180
|
}
|
|
2596
3181
|
}
|
|
2597
3182
|
offset += length;
|