gesso-framework 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
- import { a as InternalState, f as outputTargetOf, m as withBodyOf, n as computed, o as internalState, s as InputCell, u as isOutputTarget } from "./computed-_cYcdczA.js";
1
+ import { a as InternalState, c as forceInput, d as isOutputTarget, h as withBodyOf, l as input, n as computed, o as internalState, p as outputTargetOf, s as InputCell } from "./computed-DyW9SPpq.js";
2
2
  import { f as formatNodePath, g as printPropValue, r as isPortErrorMessage, u as describeStream, v as applyPatches, w as viewKeys, x as isChannelHostMessage } from "./WorkerPorts-m_hLRoPj.js";
3
3
  import { n as getComponentMetadata, t as createComponent } from "./createComponent-DJads88q.js";
4
4
  import { m as designedTheme } from "./app-De3z58x7.js";
5
- import { EMPTY, Subject, Subscription, map } from "rxjs";
6
- import { AnimationDriver, Box, Canvas2DRenderer, CanvasTextMeasurer, Column, Constraints, DefaultImageResolver, DefaultSvgResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, Stack, TEXT_RUN_ID_SEPARATOR, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiFrame, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiManualFrameClock, UiNodeType, 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, clearOverrideProperty, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, easings, fade, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isUiElement, isWebGPUAvailable, lightTheme, linear, linkOf, measure, mediaLoadsInFlight, motion, noInsets, propertyEffects, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollBounds, scrollRange, scrollbarThumb, scrollsFromRight, semanticsInertAbove, semanticsMemory, setPerformanceMarks, textRunOfRecordId, writeOverrideProperty } from "gesso-core";
5
+ import { EMPTY, Subject, Subscription, concat, map, of, skip } from "rxjs";
6
+ import { AnimationDriver, Box, Canvas2DRenderer, CanvasTextMeasurer, Column, Constraints, DefaultImageResolver, DefaultSvgResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, Stack, TEXT_RUN_ID_SEPARATOR, Text, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiFrame, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiManualFrameClock, UiNodeType, 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, clearOverrideProperty, clockOf, createCanvasSurface, createEnvironmentKey, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, durationIn, easingIn, fade, findEnvironmentKey, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isUiElement, isWebGPUAvailable, lightTheme, linear, linkOf, measure, mediaLoadsInFlight, motion, noInsets, propertyEffects, provided, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollBounds, scrollRange, scrollbarThumb, scrollsFromRight, semanticsInertAbove, semanticsMemory, setPerformanceMarks, springIn, textRunOfRecordId, writeOverrideProperty } from "gesso-core";
7
7
  //#region src/Component.ts
8
8
  /**
9
9
  * Base class for all Gesso framework components.
@@ -161,6 +161,33 @@ function bounds(label) {
161
161
  return cell;
162
162
  }
163
163
  //#endregion
164
+ //#region src/liveInput.ts
165
+ /** Where the component whose body is running keeps its live inputs, while it runs. */
166
+ let target = null;
167
+ /** Runs a component's body with its live inputs going to `into`; see `liveInput`. */
168
+ function withLiveInputs(into, run) {
169
+ const previous = target;
170
+ target = into;
171
+ try {
172
+ return run();
173
+ } finally {
174
+ target = previous;
175
+ }
176
+ }
177
+ /**
178
+ * Says what an input of the component whose body is running holds now,
179
+ * as the component keeps it, and how to read it again: a switch turned
180
+ * on, a field typed in, a list scrolled to 600. The inspector reports it
181
+ * on the component's owner report (`live`), and a design tool saves a
182
+ * moment of a played screen from it, to start the screen there again
183
+ * (`StartingInputs`). `controlled` calls it for its value, so a control
184
+ * written by its contract reports itself; outside a component's body it
185
+ * does nothing.
186
+ */
187
+ function liveInput(name, read) {
188
+ target?.set(name, read);
189
+ }
190
+ //#endregion
164
191
  //#region src/decorators.ts
165
192
  /**
166
193
  * Declares a class as a Gesso component.
@@ -455,6 +482,7 @@ function isClassComponent(component) {
455
482
  * declared the entry.
456
483
  */
457
484
  function physicalPlacement(placement, rtl) {
485
+ if (placement === "over") return placement;
458
486
  const [side, align] = placement.split("-");
459
487
  if (side === "top" || side === "bottom") {
460
488
  if (align === void 0 || !rtl) return placement;
@@ -498,6 +526,149 @@ var OverlayService = class {
498
526
  return this.entries.value.some((entry) => entry.id === id);
499
527
  }
500
528
  };
529
+ /**
530
+ * The overlays of the `OverlayScope` a node is in: where an entry
531
+ * opened there goes instead of the runtime's layer. Null outside every
532
+ * scope.
533
+ */
534
+ const OverlayScopeKey = createEnvironmentKey({
535
+ name: "gesso.overlayScope",
536
+ defaultValue: null
537
+ });
538
+ //#endregion
539
+ //#region src/app/FocusService.ts
540
+ /**
541
+ * Keyboard focus, as a store components can inject.
542
+ *
543
+ * `UiFocusManager` lives in the runtime and a component cannot reach
544
+ * it — components reach the world through stores, as they do for the
545
+ * clipboard (`ShellService`), overlays (`OverlayService`) and find
546
+ * (`FindService`). Without this a component cannot autofocus a field,
547
+ * trap the keyboard in a dialog, or put the caret in the input that
548
+ * failed validation.
549
+ *
550
+ * Nodes come from a `ref` prop. A tree's refs fire before the runtime
551
+ * installs the manager, so an action taken during the first build is
552
+ * queued and replayed once there is one; that is the same problem
553
+ * `FindService` solves for its query field, and the reason `autoFocus`
554
+ * in a dialog works on the frame it mounts.
555
+ *
556
+ * It must stay on the render thread: its actions take `UiNode`s, which
557
+ * never cross a worker boundary.
558
+ */
559
+ /** The nodes a trap taken inside is modal only within: each `OverlayScope`'s. */
560
+ const focusBounds = /* @__PURE__ */ new WeakSet();
561
+ /** Makes a node one, for `OverlayScope`. */
562
+ function boundFocus(node) {
563
+ focusBounds.add(node);
564
+ }
565
+ var FocusService = class {
566
+ /** The node holding focus, or null. A control binds its focus ring to this. */
567
+ focused = internalState(null);
568
+ /** Whether focus is confined to a subtree by an open trap. */
569
+ trapped = internalState(false);
570
+ /**
571
+ * Whether the focus held is focus the keyboard can see: reached by Tab
572
+ * or an arrow key, or by code with no pointer since, rather than by a
573
+ * press. CSS's `:focus-visible`, for something other than a ring that
574
+ * should answer the keyboard and not the click that happened to focus
575
+ * the same control: a tooltip, say.
576
+ */
577
+ focusVisible = internalState(false);
578
+ manager = null;
579
+ detach = null;
580
+ /** Actions taken before the runtime installed a manager, in order. */
581
+ queued = [];
582
+ /** Installed by the runtime; without one every action is queued. */
583
+ setManager(manager) {
584
+ this.detach?.();
585
+ this.detach = null;
586
+ this.manager = manager;
587
+ if (manager === null) {
588
+ this.queued = [];
589
+ return;
590
+ }
591
+ const focusChange = manager.onFocusChange((node) => {
592
+ this.focused.value = node;
593
+ this.focusVisible.value = manager.focusVisible;
594
+ this.trapped.value = manager.trapped;
595
+ });
596
+ manager.setFocusBounds((node) => focusBounds.has(node));
597
+ const scopeChange = manager.onScopeChange(() => {
598
+ this.trapped.value = manager.trapped;
599
+ });
600
+ this.detach = () => {
601
+ focusChange();
602
+ scopeChange();
603
+ };
604
+ const pending = this.queued;
605
+ this.queued = [];
606
+ for (const action of pending) action(manager);
607
+ this.sync(manager);
608
+ }
609
+ /**
610
+ * Gives the node keyboard focus. Non-focusable nodes are ignored.
611
+ *
612
+ * The node is scrolled into view, unless `options.preventScroll` asks
613
+ * for the page to stay where it is, as `element.focus({ preventScroll:
614
+ * true })` does in a browser.
615
+ */
616
+ focus(node, options) {
617
+ this.run((manager) => manager.focus(node, "program", options));
618
+ }
619
+ /** Drops focus without moving it anywhere. */
620
+ blur() {
621
+ this.run((manager) => manager.blur());
622
+ }
623
+ /** Moves focus to the next focusable node, wrapping around. */
624
+ focusNext() {
625
+ this.run((manager) => manager.focusNext());
626
+ }
627
+ /** Moves focus to the previous focusable node, wrapping around. */
628
+ focusPrevious() {
629
+ this.run((manager) => manager.focusPrevious());
630
+ }
631
+ /**
632
+ * The tab stops under `within` (the whole tree when not given), in
633
+ * the order Tab visits them: what a design tool numbers to show a
634
+ * screen's keyboard order. Empty before the runtime has a manager.
635
+ */
636
+ tabStops(within) {
637
+ return this.manager?.tabStops(within) ?? [];
638
+ }
639
+ /**
640
+ * Confines focus to `scope` until `releaseTrap`, moving it inside if
641
+ * it was elsewhere. Traps nest: a dialog opened over a dialog traps
642
+ * again, and each release restores its own opener.
643
+ */
644
+ trap(scope) {
645
+ this.run((manager) => manager.pushScope(scope));
646
+ }
647
+ /**
648
+ * Ends the innermost trap and returns focus to whatever held it when
649
+ * the trap was taken — the button that opened the dialog. Given the
650
+ * node trapped in, only that trap ends, and nothing when it ended
651
+ * already: a trap's subtree removed releases it, and a release after
652
+ * that would otherwise end the trap under it.
653
+ */
654
+ releaseTrap(scope) {
655
+ this.run((manager) => manager.popScope(scope));
656
+ }
657
+ run(action) {
658
+ const manager = this.manager;
659
+ if (manager === null) {
660
+ this.queued.push(action);
661
+ return;
662
+ }
663
+ action(manager);
664
+ this.sync(manager);
665
+ }
666
+ sync(manager) {
667
+ this.focused.value = manager.focusedNode;
668
+ this.focusVisible.value = manager.focusVisible;
669
+ this.trapped.value = manager.trapped;
670
+ }
671
+ };
501
672
  //#endregion
502
673
  //#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorate.js
503
674
  function __decorate(decorators, target, key, desc) {
@@ -509,7 +680,12 @@ function __decorate(decorators, target, key, desc) {
509
680
  //#endregion
510
681
  //#region src/overlay/OverlayLayer.ts
511
682
  let OverlayLayer = class OverlayLayer extends Component {
512
- overlays;
683
+ runtimeOverlays;
684
+ /** The entries to draw, when not the runtime's: an `OverlayScope`'s. */
685
+ service = input(null);
686
+ get overlays() {
687
+ return this.service.value ?? this.runtimeOverlays;
688
+ }
513
689
  render() {
514
690
  return Box({
515
691
  position: "absolute",
@@ -547,6 +723,7 @@ let OverlayLayer = class OverlayLayer extends Component {
547
723
  top: entry.top,
548
724
  bottom: entry.bottom,
549
725
  zIndex: entry.zIndex,
726
+ ...entry.placement === "over" && entry.anchor != null ? { hitTestable: false } : {},
550
727
  ...centering(entry),
551
728
  ...physicalEdges(entry, readsRightToLeft(environment)),
552
729
  ...inheritedFrom(environment),
@@ -556,7 +733,8 @@ let OverlayLayer = class OverlayLayer extends Component {
556
733
  return elements;
557
734
  }
558
735
  };
559
- __decorate([Inject(OverlayService)], OverlayLayer.prototype, "overlays", void 0);
736
+ __decorate([Inject(OverlayService)], OverlayLayer.prototype, "runtimeOverlays", void 0);
737
+ __decorate([Input()], OverlayLayer.prototype, "service", void 0);
560
738
  OverlayLayer = __decorate([Define("gesso-overlay-layer")], OverlayLayer);
561
739
  /**
562
740
  * The dim over the page, inside the backdrop rather than on it.
@@ -646,9 +824,31 @@ function inheritedFrom(node) {
646
824
  theme: environment.get(UiEnvironmentKeys.theme),
647
825
  textStyle: environment.get(UiEnvironmentKeys.textStyle),
648
826
  color: environment.get(UiEnvironmentKeys.color),
649
- contentColor: environment.get(UiEnvironmentKeys.contentColor)
827
+ contentColor: environment.get(UiEnvironmentKeys.contentColor),
828
+ provide: providedFrom(environment)
650
829
  };
651
830
  }
831
+ /** The built-in keys an entry's box carries by props of its own, or leaves to where it is drawn. */
832
+ const CARRIED_BY_PROPS = new Set(Object.values(UiEnvironmentKeys).map((key) => key.name).filter((name) => name !== UiEnvironmentKeys.clock.name && name !== UiEnvironmentKeys.motion.name));
833
+ /**
834
+ * The rest of what the entry was opened under, re-provided: the clock
835
+ * its animations run on, so a dialog opened on a settled frame is at
836
+ * rest there and one opened in a slowed player slows, the motion its
837
+ * names mean, and every key the
838
+ * application provides itself, so a component in a dialog reads with
839
+ * `ctx.environment` what the one that opened it reads.
840
+ */
841
+ function providedFrom(environment) {
842
+ const out = [];
843
+ const seen = /* @__PURE__ */ new Set();
844
+ for (let at = environment; at !== null; at = at.parent) for (const name of at.providedKeys()) {
845
+ if (seen.has(name) || CARRIED_BY_PROPS.has(name)) continue;
846
+ seen.add(name);
847
+ const key = findEnvironmentKey(name);
848
+ if (key !== void 0) out.push(provided(key, environment.get(key)));
849
+ }
850
+ return out;
851
+ }
652
852
  /**
653
853
  * The same values again whenever they change where the entry was
654
854
  * declared, for as long as it is open.
@@ -668,6 +868,7 @@ const followEnvironmentKind = defineModifier({
668
868
  host.set("textStyle", textStyle);
669
869
  host.set("color", host.environment(UiEnvironmentKeys.color, from));
670
870
  host.set("contentColor", host.environment(UiEnvironmentKeys.contentColor, from));
871
+ if (from.environment !== null) host.set("provide", providedFrom(from.environment));
671
872
  if (entry !== null) {
672
873
  const edges = physicalEdges(entry, textStyle.textDirection === "rtl");
673
874
  host.set("placement", edges.placement);
@@ -684,6 +885,36 @@ function followEnvironment(node, entry) {
684
885
  })];
685
886
  }
686
887
  //#endregion
888
+ //#region src/startingInputsKey.ts
889
+ /**
890
+ * The inputs components under a node start with, by component name,
891
+ * over what their parents pass; see `StartingInputs`. Empty outside one.
892
+ */
893
+ const StartingInputsKey = createEnvironmentKey({
894
+ name: "gesso.startingInputs",
895
+ defaultValue: {}
896
+ });
897
+ /** How many of each name have been made under each set of starting inputs, for a list's next. */
898
+ const made = /* @__PURE__ */ new WeakMap();
899
+ /** What a component named `name`, made at `anchor`, starts with: null for nothing. */
900
+ function startingInputsFor(anchor, name) {
901
+ for (let at = anchor; at !== null; at = at.parent) if (at.environment !== null) {
902
+ const all = at.environment.get(StartingInputsKey);
903
+ const given = all[name];
904
+ if (given === void 0) return null;
905
+ if (!Array.isArray(given)) return given;
906
+ let counts = made.get(all);
907
+ if (counts === void 0) {
908
+ counts = /* @__PURE__ */ new Map();
909
+ made.set(all, counts);
910
+ }
911
+ const index = counts.get(name) ?? 0;
912
+ counts.set(name, index + 1);
913
+ return given[index] ?? null;
914
+ }
915
+ return null;
916
+ }
917
+ //#endregion
687
918
  //#region src/ComponentHost.ts
688
919
  /**
689
920
  * Owns a single component instance and its lifecycle.
@@ -710,6 +941,7 @@ function followEnvironment(node, entry) {
710
941
  var ComponentHost = class {
711
942
  services;
712
943
  channels;
944
+ anchor;
713
945
  /** The class instance; undefined for a functional component. */
714
946
  instance;
715
947
  element;
@@ -731,14 +963,34 @@ var ComponentHost = class {
731
963
  inputSubscriptions = /* @__PURE__ */ new Map();
732
964
  /** A functional component's cells, by prop name, as they were handed out. */
733
965
  functionalCells = /* @__PURE__ */ new Map();
966
+ /**
967
+ * Inputs it is drawn with over its parent's, from a `StartingInputs`
968
+ * above it when it was made: a moment of a screen, a tour's second
969
+ * step. A preview goes over these, and ending one comes back to them.
970
+ */
971
+ started;
972
+ /** What the parent passed for each started input at birth: a new value from it takes over. */
973
+ startedOver = /* @__PURE__ */ new Map();
974
+ /** A started input the parent passes as a stream: the start, then the stream's changes; one per input, so it is not resubscribed. */
975
+ startedStreams = /* @__PURE__ */ new Map();
976
+ /** What the component says it holds now, by input; see `liveInput`. */
977
+ live = /* @__PURE__ */ new Map();
978
+ /** Inputs a tool draws the component with over its parent's, until it ends them; see `previewInputs`. */
979
+ previewed = null;
980
+ /** A class's input values from before they were previewed, for the ones its parent does not pass. */
981
+ beforePreview = /* @__PURE__ */ new Map();
734
982
  mountHooks = [];
735
983
  unmountHooks = [];
736
984
  /** True only while the component function runs; hooks may register then. */
737
985
  rendering = false;
738
- constructor(element, services = new ServiceRegistry(), channels = new ChannelRegistry()) {
986
+ constructor(element, services = new ServiceRegistry(), channels = new ChannelRegistry(), anchor = null) {
739
987
  this.services = services;
740
988
  this.channels = channels;
989
+ this.anchor = anchor;
741
990
  this.element = element;
991
+ const started = startingInputsFor(anchor, element.tag);
992
+ this.started = started === null ? null : { ...started };
993
+ for (const name of Object.keys(this.started ?? {})) this.startedOver.set(name, element.props[name]);
742
994
  if (isClassComponent(element.component)) {
743
995
  this.instance = new element.component();
744
996
  this.validateInputs();
@@ -793,14 +1045,68 @@ var ComponentHost = class {
793
1045
  this.wireInputs();
794
1046
  return;
795
1047
  }
796
- for (const [name, cell] of this.functionalCells) this.applyInput(name, cell, props[name], true);
1048
+ for (const [name, cell] of this.functionalCells) this.applyInput(name, cell, this.providedFor(name), true);
1049
+ }
1050
+ /**
1051
+ * Feeds the inputs `values` names from them instead of from the
1052
+ * parent, until a call with null hands them back to the parent: a
1053
+ * design tool showing one of a component's states on the live tree.
1054
+ * A later call replaces the earlier one; a new prop from the parent
1055
+ * for a previewed input waits until the preview ends.
1056
+ *
1057
+ * Each input a preview starts or ends is handed its value even when
1058
+ * it holds that value already, as a new value from the parent is: a
1059
+ * control that owns what it was started at (`controlled`) starts
1060
+ * again from it, rather than keeping what a person turned it to.
1061
+ */
1062
+ previewInputs(values) {
1063
+ const names = /* @__PURE__ */ new Set([...Object.keys(this.previewed ?? {}), ...Object.keys(values ?? {})]);
1064
+ if (this.instance !== void 0) {
1065
+ const cells = this.instance;
1066
+ const inputs = new Set(getComponentMetadata(this.element.component).inputs);
1067
+ for (const name of Object.keys(values ?? {})) if (inputs.has(name) && !this.beforePreview.has(name)) this.beforePreview.set(name, cells[name].getValue());
1068
+ this.previewed = values;
1069
+ this.wireInputs(names);
1070
+ for (const [name, value] of this.beforePreview) if (values === null || !Object.hasOwn(values, name)) {
1071
+ this.beforePreview.delete(name);
1072
+ if (this.providedFor(name) === void 0) cells[name].next(value);
1073
+ }
1074
+ return;
1075
+ }
1076
+ this.previewed = values;
1077
+ for (const name of names) {
1078
+ const cell = this.functionalCells.get(name);
1079
+ if (cell !== void 0) this.applyInput(name, cell, this.providedFor(name), true, true);
1080
+ }
1081
+ }
1082
+ /** What an input is fed from: a preview's value for it, else what it was started with, else the parent's prop. */
1083
+ providedFor(name) {
1084
+ if (this.previewed !== null && Object.hasOwn(this.previewed, name)) return this.previewed[name];
1085
+ const parent = this.element.props[name];
1086
+ if (this.started !== null && Object.hasOwn(this.started, name)) {
1087
+ if (parent !== this.startedOver.get(name)) {
1088
+ delete this.started[name];
1089
+ this.startedStreams.delete(name);
1090
+ return parent;
1091
+ }
1092
+ if (isObservable(parent)) {
1093
+ let stream = this.startedStreams.get(name);
1094
+ if (stream === void 0) {
1095
+ stream = concat(of(this.started[name]), parent.pipe(skip(1)));
1096
+ this.startedStreams.set(name, stream);
1097
+ }
1098
+ return stream;
1099
+ }
1100
+ return this.started[name];
1101
+ }
1102
+ return this.element.props[name];
797
1103
  }
798
1104
  renderFunction() {
799
1105
  const component = this.element.component;
800
1106
  const context = this.createContext();
801
1107
  this.rendering = true;
802
1108
  try {
803
- return withBodyOf(this.element.tag, () => component(this.createInputRecord(), context));
1109
+ return withLiveInputs(this.live, () => withBodyOf(this.element.tag, () => component(this.createInputRecord(), context)));
804
1110
  } finally {
805
1111
  this.rendering = false;
806
1112
  }
@@ -820,7 +1126,7 @@ var ComponentHost = class {
820
1126
  cell = new InputCell(void 0);
821
1127
  cell.label = `${this.element.tag}.${name}`;
822
1128
  this.functionalCells.set(name, cell);
823
- this.applyInput(name, cell, this.element.props[name], true);
1129
+ this.applyInput(name, cell, this.providedFor(name), true);
824
1130
  }
825
1131
  return cell;
826
1132
  };
@@ -863,18 +1169,25 @@ var ComponentHost = class {
863
1169
  const cell = bounds(label ?? `${this.element.tag}.bounds`);
864
1170
  this.subscriptions.add(() => cell.complete());
865
1171
  return cell;
1172
+ },
1173
+ environment: (key) => {
1174
+ for (let at = this.anchor; at !== null; at = at.parent) if (at.environment !== null) return at.environment.get(key);
1175
+ return key.defaultValue;
866
1176
  }
867
1177
  };
868
1178
  }
869
- wireInputs() {
1179
+ wireInputs(forced = /* @__PURE__ */ new Set()) {
870
1180
  const metadata = getComponentMetadata(this.element.component);
871
- const props = this.element.props;
872
1181
  for (const inputName of metadata.inputs) {
873
1182
  const cell = this.instance[inputName];
874
1183
  cell.label ??= `${metadata.tag}.${inputName}`;
875
- this.applyInput(inputName, cell, props[inputName], false);
1184
+ this.applyInput(inputName, cell, this.providedFor(inputName), false, forced.has(inputName));
876
1185
  }
877
1186
  }
1187
+ /** What the component says each of its inputs holds now; see `liveInput`. */
1188
+ liveInputs() {
1189
+ return new Map([...this.live].map(([name, read]) => [name, read()]));
1190
+ }
878
1191
  /**
879
1192
  * What the parent most recently supplied for each input, as given:
880
1193
  * a value, an element or a stream, whether or not the component has
@@ -894,8 +1207,8 @@ var ComponentHost = class {
894
1207
  * fallback)` supplies one — so a prop the parent stops passing is
895
1208
  * pushed through as `undefined`, which re-applies the fallback.
896
1209
  */
897
- applyInput(inputName, cell, provided, resetWhenAbsent) {
898
- if (this.inputSources.has(inputName) && this.inputSources.get(inputName) === provided) return;
1210
+ applyInput(inputName, cell, provided, resetWhenAbsent, force = false) {
1211
+ if (!force && this.inputSources.has(inputName) && this.inputSources.get(inputName) === provided) return;
899
1212
  const previous = this.inputSubscriptions.get(inputName);
900
1213
  if (previous !== void 0) {
901
1214
  previous.unsubscribe();
@@ -914,12 +1227,19 @@ var ComponentHost = class {
914
1227
  return;
915
1228
  }
916
1229
  if (isObservable(provided)) {
917
- const subscription = provided.subscribe((value) => pushInput(cell, value));
1230
+ let first = force;
1231
+ const subscription = provided.subscribe((value) => {
1232
+ if (first) {
1233
+ first = false;
1234
+ forceInput(cell, value);
1235
+ } else pushInput(cell, value);
1236
+ });
918
1237
  this.inputSubscriptions.set(inputName, subscription);
919
1238
  this.subscriptions.add(subscription);
920
1239
  return;
921
1240
  }
922
- pushInput(cell, provided);
1241
+ if (force) forceInput(cell, provided);
1242
+ else pushInput(cell, provided);
923
1243
  }
924
1244
  validateInputs() {
925
1245
  const metadata = getComponentMetadata(this.element.component);
@@ -988,7 +1308,7 @@ var ComponentHostResolver = class {
988
1308
  this.services = services;
989
1309
  this.channels = channels;
990
1310
  }
991
- resolve(element, anchorId) {
1311
+ resolve(element, anchorId, anchor) {
992
1312
  let host = this.hosts.get(anchorId);
993
1313
  if (host !== void 0 && host.component !== element.component) {
994
1314
  host.dispose();
@@ -996,7 +1316,7 @@ var ComponentHostResolver = class {
996
1316
  host = void 0;
997
1317
  }
998
1318
  if (host === void 0) {
999
- host = new ComponentHost(element, this.services, this.channels);
1319
+ host = new ComponentHost(element, this.services, this.channels, anchor ?? null);
1000
1320
  this.hosts.set(anchorId, host);
1001
1321
  this.pendingMounts.push(host);
1002
1322
  } else host.updateProps(element.props);
@@ -1728,6 +2048,44 @@ function shellFilesUnsupported(error = "There is no shell to reach files through
1728
2048
  error
1729
2049
  };
1730
2050
  }
2051
+ //#endregion
2052
+ //#region src/app/FrameService.ts
2053
+ /**
2054
+ * Every frame the runtime draws, as a component can hear it.
2055
+ *
2056
+ * The shell already receives these as `onFrame`, for an FPS readout or
2057
+ * a profiler on the host thread. A screen that wants to show its own
2058
+ * frame gap or input latency, as a demo of the thread model does,
2059
+ * needs them on this thread, and this is where the runtime puts them.
2060
+ * Injected like any service: `ctx.inject(FrameService).frames`.
2061
+ */
2062
+ var FrameService = class {
2063
+ subject = new Subject();
2064
+ flusher = null;
2065
+ /** Emits after each frame finishes, with what it cost and what it answered. */
2066
+ frames = this.subject.asObservable();
2067
+ /** Called by the runtime; not for components. */
2068
+ publish(metrics) {
2069
+ this.subject.next(metrics);
2070
+ }
2071
+ /** Called by the shell that drives the frames; not for components. */
2072
+ setFlush(flush) {
2073
+ this.flusher = flush;
2074
+ }
2075
+ /**
2076
+ * Draws a pending frame now, rather than when the display next asks
2077
+ * for one; nothing when none is pending.
2078
+ *
2079
+ * A tab in the background is sent no frames. A design editor's tool,
2080
+ * called by an agent while the editor's window is behind another,
2081
+ * changes the screen and waits for it to be drawn before reading it:
2082
+ * without this, it waits for a frame that never comes. The agent's
2083
+ * own screen tools (`uiSurface`) do the same after each action.
2084
+ */
2085
+ flush() {
2086
+ this.flusher?.();
2087
+ }
2088
+ };
1731
2089
  /** A text node's text as the tree shows it. */
1732
2090
  function treeText(text) {
1733
2091
  if (typeof text !== "string" || text === "") return;
@@ -2613,124 +2971,6 @@ var ScrollService = class {
2613
2971
  }
2614
2972
  };
2615
2973
  //#endregion
2616
- //#region src/app/FocusService.ts
2617
- /**
2618
- * Keyboard focus, as a store components can inject.
2619
- *
2620
- * `UiFocusManager` lives in the runtime and a component cannot reach
2621
- * it — components reach the world through stores, as they do for the
2622
- * clipboard (`ShellService`), overlays (`OverlayService`) and find
2623
- * (`FindService`). Without this a component cannot autofocus a field,
2624
- * trap the keyboard in a dialog, or put the caret in the input that
2625
- * failed validation.
2626
- *
2627
- * Nodes come from a `ref` prop. A tree's refs fire before the runtime
2628
- * installs the manager, so an action taken during the first build is
2629
- * queued and replayed once there is one; that is the same problem
2630
- * `FindService` solves for its query field, and the reason `autoFocus`
2631
- * in a dialog works on the frame it mounts.
2632
- *
2633
- * It must stay on the render thread: its actions take `UiNode`s, which
2634
- * never cross a worker boundary.
2635
- */
2636
- var FocusService = class {
2637
- /** The node holding focus, or null. A control binds its focus ring to this. */
2638
- focused = internalState(null);
2639
- /** Whether focus is confined to a subtree by an open trap. */
2640
- trapped = internalState(false);
2641
- /**
2642
- * Whether the focus held is focus the keyboard can see: reached by Tab
2643
- * or an arrow key, or by code with no pointer since, rather than by a
2644
- * press. CSS's `:focus-visible`, for something other than a ring that
2645
- * should answer the keyboard and not the click that happened to focus
2646
- * the same control: a tooltip, say.
2647
- */
2648
- focusVisible = internalState(false);
2649
- manager = null;
2650
- detach = null;
2651
- /** Actions taken before the runtime installed a manager, in order. */
2652
- queued = [];
2653
- /** Installed by the runtime; without one every action is queued. */
2654
- setManager(manager) {
2655
- this.detach?.();
2656
- this.detach = null;
2657
- this.manager = manager;
2658
- if (manager === null) {
2659
- this.queued = [];
2660
- return;
2661
- }
2662
- const focusChange = manager.onFocusChange((node) => {
2663
- this.focused.value = node;
2664
- this.focusVisible.value = manager.focusVisible;
2665
- });
2666
- const scopeChange = manager.onScopeChange(() => {
2667
- this.trapped.value = manager.trapped;
2668
- });
2669
- this.detach = () => {
2670
- focusChange();
2671
- scopeChange();
2672
- };
2673
- const pending = this.queued;
2674
- this.queued = [];
2675
- for (const action of pending) action(manager);
2676
- this.sync(manager);
2677
- }
2678
- /**
2679
- * Gives the node keyboard focus. Non-focusable nodes are ignored.
2680
- *
2681
- * The node is scrolled into view, unless `options.preventScroll` asks
2682
- * for the page to stay where it is, as `element.focus({ preventScroll:
2683
- * true })` does in a browser.
2684
- */
2685
- focus(node, options) {
2686
- this.run((manager) => manager.focus(node, "program", options));
2687
- }
2688
- /** Drops focus without moving it anywhere. */
2689
- blur() {
2690
- this.run((manager) => manager.blur());
2691
- }
2692
- /** Moves focus to the next focusable node, wrapping around. */
2693
- focusNext() {
2694
- this.run((manager) => manager.focusNext());
2695
- }
2696
- /** Moves focus to the previous focusable node, wrapping around. */
2697
- focusPrevious() {
2698
- this.run((manager) => manager.focusPrevious());
2699
- }
2700
- /**
2701
- * Confines focus to `scope` until `releaseTrap`, moving it inside if
2702
- * it was elsewhere. Traps nest: a dialog opened over a dialog traps
2703
- * again, and each release restores its own opener.
2704
- */
2705
- trap(scope) {
2706
- this.run((manager) => manager.pushScope(scope));
2707
- }
2708
- /**
2709
- * Ends the innermost trap and returns focus to whatever held it when
2710
- * the trap was taken — the button that opened the dialog. Given the
2711
- * node trapped in, only that trap ends, and nothing when it ended
2712
- * already: a trap's subtree removed releases it, and a release after
2713
- * that would otherwise end the trap under it.
2714
- */
2715
- releaseTrap(scope) {
2716
- this.run((manager) => manager.popScope(scope));
2717
- }
2718
- run(action) {
2719
- const manager = this.manager;
2720
- if (manager === null) {
2721
- this.queued.push(action);
2722
- return;
2723
- }
2724
- action(manager);
2725
- this.sync(manager);
2726
- }
2727
- sync(manager) {
2728
- this.focused.value = manager.focusedNode;
2729
- this.focusVisible.value = manager.focusVisible;
2730
- this.trapped.value = manager.trapped;
2731
- }
2732
- };
2733
- //#endregion
2734
2974
  //#region src/app/MediaService.ts
2735
2975
  /**
2736
2976
  * Where an `Image` finds its bitmap, an `Icon` its raster and a
@@ -3065,6 +3305,16 @@ var AnimationService = class {
3065
3305
  */
3066
3306
  setMotion(motion) {
3067
3307
  this.motionVocabulary = motion;
3308
+ this.motionListener?.(motion);
3309
+ }
3310
+ motionListener = null;
3311
+ /**
3312
+ * Called by the runtime, which provides the vocabulary at its root so
3313
+ * a name in a `transition` or a `motion` state resolves to it too; a
3314
+ * part of the tree may provide its own over it (`UiEnvironmentKeys.motion`).
3315
+ */
3316
+ setMotionListener(listener) {
3317
+ this.motionListener = listener;
3068
3318
  }
3069
3319
  get motion() {
3070
3320
  return this.motionVocabulary;
@@ -3115,7 +3365,7 @@ var AnimationService = class {
3115
3365
  tween.snap();
3116
3366
  return tween.values;
3117
3367
  }
3118
- return driver.start(tween);
3368
+ return driver.start(tween, options.clock ?? null);
3119
3369
  }
3120
3370
  /**
3121
3371
  * Pulls a number towards a value on a spring, with no duration.
@@ -3124,7 +3374,7 @@ var AnimationService = class {
3124
3374
  * so a flick that changes direction bends rather than restarting.
3125
3375
  */
3126
3376
  spring(cell, to, options = {}) {
3127
- const base = typeof options.spring === "string" || options.spring === void 0 ? this.motionVocabulary.springs[options.spring ?? "snappy"] : options.spring;
3377
+ const base = springIn(this.motionVocabulary, options.spring);
3128
3378
  const driver = this.driver;
3129
3379
  const previous = driver?.animationFor(cell);
3130
3380
  const animation = new UiSpring(cell, to, {
@@ -3143,7 +3393,7 @@ var AnimationService = class {
3143
3393
  animation.snap();
3144
3394
  return animation.values;
3145
3395
  }
3146
- return driver.start(animation);
3396
+ return driver.start(animation, options.clock ?? null);
3147
3397
  }
3148
3398
  /** Stops whatever is driving a cell, leaving it where it stands. */
3149
3399
  stop(cell) {
@@ -3162,31 +3412,10 @@ var AnimationService = class {
3162
3412
  return this.driver?.animationFor(cell);
3163
3413
  }
3164
3414
  resolveDuration(duration) {
3165
- return typeof duration === "number" ? duration : this.motionVocabulary.durations[duration];
3415
+ return durationIn(this.motionVocabulary, duration);
3166
3416
  }
3167
3417
  resolveEasing(easing) {
3168
- if (typeof easing === "function") return easing;
3169
- return this.motionVocabulary.easings[easing] ?? easings.standard;
3170
- }
3171
- };
3172
- //#endregion
3173
- //#region src/app/FrameService.ts
3174
- /**
3175
- * Every frame the runtime draws, as a component can hear it.
3176
- *
3177
- * The shell already receives these as `onFrame`, for an FPS readout or
3178
- * a profiler on the host thread. A screen that wants to show its own
3179
- * frame gap or input latency, as a demo of the thread model does,
3180
- * needs them on this thread, and this is where the runtime puts them.
3181
- * Injected like any service: `ctx.inject(FrameService).frames`.
3182
- */
3183
- var FrameService = class {
3184
- subject = new Subject();
3185
- /** Emits after each frame finishes, with what it cost and what it answered. */
3186
- frames = this.subject.asObservable();
3187
- /** Called by the runtime; not for components. */
3188
- publish(metrics) {
3189
- this.subject.next(metrics);
3418
+ return easingIn(this.motionVocabulary, easing);
3190
3419
  }
3191
3420
  };
3192
3421
  //#endregion
@@ -3238,6 +3467,19 @@ var InspectionService = class {
3238
3467
  return this.host.box?.(node) ?? this.host.inspect(node).box;
3239
3468
  }
3240
3469
  /**
3470
+ * Where the node is seen on the screen: `box` with every transform
3471
+ * above it applied, so a node in a frame drawn at half its size is
3472
+ * half its size, where it is drawn. What an overlay laid over the
3473
+ * live tree is placed by (an outline on the component under the
3474
+ * pointer). `box` keeps the untransformed box, which is where the
3475
+ * node is in the layout: what an editor reading a zoomed board in
3476
+ * the board's own coordinates (a minimap) needs.
3477
+ */
3478
+ screenBox(node) {
3479
+ if (this.host === null) throw new Error("InspectionService has nothing to measure before the runtime has built its tree.");
3480
+ return this.host.screenBox?.(node) ?? this.box(node);
3481
+ }
3482
+ /**
3241
3483
  * Draws a node as though it declared `value` for `property`, until
3242
3484
  * the preview is ended or the node leaves the tree.
3243
3485
  *
@@ -3255,6 +3497,43 @@ var InspectionService = class {
3255
3497
  endPreview(node, property) {
3256
3498
  this.host?.endPreview?.(node, property);
3257
3499
  }
3500
+ /**
3501
+ * Draws a component as though its parent passed `inputs`, over what
3502
+ * the parent did pass, until the preview is ended or the component
3503
+ * leaves the tree. The component is named by its anchor, as
3504
+ * `inspect(node).owners[].anchorId` gives it.
3505
+ *
3506
+ * What a design editor shows when a component on a played screen is
3507
+ * jumped to one of its states (Filled, Error, Disabled): the same
3508
+ * instance, still live, given that state's inputs, and the inputs it
3509
+ * was written with back when the preview ends. A later preview of the
3510
+ * same component replaces this one. The names and values are props',
3511
+ * as JSX would write them. An anchor no component holds is ignored.
3512
+ */
3513
+ previewInputs(anchorId, inputs) {
3514
+ this.host?.previewInputs?.(anchorId, inputs);
3515
+ }
3516
+ /** Ends a preview of a component's inputs: its parent's are its own again. */
3517
+ endInputPreview(anchorId) {
3518
+ this.host?.previewInputs?.(anchorId, null);
3519
+ }
3520
+ /**
3521
+ * Presses a node, or replaces the text of the field it is, as a
3522
+ * person would: a click at the middle of where it is drawn, which
3523
+ * its handlers and every handler above it get, or the edit a
3524
+ * keyboard makes once it has focus. The path the accessibility
3525
+ * mirror's actions take, for any node in the tree, record or not.
3526
+ *
3527
+ * What a design editor's agent does to play a prototype in its live
3528
+ * frames: a row pressed, an email typed, by a node it found with
3529
+ * `nodesAt` or by walking the tree. False when it did not take: a
3530
+ * node no longer in the tree, or a field that could not have focus
3531
+ * because an open dialog's trap holds it, or takes no text.
3532
+ */
3533
+ act(node, action, value) {
3534
+ if (this.host?.act === void 0) throw new Error("InspectionService cannot act before the runtime has built its tree.");
3535
+ return this.host.act(node, action, value);
3536
+ }
3258
3537
  };
3259
3538
  //#endregion
3260
3539
  //#region src/app/InputLatency.ts
@@ -3421,7 +3700,10 @@ var SmoothScroller = class {
3421
3700
  const { min, max } = this.boundsOf(node, axis);
3422
3701
  const target = clamp((pending ?? from) + delta, min, max);
3423
3702
  if (pending !== void 0 && target === pending) return;
3424
- this.animations.spring(cell, target, { spring: SCROLL_SPRING });
3703
+ this.animations.spring(cell, target, {
3704
+ spring: SCROLL_SPRING,
3705
+ clock: clockOf(node)
3706
+ });
3425
3707
  }
3426
3708
  /**
3427
3709
  * Shifts a running scroll by a coordinate correction.
@@ -3446,7 +3728,10 @@ var SmoothScroller = class {
3446
3728
  const pending = this.destinationOf(state.cell);
3447
3729
  if (pending === void 0) return;
3448
3730
  state.lastWritten = state.cell.value;
3449
- this.animations.spring(state.cell, pending + delta, { spring: SCROLL_SPRING });
3731
+ this.animations.spring(state.cell, pending + delta, {
3732
+ spring: SCROLL_SPRING,
3733
+ clock: clockOf(node)
3734
+ });
3450
3735
  }
3451
3736
  /** Whether this container is being animated right now. */
3452
3737
  isScrolling(node) {
@@ -3654,6 +3939,8 @@ var GessoRuntime = class {
3654
3939
  lastEarlyFrame = -Infinity;
3655
3940
  /** When the last discrete input was routed; see `answerInput`. */
3656
3941
  lastDiscreteInput = -Infinity;
3942
+ /** The wheel's steps since the last frame, added together; see `wheel`. */
3943
+ heldWheel = null;
3657
3944
  /** The display time the frame being drawn is for; see `FrameMetrics.time`. */
3658
3945
  frameTime = null;
3659
3946
  /** When the frame's first phase began, before its changes were collected; see `FrameMetrics.durationMs`. */
@@ -3763,6 +4050,11 @@ var GessoRuntime = class {
3763
4050
  * them. Only populated while a listener is attached.
3764
4051
  */
3765
4052
  semanticsBoxes = /* @__PURE__ */ new Map();
4053
+ /** When a frame last moved something, and when the boxes were last swept; see `updateSemantics`. */
4054
+ semanticsMovedAt = -Infinity;
4055
+ semanticsSweptAt = -Infinity;
4056
+ /** The sweep owed once a run of moving frames stops. */
4057
+ semanticsSweepTimer = null;
3766
4058
  /** What each semantics walk leaves for the next; see `SemanticsMemory`. */
3767
4059
  semanticsMemory = semanticsMemory();
3768
4060
  lastFocusedId = null;
@@ -3902,7 +4194,8 @@ var GessoRuntime = class {
3902
4194
  width: this.width,
3903
4195
  height: this.height
3904
4196
  }),
3905
- onLayout: (node, listener) => this.layoutNotifier.add(node, listener)
4197
+ onLayout: (node, listener) => this.layoutNotifier.add(node, listener),
4198
+ onFlowLayout: (node, listener) => this.layoutNotifier.addFlow(node, listener)
3906
4199
  },
3907
4200
  animations: this.animations,
3908
4201
  sharedElements: this.sharedElements
@@ -3926,6 +4219,9 @@ var GessoRuntime = class {
3926
4219
  if (options.fonts !== void 0) this.services.get(FontService).declare(options.fonts);
3927
4220
  if (!this.services.has(AnimationService)) this.services.register(AnimationService);
3928
4221
  this.services.get(AnimationService).setDriver(this.animations);
4222
+ this.services.get(AnimationService).setMotionListener((motion) => {
4223
+ if (this.graph.updateNodePropertyNow(this.graph.root, "provide", [provided(UiEnvironmentKeys.motion, motion)], DirtyFlags.Environment)) this.requestRepaint();
4224
+ });
3929
4225
  this.services.get(AudioService).setAnimations(this.services.get(AnimationService));
3930
4226
  this.smoothScroller = new SmoothScroller(this.graph, this.services.get(AnimationService), (node, axis) => {
3931
4227
  const record = this.engine.recordFor(node);
@@ -4006,8 +4302,22 @@ var GessoRuntime = class {
4006
4302
  height: box.height
4007
4303
  };
4008
4304
  },
4305
+ screenBox: (node) => {
4306
+ const box = this.engine.screenBox(node);
4307
+ return {
4308
+ x: box.x,
4309
+ y: box.y,
4310
+ width: box.width,
4311
+ height: box.height
4312
+ };
4313
+ },
4009
4314
  preview: (node, property, value) => this.previewProperty(node, property, value),
4010
- endPreview: (node, property) => this.endPreviews(node, property)
4315
+ endPreview: (node, property) => this.endPreviews(node, property),
4316
+ previewInputs: (anchorId, inputs) => this.resolver.hostFor(anchorId)?.previewInputs(inputs),
4317
+ act: (node, action, value) => this.graph.getNode(node.id) === node && this.actOn(node, value === void 0 ? { action } : {
4318
+ action,
4319
+ value
4320
+ })
4011
4321
  });
4012
4322
  this.services.get(FocusService).setManager(this.input.focus);
4013
4323
  this.services.get(EditingService).setController(this.input.editing);
@@ -4236,6 +4546,51 @@ var GessoRuntime = class {
4236
4546
  this.lastDiscreteInput = now();
4237
4547
  this.drawEarly();
4238
4548
  }
4549
+ /**
4550
+ * A step of the wheel, or of a trackpad's two fingers or its pinch,
4551
+ * held until the frame and added to the others that arrive before it.
4552
+ *
4553
+ * A trackpad sends a step on every refresh, and a handler that takes
4554
+ * longer than a refresh over one (a design tool's board zoomed over
4555
+ * hundreds of frames, each of whose names it scales) fell behind for
4556
+ * good: each step waited for the one before it, the frame waited for
4557
+ * them all, and a pinch drew nothing until the fingers stopped, then
4558
+ * jumped. Held, the steps that queue while the handler runs are one
4559
+ * step at the next frame, so the handler runs once a frame however
4560
+ * many arrive, and what it draws follows the fingers a frame at a
4561
+ * time. A step with other modifiers or in another unit is not added
4562
+ * to the held one but follows it, and any other input lets the held
4563
+ * step go first (`releaseWheel`), so nothing is answered out of order.
4564
+ */
4565
+ wheel(...step) {
4566
+ const held = this.heldWheel;
4567
+ if (held !== null && !sameWheel(held, step)) this.releaseWheel();
4568
+ if (this.heldWheel === null) {
4569
+ this.heldWheel = step;
4570
+ this.scheduler.wake();
4571
+ return;
4572
+ }
4573
+ const [, , deltaX, deltaY, modifiers, deltaMode, wheelDeltaY] = this.heldWheel;
4574
+ const [x, y, moreX, moreY, , , moreWheelY, at] = step;
4575
+ const legacy = wheelDeltaY === void 0 || moreWheelY === void 0 ? moreWheelY : wheelDeltaY + moreWheelY;
4576
+ this.heldWheel = [
4577
+ x,
4578
+ y,
4579
+ deltaX + moreX,
4580
+ deltaY + moreY,
4581
+ modifiers,
4582
+ deltaMode,
4583
+ legacy,
4584
+ at
4585
+ ];
4586
+ }
4587
+ /** Hands the held wheel step to the input now, before what follows it; see `wheel`. */
4588
+ releaseWheel() {
4589
+ const held = this.heldWheel;
4590
+ if (held === null) return;
4591
+ this.heldWheel = null;
4592
+ this.input.wheel.wheel(...held);
4593
+ }
4239
4594
  /** Draws the pending frame in a task of its own, if the guards in `answerInput` allow. */
4240
4595
  drawEarly() {
4241
4596
  if (!this.started || !this.visible || this.earlyFrame !== null || !this.scheduler.framePending) return;
@@ -4436,7 +4791,8 @@ var GessoRuntime = class {
4436
4791
  source: PREVIEW,
4437
4792
  name: "preview",
4438
4793
  order: Number.MAX_SAFE_INTEGER,
4439
- value
4794
+ value,
4795
+ snap: true
4440
4796
  }, propertyEffects(property));
4441
4797
  }
4442
4798
  endPreviews(node, property) {
@@ -4780,6 +5136,7 @@ var GessoRuntime = class {
4780
5136
  listens: this.dispatcher.listenerTypes(node),
4781
5137
  beneath: this.beneathAtPointer(node),
4782
5138
  semantics: this.semanticsOf(node),
5139
+ ...this.interactionOf(node),
4783
5140
  explanation: formatExplanation(this.engine.explain(node)),
4784
5141
  ...this.sourceReport(node)
4785
5142
  };
@@ -4827,6 +5184,27 @@ var GessoRuntime = class {
4827
5184
  const source = this.builder.sourceOf(node);
4828
5185
  return source === void 0 ? {} : { source };
4829
5186
  }
5187
+ /** The props the node's `interactive` modifiers write while hovered and pressed, later ones over earlier. */
5188
+ interactionOf(node) {
5189
+ let hovered;
5190
+ let pressed;
5191
+ for (const modifier of this.builder.modifiersFor(node)?.modifiers ?? []) {
5192
+ if (modifier.kind.name !== "interactive") continue;
5193
+ const options = modifier.args;
5194
+ if (options.hovered !== void 0) hovered = {
5195
+ ...hovered,
5196
+ ...options.hovered
5197
+ };
5198
+ if (options.pressed !== void 0) pressed = {
5199
+ ...pressed,
5200
+ ...options.pressed
5201
+ };
5202
+ }
5203
+ return hovered === void 0 && pressed === void 0 ? {} : { interaction: {
5204
+ ...hovered === void 0 ? {} : { hovered },
5205
+ ...pressed === void 0 ? {} : { pressed }
5206
+ } };
5207
+ }
4830
5208
  ownersOf(node) {
4831
5209
  const owners = [];
4832
5210
  for (let current = node; current !== null; current = current.parent) {
@@ -4838,11 +5216,14 @@ var GessoRuntime = class {
4838
5216
  const value = ownerInput(supplied);
4839
5217
  if (value !== void 0) inputs[name] = value;
4840
5218
  }
5219
+ const live = {};
5220
+ for (const [name, value] of host.liveInputs()) if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") live[name] = value;
4841
5221
  owners.push({
4842
5222
  name: getComponentMetadata(host.component).tag,
4843
5223
  anchorId: current.id,
4844
5224
  ...usedAt === void 0 ? {} : { usedAt },
4845
- ...Object.keys(inputs).length === 0 ? {} : { inputs }
5225
+ ...Object.keys(inputs).length === 0 ? {} : { inputs },
5226
+ ...Object.keys(live).length === 0 ? {} : { live }
4846
5227
  });
4847
5228
  }
4848
5229
  }
@@ -5051,6 +5432,10 @@ var GessoRuntime = class {
5051
5432
  clearTimeout(this.rendererTimer);
5052
5433
  this.rendererTimer = null;
5053
5434
  }
5435
+ if (this.semanticsSweepTimer !== null) {
5436
+ clearTimeout(this.semanticsSweepTimer);
5437
+ this.semanticsSweepTimer = null;
5438
+ }
5054
5439
  this.detachViewportInsetEnvironment?.();
5055
5440
  this.detachViewportInsetEnvironment = null;
5056
5441
  this.viewportInsetWrite?.();
@@ -5241,7 +5626,7 @@ var GessoRuntime = class {
5241
5626
  op: "add",
5242
5627
  node
5243
5628
  })),
5244
- boxes: this.collectSemanticsBoxes(),
5629
+ boxes: this.sweepSemanticsBoxes(),
5245
5630
  focused: this.focusManager.focusedNode?.id ?? null
5246
5631
  });
5247
5632
  }
@@ -5343,18 +5728,21 @@ var GessoRuntime = class {
5343
5728
  if (this.applyTextRunAction(action)) return;
5344
5729
  const node = this.graph.getNode(action.id);
5345
5730
  if (node === void 0 || !this.semantics.has(action.id)) return;
5346
- if (action.action === "focus") {
5347
- this.focusManager.focus(node);
5348
- return;
5349
- }
5350
- if (action.action === "setValue") {
5351
- this.focusManager.focus(node);
5352
- this.input.editing.replaceText(action.value ?? "");
5353
- return;
5354
- }
5731
+ this.actOn(node, action);
5732
+ }
5733
+ /**
5734
+ * A semantics action on a node: the mirror's, or `InspectionService.act`'s
5735
+ * on a node with no record of its own. False when it did not take: focus
5736
+ * refused (an open dialog's trap holds it elsewhere), or a field that
5737
+ * takes no text.
5738
+ */
5739
+ actOn(node, action) {
5740
+ if (action.action === "focus") return this.focusManager.focus(node);
5741
+ if (action.action === "setValue") return this.focusManager.focus(node) && this.input.editing.replaceText(action.value ?? "");
5355
5742
  this.focusManager.focus(node);
5356
5743
  const box = this.engine.screenBox(node);
5357
5744
  this.dispatcher.dispatch(new UiPointerEvent(UiEventType.Click, box.x + box.width / 2, box.y + box.height / 2, 1), node);
5745
+ return true;
5358
5746
  }
5359
5747
  /**
5360
5748
  * An action on an inline link, which is a run and not a node.
@@ -5398,7 +5786,7 @@ var GessoRuntime = class {
5398
5786
  else patches = this.rescopeSemantics(frame) ?? this.restructureSemantics(frame) ?? this.rebuildSemantics();
5399
5787
  }
5400
5788
  if (listener === null) return;
5401
- const boxes = moved || patches.length > 0 ? this.collectSemanticsBoxes() : EMPTY_BOXES;
5789
+ const boxes = patches.length > 0 ? this.sweepSemanticsBoxes() : moved ? this.movedSemanticsBoxes() : EMPTY_BOXES;
5402
5790
  const focused = this.focusManager.focusedNode?.id ?? null;
5403
5791
  const focusMoved = focused !== this.lastFocusedId;
5404
5792
  this.lastFocusedId = focused;
@@ -5695,6 +6083,70 @@ var GessoRuntime = class {
5695
6083
  return ids;
5696
6084
  }
5697
6085
  /**
6086
+ * The boxes for a frame that moved something but changed no meaning:
6087
+ * all of them when it is the first move after a rest, and otherwise
6088
+ * only the focused node's, with the sweep owed for when the moving
6089
+ * stops (`SEMANTICS_SETTLE_MS`), or for when the boxes have been stale
6090
+ * too long (`SEMANTICS_MOST_STALE_MS`).
6091
+ *
6092
+ * A pan or a zoom moves every box under the camera on every frame. In
6093
+ * Atelier, a board of three dozen screens panned at 15%, the sweep was
6094
+ * 3 to 4 ms of a 4 to 6 ms frame, with frames to 13 ms, past a 120 Hz
6095
+ * display's 8.3; and the main thread wrote four styles on each of 545
6096
+ * elements per frame, which in Chromium was 45% of that thread while
6097
+ * it also had the wheel events to deliver. Nobody reads the rectangle
6098
+ * of an element that is moving under a pan: a screen reader's cursor
6099
+ * is on what it rests on. The focused node is the exception, since its
6100
+ * focus ring is drawn from its element, so it moves every frame.
6101
+ */
6102
+ movedSemanticsBoxes() {
6103
+ const at = now();
6104
+ const resting = at - this.semanticsMovedAt > SEMANTICS_SETTLE_MS;
6105
+ this.semanticsMovedAt = at;
6106
+ if (resting || at - this.semanticsSweptAt > SEMANTICS_MOST_STALE_MS) return this.sweepSemanticsBoxes();
6107
+ if (this.semanticsSweepTimer === null) this.scheduleSemanticsSweep(SEMANTICS_SETTLE_MS);
6108
+ return this.focusedSemanticsBox();
6109
+ }
6110
+ /** Sweeps the boxes once the moving has stopped, and sends what changed. */
6111
+ scheduleSemanticsSweep(wait) {
6112
+ this.semanticsSweepTimer = setTimeout(() => {
6113
+ this.semanticsSweepTimer = null;
6114
+ const listener = this.semanticsListener;
6115
+ if (listener === null) return;
6116
+ const left = this.semanticsMovedAt + SEMANTICS_SETTLE_MS - now();
6117
+ if (left > 0) {
6118
+ this.scheduleSemanticsSweep(left);
6119
+ return;
6120
+ }
6121
+ const boxes = this.sweepSemanticsBoxes();
6122
+ if (boxes.length > 0) listener({
6123
+ patches: EMPTY_PATCHES,
6124
+ boxes
6125
+ });
6126
+ }, Math.max(1, wait));
6127
+ }
6128
+ sweepSemanticsBoxes() {
6129
+ this.semanticsSweptAt = now();
6130
+ if (this.semanticsSweepTimer !== null) {
6131
+ clearTimeout(this.semanticsSweepTimer);
6132
+ this.semanticsSweepTimer = null;
6133
+ }
6134
+ return this.collectSemanticsBoxes();
6135
+ }
6136
+ /** The focused node's box when it differs from the one last sent, as `collectSemanticsBoxes` would give it. */
6137
+ focusedSemanticsBox() {
6138
+ const focused = this.focusManager.focusedNode;
6139
+ if (focused === null || focused === void 0 || !this.semantics.has(focused.id) || this.engine.recordFor(focused) === void 0) return EMPTY_BOXES;
6140
+ const box = this.engine.screenBox(focused);
6141
+ const last = this.semanticsBoxes.get(focused.id);
6142
+ if (last !== void 0 && boxesEqual(last, box)) return EMPTY_BOXES;
6143
+ this.semanticsBoxes.set(focused.id, box);
6144
+ return [{
6145
+ id: focused.id,
6146
+ box
6147
+ }];
6148
+ }
6149
+ /**
5698
6150
  * The mirrored nodes whose box differs from the one last sent, and
5699
6151
  * which are on screen.
5700
6152
  *
@@ -5921,6 +6373,7 @@ var GessoRuntime = class {
5921
6373
  this.frameBegan = now();
5922
6374
  this.phaseTimings = emptyPhaseTimings();
5923
6375
  this.frameTime = time;
6376
+ this.releaseWheel();
5924
6377
  this.phaseTimings.ticks = this.timePhase(() => this.visible && this.animations.isRunning, () => this.animations.advance(time));
5925
6378
  this.scheduleAnimationTick(time);
5926
6379
  this.phaseTimings.patches = this.timePhase(() => this.replicas.some((replica) => replica.hasPendingPatches), () => {
@@ -6092,10 +6545,18 @@ var GessoRuntime = class {
6092
6545
  scrollY: record?.scrollY ?? 0
6093
6546
  };
6094
6547
  };
6548
+ const readFlow = (node) => {
6549
+ const record = this.engine.recordFor(node);
6550
+ return {
6551
+ box: record === void 0 ? NO_BOX : this.engine.worldBox(node),
6552
+ scrollX: record?.scrollX ?? 0,
6553
+ scrollY: record?.scrollY ?? 0
6554
+ };
6555
+ };
6095
6556
  this.deferredReveals = [];
6096
6557
  try {
6097
6558
  for (;;) {
6098
- if (!this.layoutNotifier.isEmpty()) this.layoutNotifier.notify(read);
6559
+ if (!this.layoutNotifier.isEmpty()) this.layoutNotifier.notify(read, readFlow);
6099
6560
  if (!this.layoutPending()) {
6100
6561
  this.finishSettling(settled.passes > 0);
6101
6562
  if (!this.layoutPending()) break;
@@ -6522,6 +6983,14 @@ function semanticIndices(list) {
6522
6983
  * one frame still hands the mirror the rows it is about to show.
6523
6984
  */
6524
6985
  const SEMANTICS_VIEWPORT_MARGIN = 400;
6986
+ /**
6987
+ * How long a run of moving frames has to pause before the mirrored
6988
+ * boxes are swept again; see `movedSemanticsBoxes`. Longer than the gap
6989
+ * between a trackpad's wheel events, so a pan is one run.
6990
+ */
6991
+ const SEMANTICS_SETTLE_MS = 120;
6992
+ /** The longest the mirrored boxes go unswept while something keeps moving: an endless animation still gets them. */
6993
+ const SEMANTICS_MOST_STALE_MS = 1e3;
6525
6994
  const EMPTY_PATCHES = [];
6526
6995
  const EMPTY_BOXES = [];
6527
6996
  function boxesEqual(a, b) {
@@ -6536,6 +7005,19 @@ function boxesEqual(a, b) {
6536
7005
  function frameChangedTree(frame) {
6537
7006
  return frameNeedsSemantics(frame);
6538
7007
  }
7008
+ /** The box a node with no record reports. */
7009
+ const NO_BOX = {
7010
+ x: 0,
7011
+ y: 0,
7012
+ width: 0,
7013
+ height: 0
7014
+ };
7015
+ /** Whether two wheel steps can be added together: the same keys held, in the same unit. */
7016
+ function sameWheel(a, b) {
7017
+ const [, , , , keysA, unitA] = a;
7018
+ const [, , , , keysB, unitB] = b;
7019
+ return unitA === unitB && (keysA?.ctrl ?? false) === (keysB?.ctrl ?? false) && (keysA?.shift ?? false) === (keysB?.shift ?? false) && (keysA?.alt ?? false) === (keysB?.alt ?? false) && (keysA?.meta ?? false) === (keysB?.meta ?? false);
7020
+ }
6539
7021
  function frameNeedsSemantics(frame) {
6540
7022
  return frame.anyFlags(DirtyFlags.Semantics | DirtyFlags.Children);
6541
7023
  }
@@ -6752,25 +7234,58 @@ async function loadModule(module, load) {
6752
7234
  }
6753
7235
  /** The export to draw, or why none can be chosen. */
6754
7236
  function chooseExport(exports, wanted, module) {
6755
- const drawable = (value) => typeof value === "function" || isUiElement(value);
7237
+ const drawable = (value) => typeof value === "function" || isUiElement(value) || examplesOf(value) !== null;
6756
7238
  if (wanted !== void 0) {
6757
- if (!drawable(exports[wanted])) return `${module} has no component or element exported as ${wanted}. It exports ${list(exports, drawable)}.`;
7239
+ const [head, ...rest] = wanted.split(".");
7240
+ let value = exports[head];
7241
+ for (const key of rest) value = typeof value === "object" && value !== null ? value[key] : void 0;
7242
+ if (!drawable(value)) return `${module} has no component or element exported as ${wanted}. It exports ${list(exports, drawable)}.`;
7243
+ const examples = examplesOf(value);
6758
7244
  return {
6759
7245
  name: wanted,
6760
- value: exports[wanted]
7246
+ value: examples === null ? value : examplesSheet(examples)
6761
7247
  };
6762
7248
  }
7249
+ const asDrawn = (value) => {
7250
+ const examples = examplesOf(value);
7251
+ return examples === null ? value : examplesSheet(examples);
7252
+ };
6763
7253
  if (drawable(exports.default)) return {
6764
7254
  name: "default",
6765
- value: exports.default
7255
+ value: asDrawn(exports.default)
6766
7256
  };
6767
7257
  const candidates = Object.keys(exports).filter((name) => /^[A-Z]/.test(name) && drawable(exports[name]));
6768
7258
  if (candidates.length === 1) return {
6769
7259
  name: candidates[0],
6770
- value: exports[candidates[0]]
7260
+ value: asDrawn(exports[candidates[0]])
6771
7261
  };
6772
7262
  return candidates.length === 0 ? `${module} exports nothing to draw: no component, and no element.` : `${module} exports ${candidates.join(", ")}; say which with export.`;
6773
7263
  }
7264
+ /**
7265
+ * An object of examples, as a component's states are kept for a design
7266
+ * tool (`export const Button = { Primary: <Button … />, … }`): each
7267
+ * element by its name, or null for anything else.
7268
+ */
7269
+ function examplesOf(value) {
7270
+ if (typeof value !== "object" || value === null || isUiElement(value) || Array.isArray(value)) return null;
7271
+ const entries = Object.entries(value).filter((entry) => isUiElement(entry[1]));
7272
+ return entries.length === 0 ? null : entries;
7273
+ }
7274
+ /** Every example, each under its name, down the frame. */
7275
+ function examplesSheet(examples) {
7276
+ return Column({
7277
+ gap: 24,
7278
+ padding: 16
7279
+ }, ...examples.map(([name, element]) => Column({
7280
+ key: name,
7281
+ gap: 8
7282
+ }, Text({
7283
+ text: name,
7284
+ fontSize: 12,
7285
+ fontWeight: "600",
7286
+ color: "textMuted"
7287
+ }), element)));
7288
+ }
6774
7289
  function list(exports, keep) {
6775
7290
  const names = Object.keys(exports).filter((name) => keep(exports[name]));
6776
7291
  return names.length === 0 ? "nothing that can be drawn" : names.join(", ");
@@ -6790,6 +7305,6 @@ function failure(text) {
6790
7305
  };
6791
7306
  }
6792
7307
  //#endregion
6793
- export { modifiersFrom as A, Define as B, treeText as C, epochNow as D, epochFromEvent as E, OverlayService as F, bounds as G, Input as H, isClassComponent as I, ServiceRegistry as K, ChannelRegistry as L, ComponentHost as M, OverlayLayer as N, isDiscreteInputMessage as O, __decorate as P, ChannelReplica as R, AudioService as S, shellFilesUnsupported as T, Output as U, Inject as V, BoundsCell as W, buildPath as _, UI_FRAME_PHASES as a, parseUrl as b, AnimationService as c, MediaService as d, FocusService as f, RouterService as g, FindService as h, GessoRuntime as i, ComponentHostResolver as j, isInputMessage as k, TextService as l, EditingService as m, renderFrame as n, InspectionService as o, ScrollService as p, Component as q, isClaimedError as r, FrameService as s, renderModule as t, FontService as u, formatUrl as v, ShellService as w, RouteState as x, parsePattern as y, Channel as z };
7308
+ export { ComponentHostResolver as A, ChannelRegistry as B, ShellService as C, isDiscreteInputMessage as D, epochNow as E, FocusService as F, Input as G, Channel as H, boundFocus as I, BoundsCell as J, Output as K, OverlayScopeKey as L, StartingInputsKey as M, OverlayLayer as N, isInputMessage as O, __decorate as P, OverlayService as R, FrameService as S, epochFromEvent as T, Define as U, ChannelReplica as V, Inject as W, ServiceRegistry as X, bounds as Y, Component as Z, parsePattern as _, UI_FRAME_PHASES as a, AudioService as b, TextService as c, ScrollService as d, EditingService as f, formatUrl as g, buildPath as h, GessoRuntime as i, ComponentHost as j, modifiersFrom as k, FontService as l, RouterService as m, renderFrame as n, InspectionService as o, FindService as p, liveInput as q, isClaimedError as r, AnimationService as s, renderModule as t, MediaService as u, parseUrl as v, shellFilesUnsupported as w, treeText as x, RouteState as y, isClassComponent as z };
6794
7309
 
6795
- //# sourceMappingURL=render-cydQ10Nd.js.map
7310
+ //# sourceMappingURL=render-W2XO444K.js.map