gesso-core 0.6.29 → 0.7.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/CHANGELOG.md +32 -0
- package/dist/{CanvasSurface-BTZ3A3MN.js → CanvasSurface-BpgbOku2.js} +1799 -588
- package/dist/CanvasSurface-BpgbOku2.js.map +1 -0
- package/dist/{index-v2fhm7Pn.d.ts → index-B5r1ETCh.d.ts} +1881 -1166
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1520 -36
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +46 -3
- package/dist/testing.js +99 -10
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/CanvasSurface-BTZ3A3MN.js.map +0 -1
|
@@ -1501,7 +1501,9 @@ const lightColors = {
|
|
|
1501
1501
|
danger: rgb8(220, 38, 38),
|
|
1502
1502
|
focusRing: rgb8(17, 24, 39),
|
|
1503
1503
|
selectionBackground: rgb8(219, 234, 254),
|
|
1504
|
-
selectionForeground: rgb8(30, 58, 138)
|
|
1504
|
+
selectionForeground: rgb8(30, 58, 138),
|
|
1505
|
+
tooltipBackground: rgb8(17, 24, 39),
|
|
1506
|
+
tooltipForeground: UiBasicColors.white
|
|
1505
1507
|
};
|
|
1506
1508
|
const darkColors = {
|
|
1507
1509
|
background: rgb8(17, 19, 24),
|
|
@@ -1524,7 +1526,9 @@ const darkColors = {
|
|
|
1524
1526
|
danger: rgb8(248, 113, 113),
|
|
1525
1527
|
focusRing: rgb8(243, 244, 246),
|
|
1526
1528
|
selectionBackground: rgb8(30, 58, 95),
|
|
1527
|
-
selectionForeground: rgb8(219, 234, 254)
|
|
1529
|
+
selectionForeground: rgb8(219, 234, 254),
|
|
1530
|
+
tooltipBackground: rgb8(55, 61, 72),
|
|
1531
|
+
tooltipForeground: rgb8(243, 244, 246)
|
|
1528
1532
|
};
|
|
1529
1533
|
/**
|
|
1530
1534
|
* Compares two color palettes for equality.
|
|
@@ -1911,6 +1915,24 @@ function isShapeName(value, scale) {
|
|
|
1911
1915
|
//#endregion
|
|
1912
1916
|
//#region src/properties/UiBoxShadow.ts
|
|
1913
1917
|
/**
|
|
1918
|
+
* The shadows a `boxShadows` value stands for: itself, or the named
|
|
1919
|
+
* size from `scale`, the node's theme's shadows. A name the scale lacks,
|
|
1920
|
+
* or no scale, is no shadow.
|
|
1921
|
+
*/
|
|
1922
|
+
function shadowsOf(value, scale) {
|
|
1923
|
+
if (Array.isArray(value)) return value;
|
|
1924
|
+
if (typeof value === "string" && scale !== void 0 && Object.prototype.hasOwnProperty.call(scale, value)) return scale[value];
|
|
1925
|
+
return NO_SHADOWS$1;
|
|
1926
|
+
}
|
|
1927
|
+
const NO_SHADOWS$1 = [];
|
|
1928
|
+
/**
|
|
1929
|
+
* Compares two `boxShadows` values: two names by name, two lists entry by entry.
|
|
1930
|
+
*/
|
|
1931
|
+
function boxShadowValuesEqual(a, b) {
|
|
1932
|
+
if (typeof a === "string" || typeof b === "string") return a === b;
|
|
1933
|
+
return boxShadowArraysEqual(a, b);
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1914
1936
|
* Creates a box shadow with the supplied parameters.
|
|
1915
1937
|
*/
|
|
1916
1938
|
function boxShadow(offsetX, offsetY, blurRadius, spreadRadius, color, inset = false) {
|
|
@@ -2147,7 +2169,8 @@ const FOREGROUNDS = {
|
|
|
2147
2169
|
controlForegroundDisabled: "controlBackground",
|
|
2148
2170
|
controlBorder: "controlBackground",
|
|
2149
2171
|
controlAccent: "controlBackground",
|
|
2150
|
-
selectionForeground: "selectionBackground"
|
|
2172
|
+
selectionForeground: "selectionBackground",
|
|
2173
|
+
tooltipForeground: "tooltipBackground"
|
|
2151
2174
|
};
|
|
2152
2175
|
/**
|
|
2153
2176
|
* A palette at high contrast.
|
|
@@ -2181,7 +2204,8 @@ const SURFACES = /* @__PURE__ */ new Set([
|
|
|
2181
2204
|
"controlBackground",
|
|
2182
2205
|
"controlBackgroundHovered",
|
|
2183
2206
|
"controlBackgroundPressed",
|
|
2184
|
-
"selectionBackground"
|
|
2207
|
+
"selectionBackground",
|
|
2208
|
+
"tooltipBackground"
|
|
2185
2209
|
]);
|
|
2186
2210
|
//#endregion
|
|
2187
2211
|
//#region src/environment/UiThemeExtension.ts
|
|
@@ -2195,6 +2219,7 @@ function defineThemeExtension(options) {
|
|
|
2195
2219
|
key: Symbol(options.name),
|
|
2196
2220
|
name: options.name,
|
|
2197
2221
|
defaults: options.defaults,
|
|
2222
|
+
...options.coarse === void 0 ? {} : { coarse: options.coarse },
|
|
2198
2223
|
equals: options.equals ?? shallowEqual
|
|
2199
2224
|
};
|
|
2200
2225
|
comparisons.set(extension.key, extension.equals);
|
|
@@ -2232,7 +2257,8 @@ function isPlainObject(value) {
|
|
|
2232
2257
|
/** What the theme carries for this extension, or its declared defaults. */
|
|
2233
2258
|
function themeExtension(theme, extension) {
|
|
2234
2259
|
const carried = theme.extensions?.get(extension.key);
|
|
2235
|
-
|
|
2260
|
+
if (carried !== void 0) return carried;
|
|
2261
|
+
return theme.pointer === "coarse" && extension.coarse !== void 0 ? extension.coarse : extension.defaults;
|
|
2236
2262
|
}
|
|
2237
2263
|
/** Whether the theme carries this extension at all, rather than falling back. */
|
|
2238
2264
|
function hasThemeExtension(theme, extension) {
|
|
@@ -2280,6 +2306,49 @@ function themeExtensionsEqual(a, b) {
|
|
|
2280
2306
|
return true;
|
|
2281
2307
|
}
|
|
2282
2308
|
//#endregion
|
|
2309
|
+
//#region src/environment/UiUnits.ts
|
|
2310
|
+
const defaultUnits = {
|
|
2311
|
+
pixelsPerInch: 96,
|
|
2312
|
+
rootFontSize: 16
|
|
2313
|
+
};
|
|
2314
|
+
/**
|
|
2315
|
+
* The conversions for a set of units, usually a theme's.
|
|
2316
|
+
*
|
|
2317
|
+
* const { rem, pt } = lengths(theme.units);
|
|
2318
|
+
* <box padding={rem(1)}><text fontSize={pt(12)} /></box>
|
|
2319
|
+
*
|
|
2320
|
+
* A theme writes its own scales the same way, from the units it is
|
|
2321
|
+
* about to carry, so the scales follow them:
|
|
2322
|
+
*
|
|
2323
|
+
* const units = { pixelsPerInch: 96, rootFontSize: 16 };
|
|
2324
|
+
* const { rem } = lengths(units);
|
|
2325
|
+
* export const theme = { ...lightTheme, units, spacing: { ...lightTheme.spacing, large: rem(1) } };
|
|
2326
|
+
*
|
|
2327
|
+
* Throws on units that cannot measure anything, a zero or negative or
|
|
2328
|
+
* non-finite number, since every length written from them would be.
|
|
2329
|
+
*/
|
|
2330
|
+
function lengths(units = defaultUnits) {
|
|
2331
|
+
check("pixelsPerInch", units.pixelsPerInch);
|
|
2332
|
+
check("rootFontSize", units.rootFontSize);
|
|
2333
|
+
const inch = units.pixelsPerInch;
|
|
2334
|
+
return {
|
|
2335
|
+
units,
|
|
2336
|
+
rem: (value) => value * units.rootFontSize,
|
|
2337
|
+
inch: (value) => value * inch,
|
|
2338
|
+
pt: (value) => value * inch / 72,
|
|
2339
|
+
mm: (value) => value * inch / 25.4
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Compares two sets of units for equality.
|
|
2344
|
+
*/
|
|
2345
|
+
function unitsEqual(a, b) {
|
|
2346
|
+
return a.pixelsPerInch === b.pixelsPerInch && a.rootFontSize === b.rootFontSize;
|
|
2347
|
+
}
|
|
2348
|
+
function check(name, value) {
|
|
2349
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`UiUnits.${name} has to be a positive number of pixels; it was ${value}.`);
|
|
2350
|
+
}
|
|
2351
|
+
//#endregion
|
|
2283
2352
|
//#region src/environment/UiTheme.ts
|
|
2284
2353
|
const lightTheme = {
|
|
2285
2354
|
colors: lightColors,
|
|
@@ -2287,6 +2356,7 @@ const lightTheme = {
|
|
|
2287
2356
|
spacing: defaultSpacing,
|
|
2288
2357
|
density: "comfortable",
|
|
2289
2358
|
contrast: "standard",
|
|
2359
|
+
units: defaultUnits,
|
|
2290
2360
|
shapes: {
|
|
2291
2361
|
none: 0,
|
|
2292
2362
|
extraSmall: 2,
|
|
@@ -2434,7 +2504,7 @@ const darkTheme = {
|
|
|
2434
2504
|
* not repaint, and it is easy to forget.
|
|
2435
2505
|
*/
|
|
2436
2506
|
function themesEqual(a, b) {
|
|
2437
|
-
return a.density === b.density && a.contrast === b.contrast && colorsEqualPalette(a.colors, b.colors) && typographyEqual(a.typography, b.typography) && shapesEqual(a.shapes, b.shapes) && shadowsEqual(a.shadows, b.shadows) && spacingEqual(a.spacing, b.spacing) && themeExtensionsEqual(a.extensions, b.extensions);
|
|
2507
|
+
return a.density === b.density && a.contrast === b.contrast && (a.pointer ?? "fine") === (b.pointer ?? "fine") && a.touch === b.touch && unitsEqual(a.units, b.units) && colorsEqualPalette(a.colors, b.colors) && typographyEqual(a.typography, b.typography) && shapesEqual(a.shapes, b.shapes) && shadowsEqual(a.shadows, b.shadows) && spacingEqual(a.spacing, b.spacing) && themeExtensionsEqual(a.extensions, b.extensions);
|
|
2438
2508
|
}
|
|
2439
2509
|
/**
|
|
2440
2510
|
* The same theme at another density.
|
|
@@ -2538,13 +2608,26 @@ function bandOf(width, breakpoints) {
|
|
|
2538
2608
|
return band;
|
|
2539
2609
|
}
|
|
2540
2610
|
/**
|
|
2541
|
-
* The width bands of a container, one value per band entered
|
|
2611
|
+
* The width bands of a container, one value per band entered, and once
|
|
2612
|
+
* more at the first size measured.
|
|
2542
2613
|
*
|
|
2543
2614
|
* What `Responsive` builds its children from, and what an author who
|
|
2544
2615
|
* wants only a number rather than a subtree can subscribe to directly.
|
|
2616
|
+
*
|
|
2617
|
+
* The first value is the size before any layout, usually zeroes, and
|
|
2618
|
+
* the first measurement is the real one. Emitting only when the band
|
|
2619
|
+
* changed left a container whose real width fell in the same band as
|
|
2620
|
+
* the guess built from the guess for good, so a builder that reads the
|
|
2621
|
+
* width (`size.width < 600` inside the `[1024]` band) was handed zero
|
|
2622
|
+
* on a 768-wide tablet. One more value at the first measurement costs
|
|
2623
|
+
* one more build at most, once, and every build after it sees a size
|
|
2624
|
+
* that was measured.
|
|
2545
2625
|
*/
|
|
2546
2626
|
function containerBands(size, breakpoints) {
|
|
2547
|
-
return size.changes.pipe(map((measured) =>
|
|
2627
|
+
return size.changes.pipe(map((measured, index) => ({
|
|
2628
|
+
band: bandOf(measured.width, breakpoints),
|
|
2629
|
+
first: index === 0
|
|
2630
|
+
})), distinctUntilChanged((before, now) => before.band === now.band && !before.first), map(({ band }) => band));
|
|
2548
2631
|
}
|
|
2549
2632
|
//#endregion
|
|
2550
2633
|
//#region src/environment/UiInsets.ts
|
|
@@ -2767,6 +2850,16 @@ const UiEnvironmentKeys = {
|
|
|
2767
2850
|
* as it changes. Provided by `Responsive` and by the
|
|
2768
2851
|
* `containerSize` property; see `UiContainerSize`.
|
|
2769
2852
|
*/
|
|
2853
|
+
/**
|
|
2854
|
+
* What the person points with here: the device's, as the runtime
|
|
2855
|
+
* reads it (CSS's `pointer` feature), or what an ancestor said with
|
|
2856
|
+
* `pointer`. A theme provided under `coarse` is turned for it
|
|
2857
|
+
* (`withPointer`). See `UiPointer`.
|
|
2858
|
+
*/
|
|
2859
|
+
pointer: createEnvironmentKey({
|
|
2860
|
+
name: "pointer",
|
|
2861
|
+
defaultValue: "fine"
|
|
2862
|
+
}),
|
|
2770
2863
|
containerSize: createEnvironmentKey({
|
|
2771
2864
|
name: "containerSize",
|
|
2772
2865
|
defaultValue: unknownContainerSize
|
|
@@ -2788,6 +2881,44 @@ const UiEnvironmentKeys = {
|
|
|
2788
2881
|
})
|
|
2789
2882
|
};
|
|
2790
2883
|
//#endregion
|
|
2884
|
+
//#region src/environment/UiPointer.ts
|
|
2885
|
+
/** The smallest target a finger can be asked to hit, in pixels: WCAG's 2.5.5, Apple's and Android's 44 to 48. */
|
|
2886
|
+
const TOUCH_TARGET = 44;
|
|
2887
|
+
/**
|
|
2888
|
+
* The same theme for another pointer.
|
|
2889
|
+
*
|
|
2890
|
+
* Turned to `coarse`, it is the theme with its `touch` laid over it and
|
|
2891
|
+
* `pointer` saying so; turned back, it is the theme it was turned from,
|
|
2892
|
+
* kept for the purpose, so a subtree that goes from touch to mouse and
|
|
2893
|
+
* back gets the identical theme each time. A theme already for that
|
|
2894
|
+
* pointer is returned as it is.
|
|
2895
|
+
*/
|
|
2896
|
+
function withPointer(theme, pointer) {
|
|
2897
|
+
if ((theme.pointer ?? "fine") === pointer) return theme;
|
|
2898
|
+
if (pointer === "fine") return fineThemes.get(theme) ?? {
|
|
2899
|
+
...theme,
|
|
2900
|
+
pointer: "fine"
|
|
2901
|
+
};
|
|
2902
|
+
const touch = theme.touch ?? {};
|
|
2903
|
+
const turned = {
|
|
2904
|
+
...theme,
|
|
2905
|
+
pointer: "coarse",
|
|
2906
|
+
typography: touch.typography === void 0 ? theme.typography : {
|
|
2907
|
+
...theme.typography,
|
|
2908
|
+
...touch.typography
|
|
2909
|
+
},
|
|
2910
|
+
spacing: touch.spacing === void 0 ? theme.spacing : {
|
|
2911
|
+
...theme.spacing,
|
|
2912
|
+
...touch.spacing
|
|
2913
|
+
},
|
|
2914
|
+
...touch.extensions === void 0 ? {} : { extensions: new Map([...theme.extensions ?? [], ...touch.extensions]) }
|
|
2915
|
+
};
|
|
2916
|
+
fineThemes.set(turned, theme);
|
|
2917
|
+
return turned;
|
|
2918
|
+
}
|
|
2919
|
+
/** Each touch theme's mouse self, for turning it back. */
|
|
2920
|
+
const fineThemes = /* @__PURE__ */ new WeakMap();
|
|
2921
|
+
//#endregion
|
|
2791
2922
|
//#region src/properties/UiPropertyDefinition.ts
|
|
2792
2923
|
/**
|
|
2793
2924
|
* Creates a property definition with the supplied metadata.
|
|
@@ -2972,6 +3103,18 @@ function resolveLength(value, base, property, keepAuto = false) {
|
|
|
2972
3103
|
}
|
|
2973
3104
|
throw new Error(`Property '${property}' has an invalid length ${describe(value)}. Use a number of pixels, percent(n), or auto.`);
|
|
2974
3105
|
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Why a value cannot be a length, or undefined when it can: the rules
|
|
3108
|
+
* `resolveLength` lays out by, for checking a value when it is written
|
|
3109
|
+
* (each length property's `validate`), where the error names the
|
|
3110
|
+
* element, rather than when it is laid out, where it would stop the
|
|
3111
|
+
* frame and everything else in it.
|
|
3112
|
+
*/
|
|
3113
|
+
function lengthProblem(value, property) {
|
|
3114
|
+
if (value === void 0 || value === null || isAutoLength(value) || isPercentLength(value)) return;
|
|
3115
|
+
if (typeof value === "number") return Number.isFinite(value) ? void 0 : `Property '${property}' must be a finite number, got ${String(value)}.`;
|
|
3116
|
+
return `Property '${property}' has an invalid length ${describe(value)}. Use a number of pixels, percent(n), or auto.`;
|
|
3117
|
+
}
|
|
2975
3118
|
function describe(value) {
|
|
2976
3119
|
if (typeof value === "string") return `'${value}'`;
|
|
2977
3120
|
try {
|
|
@@ -3385,39 +3528,49 @@ const P = 2;
|
|
|
3385
3528
|
const T = 32;
|
|
3386
3529
|
const E = 128;
|
|
3387
3530
|
const S = 256;
|
|
3531
|
+
/** A length property's check, run when a plain value is written: see `lengthProblem`. */
|
|
3532
|
+
function lengthValidator(property) {
|
|
3533
|
+
return (value) => lengthProblem(value, property);
|
|
3534
|
+
}
|
|
3388
3535
|
const UiProperties = {
|
|
3389
3536
|
width: defineProperty({
|
|
3390
3537
|
name: "width",
|
|
3538
|
+
validate: lengthValidator("width"),
|
|
3391
3539
|
defaultValue: void 0,
|
|
3392
3540
|
inherited: false,
|
|
3393
3541
|
affects: L
|
|
3394
3542
|
}),
|
|
3395
3543
|
height: defineProperty({
|
|
3396
3544
|
name: "height",
|
|
3545
|
+
validate: lengthValidator("height"),
|
|
3397
3546
|
defaultValue: void 0,
|
|
3398
3547
|
inherited: false,
|
|
3399
3548
|
affects: L
|
|
3400
3549
|
}),
|
|
3401
3550
|
minWidth: defineProperty({
|
|
3402
3551
|
name: "minWidth",
|
|
3552
|
+
validate: lengthValidator("minWidth"),
|
|
3403
3553
|
defaultValue: void 0,
|
|
3404
3554
|
inherited: false,
|
|
3405
3555
|
affects: L
|
|
3406
3556
|
}),
|
|
3407
3557
|
maxWidth: defineProperty({
|
|
3408
3558
|
name: "maxWidth",
|
|
3559
|
+
validate: lengthValidator("maxWidth"),
|
|
3409
3560
|
defaultValue: void 0,
|
|
3410
3561
|
inherited: false,
|
|
3411
3562
|
affects: L
|
|
3412
3563
|
}),
|
|
3413
3564
|
minHeight: defineProperty({
|
|
3414
3565
|
name: "minHeight",
|
|
3566
|
+
validate: lengthValidator("minHeight"),
|
|
3415
3567
|
defaultValue: void 0,
|
|
3416
3568
|
inherited: false,
|
|
3417
3569
|
affects: L
|
|
3418
3570
|
}),
|
|
3419
3571
|
maxHeight: defineProperty({
|
|
3420
3572
|
name: "maxHeight",
|
|
3573
|
+
validate: lengthValidator("maxHeight"),
|
|
3421
3574
|
defaultValue: void 0,
|
|
3422
3575
|
inherited: false,
|
|
3423
3576
|
affects: L
|
|
@@ -3523,6 +3676,7 @@ const UiProperties = {
|
|
|
3523
3676
|
}),
|
|
3524
3677
|
margin: defineProperty({
|
|
3525
3678
|
name: "margin",
|
|
3679
|
+
validate: lengthValidator("margin"),
|
|
3526
3680
|
defaultValue: void 0,
|
|
3527
3681
|
inherited: false,
|
|
3528
3682
|
affects: L
|
|
@@ -3530,6 +3684,7 @@ const UiProperties = {
|
|
|
3530
3684
|
/** Space outside the box, left and right. See `paddingX`. */
|
|
3531
3685
|
marginX: defineProperty({
|
|
3532
3686
|
name: "marginX",
|
|
3687
|
+
validate: lengthValidator("marginX"),
|
|
3533
3688
|
defaultValue: void 0,
|
|
3534
3689
|
inherited: false,
|
|
3535
3690
|
affects: L
|
|
@@ -3537,30 +3692,35 @@ const UiProperties = {
|
|
|
3537
3692
|
/** Space outside the box, top and bottom. See `paddingX`. */
|
|
3538
3693
|
marginY: defineProperty({
|
|
3539
3694
|
name: "marginY",
|
|
3695
|
+
validate: lengthValidator("marginY"),
|
|
3540
3696
|
defaultValue: void 0,
|
|
3541
3697
|
inherited: false,
|
|
3542
3698
|
affects: L
|
|
3543
3699
|
}),
|
|
3544
3700
|
marginTop: defineProperty({
|
|
3545
3701
|
name: "marginTop",
|
|
3702
|
+
validate: lengthValidator("marginTop"),
|
|
3546
3703
|
defaultValue: void 0,
|
|
3547
3704
|
inherited: false,
|
|
3548
3705
|
affects: L
|
|
3549
3706
|
}),
|
|
3550
3707
|
marginRight: defineProperty({
|
|
3551
3708
|
name: "marginRight",
|
|
3709
|
+
validate: lengthValidator("marginRight"),
|
|
3552
3710
|
defaultValue: void 0,
|
|
3553
3711
|
inherited: false,
|
|
3554
3712
|
affects: L
|
|
3555
3713
|
}),
|
|
3556
3714
|
marginBottom: defineProperty({
|
|
3557
3715
|
name: "marginBottom",
|
|
3716
|
+
validate: lengthValidator("marginBottom"),
|
|
3558
3717
|
defaultValue: void 0,
|
|
3559
3718
|
inherited: false,
|
|
3560
3719
|
affects: L
|
|
3561
3720
|
}),
|
|
3562
3721
|
marginLeft: defineProperty({
|
|
3563
3722
|
name: "marginLeft",
|
|
3723
|
+
validate: lengthValidator("marginLeft"),
|
|
3564
3724
|
defaultValue: void 0,
|
|
3565
3725
|
inherited: false,
|
|
3566
3726
|
affects: L
|
|
@@ -3568,6 +3728,7 @@ const UiProperties = {
|
|
|
3568
3728
|
/** Space outside the box on the edge the reading starts at. See `paddingStart`. */
|
|
3569
3729
|
marginStart: defineProperty({
|
|
3570
3730
|
name: "marginStart",
|
|
3731
|
+
validate: lengthValidator("marginStart"),
|
|
3571
3732
|
defaultValue: void 0,
|
|
3572
3733
|
inherited: false,
|
|
3573
3734
|
affects: L
|
|
@@ -3575,6 +3736,7 @@ const UiProperties = {
|
|
|
3575
3736
|
/** Space outside the box on the edge the reading ends at. See `paddingStart`. */
|
|
3576
3737
|
marginEnd: defineProperty({
|
|
3577
3738
|
name: "marginEnd",
|
|
3739
|
+
validate: lengthValidator("marginEnd"),
|
|
3578
3740
|
defaultValue: void 0,
|
|
3579
3741
|
inherited: false,
|
|
3580
3742
|
affects: L
|
|
@@ -3754,6 +3916,7 @@ const UiProperties = {
|
|
|
3754
3916
|
}),
|
|
3755
3917
|
flexBasis: defineProperty({
|
|
3756
3918
|
name: "flexBasis",
|
|
3919
|
+
validate: lengthValidator("flexBasis"),
|
|
3757
3920
|
defaultValue: void 0,
|
|
3758
3921
|
inherited: false,
|
|
3759
3922
|
affects: L
|
|
@@ -3819,24 +3982,28 @@ const UiProperties = {
|
|
|
3819
3982
|
}),
|
|
3820
3983
|
top: defineProperty({
|
|
3821
3984
|
name: "top",
|
|
3985
|
+
validate: lengthValidator("top"),
|
|
3822
3986
|
defaultValue: void 0,
|
|
3823
3987
|
inherited: false,
|
|
3824
3988
|
affects: L
|
|
3825
3989
|
}),
|
|
3826
3990
|
right: defineProperty({
|
|
3827
3991
|
name: "right",
|
|
3992
|
+
validate: lengthValidator("right"),
|
|
3828
3993
|
defaultValue: void 0,
|
|
3829
3994
|
inherited: false,
|
|
3830
3995
|
affects: L
|
|
3831
3996
|
}),
|
|
3832
3997
|
bottom: defineProperty({
|
|
3833
3998
|
name: "bottom",
|
|
3999
|
+
validate: lengthValidator("bottom"),
|
|
3834
4000
|
defaultValue: void 0,
|
|
3835
4001
|
inherited: false,
|
|
3836
4002
|
affects: L
|
|
3837
4003
|
}),
|
|
3838
4004
|
left: defineProperty({
|
|
3839
4005
|
name: "left",
|
|
4006
|
+
validate: lengthValidator("left"),
|
|
3840
4007
|
defaultValue: void 0,
|
|
3841
4008
|
inherited: false,
|
|
3842
4009
|
affects: L
|
|
@@ -4014,6 +4181,7 @@ const UiProperties = {
|
|
|
4014
4181
|
}),
|
|
4015
4182
|
inset: defineProperty({
|
|
4016
4183
|
name: "inset",
|
|
4184
|
+
validate: lengthValidator("inset"),
|
|
4017
4185
|
defaultValue: void 0,
|
|
4018
4186
|
inherited: false,
|
|
4019
4187
|
affects: L
|
|
@@ -4099,7 +4267,7 @@ const UiProperties = {
|
|
|
4099
4267
|
defaultValue: [],
|
|
4100
4268
|
inherited: false,
|
|
4101
4269
|
affects: P,
|
|
4102
|
-
compare:
|
|
4270
|
+
compare: boxShadowValuesEqual
|
|
4103
4271
|
}),
|
|
4104
4272
|
visible: defineProperty({
|
|
4105
4273
|
name: "visible",
|
|
@@ -4405,6 +4573,20 @@ const UiProperties = {
|
|
|
4405
4573
|
inherited: false,
|
|
4406
4574
|
affects: 64
|
|
4407
4575
|
}),
|
|
4576
|
+
/**
|
|
4577
|
+
* Takes a mouse over this subtree as a finger, as a browser's device
|
|
4578
|
+
* mode does: nothing is hovered without a press, a press hovers what
|
|
4579
|
+
* it lands on until it lifts, and the press has a finger's slop and
|
|
4580
|
+
* gestures. For a design tool playing a screen drawn for touch with a
|
|
4581
|
+
* mouse; read up the ancestor chain. A finger and a pen are
|
|
4582
|
+
* unchanged.
|
|
4583
|
+
*/
|
|
4584
|
+
touchEmulation: defineProperty({
|
|
4585
|
+
name: "touchEmulation",
|
|
4586
|
+
defaultValue: void 0,
|
|
4587
|
+
inherited: false,
|
|
4588
|
+
affects: 64
|
|
4589
|
+
}),
|
|
4408
4590
|
focusable: defineProperty({
|
|
4409
4591
|
name: "focusable",
|
|
4410
4592
|
defaultValue: void 0,
|
|
@@ -4622,6 +4804,20 @@ const UiProperties = {
|
|
|
4622
4804
|
return transformsEqual(transform(a), transform(b));
|
|
4623
4805
|
}
|
|
4624
4806
|
}),
|
|
4807
|
+
/**
|
|
4808
|
+
* Keep what this subtree paints as pixels, and copy them while only
|
|
4809
|
+
* what is above it moves or scales: a design tool's frames under a
|
|
4810
|
+
* zooming camera, a map's tiles under a pan. Canvas2D only; see
|
|
4811
|
+
* `RasterCache.ts` for when it draws them again. A hint: a subtree
|
|
4812
|
+
* that changes on most frames, or one too large to hold at the scale
|
|
4813
|
+
* it is drawn at, is drawn directly.
|
|
4814
|
+
*/
|
|
4815
|
+
rasterize: defineProperty({
|
|
4816
|
+
name: "rasterize",
|
|
4817
|
+
defaultValue: false,
|
|
4818
|
+
inherited: false,
|
|
4819
|
+
affects: P
|
|
4820
|
+
}),
|
|
4625
4821
|
text: defineProperty({
|
|
4626
4822
|
name: "text",
|
|
4627
4823
|
defaultValue: void 0,
|
|
@@ -4648,6 +4844,21 @@ const UiProperties = {
|
|
|
4648
4844
|
inherited: false,
|
|
4649
4845
|
affects: P
|
|
4650
4846
|
}),
|
|
4847
|
+
/**
|
|
4848
|
+
* The URL the node's picture was loaded from, as `imageSource` last
|
|
4849
|
+
* resolved it: an image's or an SVG's.
|
|
4850
|
+
*
|
|
4851
|
+
* Nothing lays it out or paints it. It is there for what reads a tree
|
|
4852
|
+
* as a document, an inspector, a design tool writing the screen as
|
|
4853
|
+
* HTML, since the bitmap or the drawing on the node no longer says
|
|
4854
|
+
* where it came from.
|
|
4855
|
+
*/
|
|
4856
|
+
src: defineProperty({
|
|
4857
|
+
name: "src",
|
|
4858
|
+
defaultValue: void 0,
|
|
4859
|
+
inherited: false,
|
|
4860
|
+
affects: 0
|
|
4861
|
+
}),
|
|
4651
4862
|
objectFit: defineProperty({
|
|
4652
4863
|
name: "objectFit",
|
|
4653
4864
|
defaultValue: void 0,
|
|
@@ -4794,6 +5005,18 @@ const UiProperties = {
|
|
|
4794
5005
|
affects: E
|
|
4795
5006
|
}),
|
|
4796
5007
|
/**
|
|
5008
|
+
* What the person points with in this subtree, `fine` or `coarse`,
|
|
5009
|
+
* over the device's: a design tool's frame drawn as a phone says
|
|
5010
|
+
* `coarse`, and the controls in it grow to a finger's size. See
|
|
5011
|
+
* `UiPointer`.
|
|
5012
|
+
*/
|
|
5013
|
+
pointer: defineProperty({
|
|
5014
|
+
name: "pointer",
|
|
5015
|
+
defaultValue: void 0,
|
|
5016
|
+
inherited: false,
|
|
5017
|
+
affects: E
|
|
5018
|
+
}),
|
|
5019
|
+
/**
|
|
4797
5020
|
* The type this element and its subtree are written in: a role in
|
|
4798
5021
|
* the theme's scale (`textStyle="title"`), or a style of its own.
|
|
4799
5022
|
*
|
|
@@ -5663,8 +5886,15 @@ var UiGraph = class {
|
|
|
5663
5886
|
*/
|
|
5664
5887
|
buildNodeEnvironment(node, parent = node.parent) {
|
|
5665
5888
|
let env = (parent !== null ? parent.environment : null) ?? new UiEnvironment(null);
|
|
5889
|
+
const pointer = node.getProperty("pointer");
|
|
5890
|
+
if (pointer === "fine" || pointer === "coarse") env = env.set(UiEnvironmentKeys.pointer, pointer);
|
|
5666
5891
|
const theme = node.getProperty("theme");
|
|
5667
5892
|
if (theme !== void 0) env = env.set(UiEnvironmentKeys.theme, theme);
|
|
5893
|
+
if (pointer !== void 0 || theme !== void 0) {
|
|
5894
|
+
const scoped = env.get(UiEnvironmentKeys.theme);
|
|
5895
|
+
const turned = withPointer(scoped, env.get(UiEnvironmentKeys.pointer));
|
|
5896
|
+
if (turned !== scoped) env = env.set(UiEnvironmentKeys.theme, turned);
|
|
5897
|
+
}
|
|
5668
5898
|
const textStyle = node.getProperty("textStyle");
|
|
5669
5899
|
if (textStyle !== void 0) {
|
|
5670
5900
|
const scale = env.get(UiEnvironmentKeys.theme).typography;
|
|
@@ -8149,6 +8379,14 @@ function isSubtreeInert(node) {
|
|
|
8149
8379
|
return false;
|
|
8150
8380
|
}
|
|
8151
8381
|
/** `pointerEvents` as CSS inherits it: the nearest value set from the node up, 'auto' when none is. */
|
|
8382
|
+
/** Whether a mouse over the node is taken as a finger: `touchEmulation` up the ancestor chain. */
|
|
8383
|
+
function emulatesTouch(node) {
|
|
8384
|
+
for (let current = node; current !== null; current = current.parent) {
|
|
8385
|
+
const value = current.properties.get("touchEmulation");
|
|
8386
|
+
if (value === true || value === false) return value;
|
|
8387
|
+
}
|
|
8388
|
+
return false;
|
|
8389
|
+
}
|
|
8152
8390
|
function pointerEventsOf(node) {
|
|
8153
8391
|
for (let current = node; current !== null; current = current.parent) {
|
|
8154
8392
|
const value = current.properties.get("pointerEvents");
|
|
@@ -8577,9 +8815,11 @@ const NO_SHADOWS = [];
|
|
|
8577
8815
|
* frame would allocate a shadow record per shadow on screen.
|
|
8578
8816
|
*/
|
|
8579
8817
|
const paintShadows = /* @__PURE__ */ new WeakMap();
|
|
8580
|
-
function resolvePaintShadows(node,
|
|
8581
|
-
if (
|
|
8582
|
-
const theme = node.environment !== null ? node.environment.get(UiEnvironmentKeys.theme) :
|
|
8818
|
+
function resolvePaintShadows(node, value) {
|
|
8819
|
+
if (value === void 0 || Array.isArray(value) && value.length === 0) return NO_SHADOWS;
|
|
8820
|
+
const theme = node.environment !== null ? node.environment.get(UiEnvironmentKeys.theme) : UiEnvironmentKeys.theme.defaultValue;
|
|
8821
|
+
const shadows = shadowsOf(value, theme?.shadows);
|
|
8822
|
+
if (shadows.length === 0) return NO_SHADOWS;
|
|
8583
8823
|
const cached = paintShadows.get(shadows);
|
|
8584
8824
|
if (cached !== void 0 && cached.theme === theme) return cached.shadows;
|
|
8585
8825
|
const resolved = [];
|
|
@@ -10702,6 +10942,11 @@ var UiFocusManager = class {
|
|
|
10702
10942
|
* same thing by holding, which the gesture recognizer answers.
|
|
10703
10943
|
* - Touch hover: a finger's hover is dropped when it lifts, because
|
|
10704
10944
|
* the finger is no longer anywhere. A mouse keeps its hover.
|
|
10945
|
+
* - Touch emulation: over a `touchEmulation` subtree a mouse is taken
|
|
10946
|
+
* as a finger, as a browser's device mode does. Its moves with
|
|
10947
|
+
* nothing pressed hover nothing there, and a press there is a
|
|
10948
|
+
* finger's from down to up: its slop, its gestures and its hover
|
|
10949
|
+
* dropped when it lifts.
|
|
10705
10950
|
* - Scrollbars: a press on a scroll container's thumb starts a drag
|
|
10706
10951
|
* that moves the content with the pointer; a press on the track
|
|
10707
10952
|
* beside a visible thumb pages one viewport toward the pointer.
|
|
@@ -10772,6 +11017,8 @@ var UiPointerController = class {
|
|
|
10772
11017
|
return this.lastPosition;
|
|
10773
11018
|
}
|
|
10774
11019
|
lastPosition = null;
|
|
11020
|
+
/** Whether the last hover hit was on a scrollbar; see `overScrollbar`. */
|
|
11021
|
+
hoverOnScrollbar = false;
|
|
10775
11022
|
/** The pressed node and its ancestors when the press began, deepest first. */
|
|
10776
11023
|
downPath = [];
|
|
10777
11024
|
get hoveredNode() {
|
|
@@ -10800,27 +11047,45 @@ var UiPointerController = class {
|
|
|
10800
11047
|
return this.scrollbarDrag?.node ?? null;
|
|
10801
11048
|
}
|
|
10802
11049
|
/**
|
|
11050
|
+
* Whether the pointer is on a scrollbar, or dragging a thumb: the arrow
|
|
11051
|
+
* is shown there whatever the content under the bar asks for, as over a
|
|
11052
|
+
* native scrollbar. A hit on a bar is its scroll container's, so the
|
|
11053
|
+
* cursor set above that (an editor's I-beam) was shown over the thumb.
|
|
11054
|
+
*/
|
|
11055
|
+
get overScrollbar() {
|
|
11056
|
+
return this.scrollbarDrag !== null || this.hoverOnScrollbar;
|
|
11057
|
+
}
|
|
11058
|
+
/**
|
|
10803
11059
|
* Pointer press. Hit-tests at the point, establishes hover, and
|
|
10804
11060
|
* dispatches PointerDown to the pressed node. Subsequent moves and
|
|
10805
11061
|
* the up are routed to that node until release.
|
|
10806
11062
|
*/
|
|
10807
|
-
pointerDown(x, y, buttons = 1, modifiers = noKeyModifiers(),
|
|
11063
|
+
pointerDown(x, y, buttons = 1, modifiers = noKeyModifiers(), device = MOUSE_POINTER) {
|
|
10808
11064
|
this.lastPosition = {
|
|
10809
11065
|
x,
|
|
10810
11066
|
y
|
|
10811
11067
|
};
|
|
10812
|
-
|
|
11068
|
+
let pointer = device;
|
|
11069
|
+
let event = new UiPointerEvent("pointerdown", x, y, buttons, modifiers, pointer);
|
|
10813
11070
|
if (this.downTarget !== null || this.scrollbarDrag !== null) {
|
|
10814
11071
|
this.gestures?.contactDown?.(pointer, x, y, this.downTarget, modifiers);
|
|
10815
11072
|
return event;
|
|
10816
11073
|
}
|
|
10817
11074
|
const hit = this.hitTester.hitTest(x, y);
|
|
11075
|
+
this.hoverOnScrollbar = hit?.scrollbar !== void 0;
|
|
10818
11076
|
if (hit?.scrollbar !== void 0 && this.scrollSink !== null) {
|
|
10819
11077
|
this.activePointer = pointer;
|
|
10820
11078
|
this.pressScrollbar(hit.node, hit.scrollbar.axis, hit.scrollbar.onThumb, x, y);
|
|
10821
11079
|
return event;
|
|
10822
11080
|
}
|
|
10823
11081
|
const target = hit?.node ?? null;
|
|
11082
|
+
if (pointer.kind === "mouse" && emulatesTouch(target)) {
|
|
11083
|
+
pointer = {
|
|
11084
|
+
id: pointer.id,
|
|
11085
|
+
kind: "touch"
|
|
11086
|
+
};
|
|
11087
|
+
event = new UiPointerEvent("pointerdown", x, y, buttons, modifiers, pointer);
|
|
11088
|
+
}
|
|
10824
11089
|
this.updateHover(target, x, y, buttons, modifiers, pointer);
|
|
10825
11090
|
if (target !== null && isSecondaryButton(buttons)) {
|
|
10826
11091
|
this.dispatcher.dispatch(event, target);
|
|
@@ -10856,11 +11121,12 @@ var UiPointerController = class {
|
|
|
10856
11121
|
* hover enter/leave, and dispatches PointerMove to the hovered
|
|
10857
11122
|
* node. Returns null when the move lands on empty space.
|
|
10858
11123
|
*/
|
|
10859
|
-
pointerMove(x, y, buttons = 0, modifiers = noKeyModifiers(),
|
|
11124
|
+
pointerMove(x, y, buttons = 0, modifiers = noKeyModifiers(), device = MOUSE_POINTER) {
|
|
10860
11125
|
this.lastPosition = {
|
|
10861
11126
|
x,
|
|
10862
11127
|
y
|
|
10863
11128
|
};
|
|
11129
|
+
const pointer = this.contactOf(device);
|
|
10864
11130
|
this.gestures?.contactMove?.(pointer, x, y);
|
|
10865
11131
|
if (!this.ownsPress(pointer)) return null;
|
|
10866
11132
|
if (this.scrollbarDrag !== null) {
|
|
@@ -10882,7 +11148,13 @@ var UiPointerController = class {
|
|
|
10882
11148
|
const zone = this.hitTester.scrollbarZoneAt(x, y);
|
|
10883
11149
|
if (zone !== null) this.scrollSink.revealScrollbars(zone.node);
|
|
10884
11150
|
}
|
|
10885
|
-
const
|
|
11151
|
+
const hit = this.hitTester.hitTest(x, y);
|
|
11152
|
+
this.hoverOnScrollbar = hit?.scrollbar !== void 0;
|
|
11153
|
+
const target = hit?.node ?? null;
|
|
11154
|
+
if (pointer.kind === "mouse" && emulatesTouch(target)) {
|
|
11155
|
+
this.updateHover(null, x, y, buttons, modifiers, pointer);
|
|
11156
|
+
return null;
|
|
11157
|
+
}
|
|
10886
11158
|
this.updateHover(target, x, y, buttons, modifiers, pointer);
|
|
10887
11159
|
this.selection?.pointerHover?.(target, x, y);
|
|
10888
11160
|
if (target === null) return null;
|
|
@@ -10896,7 +11168,8 @@ var UiPointerController = class {
|
|
|
10896
11168
|
* cancelled via preventDefault on the down, synthesizes a Click on
|
|
10897
11169
|
* the same node. Returns null when nothing was pressed.
|
|
10898
11170
|
*/
|
|
10899
|
-
pointerUp(x, y, buttons = 0, modifiers = noKeyModifiers(),
|
|
11171
|
+
pointerUp(x, y, buttons = 0, modifiers = noKeyModifiers(), device = MOUSE_POINTER) {
|
|
11172
|
+
const pointer = this.contactOf(device);
|
|
10900
11173
|
this.gestures?.contactUp?.(pointer);
|
|
10901
11174
|
if (!this.ownsPress(pointer)) return null;
|
|
10902
11175
|
if (this.scrollbarDrag !== null) {
|
|
@@ -10959,7 +11232,8 @@ var UiPointerController = class {
|
|
|
10959
11232
|
* Aborts the active press: dispatches PointerCancel to the pressed
|
|
10960
11233
|
* node and clears press state so no Click is synthesized.
|
|
10961
11234
|
*/
|
|
10962
|
-
pointerCancel(
|
|
11235
|
+
pointerCancel(device = MOUSE_POINTER) {
|
|
11236
|
+
const pointer = this.contactOf(device);
|
|
10963
11237
|
this.gestures?.contactUp?.(pointer);
|
|
10964
11238
|
if (!this.ownsPress(pointer)) return;
|
|
10965
11239
|
this.scrollbarDrag = null;
|
|
@@ -10989,6 +11263,10 @@ var UiPointerController = class {
|
|
|
10989
11263
|
ownsPress(pointer) {
|
|
10990
11264
|
return this.activePointer === null || this.activePointer.id === pointer.id;
|
|
10991
11265
|
}
|
|
11266
|
+
/** The contact an event is from: the press's own, which an emulated finger's mouse events are part of. */
|
|
11267
|
+
contactOf(device) {
|
|
11268
|
+
return this.activePointer !== null && this.activePointer.id === device.id ? this.activePointer : device;
|
|
11269
|
+
}
|
|
10992
11270
|
/**
|
|
10993
11271
|
* A press on the thumb starts a drag that maps pointer travel to
|
|
10994
11272
|
* scroll offset through the thumb's travel range. A press on the
|
|
@@ -11096,6 +11374,7 @@ var UiPointerController = class {
|
|
|
11096
11374
|
*/
|
|
11097
11375
|
releaseHover(x, y, modifiers, pointer) {
|
|
11098
11376
|
if (pointer.kind !== "touch") return;
|
|
11377
|
+
this.hoverOnScrollbar = false;
|
|
11099
11378
|
this.updateHover(null, x, y, 0, modifiers, pointer);
|
|
11100
11379
|
}
|
|
11101
11380
|
};
|
|
@@ -14945,86 +15224,6 @@ var MeasureSlot = class {
|
|
|
14945
15224
|
}
|
|
14946
15225
|
};
|
|
14947
15226
|
//#endregion
|
|
14948
|
-
//#region src/layout/SubtreeBounds.ts
|
|
14949
|
-
/**
|
|
14950
|
-
* Fills in the subtree bounds of every record under `root`, bottom up.
|
|
14951
|
-
*
|
|
14952
|
-
* The traversal mirrors the hit tester's exactly, `paintOrder` and
|
|
14953
|
-
* transparent fragments included, so the set of nodes a box covers is
|
|
14954
|
-
* the set of nodes the descent it replaces would have reached.
|
|
14955
|
-
*/
|
|
14956
|
-
function fillSubtreeBounds(root, records) {
|
|
14957
|
-
fillNode(root, records);
|
|
14958
|
-
}
|
|
14959
|
-
/**
|
|
14960
|
-
* A node's bounds, and its whole subtree's on the way there. Returns
|
|
14961
|
-
* null for a node with no record, a fragment above all, which has
|
|
14962
|
-
* nothing of its own to contribute to its parent's box.
|
|
14963
|
-
*/
|
|
14964
|
-
function fillNode(node, records) {
|
|
14965
|
-
const rec = records.get(node);
|
|
14966
|
-
if (rec === void 0) return null;
|
|
14967
|
-
rec.boundsMinX = rec.x;
|
|
14968
|
-
rec.boundsMinY = rec.y;
|
|
14969
|
-
rec.boundsMaxX = rec.x + rec.width;
|
|
14970
|
-
rec.boundsMaxY = rec.y + rec.height;
|
|
14971
|
-
rec.boundsUnbounded = isUnbounded(node, rec);
|
|
14972
|
-
fillChildren(node, rec.paintOrder, records, rec.clips ? null : rec);
|
|
14973
|
-
return rec;
|
|
14974
|
-
}
|
|
14975
|
-
function fillChildren(parent, order, records, into) {
|
|
14976
|
-
if (order !== null) {
|
|
14977
|
-
for (let i = 0; i < order.length; i++) union(into, fillNode(order[i], records));
|
|
14978
|
-
return;
|
|
14979
|
-
}
|
|
14980
|
-
for (let child = parent.firstChild; child !== null; child = child.nextSibling) if (child.type === "fragment") fillChildren(child, null, records, into);
|
|
14981
|
-
else union(into, fillNode(child, records));
|
|
14982
|
-
}
|
|
14983
|
-
function union(into, child) {
|
|
14984
|
-
if (into === null || child === null) return;
|
|
14985
|
-
if (child.boundsUnbounded) {
|
|
14986
|
-
into.boundsUnbounded = true;
|
|
14987
|
-
return;
|
|
14988
|
-
}
|
|
14989
|
-
if (child.boundsMinX < into.boundsMinX) into.boundsMinX = child.boundsMinX;
|
|
14990
|
-
if (child.boundsMinY < into.boundsMinY) into.boundsMinY = child.boundsMinY;
|
|
14991
|
-
if (child.boundsMaxX > into.boundsMaxX) into.boundsMaxX = child.boundsMaxX;
|
|
14992
|
-
if (child.boundsMaxY > into.boundsMaxY) into.boundsMaxY = child.boundsMaxY;
|
|
14993
|
-
}
|
|
14994
|
-
/**
|
|
14995
|
-
* Whether a node's own hit area is something its parent's box cannot
|
|
14996
|
-
* describe.
|
|
14997
|
-
*
|
|
14998
|
-
* A transform is the first case. The hit tester inverts the point into
|
|
14999
|
-
* a node's local space before it compares anything, so the box a
|
|
15000
|
-
* parent would test the point against is in the space the transform
|
|
15001
|
-
* moved the node out of: a node translated three hundred pixels up is
|
|
15002
|
-
* hit three hundred pixels up and bounded where it is not. Mapping the
|
|
15003
|
-
* box forward through the transform is possible, and rotation makes it
|
|
15004
|
-
* fiddly enough to be worth doing on its own evidence rather than
|
|
15005
|
-
* here; until then a transformed node is simply always descended into.
|
|
15006
|
-
*
|
|
15007
|
-
* Sticky is the second. A sticky node is drawn, and hit, shifted from
|
|
15008
|
-
* its record by an offset `applySticky` writes after placement, and
|
|
15009
|
-
* the shift moves the node's whole subtree without moving a single
|
|
15010
|
-
* box. The shifted box is knowable here, but it is knowable only
|
|
15011
|
-
* because the walk runs after layout has settled, and that is a
|
|
15012
|
-
* thinner reason than the rule these bounds are held to. Sticky nodes
|
|
15013
|
-
* are few, and every one of them sits inside a scroll container, whose
|
|
15014
|
-
* clip stops the flag going any further.
|
|
15015
|
-
*
|
|
15016
|
-
* Anchored nodes are deliberately not on this list. An anchored
|
|
15017
|
-
* overlay is re-placed after the pass that moved its anchor, but every
|
|
15018
|
-
* such move writes a box, and a written box invalidates these bounds,
|
|
15019
|
-
* so the walk that fills them always runs against the boxes an anchor
|
|
15020
|
-
* settled on.
|
|
15021
|
-
*/
|
|
15022
|
-
function isUnbounded(node, rec) {
|
|
15023
|
-
if (rec.sticky) return true;
|
|
15024
|
-
const transform = node.properties.get("transform");
|
|
15025
|
-
return typeof transform === "object" && transform !== null;
|
|
15026
|
-
}
|
|
15027
|
-
//#endregion
|
|
15028
15227
|
//#region src/layout/LayoutTransform.ts
|
|
15029
15228
|
/**
|
|
15030
15229
|
* Maps a container's content space into its own outer
|
|
@@ -15135,6 +15334,109 @@ function transformPointsIn(node, record, points) {
|
|
|
15135
15334
|
}
|
|
15136
15335
|
}
|
|
15137
15336
|
//#endregion
|
|
15337
|
+
//#region src/layout/SubtreeBounds.ts
|
|
15338
|
+
/**
|
|
15339
|
+
* Fills in the subtree bounds of every record under `root`, bottom up.
|
|
15340
|
+
*
|
|
15341
|
+
* The traversal mirrors the hit tester's exactly, `paintOrder` and
|
|
15342
|
+
* transparent fragments included, so the set of nodes a box covers is
|
|
15343
|
+
* the set of nodes the descent it replaces would have reached.
|
|
15344
|
+
*/
|
|
15345
|
+
function fillSubtreeBounds(root, records) {
|
|
15346
|
+
fillNode(root, records);
|
|
15347
|
+
}
|
|
15348
|
+
/**
|
|
15349
|
+
* A node's bounds, and its whole subtree's on the way there. Returns
|
|
15350
|
+
* null for a node with no record, a fragment above all, which has
|
|
15351
|
+
* nothing of its own to contribute to its parent's box.
|
|
15352
|
+
*/
|
|
15353
|
+
function fillNode(node, records) {
|
|
15354
|
+
const rec = records.get(node);
|
|
15355
|
+
if (rec === void 0) return null;
|
|
15356
|
+
rec.boundsMinX = rec.x;
|
|
15357
|
+
rec.boundsMinY = rec.y;
|
|
15358
|
+
rec.boundsMaxX = rec.x + rec.width;
|
|
15359
|
+
rec.boundsMaxY = rec.y + rec.height;
|
|
15360
|
+
rec.boundsUnbounded = isUnbounded(node, rec);
|
|
15361
|
+
fillChildren(node, rec.paintOrder, records, rec.clips ? null : rec);
|
|
15362
|
+
const transform = node.properties.get("transform");
|
|
15363
|
+
if (!rec.boundsUnbounded && typeof transform === "object" && transform !== null) mapThroughTransform(node, rec);
|
|
15364
|
+
return rec;
|
|
15365
|
+
}
|
|
15366
|
+
/**
|
|
15367
|
+
* A transformed node's bounds, carried out through its transform: the
|
|
15368
|
+
* box around where its corners land. Never smaller than what is drawn,
|
|
15369
|
+
* because a box's image under any affine transform is inside the box
|
|
15370
|
+
* around its corners' images; larger only by a rotation's corners.
|
|
15371
|
+
*/
|
|
15372
|
+
function mapThroughTransform(node, rec) {
|
|
15373
|
+
const corners = new Float64Array([
|
|
15374
|
+
rec.boundsMinX,
|
|
15375
|
+
rec.boundsMinY,
|
|
15376
|
+
rec.boundsMaxX,
|
|
15377
|
+
rec.boundsMinY,
|
|
15378
|
+
rec.boundsMaxX,
|
|
15379
|
+
rec.boundsMaxY,
|
|
15380
|
+
rec.boundsMinX,
|
|
15381
|
+
rec.boundsMaxY
|
|
15382
|
+
]);
|
|
15383
|
+
transformPointsOut(node, rec, corners);
|
|
15384
|
+
rec.boundsMinX = Math.min(corners[0], corners[2], corners[4], corners[6]);
|
|
15385
|
+
rec.boundsMinY = Math.min(corners[1], corners[3], corners[5], corners[7]);
|
|
15386
|
+
rec.boundsMaxX = Math.max(corners[0], corners[2], corners[4], corners[6]);
|
|
15387
|
+
rec.boundsMaxY = Math.max(corners[1], corners[3], corners[5], corners[7]);
|
|
15388
|
+
}
|
|
15389
|
+
function fillChildren(parent, order, records, into) {
|
|
15390
|
+
if (order !== null) {
|
|
15391
|
+
for (let i = 0; i < order.length; i++) union(into, fillNode(order[i], records));
|
|
15392
|
+
return;
|
|
15393
|
+
}
|
|
15394
|
+
for (let child = parent.firstChild; child !== null; child = child.nextSibling) if (child.type === "fragment") fillChildren(child, null, records, into);
|
|
15395
|
+
else union(into, fillNode(child, records));
|
|
15396
|
+
}
|
|
15397
|
+
function union(into, child) {
|
|
15398
|
+
if (into === null || child === null) return;
|
|
15399
|
+
if (child.boundsUnbounded) {
|
|
15400
|
+
into.boundsUnbounded = true;
|
|
15401
|
+
return;
|
|
15402
|
+
}
|
|
15403
|
+
if (child.boundsMinX < into.boundsMinX) into.boundsMinX = child.boundsMinX;
|
|
15404
|
+
if (child.boundsMinY < into.boundsMinY) into.boundsMinY = child.boundsMinY;
|
|
15405
|
+
if (child.boundsMaxX > into.boundsMaxX) into.boundsMaxX = child.boundsMaxX;
|
|
15406
|
+
if (child.boundsMaxY > into.boundsMaxY) into.boundsMaxY = child.boundsMaxY;
|
|
15407
|
+
}
|
|
15408
|
+
/**
|
|
15409
|
+
* Whether a node's own hit area is something its parent's box cannot
|
|
15410
|
+
* describe.
|
|
15411
|
+
*
|
|
15412
|
+
* A transform was the first case, and is not any more: the hit tester
|
|
15413
|
+
* inverts the point into a node's local space before it compares
|
|
15414
|
+
* anything, so the box a parent tests the point against has to be in
|
|
15415
|
+
* the space the transform moves the node into, and `mapThroughTransform`
|
|
15416
|
+
* carries it there. The evidence it was worth doing on: a design tool's
|
|
15417
|
+
* board, every frame's name drawn the same size at any zoom by a
|
|
15418
|
+
* transform, left every ancestor of every name unbounded, and nothing
|
|
15419
|
+
* on the board was ever passed over.
|
|
15420
|
+
*
|
|
15421
|
+
* Sticky is the case that remains. A sticky node is drawn, and hit, shifted from
|
|
15422
|
+
* its record by an offset `applySticky` writes after placement, and
|
|
15423
|
+
* the shift moves the node's whole subtree without moving a single
|
|
15424
|
+
* box. The shifted box is knowable here, but it is knowable only
|
|
15425
|
+
* because the walk runs after layout has settled, and that is a
|
|
15426
|
+
* thinner reason than the rule these bounds are held to. Sticky nodes
|
|
15427
|
+
* are few, and every one of them sits inside a scroll container, whose
|
|
15428
|
+
* clip stops the flag going any further.
|
|
15429
|
+
*
|
|
15430
|
+
* Anchored nodes are deliberately not on this list. An anchored
|
|
15431
|
+
* overlay is re-placed after the pass that moved its anchor, but every
|
|
15432
|
+
* such move writes a box, and a written box invalidates these bounds,
|
|
15433
|
+
* so the walk that fills them always runs against the boxes an anchor
|
|
15434
|
+
* settled on.
|
|
15435
|
+
*/
|
|
15436
|
+
function isUnbounded(_node, rec) {
|
|
15437
|
+
return rec.sticky;
|
|
15438
|
+
}
|
|
15439
|
+
//#endregion
|
|
15138
15440
|
//#region src/layout/LayoutExplanation.ts
|
|
15139
15441
|
const EPSILON$1 = 1e-6;
|
|
15140
15442
|
/**
|
|
@@ -15388,6 +15690,8 @@ function formatExplanation(explanation) {
|
|
|
15388
15690
|
//#endregion
|
|
15389
15691
|
//#region src/layout/LayoutEngine.ts
|
|
15390
15692
|
/** How long overlay scrollbars stay after the last scroll change. */
|
|
15693
|
+
/** Lengths already reported as refused, by node, so each is said once. */
|
|
15694
|
+
const refusedLengths = /* @__PURE__ */ new WeakMap();
|
|
15391
15695
|
const SCROLLBAR_LINGER_MS = 1200;
|
|
15392
15696
|
/** Portion of the linger during which the scrollbar fades out. */
|
|
15393
15697
|
const SCROLLBAR_FADE_MS = 350;
|
|
@@ -15596,7 +15900,8 @@ const reachScratch = {
|
|
|
15596
15900
|
* shadow that is on screen.
|
|
15597
15901
|
*/
|
|
15598
15902
|
function readPaintReach(node, rec) {
|
|
15599
|
-
const
|
|
15903
|
+
const value = node.properties.get("boxShadows");
|
|
15904
|
+
const shadows = typeof value === "string" ? shadowsOf(value, (node.environment?.get(UiEnvironmentKeys.theme) ?? UiEnvironmentKeys.theme.defaultValue).shadows) : value;
|
|
15600
15905
|
let left = 0;
|
|
15601
15906
|
let top = 0;
|
|
15602
15907
|
let right = 0;
|
|
@@ -16482,6 +16787,19 @@ var LayoutEngine = class {
|
|
|
16482
16787
|
return part === void 0 ? this.projectBox(node, rec.x, rec.y, rec.width, rec.height) : this.projectBox(node, rec.x + part.x, rec.y + part.y, part.width, part.height);
|
|
16483
16788
|
}
|
|
16484
16789
|
/**
|
|
16790
|
+
* Where everything under a node is drawn, through every transform
|
|
16791
|
+
* above it: its subtree's bounds, as `subtreeBoundsFor` left them,
|
|
16792
|
+
* projected as `screenBox` projects the node's own box. Null when the
|
|
16793
|
+
* subtree has no bounds to cull by (it draws past them, or has no
|
|
16794
|
+
* record). What a walk under a panned, zoomed parent passes a whole
|
|
16795
|
+
* subtree over by when none of it is on screen.
|
|
16796
|
+
*/
|
|
16797
|
+
screenBounds(node) {
|
|
16798
|
+
const rec = this.records.get(node);
|
|
16799
|
+
if (rec === void 0 || rec.boundsUnbounded) return null;
|
|
16800
|
+
return this.projectBox(node, rec.boundsMinX, rec.boundsMinY, rec.boundsMaxX - rec.boundsMinX, rec.boundsMaxY - rec.boundsMinY);
|
|
16801
|
+
}
|
|
16802
|
+
/**
|
|
16485
16803
|
* The scroll offsets each scroll ancestor would need so that `node`
|
|
16486
16804
|
* is inside its viewport, `padding` pixels from the nearest edge,
|
|
16487
16805
|
* innermost first. Nothing is changed: the caller writes the offsets
|
|
@@ -19350,14 +19668,36 @@ var LayoutEngine = class {
|
|
|
19350
19668
|
const raw = node.properties.get(property);
|
|
19351
19669
|
if (raw === void 0) return;
|
|
19352
19670
|
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
19353
|
-
const value =
|
|
19671
|
+
const value = this.lengthOrNothing(node, raw, base, property, false);
|
|
19354
19672
|
return value === "auto" ? void 0 : value;
|
|
19355
19673
|
}
|
|
19356
19674
|
lengthPropOrAuto(node, property, base) {
|
|
19357
19675
|
const raw = node.properties.get(property);
|
|
19358
19676
|
if (raw === void 0) return;
|
|
19359
19677
|
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
19360
|
-
return
|
|
19678
|
+
return this.lengthOrNothing(node, raw, base, property, true);
|
|
19679
|
+
}
|
|
19680
|
+
/**
|
|
19681
|
+
* A length, or nothing when the value cannot be one. A plain value
|
|
19682
|
+
* that cannot is refused when it is written (each length property's
|
|
19683
|
+
* `validate`); one that arrives bound, from an Observable, is caught
|
|
19684
|
+
* here. Throwing here abandoned the frame, and with it every other
|
|
19685
|
+
* element: one mock's `width: '100%'` stopped a whole design editor
|
|
19686
|
+
* drawing. So it is said, once for the node and property, and the
|
|
19687
|
+
* element is laid out as if the property were not set.
|
|
19688
|
+
*/
|
|
19689
|
+
lengthOrNothing(node, raw, base, property, keepAuto) {
|
|
19690
|
+
try {
|
|
19691
|
+
return resolveLength(raw, base, property, keepAuto);
|
|
19692
|
+
} catch (error) {
|
|
19693
|
+
const said = refusedLengths.get(node) ?? /* @__PURE__ */ new Set();
|
|
19694
|
+
if (!said.has(property)) {
|
|
19695
|
+
said.add(property);
|
|
19696
|
+
refusedLengths.set(node, said);
|
|
19697
|
+
console.error(`[gesso] ${error.message} On node '${node.id}'; laid out as if it were not set.`);
|
|
19698
|
+
}
|
|
19699
|
+
return;
|
|
19700
|
+
}
|
|
19361
19701
|
}
|
|
19362
19702
|
/**
|
|
19363
19703
|
* A margin side: the side's own value, else the logical side that
|
|
@@ -19367,7 +19707,12 @@ var LayoutEngine = class {
|
|
|
19367
19707
|
const value = props.get(side) ?? (logical !== void 0 ? props.get(logical) : void 0) ?? props.get(axis) ?? props.get("margin");
|
|
19368
19708
|
if (value === void 0) return 0;
|
|
19369
19709
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
19370
|
-
|
|
19710
|
+
try {
|
|
19711
|
+
return resolveLength(value, void 0, side, true) ?? 0;
|
|
19712
|
+
} catch (error) {
|
|
19713
|
+
console.error(`[gesso] ${error.message} Laid out as 0.`);
|
|
19714
|
+
return 0;
|
|
19715
|
+
}
|
|
19371
19716
|
}
|
|
19372
19717
|
textWrapProp(node) {
|
|
19373
19718
|
const value = node.properties.get("textWrap");
|
|
@@ -19452,335 +19797,24 @@ function hasPercentLength(props, names) {
|
|
|
19452
19797
|
return false;
|
|
19453
19798
|
}
|
|
19454
19799
|
//#endregion
|
|
19455
|
-
//#region src/rendering/
|
|
19800
|
+
//#region src/rendering/PaintPathData.ts
|
|
19456
19801
|
/**
|
|
19457
|
-
*
|
|
19802
|
+
* SVG path data, as calls on a `PaintSurface`.
|
|
19458
19803
|
*
|
|
19459
|
-
*
|
|
19460
|
-
*
|
|
19461
|
-
*
|
|
19462
|
-
*
|
|
19463
|
-
*
|
|
19464
|
-
|
|
19465
|
-
|
|
19466
|
-
|
|
19467
|
-
|
|
19468
|
-
|
|
19469
|
-
|
|
19470
|
-
|
|
19471
|
-
|
|
19472
|
-
|
|
19473
|
-
x: Math.max(0, shape.box.x),
|
|
19474
|
-
y: shape.box.y >= shape.height ? shape.box.y - shape.height : shape.box.y
|
|
19475
|
-
};
|
|
19476
|
-
}
|
|
19477
|
-
/** Horizontal padding inside a label box, each side. */
|
|
19478
|
-
const LABEL_PADDING_X = 4;
|
|
19479
|
-
/**
|
|
19480
|
-
* Draws overlay shapes onto a 2D context left in logical pixels, as the
|
|
19481
|
-
* Canvas2D renderer leaves it after a frame. Styles are assigned only
|
|
19482
|
-
* when they change, so a run of strips in one colour is one style set.
|
|
19483
|
-
*/
|
|
19484
|
-
function drawOverlayShapes(ctx, shapes) {
|
|
19485
|
-
let fillStyle;
|
|
19486
|
-
let strokeStyle;
|
|
19487
|
-
let lineWidth;
|
|
19488
|
-
const fill = (color) => {
|
|
19489
|
-
if (fillStyle !== color) {
|
|
19490
|
-
ctx.fillStyle = color;
|
|
19491
|
-
fillStyle = color;
|
|
19492
|
-
}
|
|
19493
|
-
};
|
|
19494
|
-
for (const shape of shapes) switch (shape.kind) {
|
|
19495
|
-
case "fill":
|
|
19496
|
-
fill(shape.color);
|
|
19497
|
-
ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
|
|
19498
|
-
break;
|
|
19499
|
-
case "stroke": {
|
|
19500
|
-
if (strokeStyle !== shape.color) {
|
|
19501
|
-
ctx.strokeStyle = shape.color;
|
|
19502
|
-
strokeStyle = shape.color;
|
|
19503
|
-
}
|
|
19504
|
-
if (lineWidth !== shape.lineWidth) {
|
|
19505
|
-
ctx.lineWidth = shape.lineWidth;
|
|
19506
|
-
lineWidth = shape.lineWidth;
|
|
19507
|
-
}
|
|
19508
|
-
const inset = shape.lineWidth / 2;
|
|
19509
|
-
ctx.strokeRect(shape.x + inset, shape.y + inset, Math.max(0, shape.width - shape.lineWidth), Math.max(0, shape.height - shape.lineWidth));
|
|
19510
|
-
break;
|
|
19511
|
-
}
|
|
19512
|
-
case "label": {
|
|
19513
|
-
ctx.font = shape.font;
|
|
19514
|
-
ctx.textAlign = "left";
|
|
19515
|
-
ctx.textBaseline = "alphabetic";
|
|
19516
|
-
const width = ctx.measureText(shape.text).width + 8;
|
|
19517
|
-
const { x, y } = labelOrigin(shape);
|
|
19518
|
-
fill(shape.background);
|
|
19519
|
-
ctx.fillRect(x, y, width, shape.height);
|
|
19520
|
-
fill(shape.textColor);
|
|
19521
|
-
ctx.fillText(shape.text, x + 4, y + shape.height - 4);
|
|
19522
|
-
break;
|
|
19523
|
-
}
|
|
19524
|
-
}
|
|
19525
|
-
}
|
|
19526
|
-
//#endregion
|
|
19527
|
-
//#region src/rendering/Decorations.ts
|
|
19528
|
-
/**
|
|
19529
|
-
* Places a decoration against the node's record.
|
|
19530
|
-
*
|
|
19531
|
-
* Shared by both backends rather than derived twice: the offsets, the
|
|
19532
|
-
* outset and the inherited radius are exactly the arithmetic the two
|
|
19533
|
-
* renderers would otherwise drift on.
|
|
19534
|
-
*/
|
|
19535
|
-
function decorationRect(shape, rec, nodeRadius) {
|
|
19536
|
-
const outset = shape.outset ?? 0;
|
|
19537
|
-
const horizontal = axis(rec.x, rec.width, shape.x, shape.width, shape.right, outset);
|
|
19538
|
-
const vertical = axis(rec.y, rec.height, shape.y, shape.height, shape.bottom, outset);
|
|
19539
|
-
const radius = Math.max(0, shape.radius ?? nodeRadius + outset);
|
|
19540
|
-
return {
|
|
19541
|
-
x: horizontal.start,
|
|
19542
|
-
y: vertical.start,
|
|
19543
|
-
width: horizontal.size,
|
|
19544
|
-
height: vertical.size,
|
|
19545
|
-
radius
|
|
19546
|
-
};
|
|
19547
|
-
}
|
|
19548
|
-
/**
|
|
19549
|
-
* One axis of the placement, from any two of near, size and far.
|
|
19550
|
-
*
|
|
19551
|
-
* The outset applies after the three are reconciled, growing the
|
|
19552
|
-
* rectangle by it on both sides, so it means the same thing however
|
|
19553
|
-
* the rectangle was described.
|
|
19554
|
-
*/
|
|
19555
|
-
function axis(origin, extent, near, size, far, outset) {
|
|
19556
|
-
let start;
|
|
19557
|
-
let length;
|
|
19558
|
-
if (near === void 0 && size !== void 0 && far !== void 0) {
|
|
19559
|
-
length = size;
|
|
19560
|
-
start = extent - far - size;
|
|
19561
|
-
} else {
|
|
19562
|
-
start = near ?? 0;
|
|
19563
|
-
length = size ?? extent - start - (far ?? 0);
|
|
19564
|
-
}
|
|
19565
|
-
return {
|
|
19566
|
-
start: origin + start - outset,
|
|
19567
|
-
size: Math.max(0, length + 2 * outset)
|
|
19568
|
-
};
|
|
19569
|
-
}
|
|
19570
|
-
/**
|
|
19571
|
-
* A decoration's colour, resolved against the node it decorates.
|
|
19572
|
-
*
|
|
19573
|
-
* The same rule every colour prop follows: a palette name resolves
|
|
19574
|
-
* against whatever theme the node inherits, at paint. That is why
|
|
19575
|
-
* `focusRing()` never reads the theme — it names `focusRing` and the
|
|
19576
|
-
* node's environment decides what that is, so a ring inside a dark
|
|
19577
|
-
* card is the dark palette's without the modifier knowing there was a
|
|
19578
|
-
* card.
|
|
19579
|
-
*/
|
|
19580
|
-
function decorationColor(node, shape) {
|
|
19581
|
-
return resolveColorValue(node, shape.color);
|
|
19582
|
-
}
|
|
19583
|
-
/** Whether a shape paints before the node's children or after them. */
|
|
19584
|
-
function paintsAfterChildren(shape) {
|
|
19585
|
-
return shape.after === "children";
|
|
19586
|
-
}
|
|
19587
|
-
/** Whether any shape in the list paints in the given phase; lets a renderer skip a pass. */
|
|
19588
|
-
function hasDecorationPhase(shapes, after) {
|
|
19589
|
-
for (const shape of shapes) if (paintsAfterChildren(shape) === after) return true;
|
|
19590
|
-
return false;
|
|
19591
|
-
}
|
|
19592
|
-
//#endregion
|
|
19593
|
-
//#region src/rendering/ScaledImageCache.ts
|
|
19594
|
-
/**
|
|
19595
|
-
* How many consecutive frames a size must hold before a copy is made.
|
|
19596
|
-
*
|
|
19597
|
-
* A morph transition scales its image every frame, so every frame
|
|
19598
|
-
* asks for a size no copy will ever be asked for again. Building one
|
|
19599
|
-
* anyway would add a blit to a frame that already had to resample,
|
|
19600
|
-
* making the one case that is most sensitive to frame time worse.
|
|
19601
|
-
* Two frames is enough to tell a settled layout from an animating
|
|
19602
|
-
* one, and costs an animation nothing it was not already paying.
|
|
19603
|
-
*/
|
|
19604
|
-
const FRAMES_BEFORE_COPY = 2;
|
|
19605
|
-
/**
|
|
19606
|
-
* The largest copy worth keeping, in device pixels.
|
|
19607
|
-
*
|
|
19608
|
-
* A full-bleed image on a 3440x1440 display is about 5 megapixels
|
|
19609
|
-
* and 20MB as a copy, which is worth it: that image is the single
|
|
19610
|
-
* most expensive draw in the frame. Twice that is not, so the
|
|
19611
|
-
* ceiling sits above the largest real screen and below anything a
|
|
19612
|
-
* mistake would ask for.
|
|
19613
|
-
*/
|
|
19614
|
-
const MAX_COPY_PIXELS = 12e6;
|
|
19615
|
-
/**
|
|
19616
|
-
* How many copies to keep.
|
|
19617
|
-
*
|
|
19618
|
-
* One per image visible at once, near enough. A shelf of artwork
|
|
19619
|
-
* scrolls new images in and old ones out, and the cost of losing a
|
|
19620
|
-
* copy is one resampled draw plus one blit, so this only has to be
|
|
19621
|
-
* large enough that a screenful does not evict itself.
|
|
19622
|
-
*/
|
|
19623
|
-
const MAX_ENTRIES = 48;
|
|
19624
|
-
function defaultSurfaceFactory(width, height) {
|
|
19625
|
-
return new OffscreenCanvas(width, height);
|
|
19626
|
-
}
|
|
19627
|
-
/**
|
|
19628
|
-
* Whether this environment can make a copy at all.
|
|
19629
|
-
*
|
|
19630
|
-
* `OffscreenCanvas` is the only synchronous way to resample: the
|
|
19631
|
-
* asynchronous `createImageBitmap(source, { resizeWidth })` cannot
|
|
19632
|
-
* be awaited inside a frame, and a copy that arrives a frame later
|
|
19633
|
-
* is a copy that arrives after the size it was made for has changed.
|
|
19634
|
-
*/
|
|
19635
|
-
function canScaleImages() {
|
|
19636
|
-
return typeof OffscreenCanvas === "function";
|
|
19637
|
-
}
|
|
19638
|
-
var ScaledImageCache = class {
|
|
19639
|
-
/**
|
|
19640
|
-
* Insertion-ordered, which is what makes eviction least-recently-used:
|
|
19641
|
-
* a hit deletes and re-sets its key, so the oldest entry is first.
|
|
19642
|
-
*
|
|
19643
|
-
* A `Map` rather than a `WeakMap` because eviction has to be able to
|
|
19644
|
-
* find the entry it is dropping in order to close the copy it holds,
|
|
19645
|
-
* and a weak collection cannot be walked. The strong reference to the
|
|
19646
|
-
* source is bounded by `MAX_ENTRIES` and released on eviction.
|
|
19647
|
-
*/
|
|
19648
|
-
entries = /* @__PURE__ */ new Map();
|
|
19649
|
-
/**
|
|
19650
|
-
* A number per source, so an entry can be keyed by picture *and*
|
|
19651
|
-
* size.
|
|
19652
|
-
*
|
|
19653
|
-
* One slot per source was a bug rather than a simplification. A
|
|
19654
|
-
* picture on screen at two sizes at once — Segue's now-playing bar
|
|
19655
|
-
* shows the same cover at 52px that its page shows at 340 — had the
|
|
19656
|
-
* two draws fight over the single slot: each in turn held the size
|
|
19657
|
-
* long enough to earn a copy, made it, and closed the other's. The
|
|
19658
|
-
* smaller one then alternated between a crisp copy and a raw
|
|
19659
|
-
* downsample of the source every few frames, which reads on screen as
|
|
19660
|
-
* the picture shimmering. `ScaledImageCache.spec.ts` pins it.
|
|
19661
|
-
*
|
|
19662
|
-
* `WeakMap`, so a source that goes away takes its id with it; the
|
|
19663
|
-
* entries themselves are still strongly held and evicted by count.
|
|
19664
|
-
*/
|
|
19665
|
-
sourceIds = /* @__PURE__ */ new WeakMap();
|
|
19666
|
-
nextSourceId = 1;
|
|
19667
|
-
createSurface;
|
|
19668
|
-
enabled;
|
|
19669
|
-
constructor(createSurface) {
|
|
19670
|
-
this.enabled = createSurface !== void 0 || canScaleImages();
|
|
19671
|
-
this.createSurface = createSurface ?? defaultSurfaceFactory;
|
|
19672
|
-
}
|
|
19673
|
-
/**
|
|
19674
|
-
* The image to draw for a destination of `width` x `height` device
|
|
19675
|
-
* pixels: a copy at that exact size when one is worth having and
|
|
19676
|
-
* ready, and otherwise `source` unchanged.
|
|
19677
|
-
*
|
|
19678
|
-
* Always safe to draw the result at the destination rectangle the
|
|
19679
|
-
* sizes came from. A copy is a resample of the same pixels, so the
|
|
19680
|
-
* picture is the same either way; only which of the two resamples
|
|
19681
|
-
* runs this frame differs.
|
|
19682
|
-
*/
|
|
19683
|
-
resolve(source, width, height) {
|
|
19684
|
-
if (!this.enabled) return source;
|
|
19685
|
-
const w = Math.round(width);
|
|
19686
|
-
const h = Math.round(height);
|
|
19687
|
-
if (w <= 0 || h <= 0 || w * h > MAX_COPY_PIXELS) return source;
|
|
19688
|
-
if (source.width === w && source.height === h) return source;
|
|
19689
|
-
const key = this.keyFor(source, w, h);
|
|
19690
|
-
const entry = this.entries.get(key);
|
|
19691
|
-
if (entry === void 0) {
|
|
19692
|
-
this.insert(key, {
|
|
19693
|
-
source,
|
|
19694
|
-
bitmap: null,
|
|
19695
|
-
width: w,
|
|
19696
|
-
height: h,
|
|
19697
|
-
stable: 1
|
|
19698
|
-
});
|
|
19699
|
-
return source;
|
|
19700
|
-
}
|
|
19701
|
-
this.entries.delete(key);
|
|
19702
|
-
this.entries.set(key, entry);
|
|
19703
|
-
if (entry.bitmap !== null) return entry.bitmap;
|
|
19704
|
-
entry.stable++;
|
|
19705
|
-
if (entry.stable < FRAMES_BEFORE_COPY) return source;
|
|
19706
|
-
const copy = this.copy(source, w, h);
|
|
19707
|
-
if (copy === null) return source;
|
|
19708
|
-
entry.bitmap = copy;
|
|
19709
|
-
return copy;
|
|
19710
|
-
}
|
|
19711
|
-
/** Drops every copy. Called when the renderer goes away. */
|
|
19712
|
-
dispose() {
|
|
19713
|
-
for (const entry of this.entries.values()) entry.bitmap?.close();
|
|
19714
|
-
this.entries.clear();
|
|
19715
|
-
}
|
|
19716
|
-
/** How many picture-and-size pairs are tracked. For specs and the inspector. */
|
|
19717
|
-
get size() {
|
|
19718
|
-
return this.entries.size;
|
|
19719
|
-
}
|
|
19720
|
-
/**
|
|
19721
|
-
* The cache key: this picture, at this size.
|
|
19722
|
-
*
|
|
19723
|
-
* A size that is asked for once and never again makes an entry that
|
|
19724
|
-
* holds no copy and is evicted by count like any other, which is why
|
|
19725
|
-
* a morph scaling every frame costs entries rather than copies.
|
|
19726
|
-
*/
|
|
19727
|
-
keyFor(source, width, height) {
|
|
19728
|
-
let id = this.sourceIds.get(source);
|
|
19729
|
-
if (id === void 0) {
|
|
19730
|
-
id = this.nextSourceId++;
|
|
19731
|
-
this.sourceIds.set(source, id);
|
|
19732
|
-
}
|
|
19733
|
-
return `${id}:${width}x${height}`;
|
|
19734
|
-
}
|
|
19735
|
-
insert(key, entry) {
|
|
19736
|
-
if (this.entries.size >= MAX_ENTRIES) {
|
|
19737
|
-
const oldest = this.entries.keys().next();
|
|
19738
|
-
if (!oldest.done) {
|
|
19739
|
-
this.entries.get(oldest.value)?.bitmap?.close();
|
|
19740
|
-
this.entries.delete(oldest.value);
|
|
19741
|
-
}
|
|
19742
|
-
}
|
|
19743
|
-
this.entries.set(key, entry);
|
|
19744
|
-
}
|
|
19745
|
-
/**
|
|
19746
|
-
* Resamples once, into a bitmap.
|
|
19747
|
-
*
|
|
19748
|
-
* Failure is not exceptional: a source that has been closed under
|
|
19749
|
-
* us throws here, and the honest answer is to draw the original and
|
|
19750
|
-
* let the next frame notice. So this reports null rather than
|
|
19751
|
-
* letting a frame die over an image.
|
|
19752
|
-
*/
|
|
19753
|
-
copy(source, width, height) {
|
|
19754
|
-
try {
|
|
19755
|
-
const surface = this.createSurface(width, height);
|
|
19756
|
-
const context = surface.getContext("2d");
|
|
19757
|
-
if (context === null) return null;
|
|
19758
|
-
context.drawImage(source, 0, 0, width, height);
|
|
19759
|
-
return surface.transferToImageBitmap();
|
|
19760
|
-
} catch {
|
|
19761
|
-
return null;
|
|
19762
|
-
}
|
|
19763
|
-
}
|
|
19764
|
-
};
|
|
19765
|
-
//#endregion
|
|
19766
|
-
//#region src/rendering/PaintPathData.ts
|
|
19767
|
-
/**
|
|
19768
|
-
* SVG path data, as calls on a `PaintSurface`.
|
|
19769
|
-
*
|
|
19770
|
-
* `IconRasterizer` hands its `d` string to `Path2D` and lets the
|
|
19771
|
-
* browser do this. That is the right call there, because an icon is
|
|
19772
|
-
* only ever rasterised in a browser, and it is the wrong call here for
|
|
19773
|
-
* two reasons: `Path2D` does not exist in the suite, so nothing about
|
|
19774
|
-
* a path could be asserted without a browser, and a recording that
|
|
19775
|
-
* held an opaque `Path2D` could not be compared, replayed or handed to
|
|
19776
|
-
* a second backend, which is the whole design.
|
|
19777
|
-
*
|
|
19778
|
-
* The grammar is the full one: `M L H V C S Q T A Z`, upper case
|
|
19779
|
-
* absolute and lower case relative, repeated coordinate sets after one
|
|
19780
|
-
* command letter, and the shorthand curves' reflected control points.
|
|
19781
|
-
* An unknown letter ends the parse rather than throwing, because a
|
|
19782
|
-
* half-drawn logo is a better failure in front of a person than a
|
|
19783
|
-
* blank frame.
|
|
19804
|
+
* `IconRasterizer` hands its `d` string to `Path2D` and lets the
|
|
19805
|
+
* browser do this. That is the right call there, because an icon is
|
|
19806
|
+
* only ever rasterised in a browser, and it is the wrong call here for
|
|
19807
|
+
* two reasons: `Path2D` does not exist in the suite, so nothing about
|
|
19808
|
+
* a path could be asserted without a browser, and a recording that
|
|
19809
|
+
* held an opaque `Path2D` could not be compared, replayed or handed to
|
|
19810
|
+
* a second backend, which is the whole design.
|
|
19811
|
+
*
|
|
19812
|
+
* The grammar is the full one: `M L H V C S Q T A Z`, upper case
|
|
19813
|
+
* absolute and lower case relative, repeated coordinate sets after one
|
|
19814
|
+
* command letter, and the shorthand curves' reflected control points.
|
|
19815
|
+
* An unknown letter ends the parse rather than throwing, because a
|
|
19816
|
+
* half-drawn logo is a better failure in front of a person than a
|
|
19817
|
+
* blank frame.
|
|
19784
19818
|
*/
|
|
19785
19819
|
function tracePathData(surface, d) {
|
|
19786
19820
|
const scanner = new Scanner(d);
|
|
@@ -19899,139 +19933,450 @@ function tracePathData(surface, d) {
|
|
|
19899
19933
|
default: return;
|
|
19900
19934
|
}
|
|
19901
19935
|
}
|
|
19902
|
-
}
|
|
19903
|
-
/** The previous control point mirrored through the current point. */
|
|
19904
|
-
function reflect(control, x, y) {
|
|
19905
|
-
return control === null ? [x, y] : [2 * x - control[0], 2 * y - control[1]];
|
|
19936
|
+
}
|
|
19937
|
+
/** The previous control point mirrored through the current point. */
|
|
19938
|
+
function reflect(control, x, y) {
|
|
19939
|
+
return control === null ? [x, y] : [2 * x - control[0], 2 * y - control[1]];
|
|
19940
|
+
}
|
|
19941
|
+
/**
|
|
19942
|
+
* An elliptical arc as cubic segments, by the endpoint-to-centre
|
|
19943
|
+
* conversion in the SVG specification's implementation notes.
|
|
19944
|
+
*
|
|
19945
|
+
* Nothing in the surface draws an ellipse, and nothing should: an arc
|
|
19946
|
+
* with a rotation and two radii is the only shape in the grammar that
|
|
19947
|
+
* a rectangle-and-curve vocabulary cannot state, and one conversion
|
|
19948
|
+
* here is cheaper than a primitive every consumer of the recording
|
|
19949
|
+
* would have to implement. Ninety degrees per segment keeps the error
|
|
19950
|
+
* far under a tenth of a pixel at icon sizes.
|
|
19951
|
+
*/
|
|
19952
|
+
function arcToCurves(surface, x0, y0, rx, ry, rotation, largeArc, sweep, x1, y1) {
|
|
19953
|
+
if (x0 === x1 && y0 === y1) return;
|
|
19954
|
+
let radiusX = Math.abs(rx);
|
|
19955
|
+
let radiusY = Math.abs(ry);
|
|
19956
|
+
if (radiusX === 0 || radiusY === 0) {
|
|
19957
|
+
surface.lineTo(x1, y1);
|
|
19958
|
+
return;
|
|
19959
|
+
}
|
|
19960
|
+
const cos = Math.cos(rotation);
|
|
19961
|
+
const sin = Math.sin(rotation);
|
|
19962
|
+
const dx = (x0 - x1) / 2;
|
|
19963
|
+
const dy = (y0 - y1) / 2;
|
|
19964
|
+
const px = cos * dx + sin * dy;
|
|
19965
|
+
const py = -sin * dx + cos * dy;
|
|
19966
|
+
const lambda = px * px / (radiusX * radiusX) + py * py / (radiusY * radiusY);
|
|
19967
|
+
if (lambda > 1) {
|
|
19968
|
+
const grow = Math.sqrt(lambda);
|
|
19969
|
+
radiusX *= grow;
|
|
19970
|
+
radiusY *= grow;
|
|
19971
|
+
}
|
|
19972
|
+
const numerator = radiusX * radiusX * radiusY * radiusY - radiusX * radiusX * py * py - radiusY * radiusY * px * px;
|
|
19973
|
+
const denominator = radiusX * radiusX * py * py + radiusY * radiusY * px * px;
|
|
19974
|
+
const factor = (largeArc === sweep ? -1 : 1) * Math.sqrt(Math.max(0, numerator / denominator));
|
|
19975
|
+
const cxPrime = factor * radiusX * py / radiusY;
|
|
19976
|
+
const cyPrime = -factor * radiusY * px / radiusX;
|
|
19977
|
+
const cx = cos * cxPrime - sin * cyPrime + (x0 + x1) / 2;
|
|
19978
|
+
const cy = sin * cxPrime + cos * cyPrime + (y0 + y1) / 2;
|
|
19979
|
+
const start = Math.atan2((py - cyPrime) / radiusY, (px - cxPrime) / radiusX);
|
|
19980
|
+
let sweepAngle = Math.atan2((-py - cyPrime) / radiusY, (-px - cxPrime) / radiusX) - start;
|
|
19981
|
+
if (!sweep && sweepAngle > 0) sweepAngle -= 2 * Math.PI;
|
|
19982
|
+
else if (sweep && sweepAngle < 0) sweepAngle += 2 * Math.PI;
|
|
19983
|
+
const segments = Math.max(1, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 2)));
|
|
19984
|
+
const step = sweepAngle / segments;
|
|
19985
|
+
const handle = 4 / 3 * Math.tan(step / 4);
|
|
19986
|
+
let angle = start;
|
|
19987
|
+
for (let i = 0; i < segments; i++) {
|
|
19988
|
+
const next = angle + step;
|
|
19989
|
+
const cosA = Math.cos(angle);
|
|
19990
|
+
const sinA = Math.sin(angle);
|
|
19991
|
+
const cosB = Math.cos(next);
|
|
19992
|
+
const sinB = Math.sin(next);
|
|
19993
|
+
const p1 = onEllipse(cx, cy, radiusX, radiusY, cos, sin, cosA - handle * sinA, sinA + handle * cosA);
|
|
19994
|
+
const p2 = onEllipse(cx, cy, radiusX, radiusY, cos, sin, cosB + handle * sinB, sinB - handle * cosB);
|
|
19995
|
+
const p3 = onEllipse(cx, cy, radiusX, radiusY, cos, sin, cosB, sinB);
|
|
19996
|
+
surface.bezierCurveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]);
|
|
19997
|
+
angle = next;
|
|
19998
|
+
}
|
|
19999
|
+
}
|
|
20000
|
+
/** A point on the rotated ellipse, from its unit-circle coordinates. */
|
|
20001
|
+
function onEllipse(cx, cy, rx, ry, cos, sin, ux, uy) {
|
|
20002
|
+
const x = rx * ux;
|
|
20003
|
+
const y = ry * uy;
|
|
20004
|
+
return [cx + cos * x - sin * y, cy + sin * x + cos * y];
|
|
20005
|
+
}
|
|
20006
|
+
/**
|
|
20007
|
+
* A cursor over path data.
|
|
20008
|
+
*
|
|
20009
|
+
* Path data is written without separators wherever it can be
|
|
20010
|
+
* (`M0 0l5-3.5`), so nothing here can split on whitespace: numbers are
|
|
20011
|
+
* read character by character, and the two arc flags are read as a
|
|
20012
|
+
* single digit each, which is the one place the grammar is not a plain
|
|
20013
|
+
* number list.
|
|
20014
|
+
*/
|
|
20015
|
+
var Scanner = class {
|
|
20016
|
+
source;
|
|
20017
|
+
index = 0;
|
|
20018
|
+
constructor(source) {
|
|
20019
|
+
this.source = source;
|
|
20020
|
+
}
|
|
20021
|
+
get done() {
|
|
20022
|
+
return this.index >= this.source.length;
|
|
20023
|
+
}
|
|
20024
|
+
skipSeparators() {
|
|
20025
|
+
while (this.index < this.source.length) {
|
|
20026
|
+
const code = this.source.charCodeAt(this.index);
|
|
20027
|
+
if (code === 32 || code === 9 || code === 10 || code === 13 || code === 12 || code === 44) {
|
|
20028
|
+
this.index++;
|
|
20029
|
+
continue;
|
|
20030
|
+
}
|
|
20031
|
+
return;
|
|
20032
|
+
}
|
|
20033
|
+
}
|
|
20034
|
+
/** The command letter here, consumed; undefined when a number is next. */
|
|
20035
|
+
peekCommand() {
|
|
20036
|
+
const char = this.source[this.index];
|
|
20037
|
+
if (char === void 0 || !/[a-zA-Z]/.test(char)) return;
|
|
20038
|
+
this.index++;
|
|
20039
|
+
return char;
|
|
20040
|
+
}
|
|
20041
|
+
number() {
|
|
20042
|
+
this.skipSeparators();
|
|
20043
|
+
const start = this.index;
|
|
20044
|
+
if (this.source[this.index] === "+" || this.source[this.index] === "-") this.index++;
|
|
20045
|
+
while (this.isDigit()) this.index++;
|
|
20046
|
+
if (this.source[this.index] === ".") {
|
|
20047
|
+
this.index++;
|
|
20048
|
+
while (this.isDigit()) this.index++;
|
|
20049
|
+
}
|
|
20050
|
+
const exponent = this.source[this.index];
|
|
20051
|
+
if (exponent === "e" || exponent === "E") {
|
|
20052
|
+
this.index++;
|
|
20053
|
+
if (this.source[this.index] === "+" || this.source[this.index] === "-") this.index++;
|
|
20054
|
+
while (this.isDigit()) this.index++;
|
|
20055
|
+
}
|
|
20056
|
+
const value = Number.parseFloat(this.source.slice(start, this.index));
|
|
20057
|
+
return Number.isNaN(value) ? 0 : value;
|
|
20058
|
+
}
|
|
20059
|
+
/** One character, `0` or `1`: the arc flags carry no separator. */
|
|
20060
|
+
flag() {
|
|
20061
|
+
this.skipSeparators();
|
|
20062
|
+
const char = this.source[this.index];
|
|
20063
|
+
this.index++;
|
|
20064
|
+
return char === "1";
|
|
20065
|
+
}
|
|
20066
|
+
isDigit() {
|
|
20067
|
+
const code = this.source.charCodeAt(this.index);
|
|
20068
|
+
return code >= 48 && code <= 57;
|
|
20069
|
+
}
|
|
20070
|
+
};
|
|
20071
|
+
//#endregion
|
|
20072
|
+
//#region src/rendering/ScrollSnap.ts
|
|
20073
|
+
/**
|
|
20074
|
+
* A scroll offset as it is drawn: on a whole device pixel.
|
|
20075
|
+
*
|
|
20076
|
+
* The offset a container holds stays exact, so a trackpad's steps of a
|
|
20077
|
+
* fraction of a pixel still add up; only what is drawn lands on the
|
|
20078
|
+
* pixel grid, as a browser draws scrolled content. Drawn at the exact
|
|
20079
|
+
* offset, text lands between pixels and rasterises differently each
|
|
20080
|
+
* frame, which shimmers as a flick coasts to a stop.
|
|
20081
|
+
*/
|
|
20082
|
+
function snapScroll(offset, dpr) {
|
|
20083
|
+
return Math.round(offset * dpr) / dpr;
|
|
20084
|
+
}
|
|
20085
|
+
//#endregion
|
|
20086
|
+
//#region src/rendering/OverlayShapes.ts
|
|
20087
|
+
/** Where a label's background box goes: above its box when there is room, else inside its top edge. */
|
|
20088
|
+
function labelOrigin(shape) {
|
|
20089
|
+
return {
|
|
20090
|
+
x: Math.max(0, shape.box.x),
|
|
20091
|
+
y: shape.box.y >= shape.height ? shape.box.y - shape.height : shape.box.y
|
|
20092
|
+
};
|
|
20093
|
+
}
|
|
20094
|
+
/** Horizontal padding inside a label box, each side. */
|
|
20095
|
+
const LABEL_PADDING_X = 4;
|
|
20096
|
+
/**
|
|
20097
|
+
* Draws overlay shapes onto a 2D context left in logical pixels, as the
|
|
20098
|
+
* Canvas2D renderer leaves it after a frame. Styles are assigned only
|
|
20099
|
+
* when they change, so a run of strips in one colour is one style set.
|
|
20100
|
+
*/
|
|
20101
|
+
function drawOverlayShapes(ctx, shapes) {
|
|
20102
|
+
let fillStyle;
|
|
20103
|
+
let strokeStyle;
|
|
20104
|
+
let lineWidth;
|
|
20105
|
+
const fill = (color) => {
|
|
20106
|
+
if (fillStyle !== color) {
|
|
20107
|
+
ctx.fillStyle = color;
|
|
20108
|
+
fillStyle = color;
|
|
20109
|
+
}
|
|
20110
|
+
};
|
|
20111
|
+
for (const shape of shapes) switch (shape.kind) {
|
|
20112
|
+
case "fill":
|
|
20113
|
+
fill(shape.color);
|
|
20114
|
+
ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
|
|
20115
|
+
break;
|
|
20116
|
+
case "stroke": {
|
|
20117
|
+
if (strokeStyle !== shape.color) {
|
|
20118
|
+
ctx.strokeStyle = shape.color;
|
|
20119
|
+
strokeStyle = shape.color;
|
|
20120
|
+
}
|
|
20121
|
+
if (lineWidth !== shape.lineWidth) {
|
|
20122
|
+
ctx.lineWidth = shape.lineWidth;
|
|
20123
|
+
lineWidth = shape.lineWidth;
|
|
20124
|
+
}
|
|
20125
|
+
const inset = shape.lineWidth / 2;
|
|
20126
|
+
ctx.strokeRect(shape.x + inset, shape.y + inset, Math.max(0, shape.width - shape.lineWidth), Math.max(0, shape.height - shape.lineWidth));
|
|
20127
|
+
break;
|
|
20128
|
+
}
|
|
20129
|
+
case "label": {
|
|
20130
|
+
ctx.font = shape.font;
|
|
20131
|
+
ctx.textAlign = "left";
|
|
20132
|
+
ctx.textBaseline = "alphabetic";
|
|
20133
|
+
const width = ctx.measureText(shape.text).width + 8;
|
|
20134
|
+
const { x, y } = labelOrigin(shape);
|
|
20135
|
+
fill(shape.background);
|
|
20136
|
+
ctx.fillRect(x, y, width, shape.height);
|
|
20137
|
+
fill(shape.textColor);
|
|
20138
|
+
ctx.fillText(shape.text, x + 4, y + shape.height - 4);
|
|
20139
|
+
break;
|
|
20140
|
+
}
|
|
20141
|
+
}
|
|
20142
|
+
}
|
|
20143
|
+
//#endregion
|
|
20144
|
+
//#region src/rendering/Decorations.ts
|
|
20145
|
+
/**
|
|
20146
|
+
* Places a decoration against the node's record.
|
|
20147
|
+
*
|
|
20148
|
+
* Shared by both backends rather than derived twice: the offsets, the
|
|
20149
|
+
* outset and the inherited radius are exactly the arithmetic the two
|
|
20150
|
+
* renderers would otherwise drift on.
|
|
20151
|
+
*/
|
|
20152
|
+
function decorationRect(shape, rec, nodeRadius) {
|
|
20153
|
+
const outset = shape.outset ?? 0;
|
|
20154
|
+
const horizontal = axis(rec.x, rec.width, shape.x, shape.width, shape.right, outset);
|
|
20155
|
+
const vertical = axis(rec.y, rec.height, shape.y, shape.height, shape.bottom, outset);
|
|
20156
|
+
const radius = Math.max(0, shape.radius ?? nodeRadius + outset);
|
|
20157
|
+
return {
|
|
20158
|
+
x: horizontal.start,
|
|
20159
|
+
y: vertical.start,
|
|
20160
|
+
width: horizontal.size,
|
|
20161
|
+
height: vertical.size,
|
|
20162
|
+
radius
|
|
20163
|
+
};
|
|
20164
|
+
}
|
|
20165
|
+
/**
|
|
20166
|
+
* One axis of the placement, from any two of near, size and far.
|
|
20167
|
+
*
|
|
20168
|
+
* The outset applies after the three are reconciled, growing the
|
|
20169
|
+
* rectangle by it on both sides, so it means the same thing however
|
|
20170
|
+
* the rectangle was described.
|
|
20171
|
+
*/
|
|
20172
|
+
function axis(origin, extent, near, size, far, outset) {
|
|
20173
|
+
let start;
|
|
20174
|
+
let length;
|
|
20175
|
+
if (near === void 0 && size !== void 0 && far !== void 0) {
|
|
20176
|
+
length = size;
|
|
20177
|
+
start = extent - far - size;
|
|
20178
|
+
} else {
|
|
20179
|
+
start = near ?? 0;
|
|
20180
|
+
length = size ?? extent - start - (far ?? 0);
|
|
20181
|
+
}
|
|
20182
|
+
return {
|
|
20183
|
+
start: origin + start - outset,
|
|
20184
|
+
size: Math.max(0, length + 2 * outset)
|
|
20185
|
+
};
|
|
19906
20186
|
}
|
|
19907
20187
|
/**
|
|
19908
|
-
*
|
|
19909
|
-
* conversion in the SVG specification's implementation notes.
|
|
20188
|
+
* A decoration's colour, resolved against the node it decorates.
|
|
19910
20189
|
*
|
|
19911
|
-
*
|
|
19912
|
-
*
|
|
19913
|
-
*
|
|
19914
|
-
*
|
|
19915
|
-
*
|
|
19916
|
-
*
|
|
20190
|
+
* The same rule every colour prop follows: a palette name resolves
|
|
20191
|
+
* against whatever theme the node inherits, at paint. That is why
|
|
20192
|
+
* `focusRing()` never reads the theme — it names `focusRing` and the
|
|
20193
|
+
* node's environment decides what that is, so a ring inside a dark
|
|
20194
|
+
* card is the dark palette's without the modifier knowing there was a
|
|
20195
|
+
* card.
|
|
19917
20196
|
*/
|
|
19918
|
-
function
|
|
19919
|
-
|
|
19920
|
-
let radiusX = Math.abs(rx);
|
|
19921
|
-
let radiusY = Math.abs(ry);
|
|
19922
|
-
if (radiusX === 0 || radiusY === 0) {
|
|
19923
|
-
surface.lineTo(x1, y1);
|
|
19924
|
-
return;
|
|
19925
|
-
}
|
|
19926
|
-
const cos = Math.cos(rotation);
|
|
19927
|
-
const sin = Math.sin(rotation);
|
|
19928
|
-
const dx = (x0 - x1) / 2;
|
|
19929
|
-
const dy = (y0 - y1) / 2;
|
|
19930
|
-
const px = cos * dx + sin * dy;
|
|
19931
|
-
const py = -sin * dx + cos * dy;
|
|
19932
|
-
const lambda = px * px / (radiusX * radiusX) + py * py / (radiusY * radiusY);
|
|
19933
|
-
if (lambda > 1) {
|
|
19934
|
-
const grow = Math.sqrt(lambda);
|
|
19935
|
-
radiusX *= grow;
|
|
19936
|
-
radiusY *= grow;
|
|
19937
|
-
}
|
|
19938
|
-
const numerator = radiusX * radiusX * radiusY * radiusY - radiusX * radiusX * py * py - radiusY * radiusY * px * px;
|
|
19939
|
-
const denominator = radiusX * radiusX * py * py + radiusY * radiusY * px * px;
|
|
19940
|
-
const factor = (largeArc === sweep ? -1 : 1) * Math.sqrt(Math.max(0, numerator / denominator));
|
|
19941
|
-
const cxPrime = factor * radiusX * py / radiusY;
|
|
19942
|
-
const cyPrime = -factor * radiusY * px / radiusX;
|
|
19943
|
-
const cx = cos * cxPrime - sin * cyPrime + (x0 + x1) / 2;
|
|
19944
|
-
const cy = sin * cxPrime + cos * cyPrime + (y0 + y1) / 2;
|
|
19945
|
-
const start = Math.atan2((py - cyPrime) / radiusY, (px - cxPrime) / radiusX);
|
|
19946
|
-
let sweepAngle = Math.atan2((-py - cyPrime) / radiusY, (-px - cxPrime) / radiusX) - start;
|
|
19947
|
-
if (!sweep && sweepAngle > 0) sweepAngle -= 2 * Math.PI;
|
|
19948
|
-
else if (sweep && sweepAngle < 0) sweepAngle += 2 * Math.PI;
|
|
19949
|
-
const segments = Math.max(1, Math.ceil(Math.abs(sweepAngle) / (Math.PI / 2)));
|
|
19950
|
-
const step = sweepAngle / segments;
|
|
19951
|
-
const handle = 4 / 3 * Math.tan(step / 4);
|
|
19952
|
-
let angle = start;
|
|
19953
|
-
for (let i = 0; i < segments; i++) {
|
|
19954
|
-
const next = angle + step;
|
|
19955
|
-
const cosA = Math.cos(angle);
|
|
19956
|
-
const sinA = Math.sin(angle);
|
|
19957
|
-
const cosB = Math.cos(next);
|
|
19958
|
-
const sinB = Math.sin(next);
|
|
19959
|
-
const p1 = onEllipse(cx, cy, radiusX, radiusY, cos, sin, cosA - handle * sinA, sinA + handle * cosA);
|
|
19960
|
-
const p2 = onEllipse(cx, cy, radiusX, radiusY, cos, sin, cosB + handle * sinB, sinB - handle * cosB);
|
|
19961
|
-
const p3 = onEllipse(cx, cy, radiusX, radiusY, cos, sin, cosB, sinB);
|
|
19962
|
-
surface.bezierCurveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]);
|
|
19963
|
-
angle = next;
|
|
19964
|
-
}
|
|
20197
|
+
function decorationColor(node, shape) {
|
|
20198
|
+
return resolveColorValue(node, shape.color);
|
|
19965
20199
|
}
|
|
19966
|
-
/**
|
|
19967
|
-
function
|
|
19968
|
-
|
|
19969
|
-
|
|
19970
|
-
|
|
20200
|
+
/** Whether a shape paints before the node's children or after them. */
|
|
20201
|
+
function paintsAfterChildren(shape) {
|
|
20202
|
+
return shape.after === "children";
|
|
20203
|
+
}
|
|
20204
|
+
/** Whether any shape in the list paints in the given phase; lets a renderer skip a pass. */
|
|
20205
|
+
function hasDecorationPhase(shapes, after) {
|
|
20206
|
+
for (const shape of shapes) if (paintsAfterChildren(shape) === after) return true;
|
|
20207
|
+
return false;
|
|
19971
20208
|
}
|
|
20209
|
+
//#endregion
|
|
20210
|
+
//#region src/rendering/ScaledImageCache.ts
|
|
19972
20211
|
/**
|
|
19973
|
-
*
|
|
20212
|
+
* How many consecutive frames a size must hold before a copy is made.
|
|
19974
20213
|
*
|
|
19975
|
-
*
|
|
19976
|
-
*
|
|
19977
|
-
*
|
|
19978
|
-
*
|
|
19979
|
-
*
|
|
20214
|
+
* A morph transition scales its image every frame, so every frame
|
|
20215
|
+
* asks for a size no copy will ever be asked for again. Building one
|
|
20216
|
+
* anyway would add a blit to a frame that already had to resample,
|
|
20217
|
+
* making the one case that is most sensitive to frame time worse.
|
|
20218
|
+
* Two frames is enough to tell a settled layout from an animating
|
|
20219
|
+
* one, and costs an animation nothing it was not already paying.
|
|
19980
20220
|
*/
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
|
|
20221
|
+
const FRAMES_BEFORE_COPY = 2;
|
|
20222
|
+
/**
|
|
20223
|
+
* The largest copy worth keeping, in device pixels.
|
|
20224
|
+
*
|
|
20225
|
+
* A full-bleed image on a 3440x1440 display is about 5 megapixels
|
|
20226
|
+
* and 20MB as a copy, which is worth it: that image is the single
|
|
20227
|
+
* most expensive draw in the frame. Twice that is not, so the
|
|
20228
|
+
* ceiling sits above the largest real screen and below anything a
|
|
20229
|
+
* mistake would ask for.
|
|
20230
|
+
*/
|
|
20231
|
+
const MAX_COPY_PIXELS = 12e6;
|
|
20232
|
+
/**
|
|
20233
|
+
* How many copies to keep.
|
|
20234
|
+
*
|
|
20235
|
+
* One per image visible at once, near enough. A shelf of artwork
|
|
20236
|
+
* scrolls new images in and old ones out, and the cost of losing a
|
|
20237
|
+
* copy is one resampled draw plus one blit, so this only has to be
|
|
20238
|
+
* large enough that a screenful does not evict itself.
|
|
20239
|
+
*/
|
|
20240
|
+
const MAX_ENTRIES = 48;
|
|
20241
|
+
function defaultSurfaceFactory(width, height) {
|
|
20242
|
+
return new OffscreenCanvas(width, height);
|
|
20243
|
+
}
|
|
20244
|
+
/**
|
|
20245
|
+
* Whether this environment can make a copy at all.
|
|
20246
|
+
*
|
|
20247
|
+
* `OffscreenCanvas` is the only synchronous way to resample: the
|
|
20248
|
+
* asynchronous `createImageBitmap(source, { resizeWidth })` cannot
|
|
20249
|
+
* be awaited inside a frame, and a copy that arrives a frame later
|
|
20250
|
+
* is a copy that arrives after the size it was made for has changed.
|
|
20251
|
+
*/
|
|
20252
|
+
function canScaleImages() {
|
|
20253
|
+
return typeof OffscreenCanvas === "function";
|
|
20254
|
+
}
|
|
20255
|
+
var ScaledImageCache = class {
|
|
20256
|
+
/**
|
|
20257
|
+
* Insertion-ordered, which is what makes eviction least-recently-used:
|
|
20258
|
+
* a hit deletes and re-sets its key, so the oldest entry is first.
|
|
20259
|
+
*
|
|
20260
|
+
* A `Map` rather than a `WeakMap` because eviction has to be able to
|
|
20261
|
+
* find the entry it is dropping in order to close the copy it holds,
|
|
20262
|
+
* and a weak collection cannot be walked. The strong reference to the
|
|
20263
|
+
* source is bounded by `MAX_ENTRIES` and released on eviction.
|
|
20264
|
+
*/
|
|
20265
|
+
entries = /* @__PURE__ */ new Map();
|
|
20266
|
+
/**
|
|
20267
|
+
* A number per source, so an entry can be keyed by picture *and*
|
|
20268
|
+
* size.
|
|
20269
|
+
*
|
|
20270
|
+
* One slot per source was a bug rather than a simplification. A
|
|
20271
|
+
* picture on screen at two sizes at once — Segue's now-playing bar
|
|
20272
|
+
* shows the same cover at 52px that its page shows at 340 — had the
|
|
20273
|
+
* two draws fight over the single slot: each in turn held the size
|
|
20274
|
+
* long enough to earn a copy, made it, and closed the other's. The
|
|
20275
|
+
* smaller one then alternated between a crisp copy and a raw
|
|
20276
|
+
* downsample of the source every few frames, which reads on screen as
|
|
20277
|
+
* the picture shimmering. `ScaledImageCache.spec.ts` pins it.
|
|
20278
|
+
*
|
|
20279
|
+
* `WeakMap`, so a source that goes away takes its id with it; the
|
|
20280
|
+
* entries themselves are still strongly held and evicted by count.
|
|
20281
|
+
*/
|
|
20282
|
+
sourceIds = /* @__PURE__ */ new WeakMap();
|
|
20283
|
+
nextSourceId = 1;
|
|
20284
|
+
createSurface;
|
|
20285
|
+
enabled;
|
|
20286
|
+
constructor(createSurface) {
|
|
20287
|
+
this.enabled = createSurface !== void 0 || canScaleImages();
|
|
20288
|
+
this.createSurface = createSurface ?? defaultSurfaceFactory;
|
|
19989
20289
|
}
|
|
19990
|
-
|
|
19991
|
-
|
|
19992
|
-
|
|
19993
|
-
|
|
19994
|
-
|
|
19995
|
-
|
|
19996
|
-
|
|
19997
|
-
|
|
20290
|
+
/**
|
|
20291
|
+
* The image to draw for a destination of `width` x `height` device
|
|
20292
|
+
* pixels: a copy at that exact size when one is worth having and
|
|
20293
|
+
* ready, and otherwise `source` unchanged.
|
|
20294
|
+
*
|
|
20295
|
+
* Always safe to draw the result at the destination rectangle the
|
|
20296
|
+
* sizes came from. A copy is a resample of the same pixels, so the
|
|
20297
|
+
* picture is the same either way; only which of the two resamples
|
|
20298
|
+
* runs this frame differs.
|
|
20299
|
+
*/
|
|
20300
|
+
resolve(source, width, height) {
|
|
20301
|
+
if (!this.enabled) return source;
|
|
20302
|
+
const w = Math.round(width);
|
|
20303
|
+
const h = Math.round(height);
|
|
20304
|
+
if (w <= 0 || h <= 0 || w * h > MAX_COPY_PIXELS) return source;
|
|
20305
|
+
if (source.width === w && source.height === h) return source;
|
|
20306
|
+
const key = this.keyFor(source, w, h);
|
|
20307
|
+
const entry = this.entries.get(key);
|
|
20308
|
+
if (entry === void 0) {
|
|
20309
|
+
this.insert(key, {
|
|
20310
|
+
source,
|
|
20311
|
+
bitmap: null,
|
|
20312
|
+
width: w,
|
|
20313
|
+
height: h,
|
|
20314
|
+
stable: 1
|
|
20315
|
+
});
|
|
20316
|
+
return source;
|
|
19998
20317
|
}
|
|
20318
|
+
this.entries.delete(key);
|
|
20319
|
+
this.entries.set(key, entry);
|
|
20320
|
+
if (entry.bitmap !== null) return entry.bitmap;
|
|
20321
|
+
entry.stable++;
|
|
20322
|
+
if (entry.stable < FRAMES_BEFORE_COPY) return source;
|
|
20323
|
+
const copy = this.copy(source, w, h);
|
|
20324
|
+
if (copy === null) return source;
|
|
20325
|
+
entry.bitmap = copy;
|
|
20326
|
+
return copy;
|
|
19999
20327
|
}
|
|
20000
|
-
/**
|
|
20001
|
-
|
|
20002
|
-
const
|
|
20003
|
-
|
|
20004
|
-
this.index++;
|
|
20005
|
-
return char;
|
|
20328
|
+
/** Drops every copy. Called when the renderer goes away. */
|
|
20329
|
+
dispose() {
|
|
20330
|
+
for (const entry of this.entries.values()) entry.bitmap?.close();
|
|
20331
|
+
this.entries.clear();
|
|
20006
20332
|
}
|
|
20007
|
-
|
|
20008
|
-
|
|
20009
|
-
|
|
20010
|
-
|
|
20011
|
-
|
|
20012
|
-
|
|
20013
|
-
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
|
|
20020
|
-
|
|
20333
|
+
/** How many picture-and-size pairs are tracked. For specs and the inspector. */
|
|
20334
|
+
get size() {
|
|
20335
|
+
return this.entries.size;
|
|
20336
|
+
}
|
|
20337
|
+
/**
|
|
20338
|
+
* The cache key: this picture, at this size.
|
|
20339
|
+
*
|
|
20340
|
+
* A size that is asked for once and never again makes an entry that
|
|
20341
|
+
* holds no copy and is evicted by count like any other, which is why
|
|
20342
|
+
* a morph scaling every frame costs entries rather than copies.
|
|
20343
|
+
*/
|
|
20344
|
+
keyFor(source, width, height) {
|
|
20345
|
+
let id = this.sourceIds.get(source);
|
|
20346
|
+
if (id === void 0) {
|
|
20347
|
+
id = this.nextSourceId++;
|
|
20348
|
+
this.sourceIds.set(source, id);
|
|
20021
20349
|
}
|
|
20022
|
-
|
|
20023
|
-
return Number.isNaN(value) ? 0 : value;
|
|
20350
|
+
return `${id}:${width}x${height}`;
|
|
20024
20351
|
}
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
|
|
20030
|
-
|
|
20352
|
+
insert(key, entry) {
|
|
20353
|
+
if (this.entries.size >= MAX_ENTRIES) {
|
|
20354
|
+
const oldest = this.entries.keys().next();
|
|
20355
|
+
if (!oldest.done) {
|
|
20356
|
+
this.entries.get(oldest.value)?.bitmap?.close();
|
|
20357
|
+
this.entries.delete(oldest.value);
|
|
20358
|
+
}
|
|
20359
|
+
}
|
|
20360
|
+
this.entries.set(key, entry);
|
|
20031
20361
|
}
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20362
|
+
/**
|
|
20363
|
+
* Resamples once, into a bitmap.
|
|
20364
|
+
*
|
|
20365
|
+
* Failure is not exceptional: a source that has been closed under
|
|
20366
|
+
* us throws here, and the honest answer is to draw the original and
|
|
20367
|
+
* let the next frame notice. So this reports null rather than
|
|
20368
|
+
* letting a frame die over an image.
|
|
20369
|
+
*/
|
|
20370
|
+
copy(source, width, height) {
|
|
20371
|
+
try {
|
|
20372
|
+
const surface = this.createSurface(width, height);
|
|
20373
|
+
const context = surface.getContext("2d");
|
|
20374
|
+
if (context === null) return null;
|
|
20375
|
+
context.drawImage(source, 0, 0, width, height);
|
|
20376
|
+
return surface.transferToImageBitmap();
|
|
20377
|
+
} catch {
|
|
20378
|
+
return null;
|
|
20379
|
+
}
|
|
20035
20380
|
}
|
|
20036
20381
|
};
|
|
20037
20382
|
//#endregion
|
|
@@ -20793,10 +21138,14 @@ function contentChanged(slot, key) {
|
|
|
20793
21138
|
function matches(slot, key) {
|
|
20794
21139
|
return slot.width === key.box.width && slot.height === key.box.height && slot.scale === key.box.scale && slot.environment === key.environment && slot.clipPath === key.clipPath && slot.blur === key.blur && paintValuesEqual(slot.paint, key.paint) && pathValuesEqual(slot.path, key.path);
|
|
20795
21140
|
}
|
|
20796
|
-
/**
|
|
21141
|
+
/**
|
|
21142
|
+
* A painter's colour values, against the node's own theme. \`currentColor\`
|
|
21143
|
+
* is the node's text colour, inherited as CSS's is, so an icon drawn in
|
|
21144
|
+
* it takes the colour of the label beside it: white on a selected row.
|
|
21145
|
+
*/
|
|
20797
21146
|
function resolverFor(node) {
|
|
20798
21147
|
return {
|
|
20799
|
-
color: (value) => resolveColorValue(node, value),
|
|
21148
|
+
color: (value) => value === "currentColor" ? resolveColor(node, UiProperties.color) ?? resolveColorValue(node, "text") : resolveColorValue(node, value),
|
|
20800
21149
|
gradient: (value) => resolveGradient(node, value)
|
|
20801
21150
|
};
|
|
20802
21151
|
}
|
|
@@ -20854,7 +21203,7 @@ function offscreenPaintCanvas(width, height) {
|
|
|
20854
21203
|
take: () => canvas.transferToImageBitmap()
|
|
20855
21204
|
};
|
|
20856
21205
|
}
|
|
20857
|
-
var PreciseContext = class {
|
|
21206
|
+
var PreciseContext = class PreciseContext {
|
|
20858
21207
|
inner;
|
|
20859
21208
|
dx = 0;
|
|
20860
21209
|
dy = 0;
|
|
@@ -20923,6 +21272,25 @@ var PreciseContext = class {
|
|
|
20923
21272
|
this.dy = 0;
|
|
20924
21273
|
this.inner.setTransform(a, b, c, d, e, f);
|
|
20925
21274
|
}
|
|
21275
|
+
/**
|
|
21276
|
+
* Takes another context's whole transform, a translation it holds
|
|
21277
|
+
* back included, so a group layer draws exactly where the frame would
|
|
21278
|
+
* have; less `offsetX`/`offsetY` device pixels, where the layer's
|
|
21279
|
+
* corner lies on the frame. Read through `getTransform`, a held
|
|
21280
|
+
* translation would come out as one large float, which is the
|
|
21281
|
+
* precision this class exists to keep.
|
|
21282
|
+
*/
|
|
21283
|
+
adoptTransform(from, offsetX = 0, offsetY = 0) {
|
|
21284
|
+
if (from instanceof PreciseContext) {
|
|
21285
|
+
const m = from.inner.getTransform();
|
|
21286
|
+
this.inner.setTransform(m.a, m.b, m.c, m.d, m.e - offsetX, m.f - offsetY);
|
|
21287
|
+
this.dx = from.dx;
|
|
21288
|
+
this.dy = from.dy;
|
|
21289
|
+
return;
|
|
21290
|
+
}
|
|
21291
|
+
const m = from.getTransform();
|
|
21292
|
+
this.setTransform(m.a, m.b, m.c, m.d, m.e - offsetX, m.f - offsetY);
|
|
21293
|
+
}
|
|
20926
21294
|
/** Writes a held translation out for real, for a transform it does not commute with. */
|
|
20927
21295
|
flush() {
|
|
20928
21296
|
if (this.holding) {
|
|
@@ -21445,11 +21813,11 @@ var ScrollLayerCache = class {
|
|
|
21445
21813
|
* all the way up.
|
|
21446
21814
|
*/
|
|
21447
21815
|
function dirtyAbove(frame, node) {
|
|
21448
|
-
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) if ((frame.dirtyFlagsFor(ancestor) & INHERITED) !== 0) return true;
|
|
21816
|
+
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) if ((frame.dirtyFlagsFor(ancestor) & INHERITED$1) !== 0) return true;
|
|
21449
21817
|
return false;
|
|
21450
21818
|
}
|
|
21451
21819
|
/** What an ancestor's dirt can say about how its descendants look. */
|
|
21452
|
-
const INHERITED = 192;
|
|
21820
|
+
const INHERITED$1 = 192;
|
|
21453
21821
|
/** Whether \`ancestor\` is above \`node\` in the tree. */
|
|
21454
21822
|
function contains(ancestor, node) {
|
|
21455
21823
|
for (let current = node.parent; current !== null; current = current.parent) if (current === ancestor) return true;
|
|
@@ -21618,7 +21986,7 @@ var TextRasterCache = class {
|
|
|
21618
21986
|
}
|
|
21619
21987
|
/** Lets go of text not drawn lately: removed, scrolled far away, or hidden. */
|
|
21620
21988
|
endFrame() {
|
|
21621
|
-
for (const [node, entry] of this.entries) if (this.frame - entry.used > IDLE_FRAMES) this.drop(node, entry);
|
|
21989
|
+
for (const [node, entry] of this.entries) if (this.frame - entry.used > IDLE_FRAMES$1) this.drop(node, entry);
|
|
21622
21990
|
}
|
|
21623
21991
|
dropAll() {
|
|
21624
21992
|
for (const [node, entry] of this.entries) this.drop(node, entry);
|
|
@@ -21674,10 +22042,10 @@ var TextRasterCache = class {
|
|
|
21674
22042
|
redraw(node, previous, paint, box, measurer, scaleX, scaleY, offsetX, offsetY, width, height, extent) {
|
|
21675
22043
|
let entry = previous;
|
|
21676
22044
|
if (entry !== void 0) {
|
|
21677
|
-
entry.churn = this.frame - entry.drawnAt <= CHURN_FRAMES ? entry.churn + 1 : 0;
|
|
21678
|
-
if (entry.churn >= CHURN_LIMIT) {
|
|
22045
|
+
entry.churn = this.frame - entry.drawnAt <= CHURN_FRAMES$1 ? entry.churn + 1 : 0;
|
|
22046
|
+
if (entry.churn >= CHURN_LIMIT$1) {
|
|
21679
22047
|
entry.churn = 0;
|
|
21680
|
-
entry.directUntil = this.frame + DIRECT_FRAMES;
|
|
22048
|
+
entry.directUntil = this.frame + DIRECT_FRAMES$1;
|
|
21681
22049
|
entry.used = this.frame;
|
|
21682
22050
|
this.stats.direct++;
|
|
21683
22051
|
return null;
|
|
@@ -21716,10 +22084,10 @@ var TextRasterCache = class {
|
|
|
21716
22084
|
}
|
|
21717
22085
|
/** Makes room for `needed` more pixels, letting go of what was drawn least recently. */
|
|
21718
22086
|
make(needed) {
|
|
21719
|
-
if (this.pixels + needed <= MAX_PIXELS) return;
|
|
22087
|
+
if (this.pixels + needed <= MAX_PIXELS$1) return;
|
|
21720
22088
|
const byAge = [...this.entries].sort((a, b) => a[1].used - b[1].used);
|
|
21721
22089
|
for (const [node, entry] of byAge) {
|
|
21722
|
-
if (this.pixels + needed <= MAX_PIXELS || entry.used === this.frame) break;
|
|
22090
|
+
if (this.pixels + needed <= MAX_PIXELS$1 || entry.used === this.frame) break;
|
|
21723
22091
|
this.drop(node, entry);
|
|
21724
22092
|
}
|
|
21725
22093
|
}
|
|
@@ -21826,16 +22194,612 @@ const SAME_PLACE = 1 / 256;
|
|
|
21826
22194
|
/** The largest single piece of text kept: a few screens of a long paragraph. */
|
|
21827
22195
|
const MAX_ENTRY_PIXELS = 4e6;
|
|
21828
22196
|
/** Everything kept, at most: about 128 MB. */
|
|
21829
|
-
const MAX_PIXELS = 32e6;
|
|
22197
|
+
const MAX_PIXELS$1 = 32e6;
|
|
21830
22198
|
/** Frames text is kept for without being drawn: about two seconds at 60 Hz. */
|
|
21831
|
-
const IDLE_FRAMES = 120;
|
|
22199
|
+
const IDLE_FRAMES$1 = 120;
|
|
21832
22200
|
/** Drawn again within this many frames of the last time counts toward churn. */
|
|
21833
|
-
const CHURN_FRAMES = 2;
|
|
22201
|
+
const CHURN_FRAMES$1 = 2;
|
|
21834
22202
|
/** Times in a row text may churn before it is drawn directly. */
|
|
21835
|
-
const CHURN_LIMIT = 3;
|
|
22203
|
+
const CHURN_LIMIT$1 = 3;
|
|
21836
22204
|
/** Frames churning text is drawn directly for before the cache tries it again. */
|
|
22205
|
+
const DIRECT_FRAMES$1 = 60;
|
|
22206
|
+
//#endregion
|
|
22207
|
+
//#region src/rendering/canvas2d/RasterCache.ts
|
|
22208
|
+
/**
|
|
22209
|
+
* Subtrees kept as pixels, for a node marked `rasterize`, and copied
|
|
22210
|
+
* onto the frame while only what is above them moves or scales.
|
|
22211
|
+
*
|
|
22212
|
+
* **Why.** A design tool's board is a camera over a few dozen frames,
|
|
22213
|
+
* each a whole screen of photographs, vector logos and text. Zooming
|
|
22214
|
+
* it changes nothing inside any frame, yet a full traversal draws every
|
|
22215
|
+
* one of them again on every step: in Atelier, twenty frames cost 10 to
|
|
22216
|
+
* 50 ms a step, most of it resampling the photographs and filling the
|
|
22217
|
+
* logos' paths at a scale they had not been drawn at before. Copied, a
|
|
22218
|
+
* frame is one `drawImage`.
|
|
22219
|
+
*
|
|
22220
|
+
* **What is kept.** What the node and its subtree paint, drawn once into
|
|
22221
|
+
* a canvas of its own at the device scale it is drawn at, over the
|
|
22222
|
+
* area its subtree may paint (its record's extent), relative to its own
|
|
22223
|
+
* corner: a node that moves without changing is copied where it is.
|
|
22224
|
+
*
|
|
22225
|
+
* **When it is drawn again.**
|
|
22226
|
+
*
|
|
22227
|
+
* - At once, when what it shows may have changed: anything in the
|
|
22228
|
+
* subtree was dirtied, an ancestor changed what it passes down, the
|
|
22229
|
+
* node changed size, or the frame was not vouched for.
|
|
22230
|
+
* - Once a zoom has settled, when the scale it is drawn at is not the
|
|
22231
|
+
* one it was kept at. While the scale keeps changing the pixels it
|
|
22232
|
+
* has are stretched, which is blurred or soft for a moment and keeps
|
|
22233
|
+
* the frame rate; when the scale has held for `SETTLE_MS` it is
|
|
22234
|
+
* drawn again, sharp, a few a frame, and `nextRepaint` asks the
|
|
22235
|
+
* runtime for the frame that does it, since nothing else would.
|
|
22236
|
+
* - Mid-zoom, when the stretch is past what still reads as the picture
|
|
22237
|
+
* (`MAX_STRETCH`) and the frame has the time left for it, by what it
|
|
22238
|
+
* took the last time; one that would drop the frame waits to settle.
|
|
22239
|
+
*
|
|
22240
|
+
* **When it is drawn directly instead.** A context that rotates or
|
|
22241
|
+
* skews; a node with a transform or a sticky shift of its own; a
|
|
22242
|
+
* subtree that would not fit in `MAX_RASTER_PIXELS` at the scale it is
|
|
22243
|
+
* drawn at (zoomed far into it, where only a part is on screen and
|
|
22244
|
+
* culling draws just that part); one whose pixels change without its
|
|
22245
|
+
* changes saying so (a caret, a fading scrollbar, a video) or which lifts
|
|
22246
|
+
* something to the top layer; and one drawn again on most of its recent
|
|
22247
|
+
* frames, which a cache would only draw twice.
|
|
22248
|
+
*/
|
|
22249
|
+
var RasterCache = class {
|
|
22250
|
+
createCanvas;
|
|
22251
|
+
settleMs;
|
|
22252
|
+
entries = /* @__PURE__ */ new Map();
|
|
22253
|
+
frame = 0;
|
|
22254
|
+
pixels = 0;
|
|
22255
|
+
open = false;
|
|
22256
|
+
/** Pixels drawn again this frame for a sharper scale. */
|
|
22257
|
+
spent = 0;
|
|
22258
|
+
/** The frame's own size in device pixels; nothing larger is kept. */
|
|
22259
|
+
surfacePixels = Infinity;
|
|
22260
|
+
wanted = void 0;
|
|
22261
|
+
/** Copies, draws into the cache, and draws left to the frame, since the last reset. For budget specs. */
|
|
22262
|
+
stats = {
|
|
22263
|
+
copied: 0,
|
|
22264
|
+
drawn: 0,
|
|
22265
|
+
direct: 0
|
|
22266
|
+
};
|
|
22267
|
+
constructor(createCanvas, settleMs = 120) {
|
|
22268
|
+
this.createCanvas = createCanvas;
|
|
22269
|
+
this.settleMs = settleMs;
|
|
22270
|
+
}
|
|
22271
|
+
/**
|
|
22272
|
+
* Marks what the frame's changes leave out of date, before anything is
|
|
22273
|
+
* drawn. A frame the caller could not vouch for, or one after a frame
|
|
22274
|
+
* that never finished, leaves everything out of date.
|
|
22275
|
+
*/
|
|
22276
|
+
beginFrame(changes, surfacePixels = Infinity) {
|
|
22277
|
+
this.surfacePixels = surfacePixels;
|
|
22278
|
+
const interrupted = this.open;
|
|
22279
|
+
this.open = true;
|
|
22280
|
+
this.frame++;
|
|
22281
|
+
this.spent = 0;
|
|
22282
|
+
this.wanted = void 0;
|
|
22283
|
+
if (this.entries.size === 0) return;
|
|
22284
|
+
if (interrupted || changes === void 0) {
|
|
22285
|
+
for (const entry of this.entries.values()) entry.stale = true;
|
|
22286
|
+
return;
|
|
22287
|
+
}
|
|
22288
|
+
const frame = changes.frame;
|
|
22289
|
+
for (const [node] of frame.entries()) for (let at = node; at !== null; at = at.parent) {
|
|
22290
|
+
const entry = this.entries.get(at);
|
|
22291
|
+
if (entry !== void 0) entry.stale = true;
|
|
22292
|
+
}
|
|
22293
|
+
for (const [node, entry] of this.entries) for (let ancestor = node.parent; ancestor !== null && !entry.stale; ancestor = ancestor.parent) if ((frame.dirtyFlagsFor(ancestor) & INHERITED) !== 0) entry.stale = true;
|
|
22294
|
+
}
|
|
22295
|
+
/** Lets go of what was not drawn lately: removed, culled for a while, or hidden. */
|
|
22296
|
+
endFrame() {
|
|
22297
|
+
this.open = false;
|
|
22298
|
+
for (const [node, entry] of this.entries) if (this.frame - entry.used > IDLE_FRAMES) this.drop(node, entry);
|
|
22299
|
+
}
|
|
22300
|
+
dropAll() {
|
|
22301
|
+
for (const [node, entry] of this.entries) this.drop(node, entry);
|
|
22302
|
+
}
|
|
22303
|
+
resetStats() {
|
|
22304
|
+
this.stats.copied = 0;
|
|
22305
|
+
this.stats.drawn = 0;
|
|
22306
|
+
this.stats.direct = 0;
|
|
22307
|
+
}
|
|
22308
|
+
/**
|
|
22309
|
+
* When the frame just drawn would like another though nothing changes:
|
|
22310
|
+
* a zoom settling, whose kept pixels are then drawn again at the scale
|
|
22311
|
+
* it stopped at. Undefined when nothing waits.
|
|
22312
|
+
*/
|
|
22313
|
+
get nextRepaint() {
|
|
22314
|
+
return this.wanted;
|
|
22315
|
+
}
|
|
22316
|
+
/**
|
|
22317
|
+
* Draws a node and its subtree from the cache, drawing them into it
|
|
22318
|
+
* first when what it holds is out of date. `paint` draws them into the
|
|
22319
|
+
* context it is handed, already placed, and answers whether what it
|
|
22320
|
+
* drew may be kept (see the class comment). False when the node should
|
|
22321
|
+
* be drawn directly.
|
|
22322
|
+
*/
|
|
22323
|
+
draw(ctx, node, rec, now, paint) {
|
|
22324
|
+
const m = ctx.getTransform?.();
|
|
22325
|
+
if (m === void 0 || m.b !== 0 || m.c !== 0 || !(m.a > 0) || !(m.d > 0)) return false;
|
|
22326
|
+
let entry = this.entries.get(node);
|
|
22327
|
+
if (entry !== void 0 && entry.directUntil > this.frame) {
|
|
22328
|
+
entry.used = this.frame;
|
|
22329
|
+
this.stats.direct++;
|
|
22330
|
+
return false;
|
|
22331
|
+
}
|
|
22332
|
+
const left = rec.extentMinX - rec.x - SPARE;
|
|
22333
|
+
const top = rec.extentMinY - rec.y - SPARE;
|
|
22334
|
+
const width = rec.extentMaxX - rec.extentMinX + 2 * SPARE;
|
|
22335
|
+
const height = rec.extentMaxY - rec.extentMinY + 2 * SPARE;
|
|
22336
|
+
const fits = (scaleX, scaleY) => {
|
|
22337
|
+
const canvasWidth = Math.ceil(width * scaleX) + 1;
|
|
22338
|
+
const canvasHeight = Math.ceil(height * scaleY) + 1;
|
|
22339
|
+
const pixels = canvasWidth * canvasHeight;
|
|
22340
|
+
return canvasWidth <= MAX_SIDE && canvasHeight <= MAX_SIDE && pixels <= MAX_RASTER_PIXELS && pixels <= this.surfacePixels;
|
|
22341
|
+
};
|
|
22342
|
+
if (!(width > 0) || !(height > 0) || !fits(m.a, m.d)) {
|
|
22343
|
+
if (entry !== void 0) entry.used = this.frame;
|
|
22344
|
+
this.stats.direct++;
|
|
22345
|
+
return false;
|
|
22346
|
+
}
|
|
22347
|
+
const x = m.a * (rec.x + left) + m.e;
|
|
22348
|
+
const y = m.d * (rec.y + top) + m.f;
|
|
22349
|
+
if (entry === void 0 || entry.stale || entry.width !== width || entry.height !== height || entry.left !== left || entry.top !== top) {
|
|
22350
|
+
if (entry !== void 0) {
|
|
22351
|
+
entry.churn = this.frame - entry.changedAt <= CHURN_FRAMES ? entry.churn + 1 : 0;
|
|
22352
|
+
if (entry.churn >= CHURN_LIMIT) {
|
|
22353
|
+
entry.churn = 0;
|
|
22354
|
+
entry.directUntil = this.frame + DIRECT_FRAMES;
|
|
22355
|
+
entry.used = this.frame;
|
|
22356
|
+
this.stats.direct++;
|
|
22357
|
+
return false;
|
|
22358
|
+
}
|
|
22359
|
+
}
|
|
22360
|
+
const drawn = this.redraw(node, entry, m.a, m.d, x, y, rec.x + left, rec.y + top, left, top, width, height, paint);
|
|
22361
|
+
if (drawn === null) {
|
|
22362
|
+
this.stats.direct++;
|
|
22363
|
+
return false;
|
|
22364
|
+
}
|
|
22365
|
+
drawn.changedAt = this.frame;
|
|
22366
|
+
entry = drawn;
|
|
22367
|
+
} else if (entry.scaleX !== m.a || entry.scaleY !== m.d) entry = this.resharpen(node, entry, m.a, m.d, x, y, rec.x + left, rec.y + top, now, paint);
|
|
22368
|
+
const raster = entry;
|
|
22369
|
+
raster.used = this.frame;
|
|
22370
|
+
ctx.save();
|
|
22371
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
22372
|
+
if (raster.scaleX === m.a && raster.scaleY === m.d) ctx.drawImage(raster.host, Math.round(x - raster.fractionX), Math.round(y - raster.fractionY), raster.canvasWidth, raster.canvasHeight);
|
|
22373
|
+
else {
|
|
22374
|
+
const stretchX = m.a / raster.scaleX;
|
|
22375
|
+
const stretchY = m.d / raster.scaleY;
|
|
22376
|
+
ctx.drawImage(raster.host, x - raster.fractionX * stretchX, y - raster.fractionY * stretchY, raster.canvasWidth * stretchX, raster.canvasHeight * stretchY);
|
|
22377
|
+
}
|
|
22378
|
+
ctx.restore();
|
|
22379
|
+
this.stats.copied++;
|
|
22380
|
+
return true;
|
|
22381
|
+
}
|
|
22382
|
+
/**
|
|
22383
|
+
* A subtree kept at another scale than it is drawn at: stretched while
|
|
22384
|
+
* the scale is moving, drawn again once it has settled or when it is
|
|
22385
|
+
* stretched too far, as the frame's budget allows.
|
|
22386
|
+
*/
|
|
22387
|
+
resharpen(node, entry, scaleX, scaleY, x, y, cornerX, cornerY, now, paint) {
|
|
22388
|
+
if (entry.seenX !== scaleX || entry.seenY !== scaleY) {
|
|
22389
|
+
entry.seenX = scaleX;
|
|
22390
|
+
entry.seenY = scaleY;
|
|
22391
|
+
entry.seenAt = now;
|
|
22392
|
+
}
|
|
22393
|
+
const stretch = Math.max(scaleX / entry.scaleX, scaleY / entry.scaleY, entry.scaleX / scaleX, entry.scaleY / scaleY);
|
|
22394
|
+
const settled = now - entry.seenAt >= this.settleMs;
|
|
22395
|
+
const pixels = (Math.ceil(entry.width * scaleX) + 1) * (Math.ceil(entry.height * scaleY) + 1);
|
|
22396
|
+
if (settled ? this.spent === 0 || this.spent + pixels <= SETTLED_PIXELS : stretch > MAX_STRETCH && this.spent + pixels <= MOVING_PIXELS) {
|
|
22397
|
+
this.spent += pixels;
|
|
22398
|
+
const drawn = this.redraw(node, entry, scaleX, scaleY, x, y, cornerX, cornerY, entry.left, entry.top, entry.width, entry.height, paint);
|
|
22399
|
+
if (drawn !== null) return drawn;
|
|
22400
|
+
return this.entries.get(node) ?? entry;
|
|
22401
|
+
}
|
|
22402
|
+
this.want(settled ? now + 1 : entry.seenAt + this.settleMs);
|
|
22403
|
+
return entry;
|
|
22404
|
+
}
|
|
22405
|
+
want(at) {
|
|
22406
|
+
this.wanted = this.wanted === void 0 ? at : Math.min(this.wanted, at);
|
|
22407
|
+
}
|
|
22408
|
+
/**
|
|
22409
|
+
* Draws the subtree into the node's canvas at a scale, its area's
|
|
22410
|
+
* corner at (`cornerX`, `cornerY`) in layout pixels, with the
|
|
22411
|
+
* fraction of a device pixel its corner lands on, so a copy at that
|
|
22412
|
+
* scale is the direct draw's pixels. Null when no canvas could be had,
|
|
22413
|
+
* or when what `paint` drew may not be kept; the entry then draws
|
|
22414
|
+
* directly for a while, and this frame copies what was drawn.
|
|
22415
|
+
*/
|
|
22416
|
+
redraw(node, previous, scaleX, scaleY, x, y, cornerX, cornerY, left, top, width, height, paint) {
|
|
22417
|
+
const canvasWidth = Math.ceil(width * scaleX) + 1;
|
|
22418
|
+
const canvasHeight = Math.ceil(height * scaleY) + 1;
|
|
22419
|
+
let entry = previous;
|
|
22420
|
+
if (entry !== void 0) entry.used = this.frame;
|
|
22421
|
+
if (entry === void 0 || entry.canvasWidth !== canvasWidth || entry.canvasHeight !== canvasHeight) {
|
|
22422
|
+
const growth = canvasWidth * canvasHeight - (entry === void 0 ? 0 : entry.canvasWidth * entry.canvasHeight);
|
|
22423
|
+
if (!this.make(growth)) return null;
|
|
22424
|
+
if (entry === void 0) {
|
|
22425
|
+
const host = this.createCanvas(canvasWidth, canvasHeight);
|
|
22426
|
+
const raw = host?.getContext("2d");
|
|
22427
|
+
if (host === null || raw === null || raw === void 0) return null;
|
|
22428
|
+
entry = new Raster(host, raw);
|
|
22429
|
+
this.entries.set(node, entry);
|
|
22430
|
+
}
|
|
22431
|
+
entry.host.width = canvasWidth;
|
|
22432
|
+
entry.host.height = canvasHeight;
|
|
22433
|
+
this.pixels += growth;
|
|
22434
|
+
entry.canvasWidth = canvasWidth;
|
|
22435
|
+
entry.canvasHeight = canvasHeight;
|
|
22436
|
+
} else {
|
|
22437
|
+
entry.raw.setTransform(1, 0, 0, 1, 0, 0);
|
|
22438
|
+
entry.raw.clearRect(0, 0, canvasWidth, canvasHeight);
|
|
22439
|
+
}
|
|
22440
|
+
const fractionX = x - Math.floor(x);
|
|
22441
|
+
const fractionY = y - Math.floor(y);
|
|
22442
|
+
const target = entry.ctx.reset(entry.raw);
|
|
22443
|
+
target.setTransform(scaleX, 0, 0, scaleY, fractionX - scaleX * cornerX, fractionY - scaleY * cornerY);
|
|
22444
|
+
target.globalAlpha = 1;
|
|
22445
|
+
const kept = paint(target, canvasWidth, canvasHeight);
|
|
22446
|
+
entry.scaleX = entry.seenX = scaleX;
|
|
22447
|
+
entry.scaleY = entry.seenY = scaleY;
|
|
22448
|
+
entry.fractionX = fractionX;
|
|
22449
|
+
entry.fractionY = fractionY;
|
|
22450
|
+
entry.left = left;
|
|
22451
|
+
entry.top = top;
|
|
22452
|
+
entry.width = width;
|
|
22453
|
+
entry.height = height;
|
|
22454
|
+
entry.stale = false;
|
|
22455
|
+
this.stats.drawn++;
|
|
22456
|
+
if (!kept) {
|
|
22457
|
+
entry.directUntil = this.frame + DIRECT_FRAMES;
|
|
22458
|
+
entry.stale = true;
|
|
22459
|
+
}
|
|
22460
|
+
return entry;
|
|
22461
|
+
}
|
|
22462
|
+
/** Makes room for `needed` more pixels, letting go of what was drawn least recently; false when it cannot. */
|
|
22463
|
+
make(needed) {
|
|
22464
|
+
if (this.pixels + needed <= MAX_PIXELS) return true;
|
|
22465
|
+
const byAge = [...this.entries].sort((a, b) => a[1].used - b[1].used);
|
|
22466
|
+
for (const [node, entry] of byAge) {
|
|
22467
|
+
if (this.pixels + needed <= MAX_PIXELS || entry.used === this.frame) break;
|
|
22468
|
+
this.drop(node, entry);
|
|
22469
|
+
}
|
|
22470
|
+
return this.pixels + needed <= MAX_PIXELS;
|
|
22471
|
+
}
|
|
22472
|
+
drop(node, entry) {
|
|
22473
|
+
this.entries.delete(node);
|
|
22474
|
+
this.pixels -= entry.canvasWidth * entry.canvasHeight;
|
|
22475
|
+
entry.host.width = 0;
|
|
22476
|
+
entry.host.height = 0;
|
|
22477
|
+
}
|
|
22478
|
+
};
|
|
22479
|
+
/** One subtree as pixels, and what they were drawn for. */
|
|
22480
|
+
var Raster = class {
|
|
22481
|
+
host;
|
|
22482
|
+
raw;
|
|
22483
|
+
ctx;
|
|
22484
|
+
canvasWidth = 0;
|
|
22485
|
+
canvasHeight = 0;
|
|
22486
|
+
/** The device scale it was drawn at, and the fraction of a pixel its corner sat at. */
|
|
22487
|
+
scaleX = 0;
|
|
22488
|
+
scaleY = 0;
|
|
22489
|
+
fractionX = 0;
|
|
22490
|
+
fractionY = 0;
|
|
22491
|
+
/** The area kept, from the node's corner, in layout pixels. */
|
|
22492
|
+
left = 0;
|
|
22493
|
+
top = 0;
|
|
22494
|
+
width = 0;
|
|
22495
|
+
height = 0;
|
|
22496
|
+
/** The scale it was last drawn at on the frame, and since when. */
|
|
22497
|
+
seenX = 0;
|
|
22498
|
+
seenY = 0;
|
|
22499
|
+
seenAt = 0;
|
|
22500
|
+
stale = false;
|
|
22501
|
+
used = 0;
|
|
22502
|
+
changedAt = 0;
|
|
22503
|
+
churn = 0;
|
|
22504
|
+
directUntil = 0;
|
|
22505
|
+
constructor(host, raw) {
|
|
22506
|
+
this.host = host;
|
|
22507
|
+
this.raw = raw;
|
|
22508
|
+
this.ctx = new PreciseContext(raw);
|
|
22509
|
+
}
|
|
22510
|
+
};
|
|
22511
|
+
/** What an ancestor's dirt can say about how its descendants look. */
|
|
22512
|
+
const INHERITED = 192;
|
|
22513
|
+
/** How far kept pixels are stretched, either way, before they are drawn again mid-zoom. */
|
|
22514
|
+
const MAX_STRETCH = 3;
|
|
22515
|
+
/** How many pixels a frame draws again for sharpness while a zoom moves: a phone frame or two. */
|
|
22516
|
+
const MOVING_PIXELS = 5e5;
|
|
22517
|
+
/** The same once it has settled: about a screen's worth, and the rest the next frame. */
|
|
22518
|
+
const SETTLED_PIXELS = 8e6;
|
|
22519
|
+
/** Layout pixels kept past the extent on each side, for antialiasing. */
|
|
22520
|
+
const SPARE = 2;
|
|
22521
|
+
/** The most one subtree is kept at: 4096², 64 MB. */
|
|
22522
|
+
const MAX_RASTER_PIXELS = 16777216;
|
|
22523
|
+
/** The longest side a canvas is made with; engines refuse longer. */
|
|
22524
|
+
const MAX_SIDE = 8192;
|
|
22525
|
+
/** The most all of them are kept at: 192 MB. */
|
|
22526
|
+
const MAX_PIXELS = 50331648;
|
|
22527
|
+
/**
|
|
22528
|
+
* Frames an entry is kept without being drawn: long, since a zoom in
|
|
22529
|
+
* culls what a zoom back out shows again, and drawing all of that anew
|
|
22530
|
+
* on its way back is the stutter the cache is for. \`MAX_PIXELS\` is
|
|
22531
|
+
* what bounds the memory.
|
|
22532
|
+
*/
|
|
22533
|
+
const IDLE_FRAMES = 3600;
|
|
22534
|
+
/** Out of date this many frames apart, `CHURN_LIMIT` times running, and it is drawn directly. */
|
|
22535
|
+
const CHURN_FRAMES = 3;
|
|
22536
|
+
const CHURN_LIMIT = 3;
|
|
22537
|
+
/** How long a subtree that churned, or that cannot be kept, is drawn directly. */
|
|
21837
22538
|
const DIRECT_FRAMES = 60;
|
|
21838
22539
|
//#endregion
|
|
22540
|
+
//#region src/rendering/GroupOpacity.ts
|
|
22541
|
+
/**
|
|
22542
|
+
* Group opacity: `opacity` composites a node and its subtree as one
|
|
22543
|
+
* picture, as CSS does.
|
|
22544
|
+
*
|
|
22545
|
+
* A node at half opacity with a blue background and a white label is
|
|
22546
|
+
* a faded button whose label is as white as the button is faded: the
|
|
22547
|
+
* subtree is drawn opaque, then blended once. Multiplying the opacity
|
|
22548
|
+
* into each draw instead fades the label twice, once in its own draw
|
|
22549
|
+
* and again through the half-transparent background it lands on, and
|
|
22550
|
+
* a disabled button's label came out (202,207,213) over a white page
|
|
22551
|
+
* where CSS and Figma give (255,255,255).
|
|
22552
|
+
*
|
|
22553
|
+
* The two only agree when nothing in the subtree overlaps anything
|
|
22554
|
+
* else in it, and the one case that is cheap to know is the one that
|
|
22555
|
+
* matters: everything the subtree paints is a single draw. A faded
|
|
22556
|
+
* icon, a faded line of text, a fading box. That keeps multiplying,
|
|
22557
|
+
* which costs nothing; everything else gets a layer — an offscreen
|
|
22558
|
+
* canvas in Canvas2D, a render pass into a texture in WebGPU — and the
|
|
22559
|
+
* layer is blended at the opacity. The rule is shared so the two
|
|
22560
|
+
* backends agree on which nodes get one.
|
|
22561
|
+
*/
|
|
22562
|
+
/** A second scratch, so probing a child never overwrites the walk's. */
|
|
22563
|
+
const probe = createPaintState();
|
|
22564
|
+
/** Whether a node at `opacity < 1` must be drawn into a layer and blended once. */
|
|
22565
|
+
function opacityNeedsLayer(node, rec, paint, layout) {
|
|
22566
|
+
return !paintsOnce(node, rec, paint, layout);
|
|
22567
|
+
}
|
|
22568
|
+
/**
|
|
22569
|
+
* Whether a subtree paints at most one draw, so that blending each draw
|
|
22570
|
+
* at the opacity is the same as blending the whole at it.
|
|
22571
|
+
*
|
|
22572
|
+
* Walks down a chain of nodes that paint nothing themselves and hold
|
|
22573
|
+
* one child each, the shape a wrapper takes, and stops at the first
|
|
22574
|
+
* node that paints or branches. Never longer than that chain.
|
|
22575
|
+
*/
|
|
22576
|
+
function paintsOnce(node, rec, paint, layout) {
|
|
22577
|
+
const own = ownDraws(node, rec, paint);
|
|
22578
|
+
if (own > 1) return false;
|
|
22579
|
+
const child = onlyChild(node, layout);
|
|
22580
|
+
if (child === null) return true;
|
|
22581
|
+
if (child === void 0 || own > 0) return false;
|
|
22582
|
+
const childRec = layout.recordFor(child);
|
|
22583
|
+
return !childRec.lifted && paintsOnce(child, childRec, resolvePaintState(child, probe), layout);
|
|
22584
|
+
}
|
|
22585
|
+
/**
|
|
22586
|
+
* The one child that may paint, null for none, undefined for more than
|
|
22587
|
+
* one. Through fragments, which have no box; skipping what is hidden.
|
|
22588
|
+
*/
|
|
22589
|
+
function onlyChild(node, layout) {
|
|
22590
|
+
let found = null;
|
|
22591
|
+
for (let child = node.firstChild; child !== null; child = child.nextSibling) {
|
|
22592
|
+
if (child.type === "fragment") {
|
|
22593
|
+
const inner = onlyChild(child, layout);
|
|
22594
|
+
if (inner === void 0 || inner !== null && found !== null) return;
|
|
22595
|
+
found = inner ?? found;
|
|
22596
|
+
continue;
|
|
22597
|
+
}
|
|
22598
|
+
if (layout.recordFor(child) === void 0 || !isPaintVisible(child)) continue;
|
|
22599
|
+
if (found !== null) return;
|
|
22600
|
+
found = child;
|
|
22601
|
+
}
|
|
22602
|
+
return found;
|
|
22603
|
+
}
|
|
22604
|
+
/**
|
|
22605
|
+
* How many draws a node makes of its own, counted only as far as two:
|
|
22606
|
+
* past one, the answer is already a layer.
|
|
22607
|
+
*/
|
|
22608
|
+
function ownDraws(node, rec, paint) {
|
|
22609
|
+
let draws = paint.boxShadows.length;
|
|
22610
|
+
if (paint.backgroundColor !== void 0) draws++;
|
|
22611
|
+
if (paint.backgroundGradient !== void 0) draws++;
|
|
22612
|
+
if (paint.image !== void 0 || paint.video !== void 0 || node.type === "paint") draws++;
|
|
22613
|
+
if (paint.borderWidth > 0 && paint.borderColor !== void 0) draws++;
|
|
22614
|
+
if (node.decorations !== null) draws += node.decorations.length;
|
|
22615
|
+
if (rec.scrollable || rec.textScrollbars) draws++;
|
|
22616
|
+
if (paint.editor !== void 0) draws += 2;
|
|
22617
|
+
else if (paint.text !== void 0 && paint.text.length > 0) {
|
|
22618
|
+
const plain = paint.textSelection === void 0 && paint.textMatches === void 0 && paint.spans === void 0 && paint.textDecoration === "none";
|
|
22619
|
+
draws += plain ? 1 : 2;
|
|
22620
|
+
}
|
|
22621
|
+
return draws;
|
|
22622
|
+
}
|
|
22623
|
+
/**
|
|
22624
|
+
* How far past its device-pixel bounds a layer's region reaches: the
|
|
22625
|
+
* antialiased fringe of an edge, a scroll offset snapped to a device
|
|
22626
|
+
* pixel, and the ink of a glyph that overhangs its line box.
|
|
22627
|
+
*/
|
|
22628
|
+
const REGION_SLOP = 2;
|
|
22629
|
+
/**
|
|
22630
|
+
* The part of the surface an opacity group can paint, in whole device
|
|
22631
|
+
* pixels: where its layer is drawn and blended.
|
|
22632
|
+
*
|
|
22633
|
+
* Only that much, because a layer the size of the surface is what a
|
|
22634
|
+
* small faded button cannot afford. Blending one onto the frame costs a
|
|
22635
|
+
* copy of the whole layer in Chrome's Canvas2D, about a millisecond and
|
|
22636
|
+
* a half at 2400×1800 per group, while a layer the button's size costs
|
|
22637
|
+
* hundredths of one.
|
|
22638
|
+
*
|
|
22639
|
+
* Worked out before the subtree is drawn, from its records: every box
|
|
22640
|
+
* with its outer shadows, every decoration, through every transform,
|
|
22641
|
+
* sticky shift and scroll offset on the way down, cut to every clip on
|
|
22642
|
+
* the way down except for a lifted node, which escapes them. That is
|
|
22643
|
+
* where the backends draw, so nothing drawn lies outside it but text
|
|
22644
|
+
* overflowing its own box, which the region's slop does not cover past a
|
|
22645
|
+
* glyph's overhang; layout sizes a text node to its text, so that is a
|
|
22646
|
+
* box narrower than its words. `m` is the matrix the node is drawn
|
|
22647
|
+
* under, its own transform included. Clipped to the surface, and empty
|
|
22648
|
+
* when the group is entirely off it.
|
|
22649
|
+
*/
|
|
22650
|
+
function layerRegion(node, rec, m, layout, surfaceWidth, surfaceHeight, dpr) {
|
|
22651
|
+
bounds.minX = Infinity;
|
|
22652
|
+
bounds.minY = Infinity;
|
|
22653
|
+
bounds.maxX = -Infinity;
|
|
22654
|
+
bounds.maxY = -Infinity;
|
|
22655
|
+
includeSubtree(node, rec, [
|
|
22656
|
+
m.a,
|
|
22657
|
+
m.b,
|
|
22658
|
+
m.c,
|
|
22659
|
+
m.d,
|
|
22660
|
+
m.e,
|
|
22661
|
+
m.f
|
|
22662
|
+
], null, layout, dpr);
|
|
22663
|
+
const left = Math.max(0, Math.floor(bounds.minX) - REGION_SLOP);
|
|
22664
|
+
const top = Math.max(0, Math.floor(bounds.minY) - REGION_SLOP);
|
|
22665
|
+
const right = Math.min(surfaceWidth, Math.ceil(bounds.maxX) + REGION_SLOP);
|
|
22666
|
+
const bottom = Math.min(surfaceHeight, Math.ceil(bounds.maxY) + REGION_SLOP);
|
|
22667
|
+
if (right <= left || bottom <= top) return {
|
|
22668
|
+
x: 0,
|
|
22669
|
+
y: 0,
|
|
22670
|
+
width: 0,
|
|
22671
|
+
height: 0
|
|
22672
|
+
};
|
|
22673
|
+
return {
|
|
22674
|
+
x: left,
|
|
22675
|
+
y: top,
|
|
22676
|
+
width: right - left,
|
|
22677
|
+
height: bottom - top
|
|
22678
|
+
};
|
|
22679
|
+
}
|
|
22680
|
+
/** What a region walk has covered so far; module scratch, as a walk is not re-entrant. */
|
|
22681
|
+
const bounds = {
|
|
22682
|
+
minX: Infinity,
|
|
22683
|
+
minY: Infinity,
|
|
22684
|
+
maxX: -Infinity,
|
|
22685
|
+
maxY: -Infinity
|
|
22686
|
+
};
|
|
22687
|
+
function includeSubtree(node, rec, m, clip, layout, dpr) {
|
|
22688
|
+
includeRect(m, rec.x - rec.paintReachLeft, rec.y - rec.paintReachTop, rec.width + rec.paintReachLeft + rec.paintReachRight, rec.height + rec.paintReachTop + rec.paintReachBottom, clip);
|
|
22689
|
+
if (node.decorations !== null) for (const shape of node.decorations) {
|
|
22690
|
+
const r = decorationRect(shape, rec, 0);
|
|
22691
|
+
const reach = shape.kind === "stroke" ? shape.lineWidth : 0;
|
|
22692
|
+
includeRect(m, r.x - reach, r.y - reach, r.width + reach * 2, r.height + reach * 2, clip);
|
|
22693
|
+
}
|
|
22694
|
+
if (node.firstChild === null) return;
|
|
22695
|
+
const childClip = rec.clips ? intersect(clip, rectThrough(m, rec.x, rec.y, rec.width, rec.height)) : clip;
|
|
22696
|
+
includeChildren(node, rec.scrollable ? multiply(m, [
|
|
22697
|
+
1,
|
|
22698
|
+
0,
|
|
22699
|
+
0,
|
|
22700
|
+
1,
|
|
22701
|
+
-snapScroll(rec.scrollX, dpr),
|
|
22702
|
+
-snapScroll(rec.scrollY, dpr)
|
|
22703
|
+
]) : m, childClip, layout, dpr);
|
|
22704
|
+
}
|
|
22705
|
+
function includeChildren(node, m, clip, layout, dpr) {
|
|
22706
|
+
for (let child = node.firstChild; child !== null; child = child.nextSibling) {
|
|
22707
|
+
if (child.type === "fragment") {
|
|
22708
|
+
includeChildren(child, m, clip, layout, dpr);
|
|
22709
|
+
continue;
|
|
22710
|
+
}
|
|
22711
|
+
const rec = layout.recordFor(child);
|
|
22712
|
+
if (rec === void 0 || !isPaintVisible(child)) continue;
|
|
22713
|
+
let cm = m;
|
|
22714
|
+
if (rec.stickyOffsetX !== 0 || rec.stickyOffsetY !== 0) cm = multiply(cm, [
|
|
22715
|
+
1,
|
|
22716
|
+
0,
|
|
22717
|
+
0,
|
|
22718
|
+
1,
|
|
22719
|
+
rec.stickyOffsetX,
|
|
22720
|
+
rec.stickyOffsetY
|
|
22721
|
+
]);
|
|
22722
|
+
const raw = child.properties.get("transform");
|
|
22723
|
+
const transform = raw === void 0 ? void 0 : parseTransform(raw);
|
|
22724
|
+
if (transform !== void 0) cm = multiply(cm, ownTransform(rec, transform));
|
|
22725
|
+
includeSubtree(child, rec, cm, rec.lifted ? null : clip, layout, dpr);
|
|
22726
|
+
}
|
|
22727
|
+
}
|
|
22728
|
+
/**
|
|
22729
|
+
* A node's own transform, as both backends apply it: translated, then
|
|
22730
|
+
* turned and scaled about its origin. See `UiTransform`.
|
|
22731
|
+
*/
|
|
22732
|
+
function ownTransform(rec, t) {
|
|
22733
|
+
const originX = rec.x + t.x;
|
|
22734
|
+
const originY = rec.y + t.y;
|
|
22735
|
+
const cos = Math.cos(t.rotation);
|
|
22736
|
+
const sin = Math.sin(t.rotation);
|
|
22737
|
+
const a = cos * t.scaleX;
|
|
22738
|
+
const b = sin * t.scaleX;
|
|
22739
|
+
const c = -sin * t.scaleY;
|
|
22740
|
+
const d = cos * t.scaleY;
|
|
22741
|
+
return [
|
|
22742
|
+
a,
|
|
22743
|
+
b,
|
|
22744
|
+
c,
|
|
22745
|
+
d,
|
|
22746
|
+
originX - originX * a - originY * c + t.translateX,
|
|
22747
|
+
originY - originX * b - originY * d + t.translateY
|
|
22748
|
+
];
|
|
22749
|
+
}
|
|
22750
|
+
function multiply(m, n) {
|
|
22751
|
+
return [
|
|
22752
|
+
m[0] * n[0] + m[2] * n[1],
|
|
22753
|
+
m[1] * n[0] + m[3] * n[1],
|
|
22754
|
+
m[0] * n[2] + m[2] * n[3],
|
|
22755
|
+
m[1] * n[2] + m[3] * n[3],
|
|
22756
|
+
m[0] * n[4] + m[2] * n[5] + m[4],
|
|
22757
|
+
m[1] * n[4] + m[3] * n[5] + m[5]
|
|
22758
|
+
];
|
|
22759
|
+
}
|
|
22760
|
+
/** The device-pixel bounds of a rectangle through a matrix. */
|
|
22761
|
+
function rectThrough(m, x, y, width, height) {
|
|
22762
|
+
const x1 = x + width;
|
|
22763
|
+
const y1 = y + height;
|
|
22764
|
+
const ax = m[0] * x + m[2] * y + m[4];
|
|
22765
|
+
const ay = m[1] * x + m[3] * y + m[5];
|
|
22766
|
+
const bx = m[0] * x1 + m[2] * y + m[4];
|
|
22767
|
+
const by = m[1] * x1 + m[3] * y + m[5];
|
|
22768
|
+
const cx = m[0] * x + m[2] * y1 + m[4];
|
|
22769
|
+
const cy = m[1] * x + m[3] * y1 + m[5];
|
|
22770
|
+
const dx = m[0] * x1 + m[2] * y1 + m[4];
|
|
22771
|
+
const dy = m[1] * x1 + m[3] * y1 + m[5];
|
|
22772
|
+
const left = Math.min(ax, bx, cx, dx);
|
|
22773
|
+
const top = Math.min(ay, by, cy, dy);
|
|
22774
|
+
return {
|
|
22775
|
+
x: left,
|
|
22776
|
+
y: top,
|
|
22777
|
+
width: Math.max(ax, bx, cx, dx) - left,
|
|
22778
|
+
height: Math.max(ay, by, cy, dy) - top
|
|
22779
|
+
};
|
|
22780
|
+
}
|
|
22781
|
+
function intersect(a, b) {
|
|
22782
|
+
if (a === null) return b;
|
|
22783
|
+
const left = Math.max(a.x, b.x);
|
|
22784
|
+
const top = Math.max(a.y, b.y);
|
|
22785
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
22786
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
22787
|
+
return {
|
|
22788
|
+
x: left,
|
|
22789
|
+
y: top,
|
|
22790
|
+
width: Math.max(0, right - left),
|
|
22791
|
+
height: Math.max(0, bottom - top)
|
|
22792
|
+
};
|
|
22793
|
+
}
|
|
22794
|
+
function includeRect(m, x, y, width, height, clip) {
|
|
22795
|
+
const r = clip === null ? rectThrough(m, x, y, width, height) : intersect(clip, rectThrough(m, x, y, width, height));
|
|
22796
|
+
if (r.width <= 0 || r.height <= 0) return;
|
|
22797
|
+
bounds.minX = Math.min(bounds.minX, r.x);
|
|
22798
|
+
bounds.minY = Math.min(bounds.minY, r.y);
|
|
22799
|
+
bounds.maxX = Math.max(bounds.maxX, r.x + r.width);
|
|
22800
|
+
bounds.maxY = Math.max(bounds.maxY, r.y + r.height);
|
|
22801
|
+
}
|
|
22802
|
+
//#endregion
|
|
21839
22803
|
//#region src/rendering/canvas2d/Canvas2DRenderer.ts
|
|
21840
22804
|
/**
|
|
21841
22805
|
* Canvas2D rendering backend.
|
|
@@ -21862,6 +22826,12 @@ const DIRECT_FRAMES = 60;
|
|
|
21862
22826
|
* containers clip to their viewport and translate by
|
|
21863
22827
|
* (-scrollX, -scrollY); node transforms compose on the current CTM.
|
|
21864
22828
|
*
|
|
22829
|
+
* Opacity groups: a node at `opacity < 1` whose subtree paints more
|
|
22830
|
+
* than one draw is drawn into an offscreen layer and blended onto the
|
|
22831
|
+
* frame once, as CSS composites it (`GroupOpacity.ts`). One that
|
|
22832
|
+
* paints a single draw multiplies `globalAlpha` instead, which is the
|
|
22833
|
+
* same picture for nothing.
|
|
22834
|
+
*
|
|
21865
22835
|
* State isolation: save()/restore() are used exactly when a node
|
|
21866
22836
|
* introduces opacity, a transform, or a scroll viewport. Flat style
|
|
21867
22837
|
* fields (fillStyle, font, lineWidth, ...) are assigned before each
|
|
@@ -21944,10 +22914,38 @@ var Canvas2DRenderer = class {
|
|
|
21944
22914
|
* not keep what it drew if it moved.
|
|
21945
22915
|
*/
|
|
21946
22916
|
volatileDraws = 0;
|
|
22917
|
+
/**
|
|
22918
|
+
* The canvases opacity groups are drawn in, one per depth of nesting,
|
|
22919
|
+
* so a group inside a group never draws over what holds it. Each
|
|
22920
|
+
* group sizes the one at its depth to its region (`layerRegion`).
|
|
22921
|
+
*/
|
|
22922
|
+
groups = [];
|
|
22923
|
+
/** How many groups the walk is inside. */
|
|
22924
|
+
groupDepth = 0;
|
|
21947
22925
|
constructor(options) {
|
|
21948
22926
|
this.options = options;
|
|
21949
22927
|
this.layers = new ScrollLayerCache(options.createLayerCanvas ?? offscreenLayerCanvas, options.scrollLayerSettleFrames);
|
|
21950
22928
|
this.textCache = options.textCache === false ? null : new TextRasterCache(options.createLayerCanvas ?? offscreenLayerCanvas);
|
|
22929
|
+
this.rasters = options.rasterCache === false ? null : new RasterCache(options.createLayerCanvas ?? offscreenLayerCanvas, options.rasterSettleMs);
|
|
22930
|
+
}
|
|
22931
|
+
/** Subtrees marked \`rasterize\` kept as pixels; see \`RasterCache.ts\`. */
|
|
22932
|
+
rasters;
|
|
22933
|
+
/** How deep inside a kept subtree's own walk the traversal is. One is not kept inside another. */
|
|
22934
|
+
rasterDepth = 0;
|
|
22935
|
+
/**
|
|
22936
|
+
* The device pixels the walk draws within: the surface's, or a kept
|
|
22937
|
+
* subtree's canvas while it is drawn into. An opacity group's region is
|
|
22938
|
+
* cut to them.
|
|
22939
|
+
*/
|
|
22940
|
+
boundsWidth = 0;
|
|
22941
|
+
boundsHeight = 0;
|
|
22942
|
+
/** What the raster cache did since the last reset, or null with it off. For budget specs. */
|
|
22943
|
+
get rasterCacheStats() {
|
|
22944
|
+
return this.rasters?.stats ?? null;
|
|
22945
|
+
}
|
|
22946
|
+
/** When a frame is wanted though nothing changes: a zoom settling, whose kept pixels are then drawn sharp. */
|
|
22947
|
+
nextRepaint() {
|
|
22948
|
+
return this.rasters?.nextRepaint;
|
|
21951
22949
|
}
|
|
21952
22950
|
/** Text kept as pixels; see `TextRasterCache.ts`. */
|
|
21953
22951
|
textCache;
|
|
@@ -21980,12 +22978,20 @@ var Canvas2DRenderer = class {
|
|
|
21980
22978
|
surfaceRestored() {
|
|
21981
22979
|
this.layers.dropAll();
|
|
21982
22980
|
this.textCache?.dropAll();
|
|
22981
|
+
this.rasters?.dropAll();
|
|
22982
|
+
this.groups.length = 0;
|
|
22983
|
+
}
|
|
22984
|
+
/** Kept subtrees were drawn with the fallback face. */
|
|
22985
|
+
fontsChanged() {
|
|
22986
|
+
this.rasters?.dropAll();
|
|
21983
22987
|
}
|
|
21984
22988
|
dispose() {
|
|
21985
22989
|
this.disposed = true;
|
|
21986
22990
|
this.scaledImages.dispose();
|
|
21987
22991
|
this.layers.dropAll();
|
|
21988
22992
|
this.textCache?.dropAll();
|
|
22993
|
+
this.rasters?.dropAll();
|
|
22994
|
+
this.groups.length = 0;
|
|
21989
22995
|
}
|
|
21990
22996
|
render(root, context) {
|
|
21991
22997
|
const raw = this.surface.getContext2D();
|
|
@@ -21999,13 +23005,19 @@ var Canvas2DRenderer = class {
|
|
|
21999
23005
|
this.liftedPass.length = 0;
|
|
22000
23006
|
this.paintOwner = null;
|
|
22001
23007
|
this.layerDepth = 0;
|
|
23008
|
+
this.groupDepth = 0;
|
|
22002
23009
|
this.textCache?.beginFrame();
|
|
23010
|
+
this.rasterDepth = 0;
|
|
23011
|
+
this.boundsWidth = this.surface.physicalWidth;
|
|
23012
|
+
this.boundsHeight = this.surface.physicalHeight;
|
|
23013
|
+
this.rasters?.beginFrame(context.changes, this.surface.physicalWidth * this.surface.physicalHeight);
|
|
22003
23014
|
this.layers.beginFrame(this.options.scrollLayers === false ? void 0 : context.changes, this.surface.dpr, this.surface.physicalWidth, this.surface.physicalHeight);
|
|
22004
23015
|
this.renderNode(root, context, ctx, true, false);
|
|
22005
23016
|
this.renderLifted(context, ctx);
|
|
22006
23017
|
if (context.overlay !== void 0 && context.overlay.length > 0) drawOverlayShapes(ctx, context.overlay);
|
|
22007
23018
|
this.layers.endFrame();
|
|
22008
23019
|
this.textCache?.endFrame();
|
|
23020
|
+
this.rasters?.endFrame();
|
|
22009
23021
|
}
|
|
22010
23022
|
/**
|
|
22011
23023
|
* Folds a node's style into the shared scratch and records that it
|
|
@@ -22029,58 +23041,137 @@ var Canvas2DRenderer = class {
|
|
|
22029
23041
|
return;
|
|
22030
23042
|
}
|
|
22031
23043
|
const sticky = rec.stickyOffsetX !== 0 || rec.stickyOffsetY !== 0;
|
|
22032
|
-
if (cull && !this.
|
|
23044
|
+
if (cull && !this.extentVisible(node, rec)) return;
|
|
23045
|
+
if (this.rasters !== null && this.rasterDepth === 0 && !sticky && node.properties.get("rasterize") === true && this.drawKept(node, rec, context, ctx)) return;
|
|
22033
23046
|
const paint = this.resolvePaint(node);
|
|
22034
23047
|
const hasText = paint.text !== void 0;
|
|
23048
|
+
const opacity = paint.opacity;
|
|
23049
|
+
let draw = ctx;
|
|
23050
|
+
let group = null;
|
|
23051
|
+
let region = null;
|
|
22035
23052
|
let saves = 0;
|
|
22036
23053
|
if (paint.opacity < 1 || paint.hasTransform || sticky) {
|
|
22037
23054
|
ctx.save();
|
|
22038
23055
|
saves++;
|
|
22039
23056
|
if (sticky) ctx.translate(rec.stickyOffsetX, rec.stickyOffsetY);
|
|
22040
|
-
|
|
23057
|
+
group = paint.opacity < 1 && opacityNeedsLayer(node, rec, paint, context.layout) ? this.groupLayer(ctx) : null;
|
|
23058
|
+
if (paint.opacity < 1 && group === null) ctx.globalAlpha *= paint.opacity;
|
|
22041
23059
|
if (paint.hasTransform) this.applyTransform(ctx, rec, paint);
|
|
23060
|
+
if (group !== null) {
|
|
23061
|
+
region = layerRegion(node, rec, ctx.getTransform(), context.layout, this.boundsWidth, this.boundsHeight, this.surface.dpr);
|
|
23062
|
+
group.host.width = Math.max(1, region.width);
|
|
23063
|
+
group.host.height = Math.max(1, region.height);
|
|
23064
|
+
draw = group.ctx.reset(group.raw);
|
|
23065
|
+
draw.save();
|
|
23066
|
+
group.ctx.adoptTransform(ctx, region.x, region.y);
|
|
23067
|
+
this.groupDepth++;
|
|
23068
|
+
}
|
|
22042
23069
|
}
|
|
22043
|
-
if (paint.boxShadows.length > 0) this.paintShadows(
|
|
22044
|
-
this.paintBackground(
|
|
22045
|
-
if (paint.boxShadows.length > 0) this.paintShadows(
|
|
22046
|
-
this.paintImage(
|
|
22047
|
-
if (node.type === "paint") this.paintPicture(
|
|
22048
|
-
this.paintBorder(
|
|
23070
|
+
if (paint.boxShadows.length > 0) this.paintShadows(draw, rec, paint, false);
|
|
23071
|
+
this.paintBackground(draw, rec, paint);
|
|
23072
|
+
if (paint.boxShadows.length > 0) this.paintShadows(draw, rec, paint, true);
|
|
23073
|
+
this.paintImage(draw, rec, paint);
|
|
23074
|
+
if (node.type === "paint") this.paintPicture(draw, node, rec);
|
|
23075
|
+
this.paintBorder(draw, rec, paint);
|
|
22049
23076
|
const decorations = node.decorations;
|
|
22050
|
-
if (decorations !== null && hasDecorationPhase(decorations, false)) this.paintDecorations(
|
|
23077
|
+
if (decorations !== null && hasDecorationPhase(decorations, false)) this.paintDecorations(draw, node, rec, paint, decorations, false);
|
|
23078
|
+
const mapped = cull && (paint.hasTransform || sticky) && this.pushMappedCull(rec, paint, sticky);
|
|
23079
|
+
const cullChildren = cull && (mapped || !paint.hasTransform && !sticky);
|
|
22051
23080
|
if (rec.clips) {
|
|
22052
|
-
|
|
22053
|
-
if (!borderRadiusIsZero(paint.borderRadius)) traceRoundedRect(
|
|
23081
|
+
draw.save();
|
|
23082
|
+
if (!borderRadiusIsZero(paint.borderRadius)) traceRoundedRect(draw, rec.x, rec.y, rec.width, rec.height, uniformBorderRadius(paint.borderRadius));
|
|
22054
23083
|
else {
|
|
22055
|
-
|
|
22056
|
-
|
|
23084
|
+
draw.beginPath();
|
|
23085
|
+
draw.rect(rec.x, rec.y, rec.width, rec.height);
|
|
22057
23086
|
}
|
|
22058
|
-
|
|
23087
|
+
draw.clip();
|
|
22059
23088
|
this.pushCull(rec);
|
|
22060
23089
|
}
|
|
22061
23090
|
const liftedBefore = this.liftedPass.length;
|
|
22062
|
-
if (!rec.scrollable || !this.renderScrolledThroughLayer(node, rec, context,
|
|
23091
|
+
if (!rec.scrollable || !this.renderScrolledThroughLayer(node, rec, context, draw)) {
|
|
22063
23092
|
if (rec.scrollable) {
|
|
22064
|
-
|
|
22065
|
-
|
|
23093
|
+
draw.save();
|
|
23094
|
+
draw.translate(-snapScroll(rec.scrollX, this.surface.dpr), -snapScroll(rec.scrollY, this.surface.dpr));
|
|
22066
23095
|
}
|
|
22067
|
-
this.renderChildren(node, context,
|
|
22068
|
-
if (rec.scrollable)
|
|
23096
|
+
this.renderChildren(node, context, draw, cullChildren);
|
|
23097
|
+
if (rec.scrollable) draw.restore();
|
|
22069
23098
|
}
|
|
22070
23099
|
if (rec.clips) this.popCull();
|
|
23100
|
+
if (mapped) this.popCull();
|
|
22071
23101
|
if (hasText) {
|
|
22072
23102
|
if (this.paintOwner !== node) this.resolvePaint(node);
|
|
22073
|
-
this.paintContent(
|
|
23103
|
+
this.paintContent(draw, node, rec, this.paint, context);
|
|
23104
|
+
}
|
|
23105
|
+
if (rec.liftBoundary && this.liftedPass.length > liftedBefore) this.renderLifted(context, draw, liftedBefore, node);
|
|
23106
|
+
if (rec.clips) draw.restore();
|
|
23107
|
+
if (decorations !== null && hasDecorationPhase(decorations, true)) this.paintDecorations(draw, node, rec, this.paintOwner === node ? this.paint : this.resolvePaint(node), decorations, true);
|
|
23108
|
+
if (rec.scrollable || rec.textScrollbars) this.paintScrollbars(draw, rec, context.now ?? performance.now());
|
|
23109
|
+
if (group !== null) {
|
|
23110
|
+
draw.restore();
|
|
23111
|
+
this.groupDepth--;
|
|
23112
|
+
if (region.width > 0 && region.height > 0) {
|
|
23113
|
+
ctx.save();
|
|
23114
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
23115
|
+
ctx.globalAlpha *= opacity;
|
|
23116
|
+
ctx.drawImage(group.host, region.x, region.y, region.width, region.height);
|
|
23117
|
+
ctx.restore();
|
|
23118
|
+
}
|
|
22074
23119
|
}
|
|
22075
|
-
if (rec.liftBoundary && this.liftedPass.length > liftedBefore) this.renderLifted(context, ctx, liftedBefore, node);
|
|
22076
|
-
if (rec.clips) ctx.restore();
|
|
22077
|
-
if (decorations !== null && hasDecorationPhase(decorations, true)) this.paintDecorations(ctx, node, rec, this.paintOwner === node ? this.paint : this.resolvePaint(node), decorations, true);
|
|
22078
|
-
if (rec.scrollable || rec.textScrollbars) this.paintScrollbars(ctx, rec, context.now ?? performance.now());
|
|
22079
23120
|
while (saves > 0) {
|
|
22080
23121
|
ctx.restore();
|
|
22081
23122
|
saves--;
|
|
22082
23123
|
}
|
|
22083
23124
|
}
|
|
23125
|
+
/**
|
|
23126
|
+
* Draws a node marked \`rasterize\` from its kept pixels, drawing it
|
|
23127
|
+
* into them first when they are out of date. False when it is to be
|
|
23128
|
+
* drawn directly: a transform of its own, which its extent does not
|
|
23129
|
+
* include, or anything \`RasterCache\` declines.
|
|
23130
|
+
*/
|
|
23131
|
+
drawKept(node, rec, context, ctx) {
|
|
23132
|
+
if (node.properties.get("transform") !== void 0) return false;
|
|
23133
|
+
return this.rasters.draw(ctx, node, rec, context.now ?? performance.now(), (target, width, height) => {
|
|
23134
|
+
const volatile = this.volatileDraws;
|
|
23135
|
+
const lifted = this.liftedPass.length;
|
|
23136
|
+
const boundsWidth = this.boundsWidth;
|
|
23137
|
+
const boundsHeight = this.boundsHeight;
|
|
23138
|
+
this.boundsWidth = width;
|
|
23139
|
+
this.boundsHeight = height;
|
|
23140
|
+
this.rasterDepth++;
|
|
23141
|
+
this.layerDepth++;
|
|
23142
|
+
try {
|
|
23143
|
+
this.renderNode(node, context, target, false, false);
|
|
23144
|
+
} finally {
|
|
23145
|
+
this.layerDepth--;
|
|
23146
|
+
this.rasterDepth--;
|
|
23147
|
+
this.boundsWidth = boundsWidth;
|
|
23148
|
+
this.boundsHeight = boundsHeight;
|
|
23149
|
+
}
|
|
23150
|
+
return this.volatileDraws === volatile && this.liftedPass.length === lifted;
|
|
23151
|
+
});
|
|
23152
|
+
}
|
|
23153
|
+
/**
|
|
23154
|
+
* The canvas an opacity group at the current depth is drawn in, made
|
|
23155
|
+
* the first time a group reaches that depth. Null where no canvas can
|
|
23156
|
+
* be had, or where the frame's context cannot say what transform the
|
|
23157
|
+
* layer should take on; the group then multiplies its opacity into
|
|
23158
|
+
* each draw, as before.
|
|
23159
|
+
*/
|
|
23160
|
+
groupLayer(ctx) {
|
|
23161
|
+
if (ctx.getTransform === void 0) return null;
|
|
23162
|
+
const existing = this.groups[this.groupDepth];
|
|
23163
|
+
if (existing !== void 0) return existing;
|
|
23164
|
+
const host = (this.options.createLayerCanvas ?? offscreenLayerCanvas)(1, 1);
|
|
23165
|
+
const raw = host?.getContext("2d");
|
|
23166
|
+
if (host === null || raw === null || raw === void 0) return null;
|
|
23167
|
+
const layer = {
|
|
23168
|
+
host,
|
|
23169
|
+
ctx: new PreciseContext(raw),
|
|
23170
|
+
raw
|
|
23171
|
+
};
|
|
23172
|
+
this.groups[this.groupDepth] = layer;
|
|
23173
|
+
return layer;
|
|
23174
|
+
}
|
|
22084
23175
|
renderChildren(node, context, ctx, cull) {
|
|
22085
23176
|
const order = context.layout.recordFor(node)?.paintOrder;
|
|
22086
23177
|
if (order !== null && order !== void 0) {
|
|
@@ -22358,7 +23449,7 @@ var Canvas2DRenderer = class {
|
|
|
22358
23449
|
this.paintHighlightedText(ctx, paint, context);
|
|
22359
23450
|
return;
|
|
22360
23451
|
}
|
|
22361
|
-
if (this.textCache?.draw(ctx, node, this.contentBox, paint, context.text) === true) return;
|
|
23452
|
+
if (this.rasterDepth === 0 && this.textCache?.draw(ctx, node, this.contentBox, paint, context.text) === true) return;
|
|
22362
23453
|
drawText(ctx, this.contentBox, paint, context.text);
|
|
22363
23454
|
}
|
|
22364
23455
|
}
|
|
@@ -22495,8 +23586,27 @@ var Canvas2DRenderer = class {
|
|
|
22495
23586
|
* box does not, and leave a seam where a direct draw has none. What a
|
|
22496
23587
|
* strip saves is raster, which is the cost it exists for; the walk
|
|
22497
23588
|
* costs what a direct draw's walk would.
|
|
23589
|
+
*
|
|
23590
|
+
* Strips far apart are walked apart. A walk takes in everything
|
|
23591
|
+
* between the strips it is given, and the two that come together most
|
|
23592
|
+
* often are at opposite edges: a sticky header damaged at the top on
|
|
23593
|
+
* every frame of a scroll, and the strip a scroll down exposes at the
|
|
23594
|
+
* bottom. One walk over both drew every row on screen into a clip that
|
|
23595
|
+
* kept almost none of it — each row's text one more copy for the GPU,
|
|
23596
|
+
* which at 165Hz was more copies than it could take, so scrolling down
|
|
23597
|
+
* a spreadsheet stuttered and scrolling up, its strip beside the
|
|
23598
|
+
* header, did not. Strips whose walks would meet share one, so no
|
|
23599
|
+
* pixel is painted twice.
|
|
22498
23600
|
*/
|
|
22499
23601
|
paintLayer(layer, strips, node, rec, context) {
|
|
23602
|
+
if (strips === null) {
|
|
23603
|
+
this.walkLayer(layer, null, node, rec, context);
|
|
23604
|
+
return;
|
|
23605
|
+
}
|
|
23606
|
+
for (const group of walkGroups(strips, 16 * layer.dpr)) this.walkLayer(layer, group, node, rec, context);
|
|
23607
|
+
}
|
|
23608
|
+
/** One walk of a layer's children, clipped to `strips` and culled to what reaches them. */
|
|
23609
|
+
walkLayer(layer, strips, node, rec, context) {
|
|
22500
23610
|
const lctx = new PreciseContext(layer.frontContext);
|
|
22501
23611
|
lctx.save();
|
|
22502
23612
|
if (strips !== null) {
|
|
@@ -22558,6 +23668,34 @@ var Canvas2DRenderer = class {
|
|
|
22558
23668
|
if (transform.scaleX !== 1 || transform.scaleY !== 1) ctx.scale(transform.scaleX, transform.scaleY);
|
|
22559
23669
|
ctx.translate(-originX, -originY);
|
|
22560
23670
|
}
|
|
23671
|
+
/**
|
|
23672
|
+
* Whether where a node's subtree may paint meets the visible region,
|
|
23673
|
+
* after its sticky shift and its own transform, which its extent does
|
|
23674
|
+
* not include: a node a transform moves on screen from off it is
|
|
23675
|
+
* drawn. One that turns is always drawn.
|
|
23676
|
+
*/
|
|
23677
|
+
extentVisible(node, rec) {
|
|
23678
|
+
let left = rec.extentMinX;
|
|
23679
|
+
let top = rec.extentMinY;
|
|
23680
|
+
let right = rec.extentMaxX;
|
|
23681
|
+
let bottom = rec.extentMaxY;
|
|
23682
|
+
const raw = node.properties.get("transform");
|
|
23683
|
+
const transform = raw === void 0 ? void 0 : parseTransform(raw);
|
|
23684
|
+
if (transform !== void 0) {
|
|
23685
|
+
if (transform.rotation !== 0) return true;
|
|
23686
|
+
const originX = rec.x + transform.x;
|
|
23687
|
+
const originY = rec.y + transform.y;
|
|
23688
|
+
const x0 = transform.translateX + originX + transform.scaleX * (left - originX);
|
|
23689
|
+
const x1 = transform.translateX + originX + transform.scaleX * (right - originX);
|
|
23690
|
+
const y0 = transform.translateY + originY + transform.scaleY * (top - originY);
|
|
23691
|
+
const y1 = transform.translateY + originY + transform.scaleY * (bottom - originY);
|
|
23692
|
+
left = Math.min(x0, x1);
|
|
23693
|
+
right = Math.max(x0, x1);
|
|
23694
|
+
top = Math.min(y0, y1);
|
|
23695
|
+
bottom = Math.max(y0, y1);
|
|
23696
|
+
}
|
|
23697
|
+
return this.intersectsCull(left + rec.stickyOffsetX, top + rec.stickyOffsetY, right - left, bottom - top);
|
|
23698
|
+
}
|
|
22561
23699
|
intersectsCull(x, y, width, height) {
|
|
22562
23700
|
if (this.cullWidth <= 0 || this.cullHeight <= 0) return false;
|
|
22563
23701
|
return x < this.cullX + this.cullWidth && this.cullX < x + width && y < this.cullY + this.cullHeight && this.cullY < y + height;
|
|
@@ -22599,6 +23737,36 @@ var Canvas2DRenderer = class {
|
|
|
22599
23737
|
this.cullWidth = width;
|
|
22600
23738
|
this.cullHeight = height;
|
|
22601
23739
|
}
|
|
23740
|
+
/**
|
|
23741
|
+
* Takes the visible region back through a node's sticky shift and a
|
|
23742
|
+
* transform that only moves and scales, into the coordinates its
|
|
23743
|
+
* children are laid out in. False, with nothing pushed, for one that
|
|
23744
|
+
* turns or flips.
|
|
23745
|
+
*/
|
|
23746
|
+
pushMappedCull(rec, paint, sticky) {
|
|
23747
|
+
let shiftX = sticky ? rec.stickyOffsetX : 0;
|
|
23748
|
+
let shiftY = sticky ? rec.stickyOffsetY : 0;
|
|
23749
|
+
let scaleX = 1;
|
|
23750
|
+
let scaleY = 1;
|
|
23751
|
+
let originX = 0;
|
|
23752
|
+
let originY = 0;
|
|
23753
|
+
if (paint.hasTransform) {
|
|
23754
|
+
const transform = paint.transform;
|
|
23755
|
+
if (transform.rotation !== 0 || !(transform.scaleX > 0) || !(transform.scaleY > 0)) return false;
|
|
23756
|
+
shiftX += transform.translateX;
|
|
23757
|
+
shiftY += transform.translateY;
|
|
23758
|
+
scaleX = transform.scaleX;
|
|
23759
|
+
scaleY = transform.scaleY;
|
|
23760
|
+
originX = rec.x + transform.x;
|
|
23761
|
+
originY = rec.y + transform.y;
|
|
23762
|
+
}
|
|
23763
|
+
this.cullStack.push(this.cullX, this.cullY, this.cullWidth, this.cullHeight);
|
|
23764
|
+
this.cullX = originX + (this.cullX - shiftX - originX) / scaleX;
|
|
23765
|
+
this.cullY = originY + (this.cullY - shiftY - originY) / scaleY;
|
|
23766
|
+
this.cullWidth /= scaleX;
|
|
23767
|
+
this.cullHeight /= scaleY;
|
|
23768
|
+
return true;
|
|
23769
|
+
}
|
|
22602
23770
|
popCull() {
|
|
22603
23771
|
this.cullHeight = this.cullStack.pop() ?? 0;
|
|
22604
23772
|
this.cullWidth = this.cullStack.pop() ?? 0;
|
|
@@ -22645,6 +23813,49 @@ function traceFrame(ctx, x, y, width, height) {
|
|
|
22645
23813
|
ctx.closePath();
|
|
22646
23814
|
}
|
|
22647
23815
|
/**
|
|
23816
|
+
* Strips grouped so that each group is one walk of a layer.
|
|
23817
|
+
*
|
|
23818
|
+
* Two strips share a group when less than `reach` separates them, which
|
|
23819
|
+
* is when their walks, each a spill wider than its strips, would meet.
|
|
23820
|
+
* Strips in different groups therefore never overlap, and nothing is
|
|
23821
|
+
* painted twice. A group keeps its strips in the order they came, and
|
|
23822
|
+
* the groups go in the order of their first strip.
|
|
23823
|
+
*
|
|
23824
|
+
* Merged by repeated passes rather than one: joining two strips can
|
|
23825
|
+
* bring the group within reach of a third that neither was near alone.
|
|
23826
|
+
* A frame has a handful of strips, so the passes cost nothing.
|
|
23827
|
+
*/
|
|
23828
|
+
function walkGroups(strips, reach) {
|
|
23829
|
+
const groups = strips.map((strip) => ({
|
|
23830
|
+
strips: [strip],
|
|
23831
|
+
box: { ...strip }
|
|
23832
|
+
}));
|
|
23833
|
+
let merged = true;
|
|
23834
|
+
while (merged) {
|
|
23835
|
+
merged = false;
|
|
23836
|
+
for (let i = 0; i < groups.length && !merged; i++) for (let j = i + 1; j < groups.length; j++) {
|
|
23837
|
+
const a = groups[i].box;
|
|
23838
|
+
const b = groups[j].box;
|
|
23839
|
+
if (b.x - (a.x + a.width) >= reach || a.x - (b.x + b.width) >= reach || b.y - (a.y + a.height) >= reach || a.y - (b.y + b.height) >= reach) continue;
|
|
23840
|
+
const x = Math.min(a.x, b.x);
|
|
23841
|
+
const y = Math.min(a.y, b.y);
|
|
23842
|
+
groups[i] = {
|
|
23843
|
+
strips: [...groups[i].strips, ...groups[j].strips],
|
|
23844
|
+
box: {
|
|
23845
|
+
x,
|
|
23846
|
+
y,
|
|
23847
|
+
width: Math.max(a.x + a.width, b.x + b.width) - x,
|
|
23848
|
+
height: Math.max(a.y + a.height, b.y + b.height) - y
|
|
23849
|
+
}
|
|
23850
|
+
};
|
|
23851
|
+
groups.splice(j, 1);
|
|
23852
|
+
merged = true;
|
|
23853
|
+
break;
|
|
23854
|
+
}
|
|
23855
|
+
}
|
|
23856
|
+
return groups.map((group) => group.strips);
|
|
23857
|
+
}
|
|
23858
|
+
/**
|
|
22648
23859
|
* Emits a rounded-rectangle path (clamped radius) onto a context.
|
|
22649
23860
|
*
|
|
22650
23861
|
* Uses arcTo instead of roundRect so the renderer does not depend
|
|
@@ -22744,6 +23955,6 @@ function createCanvasSurface(host) {
|
|
|
22744
23955
|
return new CanvasSurface(host);
|
|
22745
23956
|
}
|
|
22746
23957
|
//#endregion
|
|
22747
|
-
export {
|
|
23958
|
+
export { UiPlatformAdapter as $, UiNode as $a, themeExtension as $i, isVideoSurface as $n, pathValuesEqual as $r, textRunDecorations as $t, formatNumber as A, hiddenRangesOf as Aa, propertyValuesEqual as Ai, isPressable as An, UiBinding as Ao, resolveString as Ar, paragraphGeometryFrom as At, MAX_MEASURES_PER_CHILD as B, lightColors as Ba, containerBands as Bi, CARET_BLINK_MS as Bn, UiProperties as Br, isPrintable as Bt, LayoutEngine as C, shapesEqual as Ca, borderRadiusCorners as Ci, selectionRangeOf as Cn, UiTextChangeEvent as Co, wordRangeAt as Cr, scrollbarThumbs as Ct, describeOverrides as D, defaultTextStyle as Da, normalizeBorderRadius as Di, isNodeInert as Dn, wheelDeltaYOf as Do, resolveNumber as Dr, hasDrawnText as Dt, describeLength as E, typographyEqual as Ea, borderRadiusValuesEqual as Ei, isNodeHitTestable as En, noKeyModifiers as Eo, resolveBoolean as Er, UiInputDispatcher as Et, LayoutRecord as F, textContentOf as Fa, insetsEqual as Fi, hasTextLinks as Fn, inheritedPropertyNames as Fr, comparePositions as Ft, sizeGridTracks as G, colorsEqual as Ga, withContrast as Gi, editorOf as Gn, normalizeStates as Gr, drawText as Gt, FlexDirection as H, colorToHex as Ha, darkTheme as Hi, EDITOR_PROP as Hn, UI_SEMANTIC_STATES as Hr, applyCanvasTextStyle as Ht, Constraints as I, textSpansEqual as Ia, noInsets as Ii, linkHoverOf as In, propertyEffects as Ir, edgeField as It, MainAxisAlignment as J, rgb8 as Ja, lengths as Ji, isReadOnly as Jn, validateStates as Jr, runCanvasStyleInto as Jt, AlignContent as K, normalizeColor as Ka, withDensity as Ki, isEditableNode as Kn, statesEqual as Kr, fillTextRects as Kt, clampSize as L, textStylesEqual as La, observeViewportInsets as Li, linkOf as Ln, propertyIsInherited as Lr, editingGroupOf as Lt, withOverrideSource as M, spanAtOffset as Ma, withPointer as Mi, pointerEventsOf as Mn, findPropertyDefinition as Mr, wordRangeIn as Mt, accumulatedOffsetTo as N, spannedTextOf as Na, UiEnvironmentKeys as Ni, resolveCursor as Nn, getPropertyNames as Nr, textInputOf as Nt, formatConstraints as O, editableSpansOf as Oa, uniformBorderRadius as Oi, isNodeSelectable as On, UiNodeType as Oo, resolveProperty as Or, offsetAtPointIn as Ot, contentOffset as P, spansDifferOnlyInPaint as Pa, UiInsetRegistry as Pi, clearLinkHover as Pn, inheritedPropertyFlags as Pr, adjacentField as Pt, CanvasPlatformSurface as Q, UiEnvironment as Qa, noThemeExtensions as Qi, resolveColorValue as Qn, paintValuesEqual as Qr, textRunBackgrounds as Qt, constraintsEqual as R, colorsEqualPalette as Ra, UiContainerSizeSource as Ri, linkRunAtPointIn as Rn, propertyNarrowers as Rr, commandForKey as Rt, tracePathData as S, isShapeName as Sa, borderRadius as Si, selectableTextOf as Sn, UiSelectionChangeEvent as So, previousWordStart as Sr, scrollbarThumb as St, SCROLLBAR_LINGER_MS as T, isTypographyRole as Ta, borderRadiusIsZero as Ti, isNodeFocusable as Tn, UiWheelEvent as To, DEFAULT_LINE_HEIGHT_FACTOR as Tr, scrollsFromRight as Tt, parseFlexDirection as U, colorToRgba as Ua, lightTheme as Ui, caretVisibleAt as Un, isUiRole as Ur, buildFontString as Ut, isLayoutProtocol as V, UiBasicColors as Va, unknownContainerSize as Vi, CARET_WIDTH as Vn, UI_ROLES as Vr, EditableLayout as Vt, placeGridItems as W, colorValuesEqual as Wa, themesEqual as Wi, editorFor as Wn, isUiSemanticState as Wr, createCanvasTextStyle as Wt, parseCrossAxisAlignment as X, createEnvironmentKey as Xa, defineThemeExtension as Xi, nextCaretToggle as Xn, validateSubgrid as Xr, spanOf as Xt, parseAlignContent as Y, rgba as Ya, unitsEqual as Yi, maskText as Yn, UI_SUBGRID_AXES as Yr, runFontString as Yt, parseMainAxisAlignment as Z, findEnvironmentKey as Za, hasThemeExtension as Zi, syncEditorValue as Zn, inputsEqual as Zr, textMeasureRequest as Zt, paintsAfterChildren as _, boxShadowReach as _a, minmax as _i, matchRangesOf as _n, UiInputEvent as _o, lineEndAt as _r, SCROLLBAR_MIN_THUMB as _t, layerRegion as a, relativeLuminance as aa, MAX_GRADIENT_STOPS as ai, DEFAULT_FONT_SIZE as an, writeOverrideProperty as ao, layoutParagraph as ar, UiEditingController as at, labelOrigin as b, shadowsOf as ba, resolveLength as bi, clearSelectionRange as bn, UiPinchEvent as bo, nextWordEnd as br, scrollLeaderOf as bt, paintPictures as c, scaleSpacing as ca, linearGradient as ci, colorToCss as cn, hasVisualState as co, lineIndexAtY as cr, UiPinchRecognizer as ct, PaintRecorder as d, defaultShadows as da, auto as di, isPaintVisible as dn, MOUSE_POINTER as do, offsetAtPoint as dr, UiKeyboardController as dt, themeExtensionsEqual as ea, UiTransforms as ei, bumpFontStack as en, DirtyNodeSet as eo, videoFrameSize as er, capturePointer as et, replayPaint as f, shadowsEqual as fa, fr as fi, normalizeTextAlign as fn, UiBeforeInputEvent as fo, offsetAtX as fr, UiPointerController as ft, hasDecorationPhase as g, boxShadowRadius as ga, isPercentLength as gi, clearMatchRanges as gn, UiGestureEvent as go, graphemeBoundaries as gr, SCROLLBAR_INSET as gt, decorationRect as h, boxShadowArraysEqual as ha, isMinMaxTrack as hi, TEXT_MATCHES_PROP as hn, UiFocusEvent as ho, EditableTextModel as hr, SCROLLBAR_HOVER_ZONE as ht, traceRoundedRect as i, raiseContrast as ia, transformsEqual as ii, DEFAULT_FONT_FAMILY as in, writeDeclaredProperty as io, ELLIPSIS as ir, touchActionFor as it, labelNode as j, resolvedSpansOf as ja, TOUCH_TARGET as ji, isSubtreeInert as jn, closestPropertyName as jr, selectionRectsIn as jt, formatExplanation as k, flattenTextSpans as ka, defineProperty as ki, isNodeTabStop as kn, DirtyFlags as ko, resolvePropertyByName as kr, paragraphGeometry as kt, PaintTarget as l, spacingEqual as la, radialGradient as li, computeObjectFitRect as ln, visualState as lo, lineIndexForOffset as lr, UiWheelController as lt, decorationColor as m, boxShadow as ma, isFrLength as mi, resolvePaintState as mn, UiEventType as mo, selectionRects as mr, UiHitTester as mt, createCanvasSurface as n, contrastRatio as na, transform as ni, fontStackFor as nn, overrideSources as no, ParagraphTextMeasurer as nr, pointerDeviceOf as nt, opacityNeedsLayer as o, densityFactors as oa, gradientPaint as oi, DEFAULT_FONT_WEIGHT as on, UiVisualState as oo, proportionalFontMetrics as or, UiTouchScroller as ot, ScaledImageCache as p, BOX_SHADOW_BLUR_REACH as pa, isAutoLength as pi, normalizeVerticalAlign as pn, UiCompositionEvent as po, offsetForVerticalMove as pr, UiFocusManager as pt, CrossAxisAlignment as q, parseColor as qa, defaultUnits as qi, isMultiline as qn, validateRole as qr, layoutTextLines as qt, Canvas2DRenderer as r, highContrastColors as ra, transformIsIdentity as ri, registerFontStack as rn, resetOverrideWarnings as ro, spannedRunsFor as rr, prepareInputSurface as rt, PaintPictureCache as s, defaultSpacing as sa, gradientsEqual as si, DEFAULT_TEXT_COLOR as sn, defaultVisualState as so, caretRectFor as sr, UiGestureRecognizer as st, CanvasSurface as t, withThemeExtension as ta, parseTransform as ti, clearFontStacks as tn, clearOverrideProperty as to, CharacterCountTextMeasurer as tr, isImeKey as tt, EMPTY_RECORDING as u, spacingSteps as ua, validateGradient as ui, createPaintState as un, visualStatesEqual as uo, lineLimit as ur, isScrollContainer as ut, LABEL_PADDING_X as v, boxShadowValuesEqual as va, percent as vi, setMatchRanges as vn, UiKeyboardEvent as vo, lineStartAt as vr, SCROLLBAR_THICKNESS as vt, SCROLLBAR_FADE_MS as w, defaultTypography as wa, borderRadiusEqual as wi, setSelectionRange as wn, UiWheelDeltaMode as wo, UiGraph as wr, scrollbarZoneAt as wt, snapScroll as x, defaultShapes as xa, UiBorderRadiuses as xi, selectableTextNodes as xn, UiPointerEvent as xo, previousGraphemeStart as xr, scrollRange as xt, drawOverlayShapes as y, boxShadowsEqual as ya, repeat as yi, TEXT_SELECTION_PROP as yn, UiPasteEvent as yo, nextGraphemeEnd as yr, scrollBounds as yt, tightenConstraints as z, darkColors as za, bandOf as zi, setLinkHover as zn, propertyValuesEqualByName as zr, detectEditingPlatform as zt };
|
|
22748
23959
|
|
|
22749
|
-
//# sourceMappingURL=CanvasSurface-
|
|
23960
|
+
//# sourceMappingURL=CanvasSurface-BpgbOku2.js.map
|