@moku-labs/game 0.15.7 → 0.15.9

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.
Files changed (35) hide show
  1. package/dist/app/page.d.mts +1 -1
  2. package/dist/app/page.mjs +1 -1
  3. package/dist/app/system.d.mts +3 -3
  4. package/dist/app.d.mts +3 -3
  5. package/dist/app.mjs +1 -1
  6. package/dist/assets.d.mts +1 -1
  7. package/dist/{commands-C8aWbThW.d.mts → commands-5woFUxUr.d.mts} +13 -5
  8. package/dist/control.d.mts +4 -4
  9. package/dist/control.mjs +1 -1
  10. package/dist/{define-Bebctfvs.d.mts → define-B8A5IoeS.d.mts} +2 -2
  11. package/dist/{headless-DobgwTKK.d.mts → headless-CrxofjLt.d.mts} +1 -1
  12. package/dist/{index-CU4Oc2Qv.d.mts → index-DT9aYVGd.d.mts} +7 -6
  13. package/dist/index.d.mts +5 -5
  14. package/dist/index.mjs +3 -3
  15. package/dist/inspect.d.mts +6 -6
  16. package/dist/inspect.mjs +1 -1
  17. package/dist/jsx-dev-runtime.d.mts +1 -1
  18. package/dist/jsx-runtime.d.mts +1 -1
  19. package/dist/{run-DlxDjgOO.mjs → run-BqYmcNo7.mjs} +38 -5
  20. package/dist/{runtime-Bm_guAJR.d.mts → runtime-BZQQPq56.d.mts} +1 -1
  21. package/dist/{sources-OvHvyeJS.mjs → sources-QCowxSpC.mjs} +1 -1
  22. package/dist/{src-nJYt0LMr.mjs → src-Cs1bP3v7.mjs} +1044 -184
  23. package/dist/testing.d.mts +4 -4
  24. package/dist/testing.mjs +1 -1
  25. package/dist/{types-CZGuMhV8.d.mts → types-D8roM1u3.d.mts} +127 -9
  26. package/dist/{types-BkcgFOoB.d.mts → types-DDmfkONF.d.mts} +59 -7
  27. package/dist/{types-MocH_igX.d.mts → types-DK-IYk-s.d.mts} +4 -4
  28. package/dist/{types-Mj75CDO6.d.mts → types-YXykgTOp.d.mts} +2 -2
  29. package/dist/{types-C4D9UVgb.d.mts → types-i77Nrrv-.d.mts} +29 -16
  30. package/dist/{visual-BV2A3kpr.d.mts → visual-1doiBOD3.d.mts} +6 -6
  31. package/dist/{visual-2m4wJL_z.mjs → visual-CIUx49C0.mjs} +26 -6
  32. package/dist/visual.d.mts +1 -1
  33. package/dist/visual.mjs +2 -2
  34. package/llms.txt +8 -7
  35. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import { r as systemSource, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { a as finishDrafts, i as enableDraftPatches, n as deepFreeze$1, o as openDrafts, r as dropDrafts } from "./drafts-BawpSaVp.mjs";
4
4
  import { d as graphHash, f as isRestCheckpoint, h as pushEntry, i as servePaused, l as findNode, m as compact, n as captureFrame, o as collectFlows, p as slotNames, r as serveCaptures, s as describeGraph, t as cancelCaptures, u as framePath } from "./capture-D7XvoHdH.mjs";
5
- import { A as Order, C as Sprite, D as Exiting, F as system, I as systemsFor, L as tag, M as component, O as Layer, P as resource, S as Shape, T as componentsFor, _ as HudPart, b as OverlayLevel, c as Countdown, d as builtInStyles, g as Display, h as textFor, i as samePose, j as Tree, k as Now, l as Text, m as readStyle, n as drawnRect, o as visualOf$1, r as fitScaleOf, s as visualRectOf, t as CONTENT$1, v as NineSlice, w as Transform, x as Parent, y as Overlay } from "./visual-2m4wJL_z.mjs";
5
+ import { A as Now, C as Sprite, E as componentsFor, F as resource, I as system, L as systemsFor, M as Tree, N as component, O as Exiting, R as tag, S as Shape, T as arcDefaults, _ as HudPart, b as OverlayLevel, c as Countdown, d as builtInStyles, g as Display, h as textFor, i as samePose, j as Order, k as Layer, l as Text, m as readStyle, n as drawnRect, o as visualOf$1, r as fitScaleOf, s as visualRectOf, t as CONTENT$1, v as NineSlice, w as Transform, x as Parent, y as Overlay } from "./visual-CIUx49C0.mjs";
6
6
  import { a as isDev } from "./dev-BNndV-hU.mjs";
7
7
  import { i as isComponentDefinition, n as flatten, r as defineComponent } from "./flatten-BkXuscAV.mjs";
8
8
  import { envPlugin, logPlugin } from "@moku-labs/common/browser";
@@ -3337,7 +3337,7 @@ function deliver$1(state, answer) {
3337
3337
  * @param ctx - Domain context of the flow plugin.
3338
3338
  * @param answer - The answer the closed gate refused.
3339
3339
  */
3340
- function hold(ctx, answer) {
3340
+ function hold$1(ctx, answer) {
3341
3341
  if (ctx.state.fx.mode === "fast") return;
3342
3342
  if (!ctx.deps.time.isRunning()) return;
3343
3343
  if (ctx.config.holdMs <= 0 && ctx.state.gate.held !== void 0) return;
@@ -3376,7 +3376,7 @@ function createGateApi(ctx) {
3376
3376
  return {
3377
3377
  answer: (answer) => {
3378
3378
  if (ctx.state.gate.open === void 0) {
3379
- hold(ctx, answer);
3379
+ hold$1(ctx, answer);
3380
3380
  return false;
3381
3381
  }
3382
3382
  if (!accepts(ctx.state.gate, answer)) return false;
@@ -5506,7 +5506,7 @@ function startDeadline(timeoutMs) {
5506
5506
  * @param running - The promise of `run()`.
5507
5507
  * @returns `true` when the loop settled, `false` when the deadline won and the loop still runs.
5508
5508
  */
5509
- async function settle$1(ctx, running) {
5509
+ async function settle$2(ctx, running) {
5510
5510
  const quiet = running.then(noop, noop);
5511
5511
  const deadline = startDeadline(ctx.config.settleTimeoutMs);
5512
5512
  try {
@@ -5535,7 +5535,7 @@ async function stopRunner(ctx) {
5535
5535
  stop.abort("stop");
5536
5536
  state.abort = stop;
5537
5537
  }
5538
- const settled = running === void 0 || await settle$1(ctx, running);
5538
+ const settled = running === void 0 || await settle$2(ctx, running);
5539
5539
  state.running = void 0;
5540
5540
  ctx.state.gate.held = void 0;
5541
5541
  if (settled) state.abort = void 0;
@@ -24372,7 +24372,7 @@ function destroyPools(state) {
24372
24372
  * view with `applyPose`.
24373
24373
  */
24374
24374
  /** How deep a parent chain is followed; deeper is treated as a loop. */
24375
- const MAX_DEPTH$5 = 32;
24375
+ const MAX_DEPTH$6 = 32;
24376
24376
  /**
24377
24377
  * The parent an entity moves with.
24378
24378
  *
@@ -24394,7 +24394,7 @@ function parentOf$1(ecs, entity) {
24394
24394
  */
24395
24395
  function isLeaving(ecs, entity) {
24396
24396
  let current = entity;
24397
- for (let depth = 0; depth < MAX_DEPTH$5 && current !== 0; depth += 1) {
24397
+ for (let depth = 0; depth < MAX_DEPTH$6 && current !== 0; depth += 1) {
24398
24398
  if (ecs.has(current, Exiting)) return true;
24399
24399
  current = parentOf$1(ecs, current);
24400
24400
  }
@@ -24500,7 +24500,7 @@ function rootPoseInto(ecs, entity, out, own) {
24500
24500
  out.pivot.x = start.pivot.x;
24501
24501
  out.pivot.y = start.pivot.y;
24502
24502
  let current = entity;
24503
- for (let depth = 0; depth < MAX_DEPTH$5; depth += 1) {
24503
+ for (let depth = 0; depth < MAX_DEPTH$6; depth += 1) {
24504
24504
  const parent = parentOf$1(ecs, current);
24505
24505
  if (parent === 0) break;
24506
24506
  const above = ecs.get(parent, Transform) ?? Transform.defaults;
@@ -24841,7 +24841,7 @@ const RETAKE_STREAK = 3;
24841
24841
  /** Frames in a row with no mark in it before a live range is kept as a picture again (spec 23's `CALM_AFTER`). */
24842
24842
  const CALM_FRAMES = 8;
24843
24843
  /** How deep a walk up the tree goes. */
24844
- const MAX_DEPTH$4 = 32;
24844
+ const MAX_DEPTH$5 = 32;
24845
24845
  /** CSS px a view may fall short of the viewport frame and still cover it: the reference frame rounds by a fraction. */
24846
24846
  const COVER_SLACK = 1;
24847
24847
  /** The whole-frame reasons after which a kept picture is taken again: the screen changed whole. */
@@ -25005,7 +25005,7 @@ function sameList$1(left, right) {
25005
25005
  */
25006
25006
  function inRange(set, node, root) {
25007
25007
  let current = node;
25008
- for (let depth = 0; depth < MAX_DEPTH$4 && current !== null && current !== root; depth += 1) {
25008
+ for (let depth = 0; depth < MAX_DEPTH$5 && current !== null && current !== root; depth += 1) {
25009
25009
  if (set.has(current)) return true;
25010
25010
  current = current.parent;
25011
25011
  }
@@ -25527,10 +25527,22 @@ function stopOverlays(state) {
25527
25527
  }
25528
25528
  //#endregion
25529
25529
  //#region src/plugins/renderer/sync/shape-path.ts
25530
+ /**
25531
+ * @file renderer/sync — the path of a `Shape`: a rectangle, a rounded rectangle or a triangle,
25532
+ * its fill, and its stroke, solid or dashed, or an arc, a stroke alone. A dashed stroke walks the
25533
+ * outline (straight edges, and rounded corners sampled as arcs) and strokes one open line per dash.
25534
+ * An arc runs on the circle inscribed in its box, from 12 o'clock clockwise.
25535
+ */
25530
25536
  /** Chords per rounded corner of a dashed outline: under 0.3 u off the arc at a 48 u radius. */
25531
25537
  const CORNER_SAMPLES = 8;
25532
25538
  /** The gap between two dashes, as a fraction of the dash length. */
25533
25539
  const GAP_RATIO = .5;
25540
+ /** Segments of a full ring: a ring of 28 u across strays under 0.03 u off its circle. */
25541
+ const RING_SEGMENTS = 48;
25542
+ /** The fewest segments of an arc that draws: a sliver still bends. */
25543
+ const ARC_MIN_SEGMENTS = 2;
25544
+ /** 12 o'clock, where an arc starts: Pixi's angles run clockwise from 3 o'clock. */
25545
+ const TWELVE = -Math.PI / 2;
25534
25546
  /**
25535
25547
  * The corner radius the rectangle is drawn with: Pixi clamps it to half the shorter side.
25536
25548
  *
@@ -25715,8 +25727,9 @@ function dashesOf(outline, dash) {
25715
25727
  }
25716
25728
  /**
25717
25729
  * Draws the path of a shape and fills it in a colour at the alpha given. At alpha 0 the path stays unfilled, so only
25718
- * a stroke shows. A clip mask is drawn here too, never dashed. A fill-only shape is drawn white and coloured by the
25719
- * object's tint, so a colour change draws no path (spec 26 R2.1).
25730
+ * a stroke shows. A clip mask is drawn here too, never dashed; the path of an arc here is its box, which is what it
25731
+ * clips its children to. A fill-only shape is drawn white and coloured by the object's tint, so a colour change draws
25732
+ * no path (spec 26 R2.1).
25720
25733
  *
25721
25734
  * @param object - The graphics to draw on.
25722
25735
  * @param value - The shape value.
@@ -25726,7 +25739,7 @@ function dashesOf(outline, dash) {
25726
25739
  function drawShapePath(object, value, fillAlpha, color = value.fill) {
25727
25740
  object.clear();
25728
25741
  if (value.kind === "triangle") object.moveTo(0, 0).lineTo(value.w, value.h / 2).lineTo(0, value.h).closePath();
25729
- else if (value.radius > 0) object.roundRect(0, 0, value.w, value.h, value.radius);
25742
+ else if (value.kind === "rect" && value.radius > 0) object.roundRect(0, 0, value.w, value.h, value.radius);
25730
25743
  else object.rect(0, 0, value.w, value.h);
25731
25744
  if (fillAlpha <= 0) return;
25732
25745
  object.fill(fillAlpha < 1 ? {
@@ -25759,6 +25772,102 @@ function strokeShape(object, value) {
25759
25772
  }
25760
25773
  object.stroke(style);
25761
25774
  }
25775
+ /**
25776
+ * The share of the circle an arc sweeps, clamped to 0..1: anything not above 0, `NaN` too, is 0; a whole ring when
25777
+ * the value leaves it out.
25778
+ *
25779
+ * @param value - The shape value.
25780
+ * @returns The share.
25781
+ * @example
25782
+ * ```ts
25783
+ * sweepOf({ ...Shape.defaults, kind: "arc", sweep: 1.4 }); // 1
25784
+ * ```
25785
+ */
25786
+ function sweepOf(value) {
25787
+ const sweep = value.sweep ?? arcDefaults.sweep;
25788
+ if (Number.isNaN(sweep) || sweep <= 0) return 0;
25789
+ return Math.min(sweep, 1);
25790
+ }
25791
+ /**
25792
+ * The radius of the circle an arc runs on: inscribed in the shorter side of its box, inside its stroke.
25793
+ *
25794
+ * @param value - The shape value.
25795
+ * @returns The radius in reference units; 0 or below when the stroke leaves no circle.
25796
+ * @example
25797
+ * ```ts
25798
+ * arcRadiusOf({ ...Shape.defaults, kind: "arc", w: 28, h: 40, strokeWidth: 2 }); // 13
25799
+ * ```
25800
+ */
25801
+ function arcRadiusOf(value) {
25802
+ return (Math.min(value.w, value.h) - value.strokeWidth) / 2;
25803
+ }
25804
+ /**
25805
+ * Tells whether an arc draws nothing: no stroke width, no sweep, or no circle inside its stroke.
25806
+ *
25807
+ * @param value - The shape value, an arc.
25808
+ * @returns True for an arc whose `Graphics` would hold no instruction.
25809
+ * @example
25810
+ * ```ts
25811
+ * isEmptyArc({ ...Shape.defaults, kind: "arc", w: 28, h: 28, strokeWidth: 2, sweep: 0 }); // true
25812
+ * ```
25813
+ */
25814
+ function isEmptyArc(value) {
25815
+ return value.strokeWidth <= 0 || sweepOf(value) <= 0 || arcRadiusOf(value) <= 0;
25816
+ }
25817
+ /**
25818
+ * Strokes an arc: from 12 o'clock clockwise over its share of the circle, in segments as many as its angle needs (48
25819
+ * for a ring, at least 2), ended by its `cap`. A full ring is closed and has no ends. Nothing for an empty arc. Drawn
25820
+ * only when the shape changed: never rebuilt per frame.
25821
+ *
25822
+ * @param object - The graphics, cleared.
25823
+ * @param value - The shape value, an arc.
25824
+ */
25825
+ function strokeArc(object, value) {
25826
+ if (isEmptyArc(value)) return;
25827
+ const sweep = sweepOf(value);
25828
+ const radius = arcRadiusOf(value);
25829
+ const ring = sweep >= 1;
25830
+ const segments = Math.max(ARC_MIN_SEGMENTS, Math.ceil(sweep * RING_SEGMENTS));
25831
+ const step = sweep * 2 * Math.PI / segments;
25832
+ const centreX = value.w / 2;
25833
+ const centreY = value.h / 2;
25834
+ const last = ring ? segments - 1 : segments;
25835
+ object.moveTo(centreX, centreY - radius);
25836
+ for (let index = 1; index <= last; index += 1) {
25837
+ const angle = TWELVE + index * step;
25838
+ object.lineTo(centreX + radius * Math.cos(angle), centreY + radius * Math.sin(angle));
25839
+ }
25840
+ if (ring) {
25841
+ object.closePath();
25842
+ object.stroke({
25843
+ color: value.stroke,
25844
+ width: value.strokeWidth
25845
+ });
25846
+ return;
25847
+ }
25848
+ object.stroke({
25849
+ color: value.stroke,
25850
+ width: value.strokeWidth,
25851
+ cap: value.cap ?? arcDefaults.cap
25852
+ });
25853
+ }
25854
+ /**
25855
+ * Paints a shape on its graphics: an arc as its stroke alone, every other kind as its path, its fill in a colour at its
25856
+ * own fill alpha, and its stroke. The one call that draws a shape's visual.
25857
+ *
25858
+ * @param object - The graphics.
25859
+ * @param value - The shape value.
25860
+ * @param color - The colour of the fill: white for a tinted shape, the value's own otherwise.
25861
+ */
25862
+ function paintShape(object, value, color) {
25863
+ if (value.kind === "arc") {
25864
+ object.clear();
25865
+ strokeArc(object, value);
25866
+ return;
25867
+ }
25868
+ drawShapePath(object, value, value.fillAlpha, color);
25869
+ strokeShape(object, value);
25870
+ }
25762
25871
  //#endregion
25763
25872
  //#region src/plugins/renderer/sync/sizing.ts
25764
25873
  /**
@@ -26738,8 +26847,20 @@ function copyClip(drawn, value) {
26738
26847
  return drawn;
26739
26848
  }
26740
26849
  /**
26850
+ * Tells whether an arc was drawn with this value already: only what its stroke is built from, its
26851
+ * box, its stroke, its share and its ends. A new fill, fill alpha, radius or dash builds nothing.
26852
+ *
26853
+ * @param drawn - What the graphics was last drawn with, an arc too.
26854
+ * @param value - The arc now.
26855
+ * @returns True when nothing has to be drawn.
26856
+ */
26857
+ function sameArc(drawn, value) {
26858
+ return drawn.w === value.w && drawn.h === value.h && drawn.stroke === value.stroke && drawn.strokeWidth === value.strokeWidth && drawn.sweep === value.sweep && drawn.cap === value.cap;
26859
+ }
26860
+ /**
26741
26861
  * Tells whether a shape's graphics was drawn with this value already: the path, the fill and the
26742
- * stroke. Its alpha, its clip and the fill of a tinted shape (its tint) are written apart.
26862
+ * stroke; for an arc only its stroke. Its alpha, its clip and the fill of a tinted shape (its tint)
26863
+ * are written apart.
26743
26864
  *
26744
26865
  * @param drawn - What the graphics was last drawn with.
26745
26866
  * @param value - The shape value now.
@@ -26747,7 +26868,9 @@ function copyClip(drawn, value) {
26747
26868
  * @returns True when nothing has to be drawn.
26748
26869
  */
26749
26870
  function sameShape$1(drawn, value, tinted) {
26750
- return drawn !== void 0 && drawn.tinted === tinted && (tinted || drawn.fill === value.fill) && drawn.kind === value.kind && drawn.w === value.w && drawn.h === value.h && drawn.fillAlpha === value.fillAlpha && drawn.radius === value.radius && drawn.stroke === value.stroke && drawn.strokeWidth === value.strokeWidth && drawn.dash === value.dash;
26871
+ if (drawn === void 0 || drawn.tinted !== tinted || drawn.kind !== value.kind) return false;
26872
+ if (value.kind === "arc") return sameArc(drawn, value);
26873
+ return (tinted || drawn.fill === value.fill) && drawn.w === value.w && drawn.h === value.h && drawn.fillAlpha === value.fillAlpha && drawn.radius === value.radius && drawn.stroke === value.stroke && drawn.strokeWidth === value.strokeWidth && drawn.dash === value.dash && drawn.sweep === value.sweep && drawn.cap === value.cap;
26751
26874
  }
26752
26875
  /**
26753
26876
  * Keeps what a shape's graphics was drawn with, in the object it had, or in a new one the first
@@ -26760,7 +26883,7 @@ function sameShape$1(drawn, value, tinted) {
26760
26883
  */
26761
26884
  function copyShape(drawn, value, tinted) {
26762
26885
  if (drawn === void 0) {
26763
- const { kind, w, h, fill, fillAlpha, radius, stroke, strokeWidth, dash } = value;
26886
+ const { kind, w, h, fill, fillAlpha, radius, stroke, strokeWidth, dash, sweep, cap } = value;
26764
26887
  return {
26765
26888
  tinted,
26766
26889
  kind,
@@ -26771,7 +26894,9 @@ function copyShape(drawn, value, tinted) {
26771
26894
  radius,
26772
26895
  stroke,
26773
26896
  strokeWidth,
26774
- dash
26897
+ dash,
26898
+ sweep,
26899
+ cap
26775
26900
  };
26776
26901
  }
26777
26902
  const copy = drawn;
@@ -26785,10 +26910,13 @@ function copyShape(drawn, value, tinted) {
26785
26910
  copy.stroke = value.stroke;
26786
26911
  copy.strokeWidth = value.strokeWidth;
26787
26912
  copy.dash = value.dash;
26913
+ copy.sweep = value.sweep;
26914
+ copy.cap = value.cap;
26788
26915
  return copy;
26789
26916
  }
26790
26917
  /**
26791
- * Tells whether a shape draws nothing: no stroke, and no fill alpha or no area. Pure.
26918
+ * Tells whether a shape draws nothing: no stroke, and no fill alpha or no area; an arc with no stroke, no sweep or no
26919
+ * circle inside its stroke. Pure.
26792
26920
  *
26793
26921
  * @param value - The shape value.
26794
26922
  * @returns True for a shape whose `Graphics` would hold no instruction.
@@ -26798,6 +26926,7 @@ function copyShape(drawn, value, tinted) {
26798
26926
  * ```
26799
26927
  */
26800
26928
  function isEmptyShape(value) {
26929
+ if (value.kind === "arc") return isEmptyArc(value);
26801
26930
  return value.strokeWidth <= 0 && (value.fillAlpha <= 0 || value.w <= 0 || value.h <= 0);
26802
26931
  }
26803
26932
  /**
@@ -26815,8 +26944,7 @@ function drawShape(sctx, entity, view, object, value) {
26815
26944
  const tinted = value.strokeWidth <= 0;
26816
26945
  if (!sameShape$1(view.drawn, value, tinted)) {
26817
26946
  markView(sctx, entity);
26818
- drawShapePath(object, value, value.fillAlpha, tinted ? WHITE$1 : value.fill);
26819
- strokeShape(object, value);
26947
+ paintShape(object, value, tinted ? WHITE$1 : value.fill);
26820
26948
  sctx.ctx.state.sync.redraws += 1;
26821
26949
  view.drawn = copyShape(view.drawn, value, tinted);
26822
26950
  }
@@ -27963,7 +28091,7 @@ function installPassCounting(pixi, sinks) {
27963
28091
  //#endregion
27964
28092
  //#region src/plugins/renderer/sync/hit-test.ts
27965
28093
  /** How deep a parent chain or a container tree is followed. */
27966
- const MAX_DEPTH$3 = 32;
28094
+ const MAX_DEPTH$4 = 32;
27967
28095
  /** Below this effective alpha a view is treated as not there. */
27968
28096
  const MIN_ALPHA = .01;
27969
28097
  /**
@@ -28045,7 +28173,7 @@ function clipBoxOf(sctx, entity) {
28045
28173
  function clippedOut(sctx, entity, x, y) {
28046
28174
  const ecs = sctx.ctx.deps.world.ecs;
28047
28175
  let current = parentOf$1(ecs, entity);
28048
- for (let depth = 0; depth < MAX_DEPTH$3 && current !== 0; depth += 1) {
28176
+ for (let depth = 0; depth < MAX_DEPTH$4 && current !== 0; depth += 1) {
28049
28177
  const box = clipBoxOf(sctx, current);
28050
28178
  if (box !== void 0 && !inBox(box, localPoint(sctx, current, x, y))) return true;
28051
28179
  current = parentOf$1(ecs, current);
@@ -28064,7 +28192,7 @@ function clippedOut(sctx, entity, x, y) {
28064
28192
  function effectiveAlpha(object, root, off) {
28065
28193
  let alpha = 1;
28066
28194
  let current = object;
28067
- for (let depth = 0; depth < MAX_DEPTH$3 && current !== void 0; depth += 1) {
28195
+ for (let depth = 0; depth < MAX_DEPTH$4 && current !== void 0; depth += 1) {
28068
28196
  if (current === root) return alpha;
28069
28197
  if (!current.visible || off?.has(current) === true) return 0;
28070
28198
  alpha *= current.alpha;
@@ -28094,7 +28222,7 @@ function isShown$1(object, root, off) {
28094
28222
  * @returns The same array.
28095
28223
  */
28096
28224
  function topFirst(container, out, depth = 0) {
28097
- if (depth >= MAX_DEPTH$3) return out;
28225
+ if (depth >= MAX_DEPTH$4) return out;
28098
28226
  const ordered = container.children.toSorted((left, right) => left.zIndex - right.zIndex);
28099
28227
  for (let index = ordered.length - 1; index >= 0; index -= 1) {
28100
28228
  const child = ordered[index];
@@ -28183,7 +28311,7 @@ function hitAll(sctx, x, y) {
28183
28311
  * hit test walks, so a box the doors report is the box a click reaches.
28184
28312
  */
28185
28313
  /** How deep a parent chain is followed; deeper is treated as a loop. */
28186
- const MAX_DEPTH$2 = 32;
28314
+ const MAX_DEPTH$3 = 32;
28187
28315
  /**
28188
28316
  * The four corners of a box, clockwise from the top-left one.
28189
28317
  *
@@ -28277,7 +28405,7 @@ function boundsOf(sctx, entity) {
28277
28405
  function layerOf(sctx, entity) {
28278
28406
  const ecs = sctx.ctx.deps.world.ecs;
28279
28407
  let top = entity;
28280
- for (let depth = 0; depth < MAX_DEPTH$2; depth += 1) {
28408
+ for (let depth = 0; depth < MAX_DEPTH$3; depth += 1) {
28281
28409
  const parent = parentOf$1(ecs, top);
28282
28410
  if (parent === 0) break;
28283
28411
  top = parent;
@@ -28692,7 +28820,7 @@ function stepParts(sctx) {
28692
28820
  //#endregion
28693
28821
  //#region src/plugins/renderer/sync/overlay.ts
28694
28822
  /** How deep a `Parent` chain or a walk up the tree goes. */
28695
- const MAX_DEPTH$1 = 32;
28823
+ const MAX_DEPTH$2 = 32;
28696
28824
  /** The owner `ui` gives the entities it spawns: the only overlays honoured (O4.8). */
28697
28825
  const UI_OWNER_NAME = "ui";
28698
28826
  /**
@@ -28761,7 +28889,7 @@ function clampInto(sctx, entity, into, raw) {
28761
28889
  function rootOf(sctx, entity) {
28762
28890
  const ecs = sctx.ctx.deps.world.ecs;
28763
28891
  let current = entity;
28764
- for (let depth = 0; depth < MAX_DEPTH$1; depth += 1) {
28892
+ for (let depth = 0; depth < MAX_DEPTH$2; depth += 1) {
28765
28893
  const parent = parentOf$1(ecs, current);
28766
28894
  if (parent === 0) return current;
28767
28895
  current = parent;
@@ -28974,7 +29102,7 @@ function drawnAlpha(sctx, record) {
28974
29102
  const alphaFilter = sctx.deps.host.pixi()?.AlphaFilter;
28975
29103
  let alpha = 1;
28976
29104
  let node = view.wrapper ?? view.object;
28977
- for (let depth = 0; depth < MAX_DEPTH$1 && node !== null; depth += 1) {
29105
+ for (let depth = 0; depth < MAX_DEPTH$2 && node !== null; depth += 1) {
28978
29106
  if (node === root) return alpha;
28979
29107
  if (!node.visible) return 0;
28980
29108
  const owner = state.viewOfTop.get(node);
@@ -36162,7 +36290,7 @@ async function settledRecord(state, bundle) {
36162
36290
  * @param size - The size of the decoded image.
36163
36291
  * @param mips - True for a texture uploaded with its full mip chain.
36164
36292
  */
36165
- function resize(target, size, mips) {
36293
+ function resize$1(target, size, mips) {
36166
36294
  if (target === void 0 || size === void 0) return;
36167
36295
  target.width = size.width;
36168
36296
  target.height = size.height;
@@ -36184,7 +36312,7 @@ function storeFont(io, record, owner, font, sizes) {
36184
36312
  const old = record.fonts.get(file.key);
36185
36313
  record.fonts.set(file.key, font);
36186
36314
  for (const page of old?.pages ?? []) io.destroyTexture(page);
36187
- for (const [at, texture] of font.pages.entries()) resize(pages[at], sizes.get(texture), false);
36315
+ for (const [at, texture] of font.pages.entries()) resize$1(pages[at], sizes.get(texture), false);
36188
36316
  file.mb = sumMb(pages);
36189
36317
  }
36190
36318
  /**
@@ -36209,7 +36337,7 @@ function store(io, record, fresh, sizes) {
36209
36337
  const old = record.textures.get(file.key);
36210
36338
  record.textures.set(file.key, fresh.texture);
36211
36339
  if (old !== void 0) io.destroyTexture(old);
36212
- resize(file, sizes.get(fresh.texture), file.mips === true);
36340
+ resize$1(file, sizes.get(fresh.texture), file.mips === true);
36213
36341
  }
36214
36342
  /**
36215
36343
  * Makes the numbers and the picture follow the files that were stored: the megabytes of the
@@ -41376,7 +41504,7 @@ function finish(ectx, run, definition, passed) {
41376
41504
  * @param definition - The definition.
41377
41505
  * @param messages - What the compiler said.
41378
41506
  */
41379
- function settle(ectx, run, definition, messages) {
41507
+ function settle$1(ectx, run, definition, messages) {
41380
41508
  const { checks } = ectx.state;
41381
41509
  if (checks.run !== run || checks.status.get(definition) !== "pending") return;
41382
41510
  const errors = messages.filter((message) => message.type === "error");
@@ -41402,7 +41530,7 @@ function checkWgsl(ectx, run, definition) {
41402
41530
  finish(ectx, run, definition, true);
41403
41531
  return;
41404
41532
  }
41405
- device.createShaderModule({ code: definition.source }).getCompilationInfo().then((info) => settle(ectx, run, definition, info.messages), () => settle(ectx, run, definition, []));
41533
+ device.createShaderModule({ code: definition.source }).getCompilationInfo().then((info) => settle$1(ectx, run, definition, info.messages), () => settle$1(ectx, run, definition, []));
41406
41534
  }
41407
41535
  /**
41408
41536
  * Reads the errors of a WebGL info log, one per `ERROR: 0:<line>: <message>` line; a summary line
@@ -43032,7 +43160,7 @@ const EFFECTS_OWNER = Object.freeze({
43032
43160
  name: "effects"
43033
43161
  });
43034
43162
  /** How deep a parent chain is followed, as `rootPoseOf` does; deeper is treated as a loop. */
43035
- const MAX_DEPTH = 32;
43163
+ const MAX_DEPTH$1 = 32;
43036
43164
  /**
43037
43165
  * The one dynamic set of an emitter container (P9): positions, frames, rotations and colours
43038
43166
  * upload every frame; `uvs` stays static, because a frame is set at birth and the same `update()`
@@ -43075,7 +43203,7 @@ function bornFields(size) {
43075
43203
  */
43076
43204
  function topOf(ecs, host) {
43077
43205
  let top = host;
43078
- for (let depth = 0; depth < MAX_DEPTH; depth += 1) {
43206
+ for (let depth = 0; depth < MAX_DEPTH$1; depth += 1) {
43079
43207
  const parent = parentOf$1(ecs, top);
43080
43208
  if (parent === 0) break;
43081
43209
  top = parent;
@@ -45182,6 +45310,10 @@ function submit(ctx, entity, answer) {
45182
45310
  }
45183
45311
  //#endregion
45184
45312
  //#region src/plugins/input/components.ts
45313
+ const scrollableDefaults = {
45314
+ axis: "y",
45315
+ moving: false
45316
+ };
45185
45317
  /**
45186
45318
  * Fresh defaults for a component that names an intent. A function, so each component type gets
45187
45319
  * its own payload object instead of sharing one.
@@ -45281,6 +45413,13 @@ const Pressed = /*#__PURE__*/ tag("Pressed");
45281
45413
  */
45282
45414
  const PointerOver = /*#__PURE__*/ tag("PointerOver");
45283
45415
  /**
45416
+ * On every ui scroll container, internal: ui writes it, input reads it, a game never sees it. A
45417
+ * press that leaves the tap slop along `axis` goes from the pressed view to the innermost scroll
45418
+ * of that axis the view hangs in by its `Parent` chain; a finger that lands on a view inside a
45419
+ * scroll that is `moving` stops it and answers nothing.
45420
+ */
45421
+ const Scrollable = /*#__PURE__*/ component("Scrollable", scrollableDefaults);
45422
+ /**
45284
45423
  * Where the one pointer is, in reference units. Written once per frame by the frame step. `id`
45285
45424
  * names the pointer it follows, `-1` at rest.
45286
45425
  */
@@ -45290,8 +45429,12 @@ const Pointer = /*#__PURE__*/ resource("Pointer", pointerDefaults);
45290
45429
  /**
45291
45430
  * @file input plugin — who is under the finger. Every lookup goes through `renderer.sync.hitTest`,
45292
45431
  * which answers `undefined` without a DOM, and every accept function refuses a view that is
45293
- * already playing its exit. A trace takes a cell only inside a circle smaller than its box.
45432
+ * already playing its exit. A trace takes a cell only inside a circle smaller than its box. The
45433
+ * scroll a press belongs to is found up the pressed view's own `Parent` chain, never under the
45434
+ * point: a view drawn over a scroll from another root keeps its press.
45294
45435
  */
45436
+ /** How deep a `Parent` chain is followed; deeper is treated as a loop. */
45437
+ const MAX_DEPTH = 32;
45295
45438
  /**
45296
45439
  * Resolves what `app.input.*` was pointed at. A `{ projection, key }` goes through
45297
45440
  * `world.projection.entityOf`, which answers for live views only, so a view in the despawn queue
@@ -45374,6 +45517,44 @@ function findPressed(ctx, x, y) {
45374
45517
  return ctx.deps.renderer.sync.hitTest(x, y, (entity) => acceptPress(ctx, entity));
45375
45518
  }
45376
45519
  /**
45520
+ * The innermost scroll a view sits in that moves now: the view itself or the nearest ancestor of
45521
+ * its `Parent` chain that carries a `Scrollable` with `moving` true and is not on its way out. A
45522
+ * finger that lands on the view stops that scroll. A view of another root drawn over the scroll,
45523
+ * a HUD button or a popup, hangs in no chain of it, so it keeps its press. Reads only, allocates
45524
+ * nothing.
45525
+ *
45526
+ * @param ctx - Domain context of the input plugin.
45527
+ * @param entity - The view the hit test found under the finger.
45528
+ * @returns The scroll, or `undefined` when no scroll the view sits in moves.
45529
+ */
45530
+ function movingScrollOf(ctx, entity) {
45531
+ const { ecs } = ctx.deps.world;
45532
+ let current = entity;
45533
+ for (let depth = 0; depth < MAX_DEPTH && current !== 0; depth += 1) {
45534
+ if (ecs.get(current, Scrollable)?.moving === true && !ecs.has(current, Exiting)) return current;
45535
+ current = parentOf$1(ecs, current);
45536
+ }
45537
+ }
45538
+ /**
45539
+ * The innermost scroll of an axis a pressed view sits in: the view itself or the nearest ancestor
45540
+ * of its `Parent` chain that carries a `Scrollable` of that axis and is not on its way out. A list
45541
+ * under a modal is in no chain of the modal's button, so a drift on that button never pans it.
45542
+ * Reads only, allocates nothing.
45543
+ *
45544
+ * @param ctx - Domain context of the input plugin.
45545
+ * @param entity - The pressed view.
45546
+ * @param axis - The dominant axis of the pan.
45547
+ * @returns The scroll, or `undefined` when the view sits in no scroll of that axis.
45548
+ */
45549
+ function scrollOnAxisOf(ctx, entity, axis) {
45550
+ const { ecs } = ctx.deps.world;
45551
+ let current = entity;
45552
+ for (let depth = 0; depth < MAX_DEPTH && current !== 0; depth += 1) {
45553
+ if (ecs.get(current, Scrollable)?.axis === axis && !ecs.has(current, Exiting)) return current;
45554
+ current = parentOf$1(ecs, current);
45555
+ }
45556
+ }
45557
+ /**
45377
45558
  * The topmost drop target at a point.
45378
45559
  *
45379
45560
  * @param ctx - Domain context of the input plugin.
@@ -46397,6 +46578,20 @@ function direction(from, to) {
46397
46578
  return dy >= 0 ? "down" : "up";
46398
46579
  }
46399
46580
  /**
46581
+ * The axis a pan moves on: the dominant one, horizontal on an exact diagonal.
46582
+ *
46583
+ * @param from - Where the press started.
46584
+ * @param to - Where the finger is.
46585
+ * @returns The axis of the larger move.
46586
+ * @example
46587
+ * ```ts
46588
+ * axisOf({ x: 0, y: 0 }, { x: 20, y: -5 }); // "x"
46589
+ * ```
46590
+ */
46591
+ function axisOf$1(from, to) {
46592
+ return Math.abs(to.x - from.x) >= Math.abs(to.y - from.y) ? "x" : "y";
46593
+ }
46594
+ /**
46400
46595
  * Takes or releases the pointer capture of the canvas. A pointer that is already gone — a down
46401
46596
  * and an up inside one frame — cannot be captured; the browser throws and the gesture goes on.
46402
46597
  *
@@ -46415,7 +46610,7 @@ function capture(ctx, on) {
46415
46610
  }
46416
46611
  /**
46417
46612
  * Ends the gesture: the press tag goes, the capture is released and the machine forgets the
46418
- * pointer, whether it moved, the entity and its key.
46613
+ * pointer, whether it moved or caught a scroll, the entity and its key.
46419
46614
  *
46420
46615
  * @param ctx - Domain context of the input plugin.
46421
46616
  */
@@ -46426,16 +46621,30 @@ function toIdle(ctx) {
46426
46621
  ctx.state.phase = "idle";
46427
46622
  ctx.state.pointerId = void 0;
46428
46623
  ctx.state.moved = false;
46624
+ ctx.state.caught = false;
46429
46625
  ctx.state.entity = void 0;
46430
46626
  ctx.state.key = void 0;
46431
46627
  ctx.state.pressedMs = 0;
46432
46628
  }
46433
46629
  /**
46630
+ * Makes a view the pressed one: it is remembered with its projection key and tagged `Pressed`.
46631
+ *
46632
+ * @param ctx - Domain context of the input plugin.
46633
+ * @param entity - The view that takes the press.
46634
+ */
46635
+ function press(ctx, entity) {
46636
+ ctx.state.entity = entity;
46637
+ ctx.state.key = ctx.deps.world.projection.keyOf(entity);
46638
+ ctx.deps.world.ecs.tag(entity, Pressed);
46639
+ }
46640
+ /**
46434
46641
  * Takes the pointer and looks for the view under it. The pointer is taken even where no view is.
46435
46642
  * While the finger rests (`rests`) the next finger that goes down takes the pointer over and this
46436
46643
  * press is cancelled; once it starts a gesture — a drag, a move past the tap slop, a long press, a
46437
- * trace — a second finger stays ignored. A view that carries `Traceable` starts a trace instead of
46438
- * a press: it is not tagged `Pressed`.
46644
+ * trace — a second finger stays ignored. When the view under the finger sits in a moving scroll,
46645
+ * that scroll takes the press: the finger stops it, and the press is caught. A view drawn over a
46646
+ * moving scroll from another root, a HUD or a popup button, keeps its press. A view that carries
46647
+ * `Traceable` starts a trace instead of a press: it is not tagged `Pressed`.
46439
46648
  *
46440
46649
  * @param ctx - Domain context of the input plugin.
46441
46650
  * @param pointer - The `Pointer` resource of this frame.
@@ -46443,9 +46652,10 @@ function toIdle(ctx) {
46443
46652
  * @param sample - The raw sample.
46444
46653
  */
46445
46654
  function onDown(ctx, pointer, point, sample) {
46446
- const { ecs, projection } = ctx.deps.world;
46655
+ const { ecs } = ctx.deps.world;
46447
46656
  ctx.state.pointerId = sample.pointerId;
46448
46657
  ctx.state.moved = false;
46658
+ ctx.state.caught = false;
46449
46659
  ctx.state.start = {
46450
46660
  x: point.x,
46451
46661
  y: point.y
@@ -46457,16 +46667,42 @@ function onDown(ctx, pointer, point, sample) {
46457
46667
  capture(ctx, true);
46458
46668
  const entity = findPressed(ctx, point.x, point.y);
46459
46669
  if (entity === void 0) return;
46670
+ const scroll = movingScrollOf(ctx, entity);
46671
+ if (scroll !== void 0) {
46672
+ ctx.state.caught = true;
46673
+ press(ctx, scroll);
46674
+ return;
46675
+ }
46460
46676
  if (ecs.has(entity, Traceable)) {
46461
46677
  beginTrace(ctx, entity, point);
46462
46678
  return;
46463
46679
  }
46464
- ctx.state.entity = entity;
46465
- ctx.state.key = projection.keyOf(entity);
46466
- ecs.tag(entity, Pressed);
46680
+ press(ctx, entity);
46467
46681
  if (ecs.has(entity, Draggable) && ecs.has(entity, Swipeable)) ctx.log.warn("input: Draggable wins over Swipeable", { view: ctx.state.key });
46468
46682
  }
46469
46683
  /**
46684
+ * The press just left the tap slop: it goes to the scroll it pans, the innermost `Scrollable` of
46685
+ * the pressed view's own `Parent` chain (the view itself included) that moves on the dominant axis
46686
+ * of the move. The view loses `Pressed`, and the press is marked caught: it is a scroll gesture
46687
+ * now, so its release answers nothing, also when the finger came back inside the slop. A
46688
+ * draggable or swipeable view keeps its gesture, a caught press keeps its scroll, and a view that
46689
+ * sits in no scroll of that axis keeps its press, also a popup button drawn over a list.
46690
+ *
46691
+ * @param ctx - Domain context of the input plugin.
46692
+ * @param point - Where the finger is, in reference units.
46693
+ */
46694
+ function handOff(ctx, point) {
46695
+ const { caught, entity, start } = ctx.state;
46696
+ const { ecs } = ctx.deps.world;
46697
+ if (entity === void 0 || caught) return;
46698
+ if (ecs.has(entity, Draggable) || ecs.has(entity, Swipeable)) return;
46699
+ const scroll = scrollOnAxisOf(ctx, entity, axisOf$1(start, point));
46700
+ if (scroll === void 0 || scroll === entity) return;
46701
+ ecs.untag(entity, Pressed);
46702
+ ctx.state.caught = true;
46703
+ press(ctx, scroll);
46704
+ }
46705
+ /**
46470
46706
  * Turns a press on a draggable view into a drag once the finger left the start by more than
46471
46707
  * `dragStartPx`. A draggable view with no projection key cannot be carried: `mute`, `lift` and
46472
46708
  * `settle` belong to a projection, so the gesture is reported and given up.
@@ -46492,13 +46728,15 @@ function onMove(ctx, point) {
46492
46728
  * Answers the release of a press: a tap when the finger stayed inside the slop, a swipe when it
46493
46729
  * went far enough and fast enough. Neither: nothing happened, and nothing has to be undone. Every
46494
46730
  * release inside the slop runs the `onTap` listeners first, so a view that carries only
46495
- * `Touchable` is heard although it answers nothing.
46731
+ * `Touchable` is heard although it answers nothing. A caught press belongs to a scroll — it
46732
+ * stopped a moving one or was handed to one — and answers nothing and runs no listener.
46496
46733
  *
46497
46734
  * @param ctx - Domain context of the input plugin.
46498
46735
  * @param entity - The pressed view.
46499
46736
  * @param point - Where the finger let go, in reference units.
46500
46737
  */
46501
46738
  function answerRelease(ctx, entity, point) {
46739
+ if (ctx.state.caught) return;
46502
46740
  const { ecs } = ctx.deps.world;
46503
46741
  const travelled = distance(ctx.state.start, point);
46504
46742
  if (travelled <= ctx.config.tapSlopPx) {
@@ -46565,13 +46803,24 @@ function rests(ctx) {
46565
46803
  return ctx.state.phase === "pressed" && !ctx.state.moved;
46566
46804
  }
46567
46805
  /**
46806
+ * Tells whether a sample takes the press past the tap slop for the first time: the press has not
46807
+ * moved yet and the finger is farther than `tapSlopPx` from the start.
46808
+ *
46809
+ * @param ctx - Domain context of the input plugin.
46810
+ * @param point - Where the finger is, in reference units.
46811
+ * @returns True in the one sample that turns the press into a moved one.
46812
+ */
46813
+ function leavesSlop(ctx, point) {
46814
+ return ctx.state.phase === "pressed" && !ctx.state.moved && distance(ctx.state.start, point) > ctx.config.tapSlopPx;
46815
+ }
46816
+ /**
46568
46817
  * Feeds one raw sample to the machine. A leave does nothing here. A sample of another pointer is
46569
46818
  * dropped while a pointer is followed: the second finger is ignored. One exception: a press while
46570
46819
  * the followed pointer rests takes the pointer over and cancels the resting press, so a resting
46571
46820
  * thumb or palm never blocks the finger that plays. A move of an idle mouse or pen hit-tests
46572
46821
  * nothing: its point is returned, so the drain moves the hover once per frame. A move of a press
46573
- * past the tap slop marks the press as moved for good. A lost capture ends the gesture like a
46574
- * cancel.
46822
+ * past the tap slop marks the press as moved for good and, in that one sample, hands it to the
46823
+ * scroll it pans. A lost capture ends the gesture like a cancel.
46575
46824
  *
46576
46825
  * @param ctx - Domain context of the input plugin.
46577
46826
  * @param pointer - The `Pointer` resource of this frame.
@@ -46594,7 +46843,10 @@ function handleSample(ctx, pointer, sample) {
46594
46843
  break;
46595
46844
  case "move":
46596
46845
  if (isHover(ctx, pointer, sample)) return point;
46597
- if (ctx.state.phase === "pressed" && !ctx.state.moved && distance(ctx.state.start, point) > ctx.config.tapSlopPx) ctx.state.moved = true;
46846
+ if (leavesSlop(ctx, point)) {
46847
+ ctx.state.moved = true;
46848
+ handOff(ctx, point);
46849
+ }
46598
46850
  if (ctx.state.phase === "tracing") extendTrace(ctx, point);
46599
46851
  else onMove(ctx, point);
46600
46852
  break;
@@ -46767,6 +47019,7 @@ function createInputState(_ctx) {
46767
47019
  phase: "idle",
46768
47020
  pointerId: void 0,
46769
47021
  moved: false,
47022
+ caught: false,
46770
47023
  entity: void 0,
46771
47024
  key: void 0,
46772
47025
  start: {
@@ -50176,6 +50429,31 @@ const Scroll = /*#__PURE__*/ component("Scroll", {
50176
50429
  min: 0
50177
50430
  });
50178
50431
  /**
50432
+ * On every scroll container: where it stands and whether it moves, for a game system and the
50433
+ * read door. `ui` writes it in a frame where the offset or `max` changed, and once more with
50434
+ * `moving: false` when it stops; a game reads it and never writes it.
50435
+ *
50436
+ * @example
50437
+ * ```ts
50438
+ * // A board system dims the events of the order strip while it rests on the orders.
50439
+ * export const stripMist = system({
50440
+ * name: "board.stripMist",
50441
+ * phase: "animate",
50442
+ * query: [ScrollState, OrderStrip],
50443
+ * run: entities => {
50444
+ * for (const [entity, scroll] of entities) {
50445
+ * // scroll: { offset: 318, max: 390, moving: false }
50446
+ * }
50447
+ * }
50448
+ * });
50449
+ * ```
50450
+ */
50451
+ const ScrollState = /*#__PURE__*/ component("ScrollState", {
50452
+ offset: 0,
50453
+ max: 0,
50454
+ moving: false
50455
+ });
50456
+ /**
50179
50457
  * On the root entity of a popup kept beneath another popup (`popup(..., { over })`). Internal to
50180
50458
  * `ui`: not exported from the package root. Every element of that root resolves `is.covered`.
50181
50459
  */
@@ -51908,8 +52186,9 @@ function windowFormOf(node) {
51908
52186
  }
51909
52187
  /**
51910
52188
  * Refuses the props of a scroll its windowed form cannot take: some but not all of `rows`,
51911
- * `rowHeight` and `row`, or a row height that is not above 0. Called before anything is spawned or
51912
- * patched, so a refused scroll leaves no half-made element.
52189
+ * `rowHeight` and `row`, a row height that is not above 0, or `axis: "x"`, since the rows of a
52190
+ * window stand one below the other. Called before anything is spawned or patched, so a refused
52191
+ * scroll leaves no half-made element.
51913
52192
  *
51914
52193
  * @param node - The node being placed.
51915
52194
  * @throws {Error} For a windowed scroll whose props do not hold.
@@ -51919,7 +52198,50 @@ function windowFormOf(node) {
51919
52198
  * ```
51920
52199
  */
51921
52200
  function checkWindow(node) {
51922
- if (node.type === "scroll") windowFormOf(node);
52201
+ if (node.type !== "scroll") return;
52202
+ if (windowFormOf(node) !== void 0 && axisOf(node) === "x") throw new Error("[game] A windowed scroll moves on y only.\n Drop axis \"x\" or use the children form.");
52203
+ }
52204
+ /**
52205
+ * The axis a scroll moves on: `"x"` when its props say so, else `"y"`.
52206
+ *
52207
+ * @param node - The node of a scroll.
52208
+ * @returns The axis.
52209
+ * @example
52210
+ * ```ts
52211
+ * axisOf({ type: "scroll", props: { axis: "x" }, children: [] }); // "x"
52212
+ * ```
52213
+ */
52214
+ function axisOf(node) {
52215
+ return node.props.axis === "x" ? "x" : "y";
52216
+ }
52217
+ /**
52218
+ * The motion record a scroll spawns with: its axis, still at the start, not placed yet. The
52219
+ * first solve puts it at its `offset` prop.
52220
+ *
52221
+ * @param node - The node of the scroll that enters.
52222
+ * @returns A new record.
52223
+ * @example
52224
+ * ```ts
52225
+ * scrollMotionOf({ type: "scroll", props: { axis: "x" }, children: [] }).mode; // "still"
52226
+ * ```
52227
+ */
52228
+ function scrollMotionOf(node) {
52229
+ return {
52230
+ axis: axisOf(node),
52231
+ mode: "still",
52232
+ offset: 0,
52233
+ max: 0,
52234
+ velocity: 0,
52235
+ from: 0,
52236
+ to: 0,
52237
+ duration: 0,
52238
+ elapsed: 0,
52239
+ pointer: 0,
52240
+ start: 0,
52241
+ seen: 0,
52242
+ placed: false,
52243
+ atProp: false
52244
+ };
51923
52245
  }
51924
52246
  /**
51925
52247
  * Builds a spacer: an empty node of a fixed height that never shrinks.
@@ -54345,15 +54667,23 @@ const VISUALS = [
54345
54667
  ];
54346
54668
  /**
54347
54669
  * The input components of an element and the Escape marker; a disabled or covered button drops
54348
- * the answering one and the marker.
54670
+ * the answering one and the marker. A scroll's three hold its runtime state.
54349
54671
  */
54350
54672
  const INPUTS = [
54351
54673
  Tappable,
54352
54674
  Touchable,
54353
54675
  LocalWrite,
54354
54676
  Scroll,
54677
+ Scrollable,
54678
+ ScrollState,
54355
54679
  Escapable
54356
54680
  ];
54681
+ /** The components of a scroll that hold where it stands: added once, never patched by a render. */
54682
+ const SCROLL_STATE = namesOf$1([
54683
+ Scroll,
54684
+ Scrollable,
54685
+ ScrollState
54686
+ ]);
54357
54687
  /**
54358
54688
  * One list of the visual and the input types. Its own function, because lint rule L5 refuses a
54359
54689
  * collection built in a module-scope declaration.
@@ -54368,6 +54698,14 @@ function concatTypes(...lists) {
54368
54698
  const LIVE_TYPES = concatTypes(VISUALS, INPUTS);
54369
54699
  /** The owned fields of a type that gives none to a plugin: one list, never a new one. */
54370
54700
  const NO_OWNED = [];
54701
+ /** The time of a reconcile a test steps by hand: no ms pass, so no scroll moves. */
54702
+ const NO_TIME = Object.freeze({
54703
+ delta: 0,
54704
+ elapsed: 0,
54705
+ scale: 1,
54706
+ frame: 0,
54707
+ idle: false
54708
+ });
54371
54709
  /**
54372
54710
  * Tells whether a list of component values holds one of a type.
54373
54711
  *
@@ -54408,6 +54746,11 @@ const ELEMENT_OWNED = namesOf$1([
54408
54746
  ]);
54409
54747
  /** The style of a scroll's content: one object, so a render never reads as a new style. */
54410
54748
  const CONTENT_STYLE = Object.freeze({ origin: "topLeft" });
54749
+ /** The style of an x scroll's content: its children stand side by side. */
54750
+ const CONTENT_ROW_STYLE = Object.freeze({
54751
+ origin: "topLeft",
54752
+ direction: "row"
54753
+ });
54411
54754
  /**
54412
54755
  * The props, besides `style`, `state` and `content`, that a live element is written from: `bind`,
54413
54756
  * `texture`, `name` and `fit` reach its visual, `intent`, `payload`, `local` and `escape` its
@@ -54445,23 +54788,24 @@ function writesSame(previous, next) {
54445
54788
  /**
54446
54789
  * The children one element is diffed against. A scroll container holds exactly one of them: the
54447
54790
  * content the finger moves, which keeps the rects of its own children and carries them along. Its
54448
- * pivot is its top-left corner, so the scroll step writes its offset straight into `y`; it lays
54449
- * its children out as a column. The content of a windowed scroll holds the slots of its window,
54450
- * the two spacers and the rows, instead of the markup's children.
54791
+ * pivot is its top-left corner, so the scroll step writes its offset straight into `x` or `y`; it
54792
+ * lays its children out as a column, or as a row on the `x` axis. The content of a windowed scroll
54793
+ * holds the slots of its window, the two spacers and the rows, instead of the markup's children.
54451
54794
  *
54452
54795
  * @param node - The node the markup wrote.
54453
54796
  * @param slots - The slots of a windowed scroll, top spacer to bottom spacer.
54797
+ * @param axis - The axis of a scroll, the one it spawned with.
54454
54798
  * @returns The children of the element.
54455
54799
  * @example
54456
54800
  * ```ts
54457
54801
  * childrenOf({ type: "scroll", props: {}, children: [] }).length; // 1
54458
54802
  * ```
54459
54803
  */
54460
- function childrenOf(node, slots) {
54804
+ function childrenOf(node, slots, axis = "y") {
54461
54805
  if (node.type !== "scroll") return node.children;
54462
54806
  return [{
54463
54807
  type: CONTENT$1,
54464
- props: { style: CONTENT_STYLE },
54808
+ props: { style: axis === "x" ? CONTENT_ROW_STYLE : CONTENT_STYLE },
54465
54809
  children: slots ?? node.children
54466
54810
  }];
54467
54811
  }
@@ -54540,16 +54884,15 @@ function styleOf(node) {
54540
54884
  return typeof style === "object" && style !== null ? style : void 0;
54541
54885
  }
54542
54886
  /**
54543
- * Refuses the tags that cannot be placed: a text field that names no local field, a horizontal
54544
- * scroll, and a windowed scroll whose props do not hold.
54887
+ * Refuses the tags that cannot be placed: a text field that names no local field, and a windowed
54888
+ * scroll whose props do not hold.
54545
54889
  *
54546
54890
  * @param node - The node being placed.
54547
- * @throws {Error} For an `input` tag without `local`, for a horizontal scroll and for a windowed
54548
- * scroll whose props do not hold.
54891
+ * @throws {Error} For an `input` tag without `local`, and for a windowed scroll whose props do
54892
+ * not hold.
54549
54893
  */
54550
54894
  function checkTag(node) {
54551
54895
  if (node.type === "input" && typeof node.props.local !== "string") throw new Error("[game] An input needs a local field.\n Write <input local=\"name\" />.");
54552
- if (node.type === "scroll" && node.props.axis === "x") throw new Error("[game] Horizontal scroll arrives after V3.\n Use axis \"y\".");
54553
54896
  checkWindow(node);
54554
54897
  }
54555
54898
  /**
@@ -54572,16 +54915,28 @@ function answerOf(node) {
54572
54915
  /**
54573
54916
  * The input components of an element: a button answers the gate, writes local state, or only
54574
54917
  * swallows the tap; a panel swallows the tap too, so nothing under it answers; a text field takes
54575
- * the tap that starts its editing; a scroll container takes the press that moves its content. A button of a covered popup answers nothing,
54576
- * so only the top popup answers the gate. A button with the `escape` prop that answers also
54577
- * carries `Escapable`, the control the Escape key taps.
54918
+ * the tap that starts its editing; a scroll container takes the press that moves its content and
54919
+ * carries where it stands, on the axis it spawned with. A button of a covered popup answers
54920
+ * nothing, so only the top popup answers the gate. A button with the `escape` prop that answers
54921
+ * also carries `Escapable`, the control the Escape key taps.
54578
54922
  *
54579
54923
  * @param element - The element to make touchable.
54580
54924
  * @returns The component values.
54581
54925
  */
54582
54926
  function inputOf(element) {
54583
54927
  const { type, node, is } = element;
54584
- if (type === "scroll") return [Touchable(), Scroll({ axis: "y" })];
54928
+ if (type === "scroll") {
54929
+ const axis = element.scroll?.axis ?? "y";
54930
+ return [
54931
+ Touchable(),
54932
+ Scroll({ axis }),
54933
+ Scrollable({
54934
+ axis,
54935
+ moving: false
54936
+ }),
54937
+ ScrollState()
54938
+ ];
54939
+ }
54585
54940
  if (type === "panel" || type === "input") return [Touchable()];
54586
54941
  if (type !== "button") return [];
54587
54942
  if (is.disabled || is.covered) return [Touchable()];
@@ -54800,6 +55155,7 @@ function createReconciler(ctx, modules, fields) {
54800
55155
  let slotsVersion = -1;
54801
55156
  let slotsRoots = -1;
54802
55157
  let placedUnder;
55158
+ let rendered = 0;
54803
55159
  /**
54804
55160
  * Registers the key of an element under its root, so `entityOf` and `find` answer for it. A key
54805
55161
  * `ui` gave a node of its own, a spacer's, is never registered.
@@ -54896,13 +55252,16 @@ function createReconciler(ctx, modules, fields) {
54896
55252
  trackHost(state, element);
54897
55253
  registerKey(root, element);
54898
55254
  if (element.type === "input") fields.enter(element);
54899
- if (element.type === "scroll") state.scrolls.add(entity);
55255
+ if (element.type === "scroll") {
55256
+ state.scrolls.add(entity);
55257
+ element.scroll = scrollMotionOf(node);
55258
+ }
54900
55259
  modules.layout.attach(element);
54901
55260
  element.basis = {
54902
55261
  epoch: state.epoch,
54903
55262
  rowHeight: range.rowHeightOf(parent, node)
54904
55263
  };
54905
- diffChildren(root, element, childrenOf(node, range.slotsFor(element)), instance);
55264
+ diffChildren(root, element, childrenOf(node, range.slotsFor(element), element.scroll?.axis), instance);
54906
55265
  root.needsSolve = true;
54907
55266
  return entity;
54908
55267
  }
@@ -55039,11 +55398,12 @@ function createReconciler(ctx, modules, fields) {
55039
55398
  /**
55040
55399
  * Writes the visual and input components of a live element again. A visual or an input the
55041
55400
  * element no longer carries is removed, so a variant can trade the rectangle for a nine-slice and
55042
- * back, and a button that became disabled stops answering the gate. `Scroll` is only ever added:
55043
- * its offset belongs to the finger, and a new look or a new rect never resets it. The look
55044
- * written is recorded as the rest of the visual, so the rest the world holds is always the one
55045
- * the element is drawn with: a restore brings it back to exactly that. The extras of the
55046
- * `components` prop are diffed last. Every call is one write pass of `UiCounters`.
55401
+ * back, and a button that became disabled stops answering the gate. `Scroll`, `Scrollable` and
55402
+ * `ScrollState` are only ever added: where a scroll stands belongs to the finger and the scroll
55403
+ * step, and a new look or a new rect never resets it. The look written is recorded as the rest
55404
+ * of the visual, so the rest the world holds is always the one the element is drawn with: a
55405
+ * restore brings it back to exactly that. The extras of the `components` prop are diffed last.
55406
+ * Every call is one write pass of `UiCounters`.
55047
55407
  *
55048
55408
  * @param element - The element that changed.
55049
55409
  */
@@ -55060,7 +55420,8 @@ function createReconciler(ctx, modules, fields) {
55060
55420
  }
55061
55421
  /**
55062
55422
  * Writes one visual or input value onto a live element: added when the entity lacks it, else
55063
- * its fields patched, but never a tag or `Scroll`, and never a field its type gives to a plugin.
55423
+ * its fields patched, but never a tag or the state of a scroll, and never a field its type gives
55424
+ * to a plugin.
55064
55425
  *
55065
55426
  * @param element - The live element.
55066
55427
  * @param value - The value of this render.
@@ -55070,7 +55431,7 @@ function createReconciler(ctx, modules, fields) {
55070
55431
  ecs.add(element.entity, value);
55071
55432
  return;
55072
55433
  }
55073
- if (value.value === true || value.type === Scroll) return;
55434
+ if (value.value === true || SCROLL_STATE.has(value.type.componentName)) return;
55074
55435
  const owned = ecs.typeOf(value.type.componentName)?.owned ?? NO_OWNED;
55075
55436
  ecs.set(element.entity, asHandle(value.type), livePatch(value.value, owned));
55076
55437
  }
@@ -55149,6 +55510,7 @@ function createReconciler(ctx, modules, fields) {
55149
55510
  */
55150
55511
  function patch(root, element, node, instance) {
55151
55512
  checkWindow(node);
55513
+ warnAxis(element, node);
55152
55514
  const change = compare(root, element, node);
55153
55515
  const motion = node.props.motion;
55154
55516
  const loopChanged = !sameLoop(element.motion?.loop, motion?.loop);
@@ -55169,7 +55531,22 @@ function createReconciler(ctx, modules, fields) {
55169
55531
  rowHeight: change.rowHeight
55170
55532
  };
55171
55533
  }
55172
- diffChildren(root, element, childrenOf(node, range.slotsFor(element)), instance);
55534
+ diffChildren(root, element, childrenOf(node, range.slotsFor(element), element.scroll?.axis), instance);
55535
+ }
55536
+ /**
55537
+ * Warns once when a render gives a live scroll another `axis`: it keeps the one it spawned with.
55538
+ *
55539
+ * @param element - The element being patched.
55540
+ * @param node - The node of this render.
55541
+ */
55542
+ function warnAxis(element, node) {
55543
+ const axis = element.scroll?.axis;
55544
+ if (axis === void 0 || axis === axisOf(node) || element.warned.has("scroll-axis-fixed")) return;
55545
+ element.warned.add("scroll-axis-fixed");
55546
+ ctx.log.warn("ui:scroll-axis-fixed", {
55547
+ key: element.key ?? element.identity,
55548
+ axis
55549
+ });
55173
55550
  }
55174
55551
  /**
55175
55552
  * Counts a patch that brings a loop of another reference that is not really another loop: the
@@ -55826,8 +56203,10 @@ function createReconciler(ctx, modules, fields) {
55826
56203
  * again, the roots a scene placed again, the popups that may leave, the pull of the text being
55827
56204
  * typed, the scroll offsets and the windowed ranges, the bound texts that show a string of
55828
56205
  * another size, the viewport, the dirty roots and the hosted views.
56206
+ *
56207
+ * @param time - The frame's `Time`: the scrolls move by its `delta`.
55829
56208
  */
55830
- function reconcile() {
56209
+ function reconcile(time = NO_TIME) {
55831
56210
  sweep();
55832
56211
  state.reconciles += 1;
55833
56212
  if (state.remount) remountRoots();
@@ -55835,13 +56214,14 @@ function createReconciler(ctx, modules, fields) {
55835
56214
  settlePopups(ctx, state, unmountRoot);
55836
56215
  fields.pull();
55837
56216
  const containers = scrollContainers();
55838
- modules.layout.scroll(containers, lookup);
56217
+ modules.layout.scroll(containers, lookup, time);
55839
56218
  range.step(containers);
55840
56219
  markResizedTexts(ctx, state, modules.layout);
55841
56220
  markResizedIcons(ctx, state, modules.layout);
55842
56221
  const viewportChanged = modules.styles.useViewport(ctx.deps.renderer.viewport.size());
55843
56222
  if (viewportChanged) state.epoch += 1;
55844
56223
  const count = dirtyRoots(viewportChanged);
56224
+ rendered = count;
55845
56225
  for (let index = 0; index < count; index += 1) {
55846
56226
  const root = dirtyList[index];
55847
56227
  if (root !== void 0) reconcileDirtyRoot(root);
@@ -55867,16 +56247,22 @@ function createReconciler(ctx, modules, fields) {
55867
56247
  /**
55868
56248
  * The second system of phase `layout`: every root takes its slot, then one solve per marked
55869
56249
  * root, a root in a slot after the root of its slot and in its content box, then the rects, the
55870
- * rest poses and the hooks, and the hosting of the hosts that came. The hints released since the last frame step are dropped at its end, so
55871
- * the `change.Box` and `change.Transform` hooks the solve plays get the hint the reconcile
55872
- * routed, and the next frame starts with none.
56250
+ * rest poses and the hooks, the scrolls settled on what the solve and the render gave them, and
56251
+ * the hosting of the hosts that came. The hints released since the last frame step are dropped
56252
+ * at its end, so the `change.Box` and `change.Transform` hooks the solve plays get the hint the
56253
+ * reconcile routed, and the next frame starts with none.
56254
+ *
56255
+ * @param time - The frame's `Time`: a scroll left moving asks for a frame only while it runs.
55873
56256
  */
55874
- function solve() {
56257
+ function solve(time = NO_TIME) {
55875
56258
  const viewport = modules.styles.viewport();
55876
56259
  viewportBox.w = viewport?.width ?? 0;
55877
56260
  viewportBox.h = viewport?.height ?? 0;
55878
56261
  assignSlots();
55879
- if (layOutRoots(viewportBox)) ctx.deps.time.wake();
56262
+ const changed = layOutRoots(viewportBox);
56263
+ if (changed) ctx.deps.time.wake();
56264
+ if (changed || rendered > 0) modules.layout.settleScroll(scrollContainers(), lookup, time);
56265
+ rendered = 0;
55880
56266
  settleOverlays(ctx, state, modules.layout);
55881
56267
  if (state.risen.size > 0) {
55882
56268
  hostViews(ctx, state, state.risen);
@@ -56053,7 +56439,8 @@ function sceneLayersOf(ctx) {
56053
56439
  *
56054
56440
  * @param ctx - Domain context of the ui plugin.
56055
56441
  * @param modules - The styles and layout modules, injected in that order.
56056
- * @returns The module: `tree`, `find` and `lint` reach the game, the rest the frame.
56442
+ * @returns The module: `tree`, `find`, `lint`, `fill` and `scroll` reach the game, the rest the
56443
+ * frame.
56057
56444
  */
56058
56445
  function createJsxApi(ctx, modules) {
56059
56446
  const state = ctx.state.jsx;
@@ -56070,6 +56457,7 @@ function createJsxApi(ctx, modules) {
56070
56457
  const frame = createReconciler(ctx, modules, fields);
56071
56458
  const focus = createFocus(ctx, frame.markPointer, topRoot, fields);
56072
56459
  const resolvedOf = (entity) => ctx.deps.world.ecs.get(entity, Text)?.resolved;
56460
+ const lookup = (entity) => state.elements.get(entity);
56073
56461
  const find = (key) => {
56074
56462
  for (const root of sortedRoots(state, layerNames())) {
56075
56463
  const entity = state.byKey.get(root.entity)?.get(key);
@@ -56090,12 +56478,25 @@ function createJsxApi(ctx, modules) {
56090
56478
  fields.fill(entity, value);
56091
56479
  return true;
56092
56480
  },
56481
+ scroll: (key, to) => {
56482
+ const entity = find(key);
56483
+ const element = entity === void 0 ? void 0 : lookup(entity);
56484
+ if (element?.scroll === void 0) {
56485
+ ctx.log.warn("ui:scroll-without-scroll", { key });
56486
+ return false;
56487
+ }
56488
+ if (!modules.layout.scrollTo(element, to, lookup)) {
56489
+ ctx.log.warn("ui:scroll-before-solve", { key });
56490
+ return false;
56491
+ }
56492
+ return true;
56493
+ },
56093
56494
  open: fields.open,
56094
56495
  pointer: (sample) => fields.pointer(sample),
56095
56496
  tapped: (entity) => focus.tapped(entity),
56096
56497
  reconcile: frame.reconcile,
56097
- solve: () => {
56098
- frame.solve();
56498
+ solve: (time) => {
56499
+ frame.solve(time);
56099
56500
  focus.refresh();
56100
56501
  fields.place();
56101
56502
  },
@@ -57200,25 +57601,63 @@ function createPopupHandler(ctx, state, jsx) {
57200
57601
  //#endregion
57201
57602
  //#region src/plugins/ui/layout/scroll.ts
57202
57603
  /**
57203
- * @file ui/layout — vertical scroll: while the container is pressed the content moves with the
57204
- * finger through one `Transform` write. No solve, no motion — the children keep their rects. A
57205
- * windowed scroll knows its content height from `rows × rowHeight`, so its range is kept even
57206
- * when no finger is on it.
57604
+ * @file ui/layout — scroll. A scroll container moves its content on its axis: with the finger,
57605
+ * then in a fling that slows down, or in a glide to a `snap` stop; it starts at its `offset`
57606
+ * prop, glides when the prop changes, and stays inside the range every solve gives it, with hard
57607
+ * edges. The content moves by one `Transform` write on the axis: no solve, no motion, the children
57608
+ * keep their rects. `Scroll` holds the offset the way the windowed range is cut with (0 and below),
57609
+ * `ScrollState` the way a game reads it (0 to `max`), `Scrollable` whether it moves, for `input`.
57610
+ * The exact numbers live on the element's `scroll` record and the samples of the press in the
57611
+ * layout state, written in place; the components get whole reference units. A frame allocates
57612
+ * nothing.
57207
57613
  */
57614
+ /** How far ahead a release carries its velocity before the nearest `snap` stop is taken, in ms. */
57615
+ const PROJECT_MS = 260;
57616
+ /** What a fling keeps of its velocity per ms. */
57617
+ const DECAY_PER_MS = .998;
57618
+ /** The speed under which a fling stops, in reference units per ms. */
57619
+ const STOP_SPEED = .02;
57620
+ /** The shortest glide, in ms. */
57621
+ const GLIDE_MS = 260;
57622
+ /** The ms each reference unit of distance adds to a glide. */
57623
+ const GLIDE_MS_PER_UNIT = 1.1;
57624
+ /** The longest glide, in ms. */
57625
+ const GLIDE_MAX_MS = 620;
57626
+ /** The samples of the last ms before a release give its velocity. */
57627
+ const VELOCITY_MS = 100;
57628
+ /** A finger that stood still for longer than this before its release lets go with no velocity, in ms. */
57629
+ const STILL_MS = 50;
57630
+ /** The numbers of one sample in the ring: where the finger was on the axis, and when. */
57631
+ const SAMPLE_STRIDE = 2;
57632
+ /** The fewest samples a release velocity is read from. */
57633
+ const MIN_SAMPLES = 2;
57634
+ /**
57635
+ * Keeps an offset from the start inside 0 to `max`.
57636
+ *
57637
+ * @param offset - Where the content would stand, from the start.
57638
+ * @param max - The largest offset.
57639
+ * @returns The offset it stands at.
57640
+ * @example
57641
+ * ```ts
57642
+ * within(420, 390); // 390
57643
+ * ```
57644
+ */
57645
+ function within(offset, max) {
57646
+ return Math.min(max, Math.max(0, offset));
57647
+ }
57208
57648
  /**
57209
- * Keeps an offset inside the scrollable range: never below the end of the content, never above
57210
- * the top.
57649
+ * How far a length runs past a window, in whole reference units: 0 when it fits.
57211
57650
  *
57212
- * @param offset - Where the content would go.
57213
- * @param min - The most negative offset, which is viewport height minus content height.
57214
- * @returns The offset the content is written with.
57651
+ * @param length - What is scrolled.
57652
+ * @param window - What shows of it.
57653
+ * @returns The largest offset.
57215
57654
  * @example
57216
57655
  * ```ts
57217
- * clampOffset(40, -300); // 0
57656
+ * extent(696, 306); // 390
57218
57657
  * ```
57219
57658
  */
57220
- function clampOffset(offset, min) {
57221
- return Math.min(0, Math.max(min, offset));
57659
+ function extent(length, window) {
57660
+ return Math.max(0, Math.round(length - window));
57222
57661
  }
57223
57662
  /**
57224
57663
  * The content entity of a scroll container: its single child.
@@ -57232,106 +57671,491 @@ function contentOf(container, lookup) {
57232
57671
  return first === void 0 ? void 0 : lookup(first);
57233
57672
  }
57234
57673
  /**
57235
- * The height of what a container scrolls: `rows × rowHeight` for a windowed scroll, which equals
57236
- * its solved content rect, else the solved content rect.
57674
+ * The largest offset of a scroll on its axis: its content's rect past its own, or
57675
+ * `rows × rowHeight` past its height for a windowed scroll.
57237
57676
  *
57238
- * @param container - The scroll element.
57677
+ * @param container - The scroll element, with a solved rect.
57239
57678
  * @param content - Its content element.
57240
- * @returns The height in reference units.
57679
+ * @param axis - The axis it moves on.
57680
+ * @returns The largest offset, in whole reference units.
57241
57681
  */
57242
- function contentHeightOf(container, content) {
57682
+ function maxOf(container, content, axis) {
57683
+ if (axis === "x") return extent(content.rect.w, container.rect.w);
57243
57684
  const window = container.window;
57244
- return window === void 0 ? content.rect.h : window.rows * window.rowHeight;
57685
+ return extent(window === void 0 ? content.rect.h : window.rows * window.rowHeight, container.rect.h);
57686
+ }
57687
+ /**
57688
+ * The `offset` prop of the scroll's last render: 0 when it is left out or is no finite number.
57689
+ *
57690
+ * @param container - The scroll element.
57691
+ * @returns The offset it asks for, from the start.
57692
+ */
57693
+ function askedOffsetOf(container) {
57694
+ const offset = container.node.props.offset;
57695
+ return typeof offset === "number" && Number.isFinite(offset) ? offset : 0;
57696
+ }
57697
+ /**
57698
+ * The `snap` prop of the scroll's last render.
57699
+ *
57700
+ * @param container - The scroll element.
57701
+ * @returns The stops, or `undefined` when it names none.
57702
+ */
57703
+ function snapOf(container) {
57704
+ const snap = container.node.props.snap;
57705
+ return Array.isArray(snap) && snap.length > 0 ? snap : void 0;
57706
+ }
57707
+ /**
57708
+ * The stop nearest to an offset, each stop kept inside 0 to `max`; the first of two as near.
57709
+ *
57710
+ * @param snap - The stops the scroll names.
57711
+ * @param offset - Where the release would carry it.
57712
+ * @param max - The largest offset.
57713
+ * @param fallback - Where it stays when no stop is a number.
57714
+ * @returns The stop.
57715
+ * @example
57716
+ * ```ts
57717
+ * nearestStop([0, 318, 390], 300, 390, 300); // 318
57718
+ * ```
57719
+ */
57720
+ function nearestStop(snap, offset, max, fallback) {
57721
+ let nearest = fallback;
57722
+ let distance = Number.POSITIVE_INFINITY;
57723
+ for (const candidate of snap) {
57724
+ if (typeof candidate !== "number") continue;
57725
+ const kept = within(candidate, max);
57726
+ if (Math.abs(kept - offset) < distance) {
57727
+ nearest = kept;
57728
+ distance = Math.abs(kept - offset);
57729
+ }
57730
+ }
57731
+ return nearest;
57732
+ }
57733
+ /**
57734
+ * Stops a scroll where it stands.
57735
+ *
57736
+ * @param motion - Its motion.
57737
+ */
57738
+ function stop(motion) {
57739
+ motion.mode = "still";
57740
+ motion.velocity = 0;
57741
+ }
57742
+ /**
57743
+ * Starts a glide to an offset, kept inside the range, on the curve `1 − (1 − p)³` over
57744
+ * `min(620, 260 + distance × 1.1)` ms. A scroll already there stops instead.
57745
+ *
57746
+ * @param motion - Its motion.
57747
+ * @param target - Where it goes, from the start.
57748
+ */
57749
+ function glideTo(motion, target) {
57750
+ const to = within(target, motion.max);
57751
+ const distance = Math.abs(to - motion.offset);
57752
+ if (distance === 0) {
57753
+ stop(motion);
57754
+ return;
57755
+ }
57756
+ motion.mode = "glide";
57757
+ motion.velocity = 0;
57758
+ motion.from = motion.offset;
57759
+ motion.to = to;
57760
+ motion.elapsed = 0;
57761
+ motion.duration = Math.min(GLIDE_MAX_MS, GLIDE_MS + distance * GLIDE_MS_PER_UNIT);
57762
+ }
57763
+ /**
57764
+ * Where a sample of the press starts in the ring: `back` samples before the newest one, 0 being
57765
+ * the newest. The position is at the index, the time right after it.
57766
+ *
57767
+ * @param state - The layout state, which keeps the samples of the press.
57768
+ * @param back - How many samples before the newest; 0 to the number of slots minus 1.
57769
+ * @returns The index of the sample in `scrollRing`.
57770
+ */
57771
+ function slotBefore(state, back) {
57772
+ const slots = state.scrollRing.length / SAMPLE_STRIDE;
57773
+ return (state.scrollHead + slots - 1 - back) % slots * SAMPLE_STRIDE;
57774
+ }
57775
+ /**
57776
+ * Follows the finger on a held scroll. The first frame of a press, a hand-off included, takes the
57777
+ * finger where it is as the start, stops a fling or a glide where it stands, and ends the scroll's
57778
+ * standing at its `offset` prop; the offset then moves against the finger, inside 0 to `max`.
57779
+ * Every frame adds the finger's sample to the ring.
57780
+ *
57781
+ * @param state - The layout state, which keeps the samples of the press.
57782
+ * @param container - The held scroll.
57783
+ * @param motion - Its motion.
57784
+ * @param pointer - The `Pointer` resource of this frame.
57785
+ */
57786
+ function hold(state, container, motion, pointer) {
57787
+ const at = motion.axis === "x" ? pointer.x : pointer.y;
57788
+ const ring = state.scrollRing;
57789
+ const slots = ring.length / SAMPLE_STRIDE;
57790
+ if (motion.mode !== "held" || state.scrolling !== container.entity) {
57791
+ motion.mode = "held";
57792
+ motion.velocity = 0;
57793
+ motion.pointer = at;
57794
+ motion.start = motion.offset;
57795
+ motion.atProp = false;
57796
+ state.scrolling = container.entity;
57797
+ state.scrollSamples = 0;
57798
+ state.scrollMoved = state.scrollClock;
57799
+ }
57800
+ motion.offset = within(motion.start - (at - motion.pointer), motion.max);
57801
+ if (state.scrollSamples > 0 && ring[slotBefore(state, 0)] !== at) state.scrollMoved = state.scrollClock;
57802
+ ring[state.scrollHead * SAMPLE_STRIDE] = at;
57803
+ ring[state.scrollHead * SAMPLE_STRIDE + 1] = state.scrollClock;
57804
+ state.scrollHead = (state.scrollHead + 1) % slots;
57805
+ state.scrollSamples = Math.min(slots, state.scrollSamples + 1);
57806
+ }
57807
+ /**
57808
+ * The velocity of the offset at a release, in units a ms, positive toward `max`: the finger's
57809
+ * move over its samples of the last 100 ms, against the finger. 0 when the finger stood still for
57810
+ * more than 50 ms before it let go, or left a single sample.
57811
+ *
57812
+ * @param state - The layout state, which keeps the samples of the press.
57813
+ * @returns The velocity.
57814
+ */
57815
+ function releaseVelocity(state) {
57816
+ const ring = state.scrollRing;
57817
+ const now = state.scrollClock;
57818
+ if (state.scrollSamples < MIN_SAMPLES || now - state.scrollMoved > STILL_MS) return 0;
57819
+ const newest = slotBefore(state, 0);
57820
+ let oldest = newest;
57821
+ for (let back = 1; back < state.scrollSamples; back += 1) {
57822
+ const slot = slotBefore(state, back);
57823
+ if ((ring[slot + 1] ?? 0) < now - VELOCITY_MS) break;
57824
+ oldest = slot;
57825
+ }
57826
+ const span = (ring[newest + 1] ?? 0) - (ring[oldest + 1] ?? 0);
57827
+ return span > 0 ? ((ring[oldest] ?? 0) - (ring[newest] ?? 0)) / span : 0;
57828
+ }
57829
+ /**
57830
+ * Lets go of a scroll the finger held. With `snap` it glides to the stop nearest to where its
57831
+ * velocity carries in 260 ms; without, it flings, or stands when it was let go too slowly. A
57832
+ * press whose samples a later press took lets go with no velocity.
57833
+ *
57834
+ * @param state - The layout state, which keeps the samples of the press.
57835
+ * @param container - The scroll that was held.
57836
+ * @param motion - Its motion.
57837
+ * @param owned - Whether the samples are this press's.
57838
+ */
57839
+ function letGo(state, container, motion, owned) {
57840
+ const velocity = owned ? releaseVelocity(state) : 0;
57841
+ const snap = snapOf(container);
57842
+ if (snap !== void 0) {
57843
+ glideTo(motion, nearestStop(snap, motion.offset + velocity * PROJECT_MS, motion.max, motion.offset));
57844
+ return;
57845
+ }
57846
+ if (Math.abs(velocity) < STOP_SPEED) {
57847
+ stop(motion);
57848
+ return;
57849
+ }
57850
+ motion.mode = "fling";
57851
+ motion.velocity = velocity;
57852
+ }
57853
+ /**
57854
+ * Moves a flinging scroll by the frame: the offset by its velocity, the velocity down by 0.998 a
57855
+ * ms. It stops under 0.02 units a ms, or on an edge: the edges are hard and nothing passes the
57856
+ * rest of the fling to a parent.
57857
+ *
57858
+ * @param motion - Its motion.
57859
+ * @param time - The frame's `Time`.
57860
+ */
57861
+ function fling(motion, time) {
57862
+ const delta = time.delta;
57863
+ const next = motion.offset + motion.velocity * delta;
57864
+ motion.offset = within(next, motion.max);
57865
+ motion.velocity *= DECAY_PER_MS ** delta;
57866
+ if (motion.offset !== next || Math.abs(motion.velocity) < STOP_SPEED) stop(motion);
57245
57867
  }
57246
57868
  /**
57247
- * Writes an offset and the range it was clamped to, and moves the content there.
57869
+ * Moves a gliding scroll by the frame along `1 − (1 − p)³`; at its end it stands on its target.
57870
+ *
57871
+ * @param motion - Its motion.
57872
+ * @param time - The frame's `Time`.
57873
+ */
57874
+ function glide(motion, time) {
57875
+ motion.elapsed += time.delta;
57876
+ const progress = Math.min(1, motion.elapsed / motion.duration);
57877
+ if (progress === 1) {
57878
+ motion.offset = motion.to;
57879
+ stop(motion);
57880
+ return;
57881
+ }
57882
+ const left = 1 - progress;
57883
+ motion.offset = motion.from + (motion.to - motion.from) * (1 - left * left * left);
57884
+ }
57885
+ /**
57886
+ * Moves the content to a whole offset on the scroll's axis, from where the solve put it: one
57887
+ * `Transform` write, none when it stands there already. The rects of the solve are whole numbers
57888
+ * that Yoga hands out as doubles; rounded, the pose is a small integer, which `Object.assign`
57889
+ * copies into the world without a box.
57248
57890
  *
57249
57891
  * @param ctx - Domain context of the ui plugin.
57892
+ * @param state - The layout state, which keeps the patches.
57250
57893
  * @param container - The scroll element.
57251
57894
  * @param content - Its content element.
57252
- * @param scroll - What its `Scroll` holds now.
57895
+ * @param motion - Its motion.
57253
57896
  */
57254
- function moveContent(ctx, container, content, scroll) {
57897
+ function moveContent(ctx, state, container, content, motion) {
57255
57898
  const ecs = ctx.deps.world.ecs;
57256
- ecs.set(container.entity, Scroll, {
57257
- offset: scroll.offset,
57258
- min: scroll.min
57259
- });
57260
- ecs.set(content.entity, Transform, { y: content.rect.y - container.rect.y + scroll.offset });
57899
+ const pose = ecs.get(content.entity, Transform);
57900
+ const offset = Math.round(motion.offset);
57901
+ if (pose === void 0) return;
57902
+ if (motion.axis === "x") {
57903
+ const x = Math.round(content.rect.x - container.rect.x) - offset;
57904
+ if (pose.x === x) return;
57905
+ state.scrollPatches.x.x = x;
57906
+ ecs.set(content.entity, Transform, state.scrollPatches.x);
57907
+ return;
57908
+ }
57909
+ const y = Math.round(content.rect.y - container.rect.y) - offset;
57910
+ if (pose.y === y) return;
57911
+ state.scrollPatches.y.y = y;
57912
+ ecs.set(content.entity, Transform, state.scrollPatches.y);
57261
57913
  }
57262
57914
  /**
57263
- * Keeps the offset of a scroll inside the range its content height gives, and writes that range,
57264
- * when either moved: a windowed list whose rows shrank, or its first solve.
57915
+ * Writes what changed of a scroll, in whole units: `Scroll` when its offset or range moved,
57916
+ * `ScrollState` when the offset, `max` or `moving` changed, `Scrollable` when `moving` did, and
57917
+ * the content's pose. A frame where nothing changed writes nothing.
57265
57918
  *
57266
57919
  * @param ctx - Domain context of the ui plugin.
57267
- * @param container - The scroll element, with a solved rect.
57920
+ * @param state - The layout state, which keeps the patches.
57921
+ * @param container - The scroll element.
57268
57922
  * @param content - Its content element.
57269
- * @param contentHeight - The height of what it scrolls.
57270
- * @returns The offset the content stands at.
57271
- */
57272
- function keepInRange(ctx, container, content, contentHeight) {
57273
- const scroll = ctx.deps.world.ecs.get(container.entity, Scroll);
57274
- if (scroll === void 0) return 0;
57275
- const min = Math.min(0, container.rect.h - contentHeight);
57276
- const offset = clampOffset(scroll.offset, min);
57277
- if (offset !== scroll.offset || min !== scroll.min) moveContent(ctx, container, content, {
57278
- ...scroll,
57279
- offset,
57280
- min
57281
- });
57282
- return offset;
57923
+ * @param motion - Its motion.
57924
+ */
57925
+ function show(ctx, state, container, content, motion) {
57926
+ const ecs = ctx.deps.world.ecs;
57927
+ const patches = state.scrollPatches;
57928
+ const offset = Math.round(motion.offset);
57929
+ const moving = motion.mode !== "still";
57930
+ const scroll = ecs.get(container.entity, Scroll);
57931
+ const shown = ecs.get(container.entity, ScrollState);
57932
+ const scrollable = ecs.get(container.entity, Scrollable);
57933
+ const rangeMoved = scroll !== void 0 && (scroll.offset !== 0 - offset || scroll.min !== 0 - motion.max);
57934
+ const stateMoved = shown !== void 0 && (shown.offset !== offset || shown.max !== motion.max || shown.moving !== moving);
57935
+ if (rangeMoved) {
57936
+ patches.scroll.offset = 0 - offset;
57937
+ patches.scroll.min = 0 - motion.max;
57938
+ ecs.set(container.entity, Scroll, patches.scroll);
57939
+ }
57940
+ if (stateMoved) {
57941
+ patches.state.offset = offset;
57942
+ patches.state.max = motion.max;
57943
+ patches.state.moving = moving;
57944
+ ecs.set(container.entity, ScrollState, patches.state);
57945
+ }
57946
+ if (scrollable !== void 0 && scrollable.moving !== moving) {
57947
+ patches.scrollable.moving = moving;
57948
+ ecs.set(container.entity, Scrollable, patches.scrollable);
57949
+ }
57950
+ moveContent(ctx, state, container, content, motion);
57283
57951
  }
57284
57952
  /**
57285
- * Re-clamps a live windowed scroll to the content height of its new rows before its range is
57286
- * cut, so a list that shrank below the window moves up in the same frame.
57953
+ * Tells whether a scroll has nothing to do this frame: its first solve has not placed it yet, or
57954
+ * it stands still and no finger is on it.
57955
+ *
57956
+ * @param motion - Its motion.
57957
+ * @param pressed - Whether a finger is on it.
57958
+ * @returns True when the frame leaves it alone.
57959
+ */
57960
+ function isIdle(motion, pressed) {
57961
+ return !motion.placed || !pressed && motion.mode === "still";
57962
+ }
57963
+ /**
57964
+ * Moves one scroll by the frame: the finger while it is pressed, the release when it lets go,
57965
+ * then a fling or a glide, and writes what changed. A still scroll no finger is on costs a tag
57966
+ * check.
57287
57967
  *
57288
57968
  * @param ctx - Domain context of the ui plugin.
57969
+ * @param state - The layout state.
57289
57970
  * @param container - The live scroll element.
57290
- * @param contentHeight - `rows × rowHeight` of this render.
57291
57971
  * @param lookup - How a child entity becomes its element.
57292
- * @returns The offset the content stands at.
57972
+ * @param pointer - The `Pointer` resource of this frame.
57973
+ * @param time - The frame's `Time`.
57974
+ * @returns True while the scroll moves.
57293
57975
  */
57294
- function clampScroll(ctx, container, contentHeight, lookup) {
57976
+ function stepOne(ctx, state, container, lookup, pointer, time) {
57977
+ const ecs = ctx.deps.world.ecs;
57978
+ const pressed = ecs.has(container.entity, Pressed);
57979
+ const released = !pressed && state.scrolling === container.entity;
57980
+ if (released) state.scrolling = void 0;
57981
+ const motion = container.scroll;
57982
+ if (motion === void 0 || isIdle(motion, pressed)) return false;
57295
57983
  const content = contentOf(container, lookup);
57296
- if (content === void 0) return ctx.deps.world.ecs.get(container.entity, Scroll)?.offset ?? 0;
57297
- return keepInRange(ctx, container, content, contentHeight);
57984
+ if (content === void 0 || !ecs.has(container.entity, Scroll)) return false;
57985
+ if (pressed) hold(state, container, motion, pointer);
57986
+ else if (motion.mode === "held") letGo(state, container, motion, released);
57987
+ if (motion.mode === "fling") fling(motion, time);
57988
+ else if (motion.mode === "glide") glide(motion, time);
57989
+ show(ctx, state, container, content, motion);
57990
+ return motion.mode !== "still";
57298
57991
  }
57299
57992
  /**
57300
- * Moves the content of every pressed scroll container with the finger. A windowed container no
57301
- * finger is on keeps its offset inside the range of its rows.
57993
+ * Moves every scroll container by the frame: with the finger while it is pressed, then in a
57994
+ * fling or a glide after it lets go. Time is the frame's `delta`, so a time scale and a pause
57995
+ * apply. While a scroll moves, `time.wake()` keeps the frames coming; at a time scale of 0 no
57996
+ * frame is asked for: a fling or a glide stands where it is and goes on once time runs again,
57997
+ * and a finger's samples wake the loop themselves.
57302
57998
  *
57303
57999
  * @param ctx - Domain context of the ui plugin.
57304
- * @param state - The layout state, which remembers where the drag started.
58000
+ * @param state - The layout state, which keeps the samples of the press.
57305
58001
  * @param containers - The live scroll elements.
57306
58002
  * @param lookup - How a child entity becomes its element.
58003
+ * @param time - The frame's `Time`.
58004
+ */
58005
+ function stepScroll(ctx, state, containers, lookup, time) {
58006
+ const pointer = ctx.deps.world.ecs.resource(Pointer);
58007
+ let moving = false;
58008
+ state.scrollClock += time.delta;
58009
+ for (const container of containers) if (stepOne(ctx, state, container, lookup, pointer, time)) moving = true;
58010
+ if (moving && time.delta > 0) ctx.deps.time.wake();
58011
+ }
58012
+ /**
58013
+ * Takes a new range: the offset keeps inside it at once, and so does a running glide. A scroll
58014
+ * that follows its `offset` prop goes on following it in the new range: standing, it stands at
58015
+ * the prop at once; gliding to it, it glides on to it from where it is. So a prop the first range
58016
+ * was too short for is reached once the content grows, a text measured again when its font
58017
+ * arrives.
58018
+ *
58019
+ * @param motion - The motion of the scroll.
58020
+ * @param max - The largest offset the solve gives it now.
58021
+ */
58022
+ function resize(motion, max) {
58023
+ if (max === motion.max) return;
58024
+ motion.max = max;
58025
+ motion.offset = within(motion.offset, max);
58026
+ motion.from = within(motion.from, max);
58027
+ motion.to = within(motion.to, max);
58028
+ if (!motion.atProp) return;
58029
+ if (motion.mode === "glide") glideTo(motion, motion.seen);
58030
+ else motion.offset = within(motion.seen, max);
58031
+ }
58032
+ /**
58033
+ * Puts a scroll the solve just spawned at its `offset` prop, inside its range, and lets it follow
58034
+ * the prop from there. Its spawn waits in the queue of the phase: these values replace the queued
58035
+ * ones at the flush, before the first draw.
58036
+ *
58037
+ * @param ctx - Domain context of the ui plugin.
58038
+ * @param container - The scroll element, live since this solve.
58039
+ * @param content - Its content element.
58040
+ * @param motion - Its motion, not placed yet.
57307
58041
  */
57308
- function stepScroll(ctx, state, containers, lookup) {
58042
+ function place$1(ctx, container, content, motion) {
57309
58043
  const ecs = ctx.deps.world.ecs;
57310
- const pointer = ecs.resource(Pointer);
58044
+ const rest = content.rest;
58045
+ motion.placed = true;
58046
+ motion.atProp = true;
58047
+ motion.max = maxOf(container, content, motion.axis);
58048
+ motion.seen = askedOffsetOf(container);
58049
+ motion.offset = within(motion.seen, motion.max);
58050
+ const offset = Math.round(motion.offset);
58051
+ const pose = motion.axis === "x" ? {
58052
+ ...rest,
58053
+ x: content.rect.x - container.rect.x - offset
58054
+ } : {
58055
+ ...rest,
58056
+ y: content.rect.y - container.rect.y - offset
58057
+ };
58058
+ ecs.add(container.entity, Scroll({
58059
+ axis: motion.axis,
58060
+ offset: 0 - offset,
58061
+ min: 0 - motion.max
58062
+ }));
58063
+ ecs.add(container.entity, ScrollState({
58064
+ offset,
58065
+ max: motion.max,
58066
+ moving: false
58067
+ }));
58068
+ ecs.add(content.entity, Transform(pose));
58069
+ }
58070
+ /**
58071
+ * Settles a placed scroll on what the solve and the render of this frame gave it: a new range at
58072
+ * once, then a glide to a new `offset` prop, which the scroll follows from then on. A scroll the
58073
+ * finger holds only remembers the prop.
58074
+ *
58075
+ * @param ctx - Domain context of the ui plugin.
58076
+ * @param state - The layout state, which keeps the patches.
58077
+ * @param container - The scroll element.
58078
+ * @param content - Its content element.
58079
+ * @param motion - Its motion, placed.
58080
+ */
58081
+ function settle(ctx, state, container, content, motion) {
58082
+ resize(motion, maxOf(container, content, motion.axis));
58083
+ const asked = askedOffsetOf(container);
58084
+ if (asked !== motion.seen) {
58085
+ motion.seen = asked;
58086
+ if (motion.mode !== "held") {
58087
+ glideTo(motion, asked);
58088
+ motion.atProp = true;
58089
+ }
58090
+ }
58091
+ show(ctx, state, container, content, motion);
58092
+ }
58093
+ /**
58094
+ * Settles every scroll on what the solve and the render of this frame gave it. A scroll the solve
58095
+ * just spawned takes its place at its `offset` prop. A placed one keeps inside a new range at
58096
+ * once, a running glide's target too, and glides to a new `offset` prop; the same value on a
58097
+ * re-render does nothing, and a scroll the finger holds only remembers it. A scroll left moving
58098
+ * wakes the loop the way `stepScroll` does: not at a time scale of 0, where it stands.
58099
+ *
58100
+ * @param ctx - Domain context of the ui plugin.
58101
+ * @param state - The layout state, which keeps the patches.
58102
+ * @param containers - The live scroll elements.
58103
+ * @param lookup - How a child entity becomes its element.
58104
+ * @param time - The frame's `Time`.
58105
+ */
58106
+ function settleScroll(ctx, state, containers, lookup, time) {
58107
+ let moving = false;
57311
58108
  for (const container of containers) {
57312
- const scroll = ecs.get(container.entity, Scroll);
58109
+ const motion = container.scroll;
57313
58110
  const content = contentOf(container, lookup);
57314
- if (scroll === void 0 || content === void 0) continue;
57315
- if (!ecs.has(container.entity, Pressed)) {
57316
- if (state.scrolling === container.entity) state.scrolling = void 0;
57317
- if (container.window !== void 0) keepInRange(ctx, container, content, contentHeightOf(container, content));
57318
- continue;
57319
- }
57320
- if (state.scrolling !== container.entity) {
57321
- state.scrolling = container.entity;
57322
- state.scrollStart = {
57323
- pointerY: pointer.y,
57324
- offset: scroll.offset
57325
- };
57326
- }
57327
- const min = Math.min(0, container.rect.h - contentHeightOf(container, content));
57328
- const offset = clampOffset(state.scrollStart.offset + (pointer.y - state.scrollStart.pointerY), min);
57329
- moveContent(ctx, container, content, {
57330
- ...scroll,
57331
- offset,
57332
- min
57333
- });
58111
+ if (motion === void 0 || content === void 0) continue;
58112
+ if (motion.placed) settle(ctx, state, container, content, motion);
58113
+ else place$1(ctx, container, content, motion);
58114
+ if (motion.mode !== "still") moving = true;
57334
58115
  }
58116
+ if (moving && time.delta > 0) ctx.deps.time.wake();
58117
+ }
58118
+ /**
58119
+ * Re-clamps a live windowed scroll to the content height of its new rows before its range is
58120
+ * cut, so a list that shrank below the window moves up in the same frame.
58121
+ *
58122
+ * @param ctx - Domain context of the ui plugin.
58123
+ * @param state - The layout state, which keeps the patches.
58124
+ * @param container - The live scroll element.
58125
+ * @param contentHeight - `rows × rowHeight` of this render.
58126
+ * @param lookup - How a child entity becomes its element.
58127
+ * @returns The offset the content stands at, 0 and below, as `Scroll` holds it.
58128
+ */
58129
+ function clampScroll(ctx, state, container, contentHeight, lookup) {
58130
+ const motion = container.scroll;
58131
+ const content = contentOf(container, lookup);
58132
+ if (motion === void 0 || !motion.placed || content === void 0) return 0;
58133
+ resize(motion, extent(contentHeight, container.rect.h));
58134
+ show(ctx, state, container, content, motion);
58135
+ return 0 - Math.round(motion.offset);
58136
+ }
58137
+ /**
58138
+ * Puts a placed scroll at an offset at once, inside its range: a fling or a glide stops, the
58139
+ * scroll no longer follows its `offset` prop, and `ScrollState` says it stands. A finger that
58140
+ * holds it goes on from there.
58141
+ *
58142
+ * @param ctx - Domain context of the ui plugin.
58143
+ * @param state - The layout state, which keeps the patches.
58144
+ * @param container - The scroll element.
58145
+ * @param to - Where it goes, from the start; `NaN` leaves it where it is.
58146
+ * @param lookup - How a child entity becomes its element.
58147
+ * @returns True when the scroll took it; false before its first solve placed it.
58148
+ */
58149
+ function scrollTo(ctx, state, container, to, lookup) {
58150
+ const motion = container.scroll;
58151
+ const content = contentOf(container, lookup);
58152
+ if (motion === void 0 || !motion.placed || content === void 0) return false;
58153
+ motion.offset = within(Number.isNaN(to) ? motion.offset : to, motion.max);
58154
+ motion.atProp = false;
58155
+ stop(motion);
58156
+ show(ctx, state, container, content, motion);
58157
+ ctx.deps.time.wake();
58158
+ return true;
57335
58159
  }
57336
58160
  //#endregion
57337
58161
  //#region src/plugins/ui/layout/solve.ts
@@ -57546,7 +58370,9 @@ function textSourceOf(ctx) {
57546
58370
  };
57547
58371
  }
57548
58372
  /**
57549
- * Writes the style of an element onto its node and keeps its measure function in step.
58373
+ * Writes the style of an element onto its node and keeps its measure function in step. An `x`
58374
+ * scroll lays its content out the way a row does: along `x`, as wide as it is, as tall as the
58375
+ * scroll.
57550
58376
  *
57551
58377
  * @param state - The layout state.
57552
58378
  * @param source - What a text element is measured through.
@@ -57555,7 +58381,8 @@ function textSourceOf(ctx) {
57555
58381
  function writeStyle(state, source, element) {
57556
58382
  const node = state.byEntity.get(element.entity);
57557
58383
  if (node === void 0 || state.yoga === void 0) return;
57558
- applyStyleToNode(state.yoga, node, element.style, element.type, element.parentType);
58384
+ const tag = element.scroll?.axis === "x" ? "row" : element.type;
58385
+ applyStyleToNode(state.yoga, node, element.style, tag, element.parentType);
57559
58386
  installMeasure(state, node, element, source);
57560
58387
  }
57561
58388
  /**
@@ -57618,8 +58445,10 @@ function createLayoutApi(ctx) {
57618
58445
  play(ctx, element, hook);
57619
58446
  },
57620
58447
  settled: (element) => canDespawn(element),
57621
- scroll: (containers, lookup) => stepScroll(ctx, state, containers, lookup),
57622
- clampScroll: (container, contentHeight, lookup) => clampScroll(ctx, container, contentHeight, lookup),
58448
+ scroll: (containers, lookup, time) => stepScroll(ctx, state, containers, lookup, time),
58449
+ settleScroll: (containers, lookup, time) => settleScroll(ctx, state, containers, lookup, time),
58450
+ clampScroll: (container, contentHeight, lookup) => clampScroll(ctx, state, container, contentHeight, lookup),
58451
+ scrollTo: (container, to, lookup) => scrollTo(ctx, state, container, to, lookup),
57623
58452
  windowOf,
57624
58453
  popupHandler: (jsx) => createPopupHandler(ctx, state, jsx),
57625
58454
  guideHandler: () => createGuideHandler(ctx),
@@ -58195,12 +59024,12 @@ function openRegistrations(ctx, jsx, layout) {
58195
59024
  name: "ui.reconcile",
58196
59025
  phase: "layout",
58197
59026
  query: [],
58198
- run: jsx.reconcile
59027
+ run: (_rows, frame) => jsx.reconcile(frame.time)
58199
59028
  })), ecs.system(system({
58200
59029
  name: "ui.solve",
58201
59030
  phase: "layout",
58202
59031
  query: [],
58203
- run: jsx.solve
59032
+ run: (_rows, frame) => jsx.solve(frame.time)
58204
59033
  })), ecs.onAdded(Tree, (entity) => {
58205
59034
  const place = projection.keyOf(entity);
58206
59035
  jsx.mountRoot(entity, place?.projection ?? "ui", ecs.get(entity, Layer)?.name ?? "ui");
@@ -58247,6 +59076,7 @@ function stopUi(state) {
58247
59076
  state.layout.nodes = 0;
58248
59077
  state.layout.yoga = void 0;
58249
59078
  state.layout.scrolling = void 0;
59079
+ state.layout.scrollSamples = 0;
58250
59080
  state.layout.iconLine = void 0;
58251
59081
  state.jsx.components.clear();
58252
59082
  state.jsx.roots.clear();
@@ -58537,7 +59367,7 @@ function createModules(ctx) {
58537
59367
  };
58538
59368
  }
58539
59369
  /**
58540
- * Creates the ui API: the four members a game calls, flat.
59370
+ * Creates the ui API: the five members a game calls, flat.
58541
59371
  *
58542
59372
  * @param ctx - Kernel context of the ui plugin.
58543
59373
  * @returns The plugin API.
@@ -58548,7 +59378,8 @@ function createUiApi(ctx) {
58548
59378
  tree: jsx.tree,
58549
59379
  find: jsx.find,
58550
59380
  lint: jsx.lint,
58551
- fill: jsx.fill
59381
+ fill: jsx.fill,
59382
+ scroll: jsx.scroll
58552
59383
  };
58553
59384
  }
58554
59385
  //#endregion
@@ -58650,8 +59481,8 @@ function createJsxState() {
58650
59481
  //#endregion
58651
59482
  //#region src/plugins/ui/layout/state.ts
58652
59483
  /**
58653
- * Creates an empty map. Its own non-exported function, because lint rule L5 refuses a collection
58654
- * built inside an exported declaration.
59484
+ * Creates an empty map. Its own non-exported function, because lint rule L5 refuses a
59485
+ * collection built inside an exported declaration.
58655
59486
  *
58656
59487
  * @returns A map with nothing in it.
58657
59488
  */
@@ -58659,13 +59490,39 @@ function emptyMap() {
58659
59490
  return /* @__PURE__ */ new Map();
58660
59491
  }
58661
59492
  /**
59493
+ * Creates the ring of pointer samples: a position and a time for each of `SAMPLES`. Its own
59494
+ * non-exported function, for the same rule.
59495
+ *
59496
+ * @returns The ring, zeroed.
59497
+ */
59498
+ function sampleRing() {
59499
+ return /* @__PURE__ */ new Float64Array(10);
59500
+ }
59501
+ /**
59502
+ * Creates the patches the scroll step writes with, every field 0 or false.
59503
+ *
59504
+ * @returns The patches.
59505
+ */
59506
+ function scrollPatches() {
59507
+ return {
59508
+ scroll: {
59509
+ offset: 0,
59510
+ min: 0
59511
+ },
59512
+ state: {
59513
+ offset: 0,
59514
+ max: 0,
59515
+ moving: false
59516
+ },
59517
+ scrollable: { moving: false },
59518
+ x: { x: 0 },
59519
+ y: { y: 0 }
59520
+ };
59521
+ }
59522
+ /**
58662
59523
  * Creates the layout branch of the ui state. Everything starts empty: Yoga arrives in `onStart`.
58663
59524
  *
58664
59525
  * @returns The module state.
58665
- * @example
58666
- * ```ts
58667
- * createLayoutState().nodes; // 0
58668
- * ```
58669
59526
  */
58670
59527
  function createLayoutState() {
58671
59528
  return {
@@ -58675,10 +59532,12 @@ function createLayoutState() {
58675
59532
  measured: 0,
58676
59533
  solves: 0,
58677
59534
  scrolling: void 0,
58678
- scrollStart: {
58679
- pointerY: 0,
58680
- offset: 0
58681
- },
59535
+ scrollClock: 0,
59536
+ scrollRing: sampleRing(),
59537
+ scrollHead: 0,
59538
+ scrollSamples: 0,
59539
+ scrollMoved: 0,
59540
+ scrollPatches: scrollPatches(),
58682
59541
  iconLine: void 0,
58683
59542
  order: 0,
58684
59543
  cleanups: []
@@ -58740,9 +59599,10 @@ function createUiState(_ctx) {
58740
59599
  };
58741
59600
  }
58742
59601
  /**
58743
- * Ui plugin: `app.ui.tree()`, `app.ui.find(...)`, `app.ui.lint()`, `app.ui.fill(...)`, the
58744
- * text field `<input local>`, the components `Box`, `LocalWrite` and
58745
- * `DebugTrigger`, and the helpers `defineComponent`, `defineStyle`, `defineTokens` and `popup`.
59602
+ * Ui plugin: `app.ui.tree()`, `app.ui.find(...)`, `app.ui.lint()`, `app.ui.fill(...)`,
59603
+ * `app.ui.scroll(...)`, the text field `<input local>`, the components `Box`, `LocalWrite`,
59604
+ * `DebugTrigger` and `ScrollState`, and the helpers `defineComponent`, `defineStyle`,
59605
+ * `defineTokens` and `popup`.
58746
59606
  *
58747
59607
  * @example
58748
59608
  * ```ts
@@ -60313,4 +61173,4 @@ const screen = [
60313
61173
  uiPlugin
60314
61174
  ];
60315
61175
  //#endregion
60316
- export { mark as $, PROBLEM_RULES as $t, types_exports$11 as A, Alpha as At, Box as B, scenesPlugin as Bt, types_exports$4 as C, Traceable as Ct, types_exports$8 as D, messageDuration as Dt, types_exports$7 as E, messageArgument as Et, types_exports$16 as F, Noise as Ft, defineStyle as G, Frames as Gt, LocalWrite as H, animPlugin as Ht, types_exports$17 as I, Outline as It, HAPTIC_KINDS as J, frameAt as Jt, textPlugin as K, rendererPlugin as Kt, uiPlugin as L, defineFilter as Lt, types_exports$13 as M, ColorMatrix as Mt, types_exports$14 as N, Displacement as Nt, types_exports$9 as O, effectsPlugin as Ot, types_exports$15 as P, Glow as Pt, haptic as Q, PERF_LOG_VERSION as Qt, resolve as R, debugPlugin as Rt, types_exports$3 as S, Touchable as St, types_exports$6 as T, i18nPlugin as Tt, popup as U, AnimPlayer as Ut, DebugTrigger as V, assetsPlugin as Vt, defineTokens as W, Animation as Wt, external as X, PERF_CAUSES as Xt, defineAnimation as Y, MONITOR_SECTIONS as Yt, frames as Z, PERF_GAP_BUCKETS as Zt, defineBundles as _, PointerOver as _t, cubicBezier as a, exit as an, spawn as at, types_exports$1 as b, Swipeable as bt, hint as c, type as cn, tween as ct, defineScene as d, lifecyclePlugin as dn, inputPlugin as dt, SHADER_WHATS as en, parallel as et, tr as f, timePlugin as fn, Draggable as ft, music as g, Pointer as gt, defineEmitter as h, Hovered as ht, screen as i, refuseStart as in, sfx as it, types_exports$12 as j, Blur as jt, types_exports$10 as k, Emitter as kt, schedule as l, modelPlugin as ln, use as lt, flattenFeatures as m, Held as mt, createPlugin as n, worldPlugin as nn, sequence as nt, defineEffect as o, slot as on, spawned as ot, defineFeature as p, clockPlugin as pn, DropTarget as pt, platformPlugin as q, PASS_KINDS as qt, defineGame as r, flowPlugin as rn, set as rt, guide as s, to as sn, stagger as st, createApp as t, DEMAND_REASONS as tn, play$1 as tt, projection as u, SaveUnreadableError as un, wait as ut, load as v, Pressable as vt, types_exports$5 as w, Traced as wt, types_exports$2 as x, Tappable as xt, types_exports as y, Pressed as yt, defineMotion as z, audioPlugin as zt };
61176
+ export { haptic as $, PERF_LOG_VERSION as $t, types_exports$11 as A, Emitter as At, Box as B, audioPlugin as Bt, types_exports$4 as C, Touchable as Ct, types_exports$8 as D, messageArgument as Dt, types_exports$7 as E, i18nPlugin as Et, types_exports$16 as F, Glow as Ft, defineTokens as G, Animation as Gt, LocalWrite as H, assetsPlugin as Ht, types_exports$17 as I, Noise as It, platformPlugin as J, PASS_KINDS as Jt, defineStyle as K, Frames as Kt, uiPlugin as L, Outline as Lt, types_exports$13 as M, Blur as Mt, types_exports$14 as N, ColorMatrix as Nt, types_exports$9 as O, messageDuration as Ot, types_exports$15 as P, Displacement as Pt, frames as Q, PERF_GAP_BUCKETS as Qt, resolve as R, defineFilter as Rt, types_exports$3 as S, Tappable as St, types_exports$6 as T, Traced as Tt, ScrollState as U, animPlugin as Ut, DebugTrigger as V, scenesPlugin as Vt, popup as W, AnimPlayer as Wt, defineAnimation as X, MONITOR_SECTIONS as Xt, HAPTIC_KINDS as Y, frameAt as Yt, external as Z, PERF_CAUSES as Zt, defineBundles as _, Pointer as _t, cubicBezier as a, refuseStart as an, sfx as at, types_exports$1 as b, Pressed as bt, hint as c, to as cn, stagger as ct, defineScene as d, SaveUnreadableError as dn, wait as dt, PROBLEM_RULES as en, mark as et, tr as f, lifecyclePlugin as fn, inputPlugin as ft, music as g, Hovered as gt, defineEmitter as h, Held as ht, screen as i, flowPlugin as in, set as it, types_exports$12 as j, Alpha as jt, types_exports$10 as k, effectsPlugin as kt, schedule as l, type as ln, tween as lt, flattenFeatures as m, clockPlugin as mn, DropTarget as mt, createPlugin as n, DEMAND_REASONS as nn, play$1 as nt, defineEffect as o, exit as on, spawn as ot, defineFeature as p, timePlugin as pn, Draggable as pt, textPlugin as q, rendererPlugin as qt, defineGame as r, worldPlugin as rn, sequence as rt, guide as s, slot as sn, spawned as st, createApp as t, SHADER_WHATS as tn, parallel as tt, projection as u, modelPlugin as un, use as ut, load as v, PointerOver as vt, types_exports$5 as w, Traceable as wt, types_exports$2 as x, Swipeable as xt, types_exports as y, Pressable as yt, defineMotion as z, debugPlugin as zt };