gesso-framework 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- import { $ as structurallyEqual, A as printPropValue, B as channel, C as captureConsole, D as formatNodePath, E as formatAge, F as applyPatch, G as mutate, H as viewKeys, I as applyPatches, J as select, K as Resource, L as diffProjection, M as provide, N as findUnplainPath, O as formatNodeReport, P as requirePlainData, Q as equalityOf, R as isChannelClientMessage, S as workerHandle, T as describeStream, U as debounced, V as defineChannel, W as throttled, X as computed, Y as ComputedCell, Z as derive, _ as isHubMessage, a as undoable, at as isOutputTarget, b as portHandle, c as classifyStorageError, ct as trackRead, d as storageReadValue, et as InternalState, f as pick, g as APPLICATION_WORKER, h as serveChannels, i as OpfsStorage, it as into, j as ProvidedChannel, k as formatStream, l as storageErrorMessage, lt as withBodyOf, m as serve, n as persisted, nt as InputCell, o as UndoStack, ot as output, p as pickKeys, q as resource, r as IndexedDbStorage, rt as input, s as MemoryStorage, st as outputTargetOf, t as PersistedState, tt as internalState, u as storageReadFailure, v as isPortErrorMessage, w as isConsoleEntryMessage, x as servePorts, y as isPortHandshake, z as isChannelHostMessage } from "./persisted-CsTPnjkc.js";
1
+ import { $ as structurallyEqual, A as printPropValue, B as channel, C as captureConsole, D as formatNodePath, E as formatAge, F as applyPatch, G as mutate, H as viewKeys, I as applyPatches, J as select, K as Resource, L as diffProjection, M as provide, N as findUnplainPath, O as formatNodeReport, P as requirePlainData, Q as equalityOf, R as isChannelClientMessage, S as workerHandle, T as describeStream, U as debounced, V as defineChannel, W as throttled, X as computed, Y as ComputedCell, Z as derive, _ as isHubMessage, a as undoable, at as isOutputTarget, b as portHandle, c as classifyStorageError, ct as trackRead, d as storageReadValue, et as InternalState, f as pick, g as APPLICATION_WORKER, h as serveChannels, i as OpfsStorage, it as into, j as ProvidedChannel, k as formatStream, l as storageErrorMessage, lt as withBodyOf, m as serve, n as persisted, nt as InputCell, o as UndoStack, ot as output, p as pickKeys, q as resource, r as IndexedDbStorage, rt as input, s as MemoryStorage, st as outputTargetOf, t as PersistedState, tt as internalState, u as storageReadFailure, v as isPortErrorMessage, w as isConsoleEntryMessage, x as servePorts, y as isPortHandshake, z as isChannelHostMessage } from "./persisted-pix1fS1D.js";
2
2
  import { a as Each, i as show, n as getComponentMetadata, o as each, r as Show, t as createComponent } from "./createComponent-3n1ZSql8.js";
3
+ import { c as channelSchema, l as describeChannel, t as AGENT_PORT } from "./remote-xxij8zXe.js";
4
+ import { n as serveApplicationAgent } from "./app-BvlIO1G9.js";
3
5
  import { BehaviorSubject, EMPTY, Observable, Subject, Subscription, distinctUntilChanged, map } from "rxjs";
4
- import { AnimationDriver, Box, Canvas2DRenderer, CanvasPlatformSurface, CanvasTextMeasurer, Constraints, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, MARK_PREFIX, Stack, TEXT_RUN_ID_SEPARATOR, UI_ROLES, UI_SEMANTIC_STATES, UiAnimationFrameClock, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiNodeType, UiPlatformAdapter, UiPointerController, UiPointerEvent, UiScheduler, UiSelectionController, UiSharedElements, UiSpring, UiTimerFrameClock, UiTouchScroller, UiVirtualSheet, UiVirtualWindow, UiWheelController, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_SHEET_PROP, VIRTUAL_WINDOW_PROP, WebGPURenderer, buildSemanticsSubtree, buildSemanticsTree, bumpFontStack, capturePointer, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, describeOverrides, diffSemantics, dragSessionFor, easings, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isWebGPUAvailable, linear, linkOf, markInstant, markNow, measure, measureSpan, motion, noInsets, observeViewportInsets, percent, performanceMarksEnabled, pointerDeviceOf, prepareInputSurface, propertyEffects, recordsEqual, registerFontStack, resolveCursor, scrollRange, scrollbarThumb, semanticsInertAbove, setPerformanceMarks, setPerformanceMarks as setPerformanceMarks$1, textRunOfRecordId, themeTokens, touchActionFor, wheelDeltaYOf } from "gesso-core";
6
+ import { AnimationDriver, Box, Canvas2DRenderer, CanvasPlatformSurface, CanvasTextMeasurer, Constraints, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, MARK_PREFIX, Stack, TEXT_RUN_ID_SEPARATOR, UI_ROLES, UI_SEMANTIC_STATES, UiAnimationFrameClock, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiNodeType, UiPlatformAdapter, UiPointerController, UiPointerEvent, UiScheduler, UiSelectionController, UiSharedElements, UiSpring, UiTimerFrameClock, UiTouchScroller, UiVirtualSheet, UiVirtualWindow, UiWheelController, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_SHEET_PROP, VIRTUAL_WINDOW_PROP, WebGPURenderer, buildSemanticsSubtree, buildSemanticsTree, bumpFontStack, capturePointer, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, easings, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isWebGPUAvailable, linear, linkOf, markInstant, markNow, measure, measureSpan, motion, noInsets, observeViewportInsets, percent, performanceMarksEnabled, pointerDeviceOf, prepareInputSurface, propertyEffects, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollRange, scrollbarThumb, semanticsInertAbove, semanticsMemory, setPerformanceMarks, setPerformanceMarks as setPerformanceMarks$1, textRunOfRecordId, themeTokens, touchActionFor, wheelDeltaYOf } from "gesso-core";
5
7
  //#region src/Component.ts
6
8
  /**
7
9
  * Base class for all Gesso framework components.
@@ -759,6 +761,7 @@ function createChannelRegistry(registrations, onError) {
759
761
  }
760
762
  return {
761
763
  registry,
764
+ workers: handles,
762
765
  dispose: () => {
763
766
  for (const channel of local) channel.dispose();
764
767
  local.length = 0;
@@ -851,6 +854,7 @@ let OverlayLayer = class OverlayLayer extends Component {
851
854
  position: "absolute",
852
855
  anchor: entry.anchor ?? void 0,
853
856
  anchorPoint: entry.anchor == null ? entry.point : void 0,
857
+ anchorRect: entry.anchor == null ? void 0 : entry.anchorRect,
854
858
  placement: entry.placement,
855
859
  anchorOffset: entry.offset,
856
860
  top: entry.top,
@@ -859,7 +863,8 @@ let OverlayLayer = class OverlayLayer extends Component {
859
863
  left: entry.left,
860
864
  zIndex: entry.zIndex,
861
865
  ...centering(entry),
862
- ...inheritedFrom(entry.environment ?? entry.anchor ?? null)
866
+ ...inheritedFrom(entry.environment ?? entry.anchor ?? null),
867
+ modifiers: followEnvironment(entry.environment ?? entry.anchor ?? null)
863
868
  }, entry.content));
864
869
  }
865
870
  return elements;
@@ -902,11 +907,7 @@ function centering(entry) {
902
907
  }
903
908
  /**
904
909
  * The scoped values an entry's content should keep, re-provided on the
905
- * box that holds it.
906
- *
907
- * Read once, when the entry opens. An overlay that outlives a theme
908
- * change re-opens; nothing here watches, because an entry's content is
909
- * built once too.
910
+ * box that holds it, as they are when the entry opens.
910
911
  */
911
912
  function inheritedFrom(node) {
912
913
  const environment = node?.environment;
@@ -917,6 +918,28 @@ function inheritedFrom(node) {
917
918
  contentColor: environment.get(UiEnvironmentKeys.contentColor)
918
919
  };
919
920
  }
921
+ /**
922
+ * The same values again whenever they change where the entry was
923
+ * declared, for as long as it is open.
924
+ *
925
+ * Reading them once let a dialog open while the page went dark (the
926
+ * system's setting changed, or the theme a person chose arrived from
927
+ * another worker a moment after they opened it) stay light over a dark
928
+ * page until it was closed.
929
+ */
930
+ const followEnvironmentKind = defineModifier({
931
+ name: "overlayEnvironment",
932
+ attach(host, from) {
933
+ host.onEnvironment(() => {
934
+ host.set("theme", host.environment(UiEnvironmentKeys.theme, from));
935
+ host.set("textStyle", host.environment(UiEnvironmentKeys.textStyle, from));
936
+ host.set("contentColor", host.environment(UiEnvironmentKeys.contentColor, from));
937
+ }, from);
938
+ }
939
+ });
940
+ function followEnvironment(node) {
941
+ return node === null ? [] : [followEnvironmentKind(node)];
942
+ }
920
943
  //#endregion
921
944
  //#region src/ComponentElement.ts
922
945
  function isComponentElement(value) {
@@ -1143,12 +1166,12 @@ var ComponentHost = class {
1143
1166
  return;
1144
1167
  }
1145
1168
  if (isObservable(provided)) {
1146
- const subscription = provided.subscribe((value) => cell.next(value));
1169
+ const subscription = provided.subscribe((value) => pushInput(cell, value));
1147
1170
  this.inputSubscriptions.set(inputName, subscription);
1148
1171
  this.subscriptions.add(subscription);
1149
1172
  return;
1150
1173
  }
1151
- cell.next(provided);
1174
+ pushInput(cell, provided);
1152
1175
  }
1153
1176
  validateInputs() {
1154
1177
  const metadata = getComponentMetadata(this.element.component);
@@ -1173,6 +1196,27 @@ var ComponentHost = class {
1173
1196
  }
1174
1197
  }
1175
1198
  };
1199
+ /**
1200
+ * Hands an input cell a value from its parent, unless it already holds
1201
+ * that very value.
1202
+ *
1203
+ * A parent that re-renders hands every child its props again, and a
1204
+ * prop that is an Observable built in the parent's render is a new
1205
+ * source each time, which re-subscribes and replays the current value.
1206
+ * Pushed through regardless, that replay re-ran every binding derived
1207
+ * from the input. In the issue tracker's editor, inserting one block
1208
+ * re-rendered the list of 2,868, each replay rebuilt its block's text
1209
+ * runs as a new array, and every editable on the page was re-measured:
1210
+ * 12,912 nodes for one Enter. Values that cross into a component are
1211
+ * treated as immutable everywhere else in the framework (bindings hold
1212
+ * on to them, patches share structure), so the same reference is the
1213
+ * same value and there is nothing to tell the child.
1214
+ */
1215
+ function pushInput(cell, value) {
1216
+ const current = cell.getValue();
1217
+ if (current !== void 0 && Object.is(current, value)) return;
1218
+ cell.next(value);
1219
+ }
1176
1220
  //#endregion
1177
1221
  //#region src/ComponentHostResolver.ts
1178
1222
  /**
@@ -1704,6 +1748,7 @@ function filesDuringDrag(event) {
1704
1748
  var ShellService = class {
1705
1749
  handler = null;
1706
1750
  scheme = internalState("light");
1751
+ contrastState = internalState("standard");
1707
1752
  insets = internalState(noInsets);
1708
1753
  isFullscreen = internalState(false);
1709
1754
  /** Popups asked for and not yet answered, by the id sent with each. */
@@ -1712,6 +1757,9 @@ var ShellService = class {
1712
1757
  /** Storage requests asked for and not yet answered, by the id sent with each. */
1713
1758
  stores = /* @__PURE__ */ new Map();
1714
1759
  nextStorageId = 1;
1760
+ /** Clipboard writes asked for and not yet answered, by the id sent with each. */
1761
+ copies = /* @__PURE__ */ new Map();
1762
+ nextCopyId = 1;
1715
1763
  /** File requests asked for and not yet answered, by the id sent with each. */
1716
1764
  files = /* @__PURE__ */ new Map();
1717
1765
  nextFileId = 1;
@@ -1744,6 +1792,18 @@ var ShellService = class {
1744
1792
  * read it beside a channel's view; its setter stays private here.
1745
1793
  */
1746
1794
  colorScheme = this.scheme;
1795
+ /**
1796
+ * The contrast the platform is asking for: `high` when the person has
1797
+ * turned up contrast or turned on a contrast theme, `standard`
1798
+ * otherwise. Reported by the shell like `colorScheme`, and as with it,
1799
+ * what it means is the application's: `withContrast(theme, contrast)`
1800
+ * is the theme that answers it.
1801
+ *
1802
+ * const theme = combineLatest([shell.colorScheme, shell.contrast]).pipe(
1803
+ * map(([scheme, contrast]) => withContrast(scheme === 'dark' ? darkTheme : lightTheme, contrast))
1804
+ * );
1805
+ */
1806
+ contrast = this.contrastState;
1747
1807
  /** The current appearance, for code that needs it without subscribing. */
1748
1808
  get currentColorScheme() {
1749
1809
  return this.scheme.value;
@@ -1783,6 +1843,10 @@ var ShellService = class {
1783
1843
  applyColorScheme(scheme) {
1784
1844
  if (this.scheme.value !== scheme) this.scheme.value = scheme;
1785
1845
  }
1846
+ /** Called by the runtime when the shell reports the contrast; not for applications. */
1847
+ applyContrast(contrast) {
1848
+ if (this.contrastState.value !== contrast) this.contrastState.value = contrast;
1849
+ }
1786
1850
  /**
1787
1851
  * Called by the runtime when the shell reports the platform's insets.
1788
1852
  *
@@ -1793,12 +1857,44 @@ var ShellService = class {
1793
1857
  applyViewportInsets(insets) {
1794
1858
  if (!insetsEqual(this.insets.value, insets)) this.insets.value = insets;
1795
1859
  }
1796
- /** Puts text on the system clipboard. */
1860
+ /**
1861
+ * Puts text on the system clipboard, and answers whether it got there.
1862
+ *
1863
+ * Most callers ignore the answer, and may: the text is on its way the
1864
+ * moment this returns. It is for an application that tells the person
1865
+ * what happened, a "Copied" toast after a menu command, say, which
1866
+ * would be a lie on the occasions the browser refused. It does refuse:
1867
+ * the clipboard wants a focused document, and some browsers a fresh
1868
+ * gesture, and a key pressed in a render worker reaches the window's
1869
+ * clipboard a message later than it reached the canvas.
1870
+ *
1871
+ * With no shell installed the answer is false, as `openPopup`'s is,
1872
+ * rather than a promise that never settles.
1873
+ */
1797
1874
  copyText(text) {
1798
- this.handler?.({
1875
+ const handler = this.handler;
1876
+ if (handler === void 0 || handler === null) return Promise.resolve(false);
1877
+ const id = this.nextCopyId++;
1878
+ const settled = new Promise((resolve) => {
1879
+ this.copies.set(id, resolve);
1880
+ });
1881
+ handler({
1799
1882
  type: "clipboard",
1883
+ id,
1800
1884
  text
1801
1885
  });
1886
+ return settled;
1887
+ }
1888
+ /**
1889
+ * Called by the runtime when the shell reports whether a clipboard
1890
+ * write landed. Not for applications. An unknown id is ignored, on
1891
+ * `settlePopup`'s terms.
1892
+ */
1893
+ settleClipboard(id, copied) {
1894
+ const resolve = this.copies.get(id);
1895
+ if (resolve === void 0) return;
1896
+ this.copies.delete(id);
1897
+ resolve(copied);
1802
1898
  }
1803
1899
  /** Opens a URL in the user's browser, in a new tab or window. */
1804
1900
  openUrl(url) {
@@ -2591,7 +2687,7 @@ var EditingProxy = class {
2591
2687
  * the reason the field's semantics live here rather than on a second
2592
2688
  * element beside it.
2593
2689
  */
2594
- describe(record) {
2690
+ describe(record, activeDescendant, controls) {
2595
2691
  if (this.disposed) return;
2596
2692
  const textarea = this.textarea;
2597
2693
  if (record === null) {
@@ -2603,7 +2699,11 @@ var EditingProxy = class {
2603
2699
  "aria-required",
2604
2700
  "aria-invalid",
2605
2701
  "aria-readonly",
2606
- "aria-disabled"
2702
+ "aria-disabled",
2703
+ "aria-expanded",
2704
+ "aria-activedescendant",
2705
+ "aria-controls",
2706
+ "aria-autocomplete"
2607
2707
  ]) textarea.removeAttribute(attribute);
2608
2708
  return;
2609
2709
  }
@@ -2616,6 +2716,10 @@ var EditingProxy = class {
2616
2716
  setOrClear(textarea, "aria-invalid", states.has("invalid") ? "true" : void 0);
2617
2717
  setOrClear(textarea, "aria-readonly", states.has("readonly") ? "true" : void 0);
2618
2718
  setOrClear(textarea, "aria-disabled", record.disabled === true ? "true" : void 0);
2719
+ setOrClear(textarea, "aria-expanded", states.has("expanded") ? "true" : states.has("collapsed") ? "false" : void 0);
2720
+ setOrClear(textarea, "aria-activedescendant", activeDescendant);
2721
+ setOrClear(textarea, "aria-controls", controls);
2722
+ setOrClear(textarea, "aria-autocomplete", record.autocomplete);
2619
2723
  }
2620
2724
  dispose() {
2621
2725
  if (this.disposed) return;
@@ -2678,7 +2782,8 @@ var EditingProxy = class {
2678
2782
  const clipboard = event.clipboardData;
2679
2783
  event.preventDefault();
2680
2784
  const text = clipboard?.getData("text/plain") ?? "";
2681
- if (text.length > 0) this.sink.paste(text);
2785
+ const html = clipboard?.getData("text/html") ?? "";
2786
+ if (text.length > 0 || html.length > 0) this.sink.paste(text, html.length > 0 ? html : null);
2682
2787
  };
2683
2788
  /**
2684
2789
  * A paste aimed at the canvas rather than at the field.
@@ -2703,6 +2808,7 @@ var EditingProxy = class {
2703
2808
  if (clipboard === null || state === null) return;
2704
2809
  event.preventDefault();
2705
2810
  clipboard.setData("text/plain", state.text.slice(state.selectionStart, state.selectionEnd));
2811
+ if (state.html !== void 0) clipboard.setData("text/html", state.html);
2706
2812
  };
2707
2813
  const onCut = (event) => {
2708
2814
  const state = this.state;
@@ -2757,17 +2863,15 @@ var EditingProxy = class {
2757
2863
  }
2758
2864
  };
2759
2865
  /**
2760
- * Writes text to the system clipboard from the main thread. The async
2761
- * API needs a secure context and, in some browsers, a recent user
2762
- * gesture; the `execCommand` fallback covers the rest.
2866
+ * Writes text to the system clipboard from the main thread, and answers
2867
+ * whether it got there. The async API needs a secure context and, in
2868
+ * some browsers, a recent user gesture; the `execCommand` fallback
2869
+ * covers the rest. False means both refused. It never rejects.
2763
2870
  */
2764
2871
  function writeClipboard(text, doc = document) {
2765
2872
  const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
2766
- if (clipboard !== void 0 && typeof clipboard.writeText === "function") {
2767
- clipboard.writeText(text).catch(() => copyWithExecCommand(text, doc));
2768
- return;
2769
- }
2770
- copyWithExecCommand(text, doc);
2873
+ if (clipboard !== void 0 && typeof clipboard.writeText === "function") return clipboard.writeText(text).then(() => true, () => copyWithExecCommand(text, doc));
2874
+ return Promise.resolve(copyWithExecCommand(text, doc));
2771
2875
  }
2772
2876
  function copyWithExecCommand(text, doc) {
2773
2877
  const previous = doc.activeElement;
@@ -2778,7 +2882,9 @@ function copyWithExecCommand(text, doc) {
2778
2882
  doc.body.appendChild(scratch);
2779
2883
  scratch.select();
2780
2884
  try {
2781
- doc.execCommand("copy");
2885
+ return doc.execCommand("copy");
2886
+ } catch {
2887
+ return false;
2782
2888
  } finally {
2783
2889
  scratch.remove();
2784
2890
  previous?.focus?.({ preventScroll: true });
@@ -2802,7 +2908,9 @@ const STATE_ATTRIBUTES = {
2802
2908
  invalid: ["aria-invalid", "true"],
2803
2909
  required: ["aria-required", "true"],
2804
2910
  readonly: ["aria-readonly", "true"],
2805
- modal: ["aria-modal", "true"]
2911
+ modal: ["aria-modal", "true"],
2912
+ multiselectable: ["aria-multiselectable", "true"],
2913
+ current: ["aria-current", "true"]
2806
2914
  };
2807
2915
  /**
2808
2916
  * Roles whose checked state is required rather than optional.
@@ -2848,8 +2956,14 @@ const RECORD_ATTRIBUTES = [
2848
2956
  "aria-invalid",
2849
2957
  "aria-required",
2850
2958
  "aria-readonly",
2851
- "aria-modal"
2959
+ "aria-modal",
2960
+ "aria-multiselectable",
2961
+ "aria-current",
2962
+ "aria-activedescendant",
2963
+ "aria-controls",
2964
+ "aria-autocomplete"
2852
2965
  ];
2966
+ let mirrors = 0;
2853
2967
  /**
2854
2968
  * Roles whose accessible name comes from what the element contains
2855
2969
  * rather than from an `aria-label`, plus prose, which has no role at
@@ -2915,6 +3029,12 @@ var SemanticsMirror = class {
2915
3029
  applying = false;
2916
3030
  focusedId = null;
2917
3031
  disposed = false;
3032
+ /**
3033
+ * Prefixes every element's DOM id, which `aria-activedescendant`
3034
+ * refers to. Per mirror, because two apps on one page have nodes with
3035
+ * the same ids.
3036
+ */
3037
+ idPrefix = `gesso-${mirrors++}-`;
2918
3038
  constructor(canvas, sink, editing = null) {
2919
3039
  this.canvas = canvas;
2920
3040
  this.sink = sink;
@@ -2930,7 +3050,7 @@ var SemanticsMirror = class {
2930
3050
  width: "0px",
2931
3051
  height: "0px",
2932
3052
  pointerEvents: "none",
2933
- overflow: "hidden",
3053
+ overflow: "clip",
2934
3054
  margin: "0",
2935
3055
  padding: "0",
2936
3056
  border: "0",
@@ -2997,11 +3117,18 @@ var SemanticsMirror = class {
2997
3117
  record
2998
3118
  });
2999
3119
  this.ids.set(element, record.id);
3120
+ element.id = this.idPrefix + record.id;
3000
3121
  } else existing.record = record;
3001
3122
  this.describe(element, record);
3002
3123
  this.place(element, record);
3003
3124
  if (existing?.box !== void 0) this.position(existing);
3004
- if (record.id === this.focusedId) this.editing?.describe(this.editing.active ? record : null);
3125
+ if (record.id === this.focusedId) this.describeEditing(this.editing?.active === true ? record : null);
3126
+ }
3127
+ /** Hands the proxy the focused editable's record, with the DOM ids of its active descendant and what it controls. */
3128
+ describeEditing(record) {
3129
+ const active = record?.activeDescendant;
3130
+ const controls = record?.controls;
3131
+ this.editing?.describe(record, active === void 0 ? void 0 : this.idPrefix + active, controls === void 0 ? void 0 : this.idPrefix + controls);
3005
3132
  }
3006
3133
  createElement() {
3007
3134
  const element = this.doc.createElement("div");
@@ -3016,7 +3143,7 @@ var SemanticsMirror = class {
3016
3143
  padding: "0",
3017
3144
  border: "0",
3018
3145
  outline: "none",
3019
- overflow: "hidden",
3146
+ overflow: "clip",
3020
3147
  pointerEvents: "none",
3021
3148
  color: "transparent"
3022
3149
  });
@@ -3047,6 +3174,9 @@ var SemanticsMirror = class {
3047
3174
  setNumber(element, "aria-setsize", record.setSize);
3048
3175
  setNumber(element, "aria-level", record.level);
3049
3176
  if (record.valueText !== void 0) element.setAttribute("aria-valuetext", record.valueText);
3177
+ if (record.activeDescendant !== void 0) element.setAttribute("aria-activedescendant", this.idPrefix + record.activeDescendant);
3178
+ if (record.controls !== void 0) element.setAttribute("aria-controls", this.idPrefix + record.controls);
3179
+ if (record.autocomplete !== void 0) element.setAttribute("aria-autocomplete", record.autocomplete);
3050
3180
  }
3051
3181
  /**
3052
3182
  * Puts an element under its parent, at the index the record gives.
@@ -3146,7 +3276,7 @@ var SemanticsMirror = class {
3146
3276
  this.focusedId = id;
3147
3277
  const entry = id === null ? void 0 : this.entries.get(id);
3148
3278
  if (this.editing !== null && this.editing.active) {
3149
- this.editing.describe(entry?.record ?? null);
3279
+ this.describeEditing(entry?.record ?? null);
3150
3280
  this.applying = true;
3151
3281
  try {
3152
3282
  this.editing.focus();
@@ -3155,7 +3285,7 @@ var SemanticsMirror = class {
3155
3285
  }
3156
3286
  return;
3157
3287
  }
3158
- this.editing?.describe(null);
3288
+ this.describeEditing(null);
3159
3289
  this.applying = true;
3160
3290
  try {
3161
3291
  if (entry !== void 0) entry.element.focus({ preventScroll: true });
@@ -3184,9 +3314,10 @@ var SemanticsMirror = class {
3184
3314
  const onKeyUp = (event) => this.sink.keyUp?.(event);
3185
3315
  const onPaste = (event) => {
3186
3316
  const text = event.clipboardData?.getData("text/plain") ?? "";
3187
- if (text.length === 0) return;
3317
+ const html = event.clipboardData?.getData("text/html") ?? "";
3318
+ if (text.length === 0 && html.length === 0) return;
3188
3319
  event.preventDefault();
3189
- this.sink.paste?.(text);
3320
+ this.sink.paste?.(text, html.length > 0 ? html : null);
3190
3321
  };
3191
3322
  this.container.addEventListener("click", onClick);
3192
3323
  this.container.addEventListener("focusin", onFocusIn);
@@ -3402,6 +3533,49 @@ function observeColorScheme(onChange) {
3402
3533
  return observeMediaQuery("(prefers-color-scheme: dark)", (dark) => onChange(dark ? "dark" : "light"));
3403
3534
  }
3404
3535
  //#endregion
3536
+ //#region src/app/contrast.ts
3537
+ /**
3538
+ * Watches the platform's contrast preference.
3539
+ *
3540
+ * `high` when the person has asked for more contrast
3541
+ * (`prefers-contrast: more`, macOS's Increase contrast and its
3542
+ * counterparts) or forced colours are on (Windows' contrast themes). A
3543
+ * canvas gets no forced colours from the browser, which only repaints
3544
+ * the DOM, so the application has to hear about it and draw its own:
3545
+ * `withContrast(theme, contrast)` is the theme that does.
3546
+ *
3547
+ * Shell-side, reported once at once and again on change, for the reason
3548
+ * `observeColorScheme` gives.
3549
+ */
3550
+ function observeContrast(onChange) {
3551
+ let more = false;
3552
+ let forced = false;
3553
+ let last = null;
3554
+ let ready = false;
3555
+ const report = () => {
3556
+ if (!ready) return;
3557
+ const contrast = more || forced ? "high" : "standard";
3558
+ if (contrast !== last) {
3559
+ last = contrast;
3560
+ onChange(contrast);
3561
+ }
3562
+ };
3563
+ const stopMore = observeMediaQuery("(prefers-contrast: more)", (matches) => {
3564
+ more = matches;
3565
+ report();
3566
+ });
3567
+ const stopForced = observeMediaQuery("(forced-colors: active)", (matches) => {
3568
+ forced = matches;
3569
+ report();
3570
+ });
3571
+ ready = true;
3572
+ report();
3573
+ return () => {
3574
+ stopMore();
3575
+ stopForced();
3576
+ };
3577
+ }
3578
+ //#endregion
3405
3579
  //#region src/app/reducedMotion.ts
3406
3580
  /**
3407
3581
  * Watches the platform's reduced-motion preference.
@@ -3807,6 +3981,15 @@ var WorkerApp = class {
3807
3981
  get appLogic() {
3808
3982
  return this.appLogicWorker === void 0 ? void 0 : portHandle(this.appLogicWorker);
3809
3983
  }
3984
+ /**
3985
+ * Opens a port to whatever the render worker serves under `key`, or
3986
+ * returns undefined before `mount` has started it. The development
3987
+ * agent bridge asks for `gesso:agent` this way; nothing else in the
3988
+ * shell talks to the render worker except through its protocol.
3989
+ */
3990
+ openRenderPort(key) {
3991
+ return this.renderWorker === void 0 ? void 0 : portHandle(this.renderWorker).open(key);
3992
+ }
3810
3993
  mount(host) {
3811
3994
  const element = resolveHost(host);
3812
3995
  this.host = element;
@@ -3889,9 +4072,10 @@ var WorkerApp = class {
3889
4072
  type: "compositionEnd",
3890
4073
  text
3891
4074
  }),
3892
- paste: (text) => this.post({
4075
+ paste: (text, html) => this.post({
3893
4076
  type: "paste",
3894
- text
4077
+ text,
4078
+ ...html === null ? {} : { html }
3895
4079
  }),
3896
4080
  blur: () => this.post({ type: "blur" }),
3897
4081
  keyDown: (event) => this.forwardKeyDown(event, true),
@@ -3904,13 +4088,21 @@ var WorkerApp = class {
3904
4088
  }),
3905
4089
  keyDown: (event) => this.forwardKeyDown(event),
3906
4090
  keyUp: (event) => this.forwardKeyUp(event),
3907
- paste: (text) => this.post({
4091
+ paste: (text, html) => this.post({
3908
4092
  type: "paste",
3909
- text
4093
+ text,
4094
+ ...html === null ? {} : { html }
3910
4095
  })
3911
4096
  }, this.proxy);
4097
+ const webmcp = this.options.webmcp;
4098
+ if (webmcp !== void 0 && webmcp !== false) import("./webmcp-CJVFvAHe.js").then((n) => n.a).then((agent) => agent.connectWebMcp(this, webmcp === true ? {} : webmcp)).then((disconnect) => {
4099
+ if (this.renderWorker === worker) this.disconnectWebMcp = disconnect;
4100
+ else disconnect();
4101
+ }, (error) => console.warn("[gesso] WebMCP could not register the channels:", error));
3912
4102
  return () => this.dispose();
3913
4103
  }
4104
+ /** Removes the WebMCP tools `webmcp` registered, if it did. */
4105
+ disconnectWebMcp = null;
3914
4106
  /**
3915
4107
  * An error the browser raised *at the worker object*, which is not
3916
4108
  * the same thing as the worker reporting one.
@@ -4075,6 +4267,8 @@ var WorkerApp = class {
4075
4267
  this.frameHandle = requestAnimationFrame(step);
4076
4268
  }
4077
4269
  dispose() {
4270
+ this.disconnectWebMcp?.();
4271
+ this.disconnectWebMcp = null;
4078
4272
  this.ready = false;
4079
4273
  this.setFrameLoop(false);
4080
4274
  this.detachColorScheme?.();
@@ -4136,6 +4330,7 @@ var WorkerApp = class {
4136
4330
  nodes: message.nodes,
4137
4331
  measured: message.measured,
4138
4332
  relayoutRoots: message.relayoutRoots,
4333
+ layoutPasses: message.layoutPasses,
4139
4334
  at: message.at,
4140
4335
  inputLatencyMs: message.inputLatencyMs,
4141
4336
  phases: message.phases,
@@ -4182,7 +4377,11 @@ var WorkerApp = class {
4182
4377
  return;
4183
4378
  }
4184
4379
  if (message.type === "clipboard") {
4185
- writeClipboard(message.text);
4380
+ writeClipboard(message.text).then((copied) => this.post({
4381
+ type: "clipboardResult",
4382
+ id: message.id,
4383
+ copied
4384
+ }));
4186
4385
  return;
4187
4386
  }
4188
4387
  if (message.type === "openUrl") {
@@ -4522,6 +4721,10 @@ var WorkerApp = class {
4522
4721
  reduced
4523
4722
  });
4524
4723
  });
4724
+ const detachContrast = observeContrast((contrast) => this.post({
4725
+ type: "contrast",
4726
+ contrast
4727
+ }));
4525
4728
  const onPointerMove = (event) => {
4526
4729
  const { x, y } = toLocal(event.clientX, event.clientY);
4527
4730
  const move = {
@@ -4606,6 +4809,21 @@ var WorkerApp = class {
4606
4809
  canvas.addEventListener("wheel", onWheel, { passive: false });
4607
4810
  canvas.addEventListener("keydown", onKeyDown);
4608
4811
  canvas.addEventListener("keyup", onKeyUp);
4812
+ const aimedAtPage = (target) => target === null || target === document || target === document.body || target === document.documentElement;
4813
+ const onPageKeyDown = (event) => {
4814
+ if (aimedAtPage(event.target)) {
4815
+ canvas.focus({ preventScroll: true });
4816
+ this.forwardKeyDown(event);
4817
+ }
4818
+ };
4819
+ const onPageKeyUp = (event) => {
4820
+ if (aimedAtPage(event.target)) this.forwardKeyUp(event);
4821
+ };
4822
+ const pageKeys = this.options.pageKeys === true;
4823
+ if (pageKeys) {
4824
+ document.addEventListener("keydown", onPageKeyDown);
4825
+ document.addEventListener("keyup", onPageKeyUp);
4826
+ }
4609
4827
  window.addEventListener("scroll", onCanvasMayHaveMoved, {
4610
4828
  capture: true,
4611
4829
  passive: true
@@ -4613,6 +4831,7 @@ var WorkerApp = class {
4613
4831
  window.addEventListener("resize", onCanvasMayHaveMoved);
4614
4832
  return () => {
4615
4833
  detachReducedMotion();
4834
+ detachContrast();
4616
4835
  detachFullscreen();
4617
4836
  detachFileDrop();
4618
4837
  canvas.removeEventListener("mousedown", onMouseDown);
@@ -4624,6 +4843,10 @@ var WorkerApp = class {
4624
4843
  canvas.removeEventListener("wheel", onWheel);
4625
4844
  canvas.removeEventListener("keydown", onKeyDown);
4626
4845
  canvas.removeEventListener("keyup", onKeyUp);
4846
+ if (pageKeys) {
4847
+ document.removeEventListener("keydown", onPageKeyDown);
4848
+ document.removeEventListener("keyup", onPageKeyUp);
4849
+ }
4627
4850
  window.removeEventListener("scroll", onCanvasMayHaveMoved, { capture: true });
4628
4851
  window.removeEventListener("resize", onCanvasMayHaveMoved);
4629
4852
  this.dropPendingMove();
@@ -5097,10 +5320,19 @@ function parseUrl(url) {
5097
5320
  }
5098
5321
  /** Joins a path and a query back into the url the history stores. */
5099
5322
  function formatUrl(path, query = {}) {
5100
- const pairs = Object.entries(query).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
5323
+ const pairs = Object.entries(query).map(([key, value]) => `${encodeQueryPart(key)}=${encodeQueryPart(value)}`);
5101
5324
  const normalized = `/${pathSegments(path).join("/")}`;
5102
5325
  return pairs.length === 0 ? normalized : `${normalized}?${pairs.join("&")}`;
5103
5326
  }
5327
+ /**
5328
+ * Encodes a query key or value, leaving the characters a query may hold
5329
+ * as they are (`,` `:` `@` `/`), so a list of values reads as one:
5330
+ * `status=todo,done`, not `status=todo%2Cdone`. What would change how
5331
+ * the query parses (`&`, `=`, `+`, `#`) is still encoded.
5332
+ */
5333
+ function encodeQueryPart(text) {
5334
+ return encodeURIComponent(text).replace(/%2C/gi, ",").replace(/%3A/gi, ":").replace(/%40/gi, "@").replace(/%2F/gi, "/");
5335
+ }
5104
5336
  function splitOnce(text, separator) {
5105
5337
  const index = text.indexOf(separator);
5106
5338
  return index === -1 ? [text, ""] : [text.slice(0, index), text.slice(index + separator.length)];
@@ -5208,7 +5440,7 @@ var RouterService = class {
5208
5440
  */
5209
5441
  params(route) {
5210
5442
  const match = this.match.value;
5211
- if (match === null || !match.chain.includes(route)) return null;
5443
+ if (match === null || !inChain(match.chain, route)) return null;
5212
5444
  return match.params;
5213
5445
  }
5214
5446
  /** The same, as a cell, for binding a screen's title or fields. */
@@ -5300,7 +5532,7 @@ var RouterService = class {
5300
5532
  isActive(route) {
5301
5533
  return computed(() => {
5302
5534
  const match = this.match.value;
5303
- return match !== null && match.chain.includes(route);
5535
+ return match !== null && inChain(match.chain, route);
5304
5536
  });
5305
5537
  }
5306
5538
  /**
@@ -5431,6 +5663,23 @@ function sameParams(a, b) {
5431
5663
  const keys = Object.keys(left);
5432
5664
  return keys.length === Object.keys(right).length && keys.every((key) => left[key] === right[key]);
5433
5665
  }
5666
+ /**
5667
+ * Whether a route is in a matched chain: the same object, or a route
5668
+ * declared with the same path.
5669
+ *
5670
+ * Identity alone was the original test, and it is right for a table
5671
+ * loaded once. It fails silently under a dev server's hot replacement:
5672
+ * a route table that imports its screens, with screens that import the
5673
+ * table to read their params, is an import cycle, and after a hot edit
5674
+ * Vite can serve the two sides as `routes.tsx` and `routes.tsx?t=…`.
5675
+ * The table then loads twice, the router holds one copy's route objects
5676
+ * and the screens hold the other's, and every `params(route)` answers
5677
+ * null, so a team page quietly showed the default team. Matching is by
5678
+ * path already, so two routes with one path are one route to the url.
5679
+ */
5680
+ function inChain(chain, route) {
5681
+ return chain.some((link) => link === route || link.path === route.path);
5682
+ }
5434
5683
  //#endregion
5435
5684
  //#region src/app/FindService.ts
5436
5685
  /**
@@ -5550,14 +5799,61 @@ var EditingService = class {
5550
5799
  * scroll already applied: a popup placed under the caret adds the
5551
5800
  * node's position on screen and nothing else.
5552
5801
  *
5802
+ * `offset` asks about a caret at another character than the one the
5803
+ * caret is at: the start of the word being completed, so its list
5804
+ * stays put as the word grows. With an overlay's `anchorRect` and the
5805
+ * field as its anchor, the list follows the field from then on.
5806
+ *
5553
5807
  * Null when the node is not an editable, when it has not been laid
5554
5808
  * out yet, or before the runtime has wired the controller — all
5555
5809
  * three of which are "ask again next frame" rather than errors, and
5556
5810
  * all three of which happen during the frame a field first appears.
5557
5811
  */
5558
- caretRectOf(node) {
5812
+ caretRectOf(node, offset) {
5559
5813
  if (node === null || this.controller === null) return null;
5560
- return this.controller.caretRectOf(node);
5814
+ return this.controller.caretRectOf(node, offset);
5815
+ }
5816
+ /**
5817
+ * Selects from `anchor` to `focus` and focuses the field the focus is
5818
+ * in: within one field, or across the fields of an editing group (see
5819
+ * `UiEditingGroup`). What an editor does after a command over a
5820
+ * selection (making it bold, say) to leave it selected, since its
5821
+ * fields can only select their own text.
5822
+ *
5823
+ * False when the two ends aren't editables of one group, or before
5824
+ * the runtime has wired the controller.
5825
+ */
5826
+ select(anchor, focus) {
5827
+ return this.controller?.select(anchor, focus) ?? false;
5828
+ }
5829
+ };
5830
+ //#endregion
5831
+ //#region src/app/ScrollService.ts
5832
+ /**
5833
+ * Scrolling a node into view, for a component that moves a highlight
5834
+ * rather than focus.
5835
+ *
5836
+ * Focus moved from the keyboard already brings its node into view. A
5837
+ * highlight doesn't move focus: a combobox's arrows walk its list while
5838
+ * the caret stays in the field, and a grid's cursor can do the same.
5839
+ * The list has to follow the highlight all the same, and only the
5840
+ * runtime knows where the scroll containers above a node are and how
5841
+ * far each has to move. This is the asking.
5842
+ */
5843
+ var ScrollService = class {
5844
+ scroller = null;
5845
+ /** Installed by the runtime; without one nothing scrolls. */
5846
+ setScroller(scroller) {
5847
+ this.scroller = scroller;
5848
+ }
5849
+ /**
5850
+ * Scrolls every container above `node` just enough that it's inside
5851
+ * the container's viewport, `padding` pixels from the nearest edge.
5852
+ * Nothing moves when it's visible already. Before the node has been
5853
+ * laid out there is nowhere to scroll it to, and nothing happens.
5854
+ */
5855
+ scrollIntoView(node, padding = 8) {
5856
+ this.scroller?.(node, padding);
5561
5857
  }
5562
5858
  };
5563
5859
  //#endregion
@@ -5586,6 +5882,14 @@ var FocusService = class {
5586
5882
  focused = internalState(null);
5587
5883
  /** Whether focus is confined to a subtree by an open trap. */
5588
5884
  trapped = internalState(false);
5885
+ /**
5886
+ * Whether the focus held is focus the keyboard can see: reached by Tab
5887
+ * or an arrow key, or by code with no pointer since, rather than by a
5888
+ * press. CSS's `:focus-visible`, for something other than a ring that
5889
+ * should answer the keyboard and not the click that happened to focus
5890
+ * the same control: a tooltip, say.
5891
+ */
5892
+ focusVisible = internalState(false);
5589
5893
  manager = null;
5590
5894
  detach = null;
5591
5895
  /** Actions taken before the runtime installed a manager, in order. */
@@ -5601,6 +5905,7 @@ var FocusService = class {
5601
5905
  }
5602
5906
  const focusChange = manager.onFocusChange((node) => {
5603
5907
  this.focused.value = node;
5908
+ this.focusVisible.value = manager.focusVisible;
5604
5909
  });
5605
5910
  const scopeChange = manager.onScopeChange(() => {
5606
5911
  this.trapped.value = manager.trapped;
@@ -5614,9 +5919,15 @@ var FocusService = class {
5614
5919
  for (const action of pending) action(manager);
5615
5920
  this.sync(manager);
5616
5921
  }
5617
- /** Gives the node keyboard focus. Non-focusable nodes are ignored. */
5618
- focus(node) {
5619
- this.run((manager) => manager.focus(node));
5922
+ /**
5923
+ * Gives the node keyboard focus. Non-focusable nodes are ignored.
5924
+ *
5925
+ * The node is scrolled into view, unless `options.preventScroll` asks
5926
+ * for the page to stay where it is, as `element.focus({ preventScroll:
5927
+ * true })` does in a browser.
5928
+ */
5929
+ focus(node, options) {
5930
+ this.run((manager) => manager.focus(node, "program", options));
5620
5931
  }
5621
5932
  /** Drops focus without moving it anywhere. */
5622
5933
  blur() {
@@ -5656,6 +5967,7 @@ var FocusService = class {
5656
5967
  }
5657
5968
  sync(manager) {
5658
5969
  this.focused.value = manager.focusedNode;
5970
+ this.focusVisible.value = manager.focusVisible;
5659
5971
  this.trapped.value = manager.trapped;
5660
5972
  }
5661
5973
  };
@@ -6570,7 +6882,20 @@ var GessoRuntime = class {
6570
6882
  sharedElements = new UiSharedElements();
6571
6883
  focusNotifier = new FocusNotifier();
6572
6884
  environmentNotifier = new EnvironmentNotifier();
6885
+ /**
6886
+ * The semantics tree: every record by id, and each record's children
6887
+ * in order.
6888
+ *
6889
+ * A record's own `index` is only kept current for the records that
6890
+ * are sent; the order is `semanticsChildren`. Renumbering every
6891
+ * record after an insertion cost a pass over the whole tree on every
6892
+ * structural change, for indices nothing would read until a record
6893
+ * was next sent or the tree was asked for (`semanticsTree`).
6894
+ */
6573
6895
  semantics = /* @__PURE__ */ new Map();
6896
+ semanticsChildren = /* @__PURE__ */ new Map();
6897
+ /** The tree in document order with every index current, built when asked for. */
6898
+ semanticsOrdered = null;
6574
6899
  semanticsListener = null;
6575
6900
  /**
6576
6901
  * The box last reported for each mirrored node, so a frame that
@@ -6578,6 +6903,8 @@ var GessoRuntime = class {
6578
6903
  * them. Only populated while a listener is attached.
6579
6904
  */
6580
6905
  semanticsBoxes = /* @__PURE__ */ new Map();
6906
+ /** What each semantics walk leaves for the next; see `SemanticsMemory`. */
6907
+ semanticsMemory = semanticsMemory();
6581
6908
  lastFocusedId = null;
6582
6909
  /**
6583
6910
  * Where the caret was when `reload` replaced the tree, put back on
@@ -6591,6 +6918,14 @@ var GessoRuntime = class {
6591
6918
  focusAfterReload = null;
6592
6919
  /** Whether `focusAfterReload` is waiting to be applied. */
6593
6920
  restoringFocus = false;
6921
+ /**
6922
+ * Reveals asked for while the layout listeners run, kept until the
6923
+ * frame's boxes have settled; null outside `settleLayout`, where a
6924
+ * reveal is applied at once.
6925
+ */
6926
+ deferredReveals = null;
6927
+ /** Whether a frame has run out of layout passes and been warned about; see `MAX_LAYOUT_PASSES`. */
6928
+ warnedLayoutPasses = false;
6594
6929
  /** A frame changed semantics while nothing was listening; see `semanticsTree`. */
6595
6930
  semanticsStale = false;
6596
6931
  lastEditingState = null;
@@ -6678,8 +7013,8 @@ var GessoRuntime = class {
6678
7013
  focus: {
6679
7014
  isFocused: (node) => this.focusNotifier.isFocused(node),
6680
7015
  isFocusVisible: (node) => this.focusNotifier.isFocusVisible(node),
6681
- focus: (node) => {
6682
- this.focusManager.focus(node);
7016
+ focus: (node, options) => {
7017
+ this.focusManager.focus(node, "program", options);
6683
7018
  },
6684
7019
  onFocusChange: (node, listener) => this.focusNotifier.add(node, listener)
6685
7020
  },
@@ -6719,6 +7054,7 @@ var GessoRuntime = class {
6719
7054
  if (!this.services.has(FindService)) this.services.register(FindService);
6720
7055
  if (!this.services.has(FocusService)) this.services.register(FocusService);
6721
7056
  if (!this.services.has(EditingService)) this.services.register(EditingService);
7057
+ if (!this.services.has(ScrollService)) this.services.register(ScrollService);
6722
7058
  if (!this.services.has(FrameService)) this.services.register(FrameService);
6723
7059
  if (!this.services.has(MediaService)) this.services.register(MediaService);
6724
7060
  if (!this.services.has(FontService)) this.services.register(FontService);
@@ -6793,8 +7129,9 @@ var GessoRuntime = class {
6793
7129
  this.input = this.createInput();
6794
7130
  this.services.get(FocusService).setManager(this.input.focus);
6795
7131
  this.services.get(EditingService).setController(this.input.editing);
6796
- this.input.focus.onFocusChange((node, source) => {
6797
- if (node !== null && source !== "pointer") this.scrollIntoView(node);
7132
+ this.services.get(ScrollService).setScroller((node, padding) => this.scrollIntoView(node, padding));
7133
+ this.input.focus.onFocusChange((node, source, options) => {
7134
+ if (node !== null && source !== "pointer" && options.preventScroll !== true) this.scrollIntoView(node);
6798
7135
  });
6799
7136
  if (options.width !== void 0 && options.height !== void 0) this.resize(options.width, options.height, this.pixelRatio);
6800
7137
  }
@@ -7278,6 +7615,10 @@ var GessoRuntime = class {
7278
7615
  setColorScheme(scheme) {
7279
7616
  this.services.get(ShellService).applyColorScheme(scheme);
7280
7617
  }
7618
+ /** The contrast the platform asks for, passed straight to `ShellService`; see `ShellService.contrast`. */
7619
+ setContrast(contrast) {
7620
+ this.services.get(ShellService).applyContrast(contrast);
7621
+ }
7281
7622
  /**
7282
7623
  * The platform's own insets, as the shell reports them: the safe area
7283
7624
  * under a notch or a home indicator and the strip a soft keyboard
@@ -7342,6 +7683,13 @@ var GessoRuntime = class {
7342
7683
  this.services.get(ShellService).settlePopup(id, opened);
7343
7684
  }
7344
7685
  /**
7686
+ * Reports whether a clipboard write landed, settling the promise
7687
+ * `ShellService.copyText` returned.
7688
+ */
7689
+ settleClipboard(id, copied) {
7690
+ this.services.get(ShellService).settleClipboard(id, copied);
7691
+ }
7692
+ /**
7345
7693
  * Reports what the shell found in `localStorage`, settling the
7346
7694
  * promise `ShellService.requestStorage` returned.
7347
7695
  *
@@ -7562,8 +7910,20 @@ var GessoRuntime = class {
7562
7910
  * Scrolls every scroll container above `node` just enough that the
7563
7911
  * node is inside its viewport, `padding` pixels from the nearest edge.
7564
7912
  * Nothing moves when it is already visible.
7913
+ *
7914
+ * Asked for from a layout listener — `autoFocus` taking focus is the
7915
+ * usual one — it waits until the frame's layout has settled, so it
7916
+ * reveals where the node ends up rather than where the first pass
7917
+ * put it (see `settleLayout`).
7565
7918
  */
7566
7919
  scrollIntoView(node, padding = 8) {
7920
+ if (this.deferredReveals !== null) {
7921
+ this.deferredReveals.push({
7922
+ node,
7923
+ padding
7924
+ });
7925
+ return;
7926
+ }
7567
7927
  for (const adjustment of this.engine.revealAdjustments(node, padding)) {
7568
7928
  this.smoothScroller.stop(adjustment.container);
7569
7929
  adjustment.container.setProperty("scrollX", adjustment.scrollX);
@@ -7792,7 +8152,7 @@ var GessoRuntime = class {
7792
8152
  editing,
7793
8153
  selection
7794
8154
  }),
7795
- wheel: new UiWheelController(hitTester, this.dispatcher, scrollSink, () => this.appRoot ?? root),
8155
+ wheel: new UiWheelController(hitTester, this.dispatcher, scrollSink, () => this.appRoot ?? root, void 0, { pace: true }),
7796
8156
  keyboard: new UiKeyboardController(this.dispatcher, focus, () => this.appRoot ?? this.layoutRoot(), {
7797
8157
  editing,
7798
8158
  selection,
@@ -7824,7 +8184,7 @@ var GessoRuntime = class {
7824
8184
  if (listener !== null) this.semanticsTree();
7825
8185
  if (listener === null || this.semantics.size === 0) return;
7826
8186
  listener({
7827
- patches: [...this.semantics.values()].map((node) => ({
8187
+ patches: [...this.orderedSemantics().values()].map((node) => ({
7828
8188
  op: "add",
7829
8189
  node
7830
8190
  })),
@@ -7833,6 +8193,14 @@ var GessoRuntime = class {
7833
8193
  });
7834
8194
  }
7835
8195
  /**
8196
+ * The id of the node that holds focus, or null. The same id the
8197
+ * semantics tree keys its records by, so a reader of the tree can
8198
+ * say which control a key press would reach.
8199
+ */
8200
+ focusedNodeId() {
8201
+ return this.focusManager.focusedNode?.id ?? null;
8202
+ }
8203
+ /**
7836
8204
  * The semantics tree as of the last frame that changed it.
7837
8205
  *
7838
8206
  * Rebuilt here when no mirror was listening and a frame marked it
@@ -7841,10 +8209,59 @@ var GessoRuntime = class {
7841
8209
  */
7842
8210
  semanticsTree() {
7843
8211
  if (this.semanticsStale) {
7844
- this.semantics = buildSemanticsTree(this.layoutRoot());
8212
+ this.storeSemantics(buildSemanticsTree(this.layoutRoot(), this.semanticsMemory));
7845
8213
  this.semanticsStale = false;
7846
8214
  }
7847
- return this.semantics;
8215
+ return this.orderedSemantics();
8216
+ }
8217
+ /** Replaces the tree with one built in full, which is in document order. */
8218
+ storeSemantics(tree) {
8219
+ this.semantics = tree;
8220
+ this.semanticsOrdered = tree;
8221
+ this.semanticsChildren = /* @__PURE__ */ new Map();
8222
+ for (const record of tree.values()) {
8223
+ let list = this.semanticsChildren.get(record.parent);
8224
+ if (list === void 0) this.semanticsChildren.set(record.parent, list = []);
8225
+ list.push(record.id);
8226
+ }
8227
+ }
8228
+ /**
8229
+ * The tree in document order, every index current: a walk down
8230
+ * `semanticsChildren`, done when someone asks rather than per frame.
8231
+ * Records whose index had gone out of date are brought up to date in
8232
+ * the store as well.
8233
+ */
8234
+ orderedSemantics() {
8235
+ if (this.semanticsOrdered !== null) return this.semanticsOrdered;
8236
+ const ordered = /* @__PURE__ */ new Map();
8237
+ const stack = [];
8238
+ const push = (parent) => {
8239
+ const list = this.semanticsChildren.get(parent) ?? [];
8240
+ const indices = semanticIndices(list);
8241
+ for (let i = list.length - 1; i >= 0; i--) stack.push([list[i], indices[i]]);
8242
+ };
8243
+ push(null);
8244
+ while (stack.length > 0) {
8245
+ const [id, index] = stack.pop();
8246
+ let record = this.semantics.get(id);
8247
+ if (record.index !== index) {
8248
+ record = {
8249
+ ...record,
8250
+ index
8251
+ };
8252
+ this.semantics.set(id, record);
8253
+ }
8254
+ ordered.set(id, record);
8255
+ push(id);
8256
+ }
8257
+ this.semanticsOrdered = ordered;
8258
+ return ordered;
8259
+ }
8260
+ /** A record's index among its siblings now, which its own `index` may not say. */
8261
+ semanticsIndexOf(record) {
8262
+ const list = this.semanticsChildren.get(record.parent) ?? [];
8263
+ const at = list.indexOf(record.id);
8264
+ return at < 0 ? record.index : semanticIndices(list)[at];
7848
8265
  }
7849
8266
  /**
7850
8267
  * Something an assistive technology did to a mirrored element,
@@ -7925,7 +8342,7 @@ var GessoRuntime = class {
7925
8342
  const listener = this.semanticsListener;
7926
8343
  if (rebuild) {
7927
8344
  if (listener === null) this.semanticsStale = true;
7928
- else patches = this.rescopeSemantics(frame) ?? this.rebuildSemantics();
8345
+ else patches = this.rescopeSemantics(frame) ?? this.restructureSemantics(frame) ?? this.rebuildSemantics();
7929
8346
  }
7930
8347
  if (listener === null) return;
7931
8348
  const boxes = moved || patches.length > 0 ? this.collectSemanticsBoxes() : EMPTY_BOXES;
@@ -7944,9 +8361,10 @@ var GessoRuntime = class {
7944
8361
  }
7945
8362
  /** Walks the whole tree and diffs it against the last one. */
7946
8363
  rebuildSemantics() {
7947
- const next = buildSemanticsTree(this.layoutRoot());
7948
- const patches = diffSemantics(this.semantics, next);
7949
- this.semantics = next;
8364
+ const previous = this.orderedSemantics();
8365
+ const next = buildSemanticsTree(this.layoutRoot(), this.semanticsMemory);
8366
+ const patches = dropIndexShifts(previous, next, diffSemantics(previous, next));
8367
+ this.storeSemantics(next);
7950
8368
  this.semanticsStale = false;
7951
8369
  return patches;
7952
8370
  }
@@ -8002,7 +8420,7 @@ var GessoRuntime = class {
8002
8420
  const updates = [];
8003
8421
  for (const owner of owners) {
8004
8422
  const previous = this.semantics.get(owner.id);
8005
- const subtree = buildSemanticsSubtree(owner, previous.parent, previous.index, semanticsInertAbove(owner));
8423
+ const subtree = buildSemanticsSubtree(owner, previous.parent, this.semanticsIndexOf(previous), semanticsInertAbove(owner), this.semanticsMemory);
8006
8424
  if (subtree === null) return null;
8007
8425
  let covered = 0;
8008
8426
  for (const id of this.semanticIdsUnder(owner)) {
@@ -8010,13 +8428,18 @@ var GessoRuntime = class {
8010
8428
  if (next === void 0) return null;
8011
8429
  covered++;
8012
8430
  const before = this.semantics.get(id);
8013
- if (!recordsEqual(before, next)) updates.push(next);
8431
+ if (!recordsEqual({
8432
+ ...before,
8433
+ index: next.index
8434
+ }, next)) updates.push(next);
8435
+ else if (before.index !== next.index) this.semantics.set(id, next);
8014
8436
  }
8015
8437
  if (covered !== subtree.size) return null;
8016
8438
  }
8017
8439
  const patches = [];
8018
8440
  for (const record of updates) {
8019
8441
  this.semantics.set(record.id, record);
8442
+ this.semanticsOrdered?.set(record.id, record);
8020
8443
  patches.push({
8021
8444
  op: "update",
8022
8445
  node: record
@@ -8025,6 +8448,163 @@ var GessoRuntime = class {
8025
8448
  return patches;
8026
8449
  }
8027
8450
  /**
8451
+ * The patches for a frame that changed the shape of the tree, without
8452
+ * walking all of it, or null when the whole tree has to be rebuilt.
8453
+ *
8454
+ * Any frame that added or removed a child used to rebuild the tree
8455
+ * from the root, describing every node again. In a 5,000-line editor
8456
+ * that was 11 ms of every Enter, for one new paragraph. Now the walk
8457
+ * starts at the nearest record above each change, as
8458
+ * `rescopeSemantics` does for a change of meaning, and inside it a
8459
+ * subtree nothing touched keeps its records: renumbered if its
8460
+ * siblings moved, never described again (see
8461
+ * `rebuildSemanticsSubtree`). What is described is the path down to
8462
+ * each change, whatever is new, and everything under a node whose
8463
+ * own meaning changed.
8464
+ *
8465
+ * The result is spliced into `this.semantics` in place, since the map
8466
+ * is in document order and a record's subtree is a contiguous run of
8467
+ * it, so adds still arrive in document order. Removals come first.
8468
+ */
8469
+ restructureSemantics(frame) {
8470
+ if (this.semantics.size === 0 || this.semanticsStale) return null;
8471
+ const dirty = [];
8472
+ /** Nodes whose own meaning may have changed: everything under them is described afresh. */
8473
+ const meaning = /* @__PURE__ */ new Set();
8474
+ let structural = false;
8475
+ for (const [node, flags] of frame.entries()) {
8476
+ const children = (flags & DirtyFlags.Children) !== 0;
8477
+ const semantics = (flags & DirtyFlags.Semantics) !== 0;
8478
+ if (!children && !semantics) continue;
8479
+ if (!this.attached(node)) continue;
8480
+ structural ||= children;
8481
+ dirty.push(node);
8482
+ if (semantics) meaning.add(node);
8483
+ }
8484
+ if (!structural) return null;
8485
+ const owners = /* @__PURE__ */ new Set();
8486
+ for (const node of dirty) {
8487
+ const owner = this.semanticsOwnerOf(node);
8488
+ if (owner === null) return null;
8489
+ owners.add(owner);
8490
+ }
8491
+ for (const owner of owners) for (let above = owner.parent; above !== null; above = above.parent) if (owners.has(above)) {
8492
+ owners.delete(owner);
8493
+ break;
8494
+ }
8495
+ if (owners.size > SCOPED_SEMANTICS_OWNERS) return null;
8496
+ const path = /* @__PURE__ */ new Set();
8497
+ for (const node of dirty) for (let current = node; current !== null && !path.has(current); current = current.parent) {
8498
+ path.add(current);
8499
+ if (owners.has(current)) break;
8500
+ }
8501
+ const fresh = /* @__PURE__ */ new Map();
8502
+ const underChangedMeaning = (node) => {
8503
+ const known = fresh.get(node);
8504
+ if (known !== void 0) return known;
8505
+ const answer = meaning.has(node) || !owners.has(node) && node.parent !== null && underChangedMeaning(node.parent);
8506
+ fresh.set(node, answer);
8507
+ return answer;
8508
+ };
8509
+ const unchanged = (node) => !path.has(node) && (meaning.size === 0 || !underChangedMeaning(node));
8510
+ const positions = /* @__PURE__ */ new Map();
8511
+ const reuse = {
8512
+ previous: this.semantics,
8513
+ childrenOf: (parent) => this.semanticsChildren.get(parent) ?? [],
8514
+ positionOf: (parent, id) => {
8515
+ let index = positions.get(parent);
8516
+ if (index === void 0) {
8517
+ index = new Map((this.semanticsChildren.get(parent) ?? []).map((child, i) => [child, i]));
8518
+ positions.set(parent, index);
8519
+ }
8520
+ return index.get(id) ?? -1;
8521
+ },
8522
+ memory: this.semanticsMemory,
8523
+ unchanged
8524
+ };
8525
+ const walks = [];
8526
+ for (const owner of owners) {
8527
+ const before = this.semantics.get(owner.id);
8528
+ if (before === void 0) return null;
8529
+ const walked = rewalkSemantics(owner, before.parent, this.semanticsIndexOf(before), semanticsInertAbove(owner), reuse);
8530
+ if (walked === null) return null;
8531
+ walks.push(walked);
8532
+ }
8533
+ const present = /* @__PURE__ */ new Set();
8534
+ for (const walked of walks) for (const list of walked.children.values()) for (const id of list) present.add(id);
8535
+ const removals = [];
8536
+ const remove = (id) => {
8537
+ removals.push({
8538
+ op: "remove",
8539
+ id
8540
+ });
8541
+ for (const child of this.semanticsChildren.get(id) ?? []) remove(child);
8542
+ this.semanticsChildren.delete(id);
8543
+ this.semantics.delete(id);
8544
+ };
8545
+ const formerly = /* @__PURE__ */ new Map();
8546
+ for (const walked of walks) for (const parent of walked.children.keys()) formerly.set(parent, this.semanticsChildren.get(parent) ?? []);
8547
+ for (const list of formerly.values()) for (const id of list) if (!present.has(id) && this.semantics.has(id)) remove(id);
8548
+ const reordered = /* @__PURE__ */ new Set();
8549
+ for (const walked of walks) for (const [parent, list] of walked.children) {
8550
+ const old = formerly.get(parent) ?? [];
8551
+ if (old.length === 0) continue;
8552
+ const was = new Map(old.map((id, i) => [id, i]));
8553
+ let last = -1;
8554
+ for (const id of list) {
8555
+ const at = was.get(id);
8556
+ if (at === void 0) continue;
8557
+ if (at < last) {
8558
+ reordered.add(parent);
8559
+ break;
8560
+ }
8561
+ last = at;
8562
+ }
8563
+ }
8564
+ const changes = [];
8565
+ for (const walked of walks) {
8566
+ for (const { id, parent, index } of walked.placed) {
8567
+ const record = walked.records.get(id);
8568
+ const before = this.semantics.get(id);
8569
+ if (record !== void 0) {
8570
+ if (before === void 0) changes.push({
8571
+ op: "add",
8572
+ node: record
8573
+ });
8574
+ else if (before.parent !== record.parent || reordered.has(parent) || !recordsEqual({
8575
+ ...before,
8576
+ index: record.index
8577
+ }, record)) changes.push({
8578
+ op: "update",
8579
+ node: record
8580
+ });
8581
+ this.semantics.set(id, record);
8582
+ } else if (before === void 0) return null;
8583
+ else if (before.parent !== parent || reordered.has(parent)) {
8584
+ const moved = {
8585
+ ...before,
8586
+ parent,
8587
+ index
8588
+ };
8589
+ changes.push({
8590
+ op: "update",
8591
+ node: moved
8592
+ });
8593
+ this.semantics.set(id, moved);
8594
+ }
8595
+ }
8596
+ for (const [parent, list] of walked.children) this.semanticsChildren.set(parent, list);
8597
+ }
8598
+ this.semanticsOrdered = null;
8599
+ return [...removals, ...changes];
8600
+ }
8601
+ /** Whether a node is still in the tree being laid out. */
8602
+ attached(node) {
8603
+ const root = this.layoutRoot();
8604
+ for (let current = node; current !== null; current = current.parent) if (current === root) return true;
8605
+ return false;
8606
+ }
8607
+ /**
8028
8608
  * The nearest node at or above `node` that holds a semantics record,
8029
8609
  * or null when nothing above it does.
8030
8610
  */
@@ -8085,22 +8665,75 @@ var GessoRuntime = class {
8085
8665
  *
8086
8666
  * Ids that have left the tree are dropped here rather than tracked,
8087
8667
  * since a removal patch has already told the mirror about them.
8668
+ *
8669
+ * **Found by walking what is on screen, not by checking everything.**
8670
+ * Bounding what was sent still computed every node's box to find out
8671
+ * whether it was on screen: a 5,000-line document has three thousand
8672
+ * fields, and the sweep cost 4 to 24 ms of every keystroke. The walk
8673
+ * goes down from the root carrying the scroll and sticky offsets that
8674
+ * `visibleBox` adds up per node, and passes over any subtree whose
8675
+ * bounds (the hit tester's, see `subtreeBoundsFor`) are off screen.
8676
+ * A node it passes over is one this skipped before anyway.
8088
8677
  */
8089
8678
  collectSemanticsBoxes() {
8090
8679
  const changed = [];
8091
- const focused = this.focusManager.focusedNode?.id;
8092
- for (const id of this.semantics.keys()) {
8093
- const node = this.graph.getNode(id);
8094
- if (node === void 0 || this.engine.recordFor(node) === void 0) continue;
8095
- const box = this.engine.visibleBox(node);
8680
+ const focused = this.focusManager.focusedNode;
8681
+ let focusedSeen = false;
8682
+ const offer = (id, box) => {
8096
8683
  const last = this.semanticsBoxes.get(id);
8097
- if (last !== void 0 && boxesEqual(last, box)) continue;
8098
- if (id !== focused && !this.onScreen(box)) continue;
8684
+ if (last !== void 0 && boxesEqual(last, box)) return;
8099
8685
  this.semanticsBoxes.set(id, box);
8100
8686
  changed.push({
8101
8687
  id,
8102
8688
  box
8103
8689
  });
8690
+ };
8691
+ const root = this.root;
8692
+ if (root !== void 0) {
8693
+ this.engine.subtreeBoundsFor(root);
8694
+ const stack = [[
8695
+ root,
8696
+ 0,
8697
+ 0
8698
+ ]];
8699
+ while (stack.length > 0) {
8700
+ const [node, dx, dy] = stack.pop();
8701
+ const rec = this.engine.recordFor(node);
8702
+ let childX = dx;
8703
+ let childY = dy;
8704
+ if (rec !== void 0) {
8705
+ const x = dx + rec.stickyOffsetX;
8706
+ const y = dy + rec.stickyOffsetY;
8707
+ if (!rec.boundsUnbounded && node !== focused && !this.onScreen({
8708
+ x: rec.boundsMinX + x,
8709
+ y: rec.boundsMinY + y,
8710
+ width: rec.boundsMaxX - rec.boundsMinX,
8711
+ height: rec.boundsMaxY - rec.boundsMinY
8712
+ })) continue;
8713
+ if (this.semantics.has(node.id)) {
8714
+ const box = {
8715
+ x: rec.x + x,
8716
+ y: rec.y + y,
8717
+ width: rec.width,
8718
+ height: rec.height
8719
+ };
8720
+ if (node === focused) {
8721
+ focusedSeen = true;
8722
+ offer(node.id, box);
8723
+ } else if (this.onScreen(box)) offer(node.id, box);
8724
+ }
8725
+ childX = x - (rec.scrollable ? rec.scrollX : 0);
8726
+ childY = y - (rec.scrollable ? rec.scrollY : 0);
8727
+ }
8728
+ for (let child = node.lastChild; child !== null; child = child.previousSibling) stack.push([
8729
+ child,
8730
+ childX,
8731
+ childY
8732
+ ]);
8733
+ }
8734
+ }
8735
+ if (focused !== null && focused !== void 0 && !focusedSeen && this.semantics.has(focused.id)) {
8736
+ if (this.engine.recordFor(focused) !== void 0) offer(focused.id, this.engine.visibleBox(focused));
8104
8737
  }
8105
8738
  if (this.semanticsBoxes.size > this.semantics.size) {
8106
8739
  for (const id of this.semanticsBoxes.keys()) if (!this.semantics.has(id)) this.semanticsBoxes.delete(id);
@@ -8143,13 +8776,18 @@ var GessoRuntime = class {
8143
8776
  * every pass, so writing the raw offset here is enough.
8144
8777
  */
8145
8778
  createScrollSink() {
8779
+ const going = (node, record, axis) => {
8780
+ const written = node.getProperty(axis === "x" ? "scrollX" : "scrollY");
8781
+ const offset = typeof written === "number" ? written : axis === "x" ? record.scrollX : record.scrollY;
8782
+ return Math.min(Math.max(offset, 0), scrollRange(record, axis));
8783
+ };
8146
8784
  return {
8147
8785
  containerState: (node) => {
8148
8786
  const record = this.engine.recordFor(node);
8149
8787
  if (record === void 0) return;
8150
8788
  return {
8151
- scrollX: record.scrollX,
8152
- scrollY: record.scrollY,
8789
+ scrollX: going(node, record, "x"),
8790
+ scrollY: going(node, record, "y"),
8153
8791
  maxScrollX: scrollRange(record, "x"),
8154
8792
  maxScrollY: scrollRange(record, "y"),
8155
8793
  horizontal: node.getProperty("direction") === "row" || node.type === UiNodeType.Row,
@@ -8161,13 +8799,13 @@ var GessoRuntime = class {
8161
8799
  const record = this.engine.recordFor(node);
8162
8800
  if (record === void 0) return;
8163
8801
  if (behavior === "smooth") {
8164
- if (dx !== 0) this.smoothScroller.scrollBy(node, "scrollX", dx, record.scrollX);
8165
- if (dy !== 0) this.smoothScroller.scrollBy(node, "scrollY", dy, record.scrollY);
8802
+ if (dx !== 0) this.smoothScroller.scrollBy(node, "scrollX", dx, going(node, record, "x"));
8803
+ if (dy !== 0) this.smoothScroller.scrollBy(node, "scrollY", dy, going(node, record, "y"));
8166
8804
  return;
8167
8805
  }
8168
8806
  this.smoothScroller.stop(node);
8169
- if (dx !== 0) node.setProperty("scrollX", record.scrollX + dx);
8170
- if (dy !== 0) node.setProperty("scrollY", record.scrollY + dy);
8807
+ if (dx !== 0) node.setProperty("scrollX", going(node, record, "x") + dx);
8808
+ if (dy !== 0) node.setProperty("scrollY", going(node, record, "y") + dy);
8171
8809
  this.graph.markDirty(node, DirtyFlags.Transform);
8172
8810
  },
8173
8811
  scrollContainers: () => this.engine.scrollContainers(),
@@ -8207,6 +8845,7 @@ var GessoRuntime = class {
8207
8845
  for (const replica of this.replicas) replica.flush();
8208
8846
  });
8209
8847
  this.phaseTimings.environment = this.timePhase(() => this.graph.hasEnvironmentDirty(), () => this.graph.processEnvironmentDirty());
8848
+ if (this.input.wheel.advance(time)) this.scheduler.wake();
8210
8849
  this.phaseTimings.virtualize = this.timePhase(() => this.hasVirtualWindows(), () => this.updateVirtualWindows());
8211
8850
  }
8212
8851
  hasVirtualWindows() {
@@ -8306,29 +8945,131 @@ var GessoRuntime = class {
8306
8945
  if (rec === void 0) return 0;
8307
8946
  return column ? rec.measuredHeight + rec.marginTop + rec.marginBottom : rec.measuredWidth + rec.marginLeft + rec.marginRight;
8308
8947
  }
8309
- handleFrame(frame) {
8310
- const root = this.root;
8311
- if (root === void 0) return;
8312
- const started = now();
8313
- this.focusManager.settleScope();
8314
- const laidOut = frameNeedsLayout(frame);
8315
- this.phaseTimings.layout = this.timePhase(() => laidOut, () => this.engine.layoutForFrame(frame, this.constraints, root));
8316
- if (laidOut) {
8317
- this.laidOutOnce = true;
8318
- this.inspector.recordLayout(started);
8319
- }
8320
- if (!this.layoutNotifier.isEmpty()) this.layoutNotifier.notify((node) => {
8948
+ /**
8949
+ * Tells the layout listeners what moved, and lays out again for as
8950
+ * long as what they do in answer needs it, so the frame paints the
8951
+ * layout they settle on rather than the one they were told about.
8952
+ *
8953
+ * The browser's answer to the same problem. A `ResizeObserver`
8954
+ * callback runs after layout and before paint, and when it changes
8955
+ * layout the browser lays out again before it paints rather than
8956
+ * showing the stale boxes for a frame. The listeners here are
8957
+ * `breakpoint`, `sizeContainer` (and so `Responsive`),
8958
+ * `scrollPosition`, `autoFocus` and anything else on `host.onLayout`.
8959
+ * Before this a page whose breakpoint gave it wide padding was
8960
+ * painted with its narrow padding on the frame it appeared, and
8961
+ * jumped 16 px on the next.
8962
+ *
8963
+ * A pass after the first lays out only what the listeners dirtied,
8964
+ * the same incremental pass any frame runs, and then tells only the
8965
+ * listeners whose boxes it changed. A frame whose listeners wrote
8966
+ * nothing that lays out, which is almost every frame, runs no second
8967
+ * pass: it pays one look at the dirty set.
8968
+ *
8969
+ * **Bounded.** Listeners that keep moving each other's boxes would
8970
+ * otherwise never let the frame paint. ResizeObserver bounds its loop
8971
+ * by tree depth; this one is bounded by count, `MAX_LAYOUT_PASSES` in
8972
+ * all, which is easier to reason about and more than a real chain
8973
+ * needs (a breakpoint inside a `Responsive` inside a `Responsive` is
8974
+ * four passes). Past it the frame paints what it has, the rest waits
8975
+ * for the next frame as it always used to, and a warning says so
8976
+ * once, as the browser's "ResizeObserver loop completed with
8977
+ * undelivered notifications" does.
8978
+ *
8979
+ * **Reveals wait for the end.** `autoFocus` takes focus from a layout
8980
+ * listener, on its node's first layout, and focus reveals the node:
8981
+ * revealed from the first pass's boxes, it scrolled to where the node
8982
+ * was before a breakpoint moved it. A reveal asked for while the
8983
+ * listeners run is applied once they are quiet, from final boxes, and
8984
+ * its scroll laid out like any other write. So is the focus a hot
8985
+ * reload restores, after the listeners rather than before because
8986
+ * `autoFocus` is one of them and the restore has to be the last word:
8987
+ * every node in a subtree a reload replaced has its first layout on
8988
+ * this frame.
8989
+ */
8990
+ settleLayout(frame, root, laidOut) {
8991
+ const stats = this.engine.stats;
8992
+ const settled = {
8993
+ frame,
8994
+ passes: laidOut ? 1 : 0,
8995
+ measured: laidOut ? stats.measured : 0,
8996
+ relayoutRoots: laidOut && !stats.fullLayout ? stats.relayoutRoots : 0
8997
+ };
8998
+ let fromRoot = laidOut && stats.fullLayout;
8999
+ const read = (node) => {
8321
9000
  const record = this.engine.recordFor(node);
8322
9001
  return {
8323
9002
  box: this.engine.visibleBox(node),
8324
9003
  scrollX: record?.scrollX ?? 0,
8325
9004
  scrollY: record?.scrollY ?? 0
8326
9005
  };
8327
- });
9006
+ };
9007
+ this.deferredReveals = [];
9008
+ try {
9009
+ for (;;) {
9010
+ if (!this.layoutNotifier.isEmpty()) this.layoutNotifier.notify(read);
9011
+ if (!this.layoutPending()) {
9012
+ this.finishSettling(settled.passes > 0);
9013
+ if (!this.layoutPending()) break;
9014
+ }
9015
+ if (settled.passes >= MAX_LAYOUT_PASSES) {
9016
+ this.warnLayoutPasses();
9017
+ this.finishSettling(true);
9018
+ break;
9019
+ }
9020
+ this.phaseTimings.environment += this.timePhase(() => this.graph.hasEnvironmentDirty(), () => this.graph.processEnvironmentDirty());
9021
+ const more = this.scheduler.recollect(frame);
9022
+ settled.frame = settled.frame.merged(more);
9023
+ this.phaseTimings.layout += this.timePhase(() => true, () => this.engine.layoutForFrame(more, this.constraints, root));
9024
+ settled.passes++;
9025
+ settled.measured += stats.measured;
9026
+ settled.relayoutRoots += stats.fullLayout ? 0 : stats.relayoutRoots;
9027
+ fromRoot ||= stats.fullLayout;
9028
+ }
9029
+ } finally {
9030
+ this.deferredReveals = null;
9031
+ }
9032
+ if (fromRoot) settled.relayoutRoots = 0;
9033
+ return settled;
9034
+ }
9035
+ /**
9036
+ * Whether something written since the frame was collected has to be
9037
+ * laid out before the frame paints: the same flags that make a frame
9038
+ * lay out, or an inherited value still to be handed down.
9039
+ */
9040
+ layoutPending() {
9041
+ return this.graph.hasEnvironmentDirty() || this.graph.getDirtyNodes().anyFlags(LAYOUT_DIRT);
9042
+ }
9043
+ /** Applies what waits for settled boxes: a reload's focus, then the reveals asked for. */
9044
+ finishSettling(laidOut) {
9045
+ const reveals = this.deferredReveals ?? [];
9046
+ this.deferredReveals = null;
8328
9047
  if (this.restoringFocus && laidOut) {
8329
9048
  this.restoringFocus = false;
8330
9049
  this.restoreFocusAfterReload();
8331
9050
  }
9051
+ for (const { node, padding } of reveals) if (this.engine.recordFor(node) !== void 0) this.scrollIntoView(node, padding);
9052
+ this.deferredReveals = [];
9053
+ }
9054
+ warnLayoutPasses() {
9055
+ if (this.warnedLayoutPasses) return;
9056
+ this.warnedLayoutPasses = true;
9057
+ console.warn(`Gesso: a frame laid out ${MAX_LAYOUT_PASSES} times and its layout listeners were still changing it, so it was painted unsettled and the rest left for the next frame. Something on \`host.onLayout\` (a breakpoint, a sizeContainer, a Responsive, a modifier of your own) is writing a property that changes the box it is listening to, back and forth. Shown once.`);
9058
+ }
9059
+ handleFrame(collected) {
9060
+ const root = this.root;
9061
+ if (root === void 0) return;
9062
+ const started = now();
9063
+ this.focusManager.settleScope();
9064
+ const firstPass = frameNeedsLayout(collected);
9065
+ this.phaseTimings.layout = this.timePhase(() => firstPass, () => this.engine.layoutForFrame(collected, this.constraints, root));
9066
+ const settled = this.settleLayout(collected, root, firstPass);
9067
+ const frame = settled.frame;
9068
+ const laidOut = settled.passes > 0;
9069
+ if (laidOut) {
9070
+ this.laidOutOnce = true;
9071
+ this.inspector.recordLayout(started);
9072
+ }
8332
9073
  this.sendEditingState();
8333
9074
  const rebuildSemantics = frameNeedsSemantics(frame);
8334
9075
  this.phaseTimings.semantics = this.timePhase(() => rebuildSemantics || this.semanticsListener !== null && (laidOut || this.focusManager.focusedNode?.id !== this.lastFocusedId), () => this.updateSemantics(frame, rebuildSemantics, laidOut));
@@ -8362,8 +9103,9 @@ var GessoRuntime = class {
8362
9103
  frame: frame.id,
8363
9104
  durationMs: elapsed,
8364
9105
  nodes: frame.size,
8365
- measured: this.engine.stats.measured,
8366
- relayoutRoots: this.engine.stats.fullLayout ? 0 : this.engine.stats.relayoutRoots,
9106
+ measured: settled.measured,
9107
+ relayoutRoots: settled.relayoutRoots,
9108
+ layoutPasses: settled.passes,
8367
9109
  at: finished,
8368
9110
  inputLatencyMs: this.inputLatency.take(epochAt(finished)),
8369
9111
  phases: this.phaseTimings,
@@ -8589,7 +9331,7 @@ function timeOrigin() {
8589
9331
  }
8590
9332
  function editingStatesEqual(a, b) {
8591
9333
  if (a === null || b === null) return a === b;
8592
- return a.text === b.text && a.selectionStart === b.selectionStart && a.selectionEnd === b.selectionEnd && a.multiline === b.multiline && a.composing === b.composing && a.caret.x === b.caret.x && a.caret.y === b.caret.y && a.caret.width === b.caret.width && a.caret.height === b.caret.height;
9334
+ return a.text === b.text && a.selectionStart === b.selectionStart && a.selectionEnd === b.selectionEnd && a.multiline === b.multiline && a.composing === b.composing && a.html === b.html && a.caret.x === b.caret.x && a.caret.y === b.caret.y && a.caret.width === b.caret.width && a.caret.height === b.caret.height;
8593
9335
  }
8594
9336
  /** A canvas for the text measurer when the draw canvas is not a 2D one. */
8595
9337
  function createMeasureCanvas() {
@@ -8609,6 +9351,14 @@ function emptyGpuTimings() {
8609
9351
  encode: 0
8610
9352
  };
8611
9353
  }
9354
+ /**
9355
+ * The most layout passes one frame runs before it paints; see
9356
+ * `settleLayout`. One for the frame and up to seven more for what its
9357
+ * layout listeners change.
9358
+ */
9359
+ const MAX_LAYOUT_PASSES = 8;
9360
+ /** Dirt that makes a frame lay out; `frameNeedsLayout` asks the same of a collected frame. */
9361
+ const LAYOUT_DIRT = DirtyFlags.Layout | DirtyFlags.Children | DirtyFlags.SubtreeLayout | DirtyFlags.Transform;
8612
9362
  function emptyPhaseTimings() {
8613
9363
  return {
8614
9364
  ticks: 0,
@@ -8628,7 +9378,7 @@ function emptyPhaseTimings() {
8628
9378
  * spending an immeasurable amount of time deciding to do nothing.
8629
9379
  */
8630
9380
  function frameNeedsLayout(frame) {
8631
- return frame.anyFlags(DirtyFlags.Layout | DirtyFlags.Children | DirtyFlags.SubtreeLayout | DirtyFlags.Transform);
9381
+ return frame.anyFlags(LAYOUT_DIRT);
8632
9382
  }
8633
9383
  /**
8634
9384
  * Semantics follow the properties that carry them and the shape of the
@@ -8648,6 +9398,18 @@ function frameNeedsLayout(frame) {
8648
9398
  */
8649
9399
  const SCOPED_SEMANTICS_OWNERS = 32;
8650
9400
  /**
9401
+ * The index each id in a parent's list of children has. A paragraph's
9402
+ * text runs are numbered among themselves and its other children among
9403
+ * themselves, as `buildSemanticsTree` numbers them.
9404
+ */
9405
+ function semanticIndices(list) {
9406
+ const out = [];
9407
+ let runs = 0;
9408
+ let others = 0;
9409
+ for (const id of list) out.push(id.includes(TEXT_RUN_ID_SEPARATOR) ? runs++ : others++);
9410
+ return out;
9411
+ }
9412
+ /**
8651
9413
  * How far outside the canvas a mirrored box is still worth reporting.
8652
9414
  *
8653
9415
  * One screenful of slack, so a fling that covers a lot of ground in
@@ -8689,6 +9451,14 @@ function frameNeedsSemantics(frame) {
8689
9451
  */
8690
9452
  var GessoApp = class {
8691
9453
  runtime;
9454
+ /**
9455
+ * The clock the runtime was given, its callback, and whether a frame
9456
+ * is waiting on it: what `uiHost().flush` needs to run that frame now,
9457
+ * since a background tab gets no animation frames to run it.
9458
+ */
9459
+ clock;
9460
+ frameCallback;
9461
+ framePending = false;
8692
9462
  canvas;
8693
9463
  host;
8694
9464
  inputEnabled;
@@ -8709,6 +9479,7 @@ var GessoApp = class {
8709
9479
  detachFullscreen = null;
8710
9480
  fullscreen = false;
8711
9481
  detachReducedMotion = null;
9482
+ detachContrast = null;
8712
9483
  /** Stops watching `prefers-color-scheme`; null while overridden. */
8713
9484
  detachColorScheme = null;
8714
9485
  /** Stops watching `visualViewport` for the safe area and the keyboard. */
@@ -8731,7 +9502,24 @@ var GessoApp = class {
8731
9502
  routes: options.routes,
8732
9503
  media: options.media,
8733
9504
  fonts: options.fonts,
8734
- clock: options.clock ?? ((callback) => new UiAnimationFrameClock(callback)),
9505
+ clock: (callback) => {
9506
+ const clock = (options.clock ?? ((onFrame) => new UiAnimationFrameClock(onFrame)))((time) => {
9507
+ this.framePending = false;
9508
+ callback(time);
9509
+ });
9510
+ this.clock = clock;
9511
+ this.frameCallback = callback;
9512
+ return {
9513
+ requestFrame: () => {
9514
+ this.framePending = true;
9515
+ clock.requestFrame();
9516
+ },
9517
+ cancelFrame: () => {
9518
+ this.framePending = false;
9519
+ clock.cancelFrame();
9520
+ }
9521
+ };
9522
+ },
8735
9523
  dpr: devicePixelRatio()
8736
9524
  });
8737
9525
  const input = this.runtime.input;
@@ -8741,6 +9529,30 @@ var GessoApp = class {
8741
9529
  keyboardController: input.keyboard
8742
9530
  });
8743
9531
  }
9532
+ /**
9533
+ * The running app, as the agent's screen tools need it: its semantics
9534
+ * tree and focus, the mirror's actions, keys, and a way to run a
9535
+ * pending frame now. See `uiSurface` in `gesso-framework/agent`.
9536
+ */
9537
+ uiHost() {
9538
+ const runtime = this.runtime;
9539
+ return {
9540
+ semanticsTree: () => runtime.semanticsTree(),
9541
+ focusedNodeId: () => runtime.focusedNodeId(),
9542
+ applySemanticsAction: (action) => runtime.applySemanticsAction(action),
9543
+ key: (key, modifiers) => {
9544
+ runtime.input.keyboard.keyDown(key, modifiers);
9545
+ runtime.input.keyboard.keyUp(key, modifiers);
9546
+ },
9547
+ flush: () => {
9548
+ if (this.framePending && this.clock !== void 0 && this.frameCallback !== void 0) {
9549
+ this.framePending = false;
9550
+ this.clock.cancelFrame();
9551
+ this.frameCallback(performance.now());
9552
+ }
9553
+ }
9554
+ };
9555
+ }
8744
9556
  /** The runtime services a component in this app can inject. */
8745
9557
  get services() {
8746
9558
  return this.runtime.services;
@@ -8877,6 +9689,8 @@ var GessoApp = class {
8877
9689
  this.detachFullscreen = null;
8878
9690
  this.detachReducedMotion?.();
8879
9691
  this.detachReducedMotion = null;
9692
+ this.detachContrast?.();
9693
+ this.detachContrast = null;
8880
9694
  this.detachColorScheme?.();
8881
9695
  this.detachColorScheme = null;
8882
9696
  this.detachViewportInsets?.();
@@ -8901,7 +9715,7 @@ var GessoApp = class {
8901
9715
  compositionStart: () => editing.compositionStart(),
8902
9716
  compositionUpdate: (text, caret) => editing.compositionUpdate(text, caret),
8903
9717
  compositionEnd: (text) => editing.compositionEnd(text),
8904
- paste: (text) => editing.paste(text),
9718
+ paste: (text, html) => editing.paste(text, html),
8905
9719
  blur: () => this.runtime.input.focus.blur()
8906
9720
  });
8907
9721
  this.runtime.setTextInputSource("proxy");
@@ -8934,6 +9748,7 @@ var GessoApp = class {
8934
9748
  this.runtime.setFullscreen(isDocumentFullscreen(canvas));
8935
9749
  }
8936
9750
  this.detachReducedMotion = observeReducedMotion((reduced) => this.runtime.setReducedMotion(reduced));
9751
+ this.detachContrast = observeContrast((contrast) => this.runtime.setContrast(contrast));
8937
9752
  this.setColorScheme(this.colorSchemePreference);
8938
9753
  }
8939
9754
  /**
@@ -8949,7 +9764,7 @@ var GessoApp = class {
8949
9764
  if (!this.accessibilityEnabled) return;
8950
9765
  const mirror = new SemanticsMirror(canvas, {
8951
9766
  action: (action) => this.runtime.applySemanticsAction(action),
8952
- paste: (text) => this.runtime.input.editing.paste(text)
9767
+ paste: (text, html) => this.runtime.input.editing.paste(text, html)
8953
9768
  }, this.proxy);
8954
9769
  this.mirror = mirror;
8955
9770
  this.runtime.onSemantics((update) => mirror.apply(update));
@@ -8994,10 +9809,11 @@ var GessoApp = class {
8994
9809
  if (request.type === "popup") this.runtime.settlePopup(request.id, false);
8995
9810
  else if (request.type === "storage") this.runtime.settleStorage(request.id, shellStorageDenied());
8996
9811
  else if (request.type === "file") this.runtime.settleFile(request.id, shellFilesUnsupported("There is no document to reach files through."));
9812
+ else if (request.type === "clipboard") this.runtime.settleClipboard(request.id, false);
8997
9813
  return;
8998
9814
  }
8999
9815
  if (request.type === "clipboard") {
9000
- writeClipboard(request.text, this.canvas.ownerDocument);
9816
+ writeClipboard(request.text, this.canvas.ownerDocument).then((copied) => this.runtime.settleClipboard(request.id, copied));
9001
9817
  return;
9002
9818
  }
9003
9819
  if (request.type === "fullscreen") {
@@ -9070,6 +9886,10 @@ var GessoAppBuilder = class {
9070
9886
  mediaOptions;
9071
9887
  fontDeclarations;
9072
9888
  app;
9889
+ /** The mounted app's channel registry, for the workers an agent port asks. */
9890
+ channelHandle;
9891
+ webmcpChoice = false;
9892
+ disconnectWebMcp = null;
9073
9893
  colorSchemePreference = "auto";
9074
9894
  constructor(root) {
9075
9895
  this.root = root;
@@ -9260,13 +10080,61 @@ var GessoAppBuilder = class {
9260
10080
  if (this.errorListener !== void 0) app.onError(this.errorListener);
9261
10081
  if (this.devtoolsListener !== null) app.onDevtools(this.devtoolsListener);
9262
10082
  this.app = app;
10083
+ this.channelHandle = channels;
9263
10084
  app.mount();
10085
+ this.connectWebMcp(app);
9264
10086
  return () => {
10087
+ this.disconnectWebMcp?.();
10088
+ this.disconnectWebMcp = null;
9265
10089
  this.app = void 0;
10090
+ this.channelHandle = void 0;
9266
10091
  app.dispose();
9267
10092
  channels.dispose();
9268
10093
  };
9269
10094
  }
10095
+ /**
10096
+ * Offer the app's channels and screen to an AI agent in the browser,
10097
+ * through WebMCP (default false). The single-thread form of
10098
+ * `createApp({ webmcp })`: registered once `mountSync` has run,
10099
+ * removed when the app is unmounted. Call before `mountSync`.
10100
+ */
10101
+ useWebMcp(choice = true) {
10102
+ this.webmcpChoice = choice;
10103
+ return this;
10104
+ }
10105
+ /**
10106
+ * Opens a port to what this app serves under `key`, or returns
10107
+ * undefined before `mountSync`.
10108
+ *
10109
+ * The same question `WorkerApp.openRenderPort` answers, asked of the
10110
+ * page instead of a render worker, because in this configuration the
10111
+ * page is the thread that draws. It serves one thing, `gesso:agent`:
10112
+ * the channels fed here, the workers behind them, and the screen.
10113
+ * The agent code is loaded the first time one is opened.
10114
+ */
10115
+ openRenderPort(key) {
10116
+ const app = this.app;
10117
+ if (app === void 0 || key !== "gesso:agent") return;
10118
+ const pair = new MessageChannel();
10119
+ const registrations = this.channelRegistrations;
10120
+ import("./app-BvlIO1G9.js").then((n) => n.t).then((agent) => agent.serveApplicationAgent(pair.port2, {
10121
+ channels: () => registrations.filter((registration) => registration.source !== void 0).map((registration) => ({
10122
+ token: registration.token,
10123
+ source: registration.source
10124
+ })),
10125
+ workers: () => this.channelHandle?.workers ?? [],
10126
+ ui: () => this.app === app ? app.uiHost() : void 0
10127
+ }));
10128
+ return pair.port1;
10129
+ }
10130
+ connectWebMcp(app) {
10131
+ const choice = this.webmcpChoice;
10132
+ if (choice === false) return;
10133
+ import("./webmcp-CJVFvAHe.js").then((n) => n.a).then((agent) => agent.connectWebMcp(this, choice === true ? {} : choice)).then((disconnect) => {
10134
+ if (this.app === app) this.disconnectWebMcp = disconnect;
10135
+ else disconnect();
10136
+ }, (error) => console.warn("[gesso] WebMCP could not register the channels:", error));
10137
+ }
9270
10138
  };
9271
10139
  function requireElement(selector) {
9272
10140
  const element = document.querySelector(selector);
@@ -9818,6 +10686,11 @@ var RenderWorkerApp = class {
9818
10686
  this.root = typeof root === "function" ? createComponent(root) : root;
9819
10687
  this.host = host;
9820
10688
  this.host.onmessage = (event) => this.receive(event.data);
10689
+ servePorts((key, port) => {
10690
+ if (key !== "gesso:agent") return false;
10691
+ this.serveAgent(port);
10692
+ return true;
10693
+ }, () => [AGENT_PORT], host);
9821
10694
  this.host.addEventListener("error", (event) => {
9822
10695
  const error = event.error;
9823
10696
  this.reportUncaught(error ?? event.message ?? "Unknown error", error === void 0 ? locationOf(event) : void 0);
@@ -9995,6 +10868,41 @@ var RenderWorkerApp = class {
9995
10868
  * console is the worker global's, and this class is what owns the
9996
10869
  * global.
9997
10870
  */
10871
+ /**
10872
+ * Answers an agent port with every channel this application can
10873
+ * reach: the ones fed from this thread, and whatever each worker
10874
+ * behind them serves, asked over a port of its own. Imported rather
10875
+ * than loaded on demand: a worker built as an IIFE, which is Vite's
10876
+ * default, cannot split off a chunk, and a dynamic import here failed
10877
+ * every such build. Nothing runs until an agent asks.
10878
+ */
10879
+ serveAgent(port) {
10880
+ serveApplicationAgent(port, {
10881
+ channels: () => this.channelRegistrations.filter((registration) => registration.source !== void 0).map((registration) => ({
10882
+ token: registration.token,
10883
+ source: registration.source
10884
+ })),
10885
+ workers: () => {
10886
+ const workers = new Set(this.channels?.workers ?? []);
10887
+ if (this.appLogicWorker !== void 0) workers.add(this.appLogicWorker);
10888
+ return workers;
10889
+ },
10890
+ ui: () => {
10891
+ const runtime = this.runtime;
10892
+ if (runtime === void 0) return;
10893
+ return {
10894
+ semanticsTree: () => runtime.semanticsTree(),
10895
+ focusedNodeId: () => runtime.focusedNodeId(),
10896
+ applySemanticsAction: (action) => runtime.applySemanticsAction(action),
10897
+ key: (key, modifiers) => {
10898
+ runtime.input.keyboard.keyDown(key, modifiers);
10899
+ runtime.input.keyboard.keyUp(key, modifiers);
10900
+ },
10901
+ flush: () => this.clock?.tick(performance.now())
10902
+ };
10903
+ }
10904
+ });
10905
+ }
9998
10906
  setConsoleForwarding(enabled) {
9999
10907
  this.restoreConsole?.();
10000
10908
  this.restoreConsole = null;
@@ -10048,7 +10956,7 @@ var RenderWorkerApp = class {
10048
10956
  runtime.input.pointer.pointerCancel(message.pointer);
10049
10957
  break;
10050
10958
  case "wheel":
10051
- runtime.input.wheel.wheel(message.x, message.y, message.deltaX, message.deltaY, message.modifiers, message.deltaMode, message.wheelDeltaY);
10959
+ runtime.input.wheel.wheel(message.x, message.y, message.deltaX, message.deltaY, message.modifiers, message.deltaMode, message.wheelDeltaY, message.at === void 0 ? void 0 : message.at - performance.timeOrigin);
10052
10960
  break;
10053
10961
  case "keyDown":
10054
10962
  runtime.input.keyboard.keyDown(message.key, message.modifiers, message.textFollows);
@@ -10069,7 +10977,7 @@ var RenderWorkerApp = class {
10069
10977
  runtime.input.editing.compositionEnd(message.text);
10070
10978
  break;
10071
10979
  case "paste":
10072
- runtime.input.editing.paste(message.text);
10980
+ runtime.input.editing.paste(message.text, message.html ?? null);
10073
10981
  break;
10074
10982
  case "blur":
10075
10983
  runtime.input.focus.blur();
@@ -10080,6 +10988,9 @@ var RenderWorkerApp = class {
10080
10988
  case "colorScheme":
10081
10989
  runtime.setColorScheme(message.scheme);
10082
10990
  break;
10991
+ case "contrast":
10992
+ runtime.setContrast(message.contrast);
10993
+ break;
10083
10994
  case "viewportInsets":
10084
10995
  runtime.setViewportInsets(message.insets);
10085
10996
  break;
@@ -10089,6 +11000,9 @@ var RenderWorkerApp = class {
10089
11000
  case "popupResult":
10090
11001
  runtime.settlePopup(message.id, message.opened);
10091
11002
  break;
11003
+ case "clipboardResult":
11004
+ runtime.settleClipboard(message.id, message.copied);
11005
+ break;
10092
11006
  case "storageResult":
10093
11007
  runtime.settleStorage(message.id, message.result);
10094
11008
  break;
@@ -10232,6 +11146,7 @@ var RenderWorkerApp = class {
10232
11146
  this.runtime.onShellRequest((request) => {
10233
11147
  if (request.type === "clipboard") this.host.postMessage({
10234
11148
  type: "clipboard",
11149
+ id: request.id,
10235
11150
  text: request.text
10236
11151
  });
10237
11152
  else if (request.type === "openUrl") this.host.postMessage({
@@ -10310,6 +11225,7 @@ var RenderWorkerApp = class {
10310
11225
  nodes: metrics.nodes,
10311
11226
  measured: metrics.measured,
10312
11227
  relayoutRoots: metrics.relayoutRoots,
11228
+ layoutPasses: metrics.layoutPasses,
10313
11229
  at: metrics.at,
10314
11230
  inputLatencyMs: metrics.inputLatencyMs,
10315
11231
  phases: metrics.phases,
@@ -10329,6 +11245,6 @@ function locationOf(event) {
10329
11245
  return `${event.filename}:${line}:${column}`;
10330
11246
  }
10331
11247
  //#endregion
10332
- export { APPLICATION_WORKER, AnimationService, AudioService, AudioSink, BoundsCell, Channel, ChannelRegistry, ChannelReplica, Component, ComponentHost, ComponentHostResolver, ComputedCell, Define, Each, EditingProxy, EditingService, FanCell, FanOut, FindService, FocusService, FontService, FrameService, GessoApp, GessoAppBuilder, GessoRuntime, IndexedDbStorage, Inject, Input, InputCell, InternalState, MARK_PREFIX, MediaService, MemoryStorage, OpfsStorage, Output, OverlayLayer, OverlayService, PersistedState, Presence, ProvidedChannel, RenderWorkerApp, Resource, RouteState, RouterOutlet, RouterService, SemanticsMirror, ServiceRegistry, ShellService, ShellStorage, Show, TextService, ThemeTokenCell, UI_FRAME_PHASES, UI_ROLES, UI_SEMANTIC_STATES, UndoStack, WorkerApp, applyPatch, applyPatches, audioClock, bind, bounds, buildPath, channel, classifyStorageError, computed, controlled, createApp, createChannelRegistry, createComponent, createShellHistory, createSyncApp, debounced, defineChannel, derive, describeStream, diffProjection, each, fanOut, findUnplainPath, formatAge, formatNodePath, formatNodeReport, formatStream, formatUrl, input, internalState, into, isChannelClientMessage, isChannelHostMessage, isClassComponent, isComponentElement, isHubMessage, isOutputTarget, isPortErrorMessage, isPortHandshake, markInstant, markNow, measureSpan, mutate, observeColorScheme, observeMediaQuery, observeReducedMotion, output, parseUrl, performShellStorage, performanceMarksEnabled, persisted, pick, pickKeys, portHandle, printPropValue, provide, registerUndoShortcuts, renderRoot, requirePlainData, resource, route, select, serve, serveChannels, servePorts, setPerformanceMarks, shellStorageDenied, show, storageErrorMessage, storageReadFailure, storageReadValue, structurallyEqual, themeTokenCell, throttled, to, treeText, undoable, viewKeys, workerHandle, writeClipboard };
11248
+ export { APPLICATION_WORKER, AnimationService, AudioService, AudioSink, BoundsCell, Channel, ChannelRegistry, ChannelReplica, Component, ComponentHost, ComponentHostResolver, ComputedCell, Define, Each, EditingProxy, EditingService, FanCell, FanOut, FindService, FocusService, FontService, FrameService, GessoApp, GessoAppBuilder, GessoRuntime, IndexedDbStorage, Inject, Input, InputCell, InternalState, MARK_PREFIX, MediaService, MemoryStorage, OpfsStorage, Output, OverlayLayer, OverlayService, PersistedState, Presence, ProvidedChannel, RenderWorkerApp, Resource, RouteState, RouterOutlet, RouterService, ScrollService, SemanticsMirror, ServiceRegistry, ShellService, ShellStorage, Show, TextService, ThemeTokenCell, UI_FRAME_PHASES, UI_ROLES, UI_SEMANTIC_STATES, UndoStack, WorkerApp, applyPatch, applyPatches, audioClock, bind, bounds, buildPath, channel, channelSchema, classifyStorageError, computed, controlled, createApp, createChannelRegistry, createComponent, createShellHistory, createSyncApp, debounced, defineChannel, derive, describeChannel, describeStream, diffProjection, each, fanOut, findUnplainPath, formatAge, formatNodePath, formatNodeReport, formatStream, formatUrl, input, internalState, into, isChannelClientMessage, isChannelHostMessage, isClassComponent, isComponentElement, isHubMessage, isOutputTarget, isPortErrorMessage, isPortHandshake, markInstant, markNow, measureSpan, mutate, observeColorScheme, observeMediaQuery, observeReducedMotion, output, parseUrl, performShellStorage, performanceMarksEnabled, persisted, pick, pickKeys, portHandle, printPropValue, provide, registerUndoShortcuts, renderRoot, requirePlainData, resource, route, select, serve, serveChannels, servePorts, setPerformanceMarks, shellStorageDenied, show, storageErrorMessage, storageReadFailure, storageReadValue, structurallyEqual, themeTokenCell, throttled, to, treeText, undoable, viewKeys, workerHandle, writeClipboard };
10333
11249
 
10334
11250
  //# sourceMappingURL=index.js.map