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