gesso-core 0.6.30 → 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.
@@ -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
- return carried === void 0 ? extension.defaults : carried;
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) => bandOf(measured.width, breakpoints)), distinctUntilChanged());
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: boxShadowArraysEqual
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, shadows) {
8581
- if (!Array.isArray(shadows) || shadows.length === 0) return NO_SHADOWS;
8582
- const theme = node.environment !== null ? node.environment.get(UiEnvironmentKeys.theme) : void 0;
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(), pointer = MOUSE_POINTER) {
11063
+ pointerDown(x, y, buttons = 1, modifiers = noKeyModifiers(), device = MOUSE_POINTER) {
10808
11064
  this.lastPosition = {
10809
11065
  x,
10810
11066
  y
10811
11067
  };
10812
- const event = new UiPointerEvent("pointerdown", x, y, buttons, modifiers, pointer);
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(), pointer = MOUSE_POINTER) {
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 target = this.hitTester.hitTest(x, y)?.node ?? null;
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(), pointer = MOUSE_POINTER) {
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(pointer = MOUSE_POINTER) {
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 shadows = node.properties.get("boxShadows");
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 = resolveLength(raw, base, property);
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 resolveLength(raw, base, property, true);
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
- return resolveLength(value, void 0, side, true) ?? 0;
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/ScrollSnap.ts
19800
+ //#region src/rendering/PaintPathData.ts
19456
19801
  /**
19457
- * A scroll offset as it is drawn: on a whole device pixel.
19802
+ * SVG path data, as calls on a `PaintSurface`.
19458
19803
  *
19459
- * The offset a container holds stays exact, so a trackpad's steps of a
19460
- * fraction of a pixel still add up; only what is drawn lands on the
19461
- * pixel grid, as a browser draws scrolled content. Drawn at the exact
19462
- * offset, text lands between pixels and rasterises differently each
19463
- * frame, which shimmers as a flick coasts to a stop.
19464
- */
19465
- function snapScroll(offset, dpr) {
19466
- return Math.round(offset * dpr) / dpr;
19467
- }
19468
- //#endregion
19469
- //#region src/rendering/OverlayShapes.ts
19470
- /** Where a label's background box goes: above its box when there is room, else inside its top edge. */
19471
- function labelOrigin(shape) {
19472
- return {
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);
@@ -19905,133 +19939,444 @@ function reflect(control, x, y) {
19905
19939
  return control === null ? [x, y] : [2 * x - control[0], 2 * y - control[1]];
19906
19940
  }
19907
19941
  /**
19908
- * An elliptical arc as cubic segments, by the endpoint-to-centre
19909
- * conversion in the SVG specification's implementation notes.
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.
19910
20167
  *
19911
- * Nothing in the surface draws an ellipse, and nothing should: an arc
19912
- * with a rotation and two radii is the only shape in the grammar that
19913
- * a rectangle-and-curve vocabulary cannot state, and one conversion
19914
- * here is cheaper than a primitive every consumer of the recording
19915
- * would have to implement. Ninety degrees per segment keeps the error
19916
- * far under a tenth of a pixel at icon sizes.
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.
19917
20171
  */
19918
- function arcToCurves(surface, x0, y0, rx, ry, rotation, largeArc, sweep, x1, y1) {
19919
- if (x0 === x1 && y0 === y1) return;
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;
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);
19964
20181
  }
20182
+ return {
20183
+ start: origin + start - outset,
20184
+ size: Math.max(0, length + 2 * outset)
20185
+ };
19965
20186
  }
19966
- /** A point on the rotated ellipse, from its unit-circle coordinates. */
19967
- function onEllipse(cx, cy, rx, ry, cos, sin, ux, uy) {
19968
- const x = rx * ux;
19969
- const y = ry * uy;
19970
- return [cx + cos * x - sin * y, cy + sin * x + cos * y];
20187
+ /**
20188
+ * A decoration's colour, resolved against the node it decorates.
20189
+ *
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.
20196
+ */
20197
+ function decorationColor(node, shape) {
20198
+ return resolveColorValue(node, shape.color);
20199
+ }
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
- * A cursor over path data.
20212
+ * How many consecutive frames a size must hold before a copy is made.
19974
20213
  *
19975
- * Path data is written without separators wherever it can be
19976
- * (`M0 0l5-3.5`), so nothing here can split on whitespace: numbers are
19977
- * read character by character, and the two arc flags are read as a
19978
- * single digit each, which is the one place the grammar is not a plain
19979
- * number list.
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
- var Scanner = class {
19982
- source;
19983
- index = 0;
19984
- constructor(source) {
19985
- this.source = source;
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;
19986
20289
  }
19987
- get done() {
19988
- return this.index >= this.source.length;
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;
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;
19989
20327
  }
19990
- skipSeparators() {
19991
- while (this.index < this.source.length) {
19992
- const code = this.source.charCodeAt(this.index);
19993
- if (code === 32 || code === 9 || code === 10 || code === 13 || code === 12 || code === 44) {
19994
- this.index++;
19995
- continue;
19996
- }
19997
- return;
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();
20332
+ }
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);
19998
20349
  }
20350
+ return `${id}:${width}x${height}`;
19999
20351
  }
20000
- /** The command letter here, consumed; undefined when a number is next. */
20001
- peekCommand() {
20002
- const char = this.source[this.index];
20003
- if (char === void 0 || !/[a-zA-Z]/.test(char)) return;
20004
- this.index++;
20005
- return char;
20006
- }
20007
- number() {
20008
- this.skipSeparators();
20009
- const start = this.index;
20010
- if (this.source[this.index] === "+" || this.source[this.index] === "-") this.index++;
20011
- while (this.isDigit()) this.index++;
20012
- if (this.source[this.index] === ".") {
20013
- this.index++;
20014
- while (this.isDigit()) this.index++;
20015
- }
20016
- const exponent = this.source[this.index];
20017
- if (exponent === "e" || exponent === "E") {
20018
- this.index++;
20019
- if (this.source[this.index] === "+" || this.source[this.index] === "-") this.index++;
20020
- while (this.isDigit()) this.index++;
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
+ }
20021
20359
  }
20022
- const value = Number.parseFloat(this.source.slice(start, this.index));
20023
- return Number.isNaN(value) ? 0 : value;
20024
- }
20025
- /** One character, `0` or `1`: the arc flags carry no separator. */
20026
- flag() {
20027
- this.skipSeparators();
20028
- const char = this.source[this.index];
20029
- this.index++;
20030
- return char === "1";
20360
+ this.entries.set(key, entry);
20031
20361
  }
20032
- isDigit() {
20033
- const code = this.source.charCodeAt(this.index);
20034
- return code >= 48 && code <= 57;
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
- /** A painter's colour values, against the node's own theme. */
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.intersectsCull(rec.extentMinX + rec.stickyOffsetX, rec.extentMinY + rec.stickyOffsetY, rec.extentMaxX - rec.extentMinX, rec.extentMaxY - rec.extentMinY)) return;
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
- if (paint.opacity < 1) ctx.globalAlpha *= paint.opacity;
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(ctx, rec, paint, false);
22044
- this.paintBackground(ctx, rec, paint);
22045
- if (paint.boxShadows.length > 0) this.paintShadows(ctx, rec, paint, true);
22046
- this.paintImage(ctx, rec, paint);
22047
- if (node.type === "paint") this.paintPicture(ctx, node, rec);
22048
- this.paintBorder(ctx, rec, paint);
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(ctx, node, rec, paint, decorations, false);
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
- ctx.save();
22053
- if (!borderRadiusIsZero(paint.borderRadius)) traceRoundedRect(ctx, rec.x, rec.y, rec.width, rec.height, uniformBorderRadius(paint.borderRadius));
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
- ctx.beginPath();
22056
- ctx.rect(rec.x, rec.y, rec.width, rec.height);
23084
+ draw.beginPath();
23085
+ draw.rect(rec.x, rec.y, rec.width, rec.height);
22057
23086
  }
22058
- ctx.clip();
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, ctx)) {
23091
+ if (!rec.scrollable || !this.renderScrolledThroughLayer(node, rec, context, draw)) {
22063
23092
  if (rec.scrollable) {
22064
- ctx.save();
22065
- ctx.translate(-snapScroll(rec.scrollX, this.surface.dpr), -snapScroll(rec.scrollY, this.surface.dpr));
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, ctx, cull && !paint.hasTransform && !sticky);
22068
- if (rec.scrollable) ctx.restore();
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(ctx, node, rec, this.paint, context);
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
  }
@@ -22577,6 +23668,34 @@ var Canvas2DRenderer = class {
22577
23668
  if (transform.scaleX !== 1 || transform.scaleY !== 1) ctx.scale(transform.scaleX, transform.scaleY);
22578
23669
  ctx.translate(-originX, -originY);
22579
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
+ }
22580
23699
  intersectsCull(x, y, width, height) {
22581
23700
  if (this.cullWidth <= 0 || this.cullHeight <= 0) return false;
22582
23701
  return x < this.cullX + this.cullWidth && this.cullX < x + width && y < this.cullY + this.cullHeight && this.cullY < y + height;
@@ -22618,6 +23737,36 @@ var Canvas2DRenderer = class {
22618
23737
  this.cullWidth = width;
22619
23738
  this.cullHeight = height;
22620
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
+ }
22621
23770
  popCull() {
22622
23771
  this.cullHeight = this.cullStack.pop() ?? 0;
22623
23772
  this.cullWidth = this.cullStack.pop() ?? 0;
@@ -22806,6 +23955,6 @@ function createCanvasSurface(host) {
22806
23955
  return new CanvasSurface(host);
22807
23956
  }
22808
23957
  //#endregion
22809
- export { isImeKey as $, hasVisualState as $a, densityFactors as $i, CharacterCountTextMeasurer as $n, parseTransform as $r, clearFontStacks as $t, withOverrideSource as A, darkColors as Aa, UiInsetRegistry as Ai, pointerEventsOf as An, findPropertyDefinition as Ar, wordRangeIn as At, FlexDirection as B, rgba as Ba, themesEqual as Bi, EDITOR_PROP as Bn, UI_SEMANTIC_STATES as Br, applyCanvasTextStyle as Bt, SCROLLBAR_LINGER_MS as C, spanAtOffset as Ca, borderRadiusIsZero as Ci, isNodeFocusable as Cn, DEFAULT_LINE_HEIGHT_FACTOR as Cr, scrollsFromRight as Ct, formatExplanation as D, textSpansEqual as Da, defineProperty as Di, isNodeTabStop as Dn, resolvePropertyByName as Dr, paragraphGeometry as Dt, formatConstraints as E, textContentOf as Ea, uniformBorderRadius as Ei, isNodeSelectable as En, resolveProperty as Er, offsetAtPointIn as Et, clampSize as F, colorValuesEqual as Fa, bandOf as Fi, linkOf as Fn, propertyIsInherited as Fr, editingGroupOf as Ft, CrossAxisAlignment as G, DirtyNodeSet as Ga, noThemeExtensions as Gi, isMultiline as Gn, validateRole as Gr, layoutTextLines as Gt, placeGridItems as H, findEnvironmentKey as Ha, withDensity as Hi, editorFor as Hn, isUiSemanticState as Hr, createCanvasTextStyle as Ht, constraintsEqual as I, colorsEqual as Ia, containerBands as Ii, linkRunAtPointIn as In, propertyNarrowers as Ir, commandForKey as It, parseCrossAxisAlignment as J, resetOverrideWarnings as Ja, withThemeExtension as Ji, nextCaretToggle as Jn, validateSubgrid as Jr, spanOf as Jt, MainAxisAlignment as K, clearOverrideProperty as Ka, themeExtension as Ki, isReadOnly as Kn, validateStates as Kr, runCanvasStyleInto as Kt, tightenConstraints as L, normalizeColor as La, unknownContainerSize as Li, setLinkHover as Ln, propertyValuesEqualByName as Lr, detectEditingPlatform as Lt, contentOffset as M, UiBasicColors as Ma, noInsets as Mi, clearLinkHover as Mn, inheritedPropertyFlags as Mr, adjacentField as Mt, LayoutRecord as N, colorToHex as Na, observeViewportInsets as Ni, hasTextLinks as Nn, inheritedPropertyNames as Nr, comparePositions as Nt, formatNumber as O, textStylesEqual as Oa, propertyValuesEqual as Oi, isPressable as On, resolveString as Or, paragraphGeometryFrom as Ot, Constraints as P, colorToRgba as Pa, UiContainerSizeSource as Pi, linkHoverOf as Pn, propertyEffects as Pr, edgeField as Pt, capturePointer as Q, defaultVisualState as Qa, relativeLuminance as Qi, videoFrameSize as Qn, UiTransforms as Qr, bumpFontStack as Qt, MAX_MEASURES_PER_CHILD as R, parseColor as Ra, darkTheme as Ri, CARET_BLINK_MS as Rn, UiProperties as Rr, isPrintable as Rt, SCROLLBAR_FADE_MS as S, resolvedSpansOf as Sa, borderRadiusEqual as Si, setSelectionRange as Sn, UiGraph as Sr, scrollbarZoneAt as St, describeOverrides as T, spansDifferOnlyInPaint as Ta, normalizeBorderRadius as Ti, isNodeInert as Tn, resolveNumber as Tr, hasDrawnText as Tt, sizeGridTracks as U, UiEnvironment as Ua, defineThemeExtension as Ui, editorOf as Un, normalizeStates as Ur, drawText as Ut, parseFlexDirection as V, createEnvironmentKey as Va, withContrast as Vi, caretVisibleAt as Vn, isUiRole as Vr, buildFontString as Vt, AlignContent as W, UiNode as Wa, hasThemeExtension as Wi, isEditableNode as Wn, statesEqual as Wr, fillTextRects as Wt, CanvasPlatformSurface as X, writeOverrideProperty as Xa, highContrastColors as Xi, resolveColorValue as Xn, paintValuesEqual as Xr, textRunBackgrounds as Xt, parseMainAxisAlignment as Y, writeDeclaredProperty as Ya, contrastRatio as Yi, syncEditorValue as Yn, inputsEqual as Yr, textMeasureRequest as Yt, UiPlatformAdapter as Z, UiVisualState as Za, raiseContrast as Zi, isVideoSurface as Zn, pathValuesEqual as Zr, textRunDecorations as Zt, LABEL_PADDING_X as _, typographyEqual as _a, repeat as _i, TEXT_SELECTION_PROP as _n, noKeyModifiers as _o, nextGraphemeEnd as _r, scrollBounds as _t, PaintPictureCache as a, shadowsEqual as aa, gradientsEqual as ai, DEFAULT_TEXT_COLOR as an, UiEventType as ao, caretRectFor as ar, UiGestureRecognizer as at, snapScroll as b, flattenTextSpans as ba, borderRadius as bi, selectableTextOf as bn, DirtyFlags as bo, previousWordStart as br, scrollbarThumb as bt, EMPTY_RECORDING as c, boxShadowArraysEqual as ca, validateGradient as ci, createPaintState as cn, UiInputEvent as co, lineLimit as cr, isScrollContainer as ct, tracePathData as d, boxShadowsEqual as da, isAutoLength as di, normalizeVerticalAlign as dn, UiPinchEvent as do, offsetForVerticalMove as dr, UiFocusManager as dt, defaultSpacing as ea, transform as ei, fontStackFor as en, visualState as eo, ParagraphTextMeasurer as er, pointerDeviceOf as et, ScaledImageCache as f, defaultShapes as fa, isFrLength as fi, resolvePaintState as fn, UiPointerEvent as fo, selectionRects as fr, UiHitTester as ft, paintsAfterChildren as g, isTypographyRole as ga, percent as gi, setMatchRanges as gn, UiWheelEvent as go, lineStartAt as gr, SCROLLBAR_THICKNESS as gt, hasDecorationPhase as h, defaultTypography as ha, minmax as hi, matchRangesOf as hn, UiWheelDeltaMode as ho, lineEndAt as hr, SCROLLBAR_MIN_THUMB as ht, traceRoundedRect as i, defaultShadows as ia, gradientPaint as ii, DEFAULT_FONT_WEIGHT as in, UiCompositionEvent as io, proportionalFontMetrics as ir, UiTouchScroller as it, accumulatedOffsetTo as j, lightColors as ja, insetsEqual as ji, resolveCursor as jn, getPropertyNames as jr, textInputOf as jt, labelNode as k, colorsEqualPalette as ka, UiEnvironmentKeys as ki, isSubtreeInert as kn, closestPropertyName as kr, selectionRectsIn as kt, PaintRecorder as l, boxShadowRadius as la, auto as li, isPaintVisible as ln, UiKeyboardEvent as lo, offsetAtPoint as lr, UiKeyboardController as lt, decorationRect as m, shapesEqual as ma, isPercentLength as mi, clearMatchRanges as mn, UiTextChangeEvent as mo, graphemeBoundaries as mr, SCROLLBAR_INSET as mt, createCanvasSurface as n, spacingEqual as na, transformsEqual as ni, DEFAULT_FONT_FAMILY as nn, MOUSE_POINTER as no, ELLIPSIS as nr, touchActionFor as nt, paintPictures as o, BOX_SHADOW_BLUR_REACH as oa, linearGradient as oi, colorToCss as on, UiFocusEvent as oo, lineIndexAtY as or, UiPinchRecognizer as ot, decorationColor as p, isShapeName as pa, isMinMaxTrack as pi, TEXT_MATCHES_PROP as pn, UiSelectionChangeEvent as po, EditableTextModel as pr, SCROLLBAR_HOVER_ZONE as pt, parseAlignContent as q, overrideSources as qa, themeExtensionsEqual as qi, maskText as qn, UI_SUBGRID_AXES as qr, runFontString as qt, Canvas2DRenderer as r, spacingSteps as ra, MAX_GRADIENT_STOPS as ri, DEFAULT_FONT_SIZE as rn, UiBeforeInputEvent as ro, layoutParagraph as rr, UiEditingController as rt, PaintTarget as s, boxShadow as sa, radialGradient as si, computeObjectFitRect as sn, UiGestureEvent as so, lineIndexForOffset as sr, UiWheelController as st, CanvasSurface as t, scaleSpacing as ta, transformIsIdentity as ti, registerFontStack as tn, visualStatesEqual as to, spannedRunsFor as tr, prepareInputSurface as tt, replayPaint as u, boxShadowReach as ua, fr as ui, normalizeTextAlign as un, UiPasteEvent as uo, offsetAtX as ur, UiPointerController as ut, drawOverlayShapes as v, defaultTextStyle as va, resolveLength as vi, clearSelectionRange as vn, wheelDeltaYOf as vo, nextWordEnd as vr, scrollLeaderOf as vt, describeLength as w, spannedTextOf as wa, borderRadiusValuesEqual as wi, isNodeHitTestable as wn, resolveBoolean as wr, UiInputDispatcher as wt, LayoutEngine as x, hiddenRangesOf as xa, borderRadiusCorners as xi, selectionRangeOf as xn, UiBinding as xo, wordRangeAt as xr, scrollbarThumbs as xt, labelOrigin as y, editableSpansOf as ya, UiBorderRadiuses as yi, selectableTextNodes as yn, UiNodeType as yo, previousGraphemeStart as yr, scrollRange as yt, isLayoutProtocol as z, rgb8 as za, lightTheme as zi, CARET_WIDTH as zn, UI_ROLES as zr, EditableLayout as zt };
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 };
22810
23959
 
22811
- //# sourceMappingURL=CanvasSurface-CspYfnND.js.map
23960
+ //# sourceMappingURL=CanvasSurface-BpgbOku2.js.map