@moku-labs/game 0.15.14 → 0.15.17

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 (33) 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-aLTGwuyk.d.mts → commands-DEVJzdsr.d.mts} +5 -5
  8. package/dist/control.d.mts +4 -4
  9. package/dist/{define-DbxBzO-G.d.mts → define-BfWdvNHC.d.mts} +2 -2
  10. package/dist/{headless-DHtavbqk.d.mts → headless-ByFLyHxx.d.mts} +1 -1
  11. package/dist/{index-YUceiUFm.d.mts → index-BYDIzUDh.d.mts} +6 -5
  12. package/dist/index.d.mts +4 -4
  13. package/dist/index.mjs +2 -2
  14. package/dist/inspect.d.mts +6 -6
  15. package/dist/inspect.mjs +1 -1
  16. package/dist/jsx-dev-runtime.d.mts +1 -1
  17. package/dist/jsx-runtime.d.mts +1 -1
  18. package/dist/{runtime-B5OD9JTT.d.mts → runtime-CrF7Cx-b.d.mts} +1 -1
  19. package/dist/{sources-Fbq_PFVH.mjs → sources-BYKXYPhd.mjs} +1 -1
  20. package/dist/{src-Co5sO4z1.mjs → src-sg41xWNr.mjs} +471 -75
  21. package/dist/testing.d.mts +4 -4
  22. package/dist/testing.mjs +1 -1
  23. package/dist/{types-3bEK86Z1.d.mts → types-Bi6hj1OB.d.mts} +2 -53
  24. package/dist/{types-Dm4tM5nH.d.mts → types-D8zAAXLt.d.mts} +4 -4
  25. package/dist/{types-CuEK0Vam.d.mts → types-DOldmhIw.d.mts} +21 -5
  26. package/dist/{types-xmH1X8qd.d.mts → types-ae0jg0r4.d.mts} +133 -14
  27. package/dist/{types-_a6-ALzz.d.mts → types-qsE-I59e.d.mts} +2 -2
  28. package/dist/{visual-CbGKzlj0.mjs → visual-DNM5oFYx.mjs} +40 -9
  29. package/dist/{visual-DExHHx-a.d.mts → visual-Hj0oNqPP.d.mts} +6 -6
  30. package/dist/visual.d.mts +1 -1
  31. package/dist/visual.mjs +1 -1
  32. package/llms.txt +1 -1
  33. 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 { $ as component, A as localPoseOf, B as OverlayLevel, C as builtInStyles, D as textFor, E as readStyle, F as rootPoseOf, G as arcDefaults, H as Shape, I as Display, J as Exiting, K as componentsFor, L as HudPart, M as poseUnder, N as rootPointOf, O as applyPose, P as rootPoseInto, Q as Tree, R as NineSlice, U as Sprite, V as Parent, W as Transform, X as Now, Y as Layer, Z as Order, _ as uiFor, a as visualOf$1, b as Countdown, c as Covered, d as LocalWrite, f as Scroll, h as UiCounters, i as samePose, it as tag, j as parentOf$1, k as isLeaving, l as DebugTrigger, m as UI_OWNER, n as drawnRect, nt as system, o as visualRectOf, p as ScrollState, r as fitScaleOf, rt as systemsFor, s as Box, t as CONTENT$1, tt as resource, u as Escapable, x as Text, z as Overlay } from "./visual-CbGKzlj0.mjs";
5
+ import { $ as component, A as localPoseOf, B as OverlayLevel, C as builtInStyles, D as textFor, E as readStyle, F as rootPoseOf, G as arcDefaults, H as Shape, I as Display, J as Exiting, K as componentsFor, L as HudPart, M as poseUnder, N as rootPointOf, O as applyPose, P as rootPoseInto, Q as Tree, R as NineSlice, U as Sprite, V as Parent, W as Transform, X as Now, Y as Layer, Z as Order, _ as uiFor, a as visualOf$1, b as Countdown, c as Covered, d as LocalWrite, f as Scroll, h as UiCounters, i as samePose, it as tag, j as parentOf$1, k as isLeaving, l as DebugTrigger, m as UI_OWNER, n as drawnRect, nt as system, o as visualRectOf, p as ScrollState, r as fitScaleOf, rt as systemsFor, s as Box, t as CONTENT$1, tt as resource, u as Escapable, x as Text, z as Overlay } from "./visual-DNM5oFYx.mjs";
6
6
  import { a as isDev } from "./dev-BNndV-hU.mjs";
7
7
  import { n as isComponentDefinition } from "./component-DGg5DqKK.mjs";
8
8
  import { n as flatten } from "./flatten-nUX9PpGo.mjs";
@@ -34675,7 +34675,7 @@ function pickVictim(state) {
34675
34675
  const entry = state.manifest.bundles[name];
34676
34676
  if (record.status !== "loaded" || entry === void 0) continue;
34677
34677
  if (state.pinned.has(name) || isPermanent(entry.tier)) continue;
34678
- if (state.sceneNamed.includes(name)) continue;
34678
+ if (state.sceneNamed.includes(name) || state.sceneLeaving.includes(name)) continue;
34679
34679
  if (state.queue?.bundles.includes(name) === true) continue;
34680
34680
  if (record.lastUsed >= smallest) continue;
34681
34681
  smallest = record.lastUsed;
@@ -34830,8 +34830,9 @@ function featuresOnStack(ctx) {
34830
34830
  }
34831
34831
  /**
34832
34832
  * Tells whether a bundle must stay where the game stands now: it is pinned (a bundle of the node
34833
- * entered last), the mounted scene names it (the last list `sceneBundles` received), its feature
34834
- * is in `onStack`, or its tier is permanent.
34833
+ * entered last), the mounted scene names it (the last list `sceneBundles` received), a leaving
34834
+ * scene named it and its elements still play their exit, its feature is in `onStack`, or its tier
34835
+ * is permanent.
34835
34836
  *
34836
34837
  * @param ctx - Domain context of the plugin.
34837
34838
  * @param name - Name of the bundle.
@@ -34841,25 +34842,66 @@ function featuresOnStack(ctx) {
34841
34842
  */
34842
34843
  function isHeld(ctx, name, entry, onStack) {
34843
34844
  const state = ctx.state;
34844
- return state.pinned.has(name) || state.sceneNamed.includes(name) || onStack.has(entry.feature) || isPermanent(entry.tier);
34845
+ return state.pinned.has(name) || state.sceneNamed.includes(name) || state.sceneLeaving.includes(name) || onStack.has(entry.feature) || isPermanent(entry.tier);
34845
34846
  }
34846
34847
  /**
34847
- * Unloads every loaded `"leave"` bundle nobody holds any more, with the reason `"leave"`. A
34848
- * running load is never aborted by it, and a `"budget"` bundle is never touched. Headless it does
34849
- * nothing.
34848
+ * Unloads every loaded `"leave"` bundle nobody holds any more, with the reason `"leave"`; a
34849
+ * leaving scene holds its bundles only while anything carries `Exiting`. A running load is never
34850
+ * aborted by it, and a `"budget"` bundle is never touched. Headless it does nothing.
34850
34851
  *
34851
34852
  * @param ctx - Domain context of the plugin.
34852
34853
  */
34853
34854
  function releaseLeftBundles(ctx) {
34854
34855
  const state = ctx.state;
34855
34856
  if (state.io === void 0) return;
34857
+ if (state.sceneLeaving.length > 0 && state.exiting === 0) state.sceneLeaving = [];
34856
34858
  const onStack = featuresOnStack(ctx);
34857
- for (const [name, record] of state.records) {
34858
- const entry = state.manifest.bundles[name];
34859
- if (record.status !== "loaded" || entry?.release !== "leave") continue;
34860
- if (isHeld(ctx, name, entry, onStack)) continue;
34861
- unloadBundle(ctx, name, "leave");
34862
- }
34859
+ for (const name of state.records.keys()) unloadIfLeft(ctx, name, onStack);
34860
+ }
34861
+ /**
34862
+ * Takes the list the mounted scene names now. A bundle the list named before and names no more
34863
+ * stays held as a leaving scene's, until nothing carries `Exiting` (F51); a name the list takes
34864
+ * again is the mounted scene's once more.
34865
+ *
34866
+ * @param state - The plugin state.
34867
+ * @param named - The bundles the mounted scene names now.
34868
+ */
34869
+ function nameSceneBundles(state, named) {
34870
+ const leaving = state.sceneLeaving.filter((name) => !named.includes(name));
34871
+ for (const name of state.sceneNamed) if (!named.includes(name) && !leaving.includes(name)) leaving.push(name);
34872
+ state.sceneLeaving = leaving;
34873
+ state.sceneNamed = [...named];
34874
+ }
34875
+ /**
34876
+ * Lets the bundles a leaving scene named go once nothing carries `Exiting` any more: each loaded
34877
+ * `"leave"` bundle among them that nothing else holds is unloaded with the reason `"leave"`, at
34878
+ * once, not at the next rest. A bundle of another release stays, as it would have. The world calls
34879
+ * it through `onRemoved(Exiting)`, so the check costs a length read while no scene leaves, and
34880
+ * a count read while one does.
34881
+ *
34882
+ * @param ctx - Domain context of the plugin.
34883
+ */
34884
+ function releaseSceneLeft(ctx) {
34885
+ const state = ctx.state;
34886
+ if (state.sceneLeaving.length === 0 || state.exiting > 0) return;
34887
+ const names = state.sceneLeaving;
34888
+ state.sceneLeaving = [];
34889
+ if (state.io === void 0) return;
34890
+ const onStack = featuresOnStack(ctx);
34891
+ for (const name of names) unloadIfLeft(ctx, name, onStack);
34892
+ }
34893
+ /**
34894
+ * Unloads one bundle with the reason `"leave"` when it is a loaded `"leave"` bundle nobody holds.
34895
+ *
34896
+ * @param ctx - Domain context of the plugin.
34897
+ * @param name - Name of the bundle.
34898
+ * @param onStack - The features on the stack, from `featuresOnStack`.
34899
+ */
34900
+ function unloadIfLeft(ctx, name, onStack) {
34901
+ const entry = ctx.state.manifest.bundles[name];
34902
+ if (ctx.state.records.get(name)?.status !== "loaded" || entry?.release !== "leave") return;
34903
+ if (isHeld(ctx, name, entry, onStack)) return;
34904
+ unloadBundle(ctx, name, "leave");
34863
34905
  }
34864
34906
  //#endregion
34865
34907
  //#region src/plugins/scenes/bundles.ts
@@ -35337,21 +35379,22 @@ function startPreload(ctx) {
35337
35379
  //#endregion
35338
35380
  //#region src/plugins/assets/lifecycle.ts
35339
35381
  /**
35340
- * @file assets plugin — lifecycle: the three registrations of `onInit`, the boot of `onStart`
35382
+ * @file assets plugin — lifecycle: the five registrations of `onInit`, the boot of `onStart`
35341
35383
  * and the teardown that frees every texture and cancels every fetch.
35342
35384
  */
35343
35385
  /**
35344
- * Resolves the dependency APIs `flow`, `renderer`, `time` and `model` with `ctx.require`.
35386
+ * Resolves the dependency APIs `flow`, `renderer`, `time`, `model` and `world` with `ctx.require`.
35345
35387
  *
35346
35388
  * @param ctx - Kernel context of the assets plugin.
35347
- * @returns The four dependency APIs.
35389
+ * @returns The five dependency APIs.
35348
35390
  */
35349
35391
  function resolveDeps$5(ctx) {
35350
35392
  return {
35351
35393
  flow: ctx.require(flowPlugin),
35352
35394
  renderer: ctx.require(rendererPlugin),
35353
35395
  time: ctx.require(timePlugin),
35354
- model: ctx.require(modelPlugin)
35396
+ model: ctx.require(modelPlugin),
35397
+ world: ctx.require(worldPlugin)
35355
35398
  };
35356
35399
  }
35357
35400
  /**
@@ -35451,19 +35494,20 @@ async function enterNode(ctx, node, run) {
35451
35494
  }
35452
35495
  /**
35453
35496
  * Connects the plugin in `onInit`: the `load` stage of `onEnter`, the handler of the `load`
35454
- * effect and the texture provider of `renderer`. The three removers go to the state, because
35455
- * `onStop` has nothing but the state.
35497
+ * effect, the texture provider of `renderer`, and the start and the end of an exit in `world`,
35498
+ * counted so a leaving scene's bundles go when the last exit ends (F51). The five removers go to
35499
+ * the state, because `onStop` has nothing but the state.
35456
35500
  *
35457
35501
  * @param ctx - Kernel context of the assets plugin.
35458
- * @example
35459
- * ```ts
35460
- * connectAssets(ctx);
35461
- * ctx.state.removers.length; // 3
35462
- * ```
35463
35502
  */
35464
35503
  function connectAssets(ctx) {
35465
35504
  const assets = withDeps$6(ctx);
35466
- assets.state.removers.push(assets.deps.flow.onEnter("load", (node, run) => enterNode(assets, node, run)), assets.deps.flow.fx.handle("load", (descriptor, run) => runLoadEffect(assets, descriptor, run), { runInFast: true }), assets.deps.renderer.sync.textures.provide((key) => lookupTexture(assets, key)));
35505
+ assets.state.removers.push(assets.deps.flow.onEnter("load", (node, run) => enterNode(assets, node, run)), assets.deps.flow.fx.handle("load", (descriptor, run) => runLoadEffect(assets, descriptor, run), { runInFast: true }), assets.deps.renderer.sync.textures.provide((key) => lookupTexture(assets, key)), assets.deps.world.ecs.onAdded(Exiting, () => {
35506
+ assets.state.exiting += 1;
35507
+ }), assets.deps.world.ecs.onRemoved(Exiting, () => {
35508
+ assets.state.exiting -= 1;
35509
+ releaseSceneLeft(assets);
35510
+ }));
35467
35511
  }
35468
35512
  /**
35469
35513
  * Picks the I/O seam: the configured one, the browser pair over `renderer.sync.textures` when
@@ -35539,7 +35583,7 @@ async function startAssets(ctx) {
35539
35583
  }
35540
35584
  /**
35541
35585
  * Frees everything in `onStop`: the preload and every running load are aborted, every texture is
35542
- * destroyed, the three registrations are removed and the registries are emptied, the companions
35586
+ * destroyed, the five registrations are removed and the registries are emptied, the companions
35543
35587
  * too. A teardown context carries the state and nothing else, so no other plugin is called here:
35544
35588
  * no companion's `unload` either, its plugin stopped before this one.
35545
35589
  *
@@ -35571,6 +35615,8 @@ function releaseAll(state) {
35571
35615
  state.current = void 0;
35572
35616
  state.sceneId = void 0;
35573
35617
  state.sceneNamed = [];
35618
+ state.sceneLeaving = [];
35619
+ state.exiting = 0;
35574
35620
  state.restPending = false;
35575
35621
  state.stamps = void 0;
35576
35622
  state.swapping = void 0;
@@ -35717,9 +35763,7 @@ function createAssetsApi(ctx) {
35717
35763
  font: (key) => loadedRecord(assets, key)?.fonts.get(key),
35718
35764
  audio: (key) => loadedRecord(assets, key)?.audio.get(key),
35719
35765
  usage: () => usage(assets),
35720
- sceneBundles: (named) => {
35721
- ctx.state.sceneNamed = [...named];
35722
- },
35766
+ sceneBundles: (named) => nameSceneBundles(ctx.state, named),
35723
35767
  has: (bundle) => Object.hasOwn(ctx.state.manifest.bundles, bundle),
35724
35768
  companions: { add: (companion) => addCompanion(ctx.state, companion) }
35725
35769
  };
@@ -36387,6 +36431,8 @@ function createAssetsState() {
36387
36431
  stamps: void 0,
36388
36432
  swapping: void 0,
36389
36433
  sceneNamed: [],
36434
+ sceneLeaving: [],
36435
+ exiting: 0,
36390
36436
  companions: [],
36391
36437
  restPending: false
36392
36438
  };
@@ -36411,7 +36457,8 @@ const assetsPlugin = /*#__PURE__*/ createPlugin$1("assets", {
36411
36457
  flowPlugin,
36412
36458
  rendererPlugin,
36413
36459
  timePlugin,
36414
- modelPlugin
36460
+ modelPlugin,
36461
+ worldPlugin
36415
36462
  ],
36416
36463
  config: {
36417
36464
  manifest: void 0,
@@ -47931,6 +47978,12 @@ const platformPlugin = /*#__PURE__*/ createPlugin$1("platform", {
47931
47978
  const FALLBACK_ADVANCE = .6;
47932
47979
  /** Line height of a style whose font is not loaded, as a share of the style size. */
47933
47980
  const FALLBACK_LINE = 1.2;
47981
+ /**
47982
+ * How much wider than its box a line of a `"box"` style may be. `ui` lays a text out in whole units
47983
+ * and rounds its measure with this tolerance, so a line that adds up to a hair over a whole unit
47984
+ * (ten glyphs of 14.4 are 144.00000000000003) stays on the line the measure kept it on.
47985
+ */
47986
+ const BOX_SLACK = 1e-4;
47934
47987
  /** Splits a run's text into words, runs of spaces and line breaks, separators kept. */
47935
47988
  const TOKENS = /(\n| +)/;
47936
47989
  /**
@@ -48405,13 +48458,33 @@ function emptyKeys() {
48405
48458
  return /* @__PURE__ */ new Set();
48406
48459
  }
48407
48460
  /**
48461
+ * The width a block of a style wraps at: the width of the style, the box for a `"box"` style that
48462
+ * has one, or nothing for one line per `\n` segment.
48463
+ *
48464
+ * @param style - The style of the label.
48465
+ * @param box - The laid-out width of the text element; 0 when it has none.
48466
+ * @returns The wrap width in reference pixels, or `undefined` for no wrapping.
48467
+ * @example
48468
+ * ```ts
48469
+ * wrapOf({ ...bodyStyle, wrap: "box" }, 0); // undefined: no box, one line
48470
+ * ```
48471
+ */
48472
+ function wrapOf(style, box) {
48473
+ if (style.wrap === "none") return void 0;
48474
+ if (style.wrap !== "box") return style.wrap;
48475
+ return box > 0 ? box + BOX_SLACK : void 0;
48476
+ }
48477
+ /**
48408
48478
  * Lays a block of runs out: the lines, the width of the widest one and the total height. The
48409
- * alignment of the style changes nothing here; it moves the lines when they are drawn.
48479
+ * alignment of the style changes nothing here; it moves the lines when they are drawn. A style
48480
+ * with `wrap: "box"` wraps at `box`, the width `ui` laid its text element out at, and is one line
48481
+ * without one; any other style ignores `box`.
48410
48482
  *
48411
48483
  * @param runs - What `parseTags` made of the resolved string.
48412
48484
  * @param style - The style of the label.
48413
48485
  * @param tables - The advance tables that are loaded, by font key.
48414
48486
  * @param options - Where a warning goes.
48487
+ * @param box - The width a `"box"` style wraps at, in reference pixels; 0 for none.
48415
48488
  * @returns The lines and the size of the block in reference pixels.
48416
48489
  * @example
48417
48490
  * ```ts
@@ -48419,13 +48492,14 @@ function emptyKeys() {
48419
48492
  * // { lines: [{ runs: [], width: 0 }], width: 0, height: 38.4 }
48420
48493
  * ```
48421
48494
  */
48422
- function layoutRuns(runs, style, tables, options) {
48495
+ function layoutRuns(runs, style, tables, options, box = 0) {
48423
48496
  const once = onceOnly(options);
48424
- const lines = style.wrap === "none" ? layoutFixed(runs, style, tables, once) : layoutWrapped(runs, {
48497
+ const wrap = wrapOf(style, box);
48498
+ const lines = wrap === void 0 ? layoutFixed(runs, style, tables, once) : layoutWrapped(runs, {
48425
48499
  style,
48426
48500
  tables,
48427
48501
  options: once
48428
- }, style.wrap);
48502
+ }, wrap);
48429
48503
  let width = 0;
48430
48504
  for (const line of lines) width = Math.max(width, line.width);
48431
48505
  return {
@@ -48720,20 +48794,34 @@ function styleOf$1(ctx, name) {
48720
48794
  return ctx.state.styles.get("body") ?? builtInStyles(ctx.config.fonts).body;
48721
48795
  }
48722
48796
  /**
48797
+ * Tells whether a style wraps at the box of its text element. An unknown name is drawn as `body`,
48798
+ * so it answers for `body`. Two map reads, no warning: the frame step asks it.
48799
+ *
48800
+ * @param ctx - Domain context of the text plugin.
48801
+ * @param styleName - The style name a component wrote.
48802
+ * @returns True for a style with `wrap: "box"`.
48803
+ */
48804
+ function wrapsAtBox(ctx, styleName) {
48805
+ return (ctx.state.styles.get(styleName) ?? ctx.state.styles.get("body"))?.wrap === "box";
48806
+ }
48807
+ /**
48723
48808
  * The layout of one resolved string in one style, from the cache or freshly laid out. The cache
48724
- * is what makes `measure` cheap enough for `ui` to ask per invalidation.
48809
+ * is what makes `measure` cheap enough for `ui` to ask per invalidation. A `"box"` style with a
48810
+ * box is cached per width too; any other style ignores the box and keeps one layout.
48725
48811
  *
48726
48812
  * @param ctx - Domain context of the text plugin.
48727
48813
  * @param resolved - The tagged string.
48728
48814
  * @param styleName - The style name.
48815
+ * @param box - The width a `"box"` style wraps at; 0 for one line.
48729
48816
  * @returns The lines and the size of the block.
48730
48817
  */
48731
- function layoutFor(ctx, resolved, styleName) {
48732
- const key = `${styleName}${CACHE_SEPARATOR}${resolved}`;
48818
+ function layoutFor(ctx, resolved, styleName, box = 0) {
48819
+ const width = box > 0 && wrapsAtBox(ctx, styleName) ? box : 0;
48820
+ const key = width > 0 ? `${styleName}${CACHE_SEPARATOR}${width}${CACHE_SEPARATOR}${resolved}` : `${styleName}${CACHE_SEPARATOR}${resolved}`;
48733
48821
  const hit = ctx.state.cache.get(key);
48734
48822
  if (hit !== void 0) return hit;
48735
48823
  const warn = warnFor(ctx);
48736
- const layout = layoutRuns(parseTags(resolved, warn), styleOf$1(ctx, styleName), ctx.state.tables, { warn });
48824
+ const layout = layoutRuns(parseTags(resolved, warn), styleOf$1(ctx, styleName), ctx.state.tables, { warn }, width);
48737
48825
  if (ctx.state.cache.size >= CACHE_LIMIT) {
48738
48826
  const oldest = ctx.state.cache.keys().next().value;
48739
48827
  if (oldest !== void 0) ctx.state.cache.delete(oldest);
@@ -48942,7 +49030,8 @@ function remember(ctx, entity, text, frame, unit) {
48942
49030
  bindRef: text.bind,
48943
49031
  bind: bindKeyOf(text.bind),
48944
49032
  locale: frame.locale,
48945
- unit
49033
+ unit,
49034
+ box: text.box
48946
49035
  });
48947
49036
  return;
48948
49037
  }
@@ -48954,6 +49043,23 @@ function remember(ctx, entity, text, frame, unit) {
48954
49043
  seen.style = text.style;
48955
49044
  seen.locale = frame.locale;
48956
49045
  seen.unit = unit;
49046
+ seen.box = text.box;
49047
+ }
49048
+ /**
49049
+ * Tells whether a label of a `"box"` style was laid out at another box than it carries now. The
49050
+ * box of a label of any other style is only noted: it draws the same at every width, so a resize
49051
+ * resolves nothing. A label whose box stands costs one compare.
49052
+ *
49053
+ * @param ctx - Domain context of the text plugin.
49054
+ * @param seen - What was resolved for the label last.
49055
+ * @param text - Its component value.
49056
+ * @returns True when the label has to be laid out at its new box.
49057
+ */
49058
+ function boxMoved(ctx, seen, text) {
49059
+ if (seen.box === text.box) return false;
49060
+ if (wrapsAtBox(ctx, text.style)) return true;
49061
+ seen.box = text.box;
49062
+ return false;
48957
49063
  }
48958
49064
  /**
48959
49065
  * Keeps the measured size of a label: made once, then overwritten in place.
@@ -48983,7 +49089,7 @@ function keepSize(ctx, entity, layout) {
48983
49089
  * @returns True when nothing has to be formatted or written.
48984
49090
  */
48985
49091
  function isShown(seen, text, unit, format, frame) {
48986
- return seen !== void 0 && seen.unit === unit && seen.style === text.style && isSameBind(seen, text.bind) && (format !== "duration" || seen.locale === frame.locale);
49092
+ return seen.unit === unit && seen.style === text.style && isSameBind(seen, text.bind) && (format !== "duration" || seen.locale === frame.locale);
48987
49093
  }
48988
49094
  /**
48989
49095
  * Writes the new string of a bound label, its measured size, and for a countdown the time left,
@@ -49001,7 +49107,7 @@ function showBound(ctx, entity, text, next, value) {
49001
49107
  ctx.deps.world.ecs.set(entity, Text, { resolved: next });
49002
49108
  if (value !== void 0 && text.bind !== void 0 && isCountdownLeft(text.bind)) ctx.deps.world.ecs.set(entity, Countdown, { left: value });
49003
49109
  }
49004
- if (changed || !ctx.state.measured.has(entity)) keepSize(ctx, entity, layoutFor(ctx, next, text.style));
49110
+ if (changed || !ctx.state.measured.has(entity)) keepSize(ctx, entity, layoutFor(ctx, next, text.style, text.box));
49005
49111
  }
49006
49112
  /**
49007
49113
  * Writes the string a bound label already shows once more, and measures it again. A mark on a
@@ -49015,7 +49121,7 @@ function showBound(ctx, entity, text, next, value) {
49015
49121
  */
49016
49122
  function showAgain(ctx, entity, text) {
49017
49123
  ctx.deps.world.ecs.set(entity, Text, { resolved: text.resolved });
49018
- keepSize(ctx, entity, layoutFor(ctx, text.resolved, text.style));
49124
+ keepSize(ctx, entity, layoutFor(ctx, text.resolved, text.style, text.box));
49019
49125
  }
49020
49126
  /**
49021
49127
  * Reports a bound label whose style is not a digits style, once per style. An unknown style name
@@ -49033,8 +49139,9 @@ function warnWithoutDigits(ctx, styleName) {
49033
49139
  * One frame of a bound label: the value read every frame, the string built and written only
49034
49140
  * when its unit moved. A label that stands still costs a read and a few compares and keeps
49035
49141
  * nothing: no tag is parsed, no key is built. A marked label that stands still is written once
49036
- * with the string it shows. The style is checked for digits only when the label is written, so
49037
- * a label on a wrong style builds no warning key per frame.
49142
+ * with the string it shows; one of a `"box"` style that ui laid out at another box is measured at
49143
+ * it. The style is checked for digits only when the label is written, so a label on a wrong style
49144
+ * builds no warning key per frame.
49038
49145
  *
49039
49146
  * @param ctx - Domain context of the text plugin.
49040
49147
  * @param entity - The entity.
@@ -49047,10 +49154,9 @@ function stepBound(ctx, entity, text, bind, frame) {
49047
49154
  const format = value === void 0 ? "int" : formatOf(ctx, entity, bind, value);
49048
49155
  const unit = value === void 0 ? void 0 : unitOf(value, format);
49049
49156
  const marked = ctx.state.dirty.delete(entity);
49050
- if (isShown(ctx.state.seen.get(entity), text, unit, format, frame)) {
49051
- if (!marked) return;
49052
- warnWithoutDigits(ctx, text.style);
49053
- showAgain(ctx, entity, text);
49157
+ const seen = ctx.state.seen.get(entity);
49158
+ if (seen !== void 0 && isShown(seen, text, unit, format, frame)) {
49159
+ standStill(ctx, entity, text, seen, marked);
49054
49160
  return;
49055
49161
  }
49056
49162
  warnWithoutDigits(ctx, text.style);
@@ -49058,8 +49164,27 @@ function stepBound(ctx, entity, text, bind, frame) {
49058
49164
  remember(ctx, entity, text, frame, unit);
49059
49165
  }
49060
49166
  /**
49061
- * Tells whether a label has to be resolved again. Change sets are cleared every frame and `ui`
49062
- * writes `Text` after this system, so the comparison is against what was seen, not `changed`.
49167
+ * One frame of a bound label whose unit stands still: a marked one is written once as it is, one of
49168
+ * a `"box"` style that ui laid out at another box is measured at it. Either way the box is noted.
49169
+ *
49170
+ * @param ctx - Domain context of the text plugin.
49171
+ * @param entity - The entity.
49172
+ * @param text - Its component value.
49173
+ * @param seen - What was resolved for the label last.
49174
+ * @param marked - Whether the label was marked this frame.
49175
+ */
49176
+ function standStill(ctx, entity, text, seen, marked) {
49177
+ if (marked) {
49178
+ warnWithoutDigits(ctx, text.style);
49179
+ showAgain(ctx, entity, text);
49180
+ } else if (boxMoved(ctx, seen, text)) keepSize(ctx, entity, layoutFor(ctx, text.resolved, text.style, text.box));
49181
+ else return;
49182
+ seen.box = text.box;
49183
+ }
49184
+ /**
49185
+ * Tells whether a label has to be resolved again: its content, style, bind or locale moved, or
49186
+ * ui laid a label of a `"box"` style out at another box. Change sets are cleared every frame and
49187
+ * `ui` writes `Text` after this system, so the comparison is against what was seen, not `changed`.
49063
49188
  *
49064
49189
  * @param ctx - Domain context of the text plugin.
49065
49190
  * @param entity - The entity.
@@ -49071,7 +49196,7 @@ function isStale(ctx, entity, text, frame) {
49071
49196
  if (ctx.state.dirty.has(entity)) return true;
49072
49197
  const seen = ctx.state.seen.get(entity);
49073
49198
  if (seen === void 0) return true;
49074
- return seen.content !== text.content || seen.style !== text.style || !isSameBind(seen, text.bind) || seen.locale !== frame.locale;
49199
+ return seen.content !== text.content || seen.style !== text.style || !isSameBind(seen, text.bind) || seen.locale !== frame.locale || boxMoved(ctx, seen, text);
49075
49200
  }
49076
49201
  /**
49077
49202
  * One frame of one label.
@@ -49087,7 +49212,7 @@ function stepText(ctx, entity, text, frame) {
49087
49212
  return;
49088
49213
  }
49089
49214
  if (!isStale(ctx, entity, text, frame)) return;
49090
- keepSize(ctx, entity, layoutFor(ctx, resolveContent(ctx, entity, text), text.style));
49215
+ keepSize(ctx, entity, layoutFor(ctx, resolveContent(ctx, entity, text), text.style, text.box));
49091
49216
  ctx.state.dirty.delete(entity);
49092
49217
  remember(ctx, entity, text, frame);
49093
49218
  }
@@ -49661,15 +49786,19 @@ function buildRun(ctx, pixi, run, style, at) {
49661
49786
  }
49662
49787
  /**
49663
49788
  * The style and the laid-out block of one component value, under the generation of this moment.
49789
+ * A `"box"` style wraps at the box `ui` wrote; with none (a label outside a ui layout) it is one
49790
+ * line, and a dev warning says so once per style.
49664
49791
  *
49665
49792
  * @param ctx - Domain context of the text plugin.
49666
49793
  * @param value - The component value.
49667
49794
  * @returns What the container is filled from.
49668
49795
  */
49669
49796
  function labelOf(ctx, value) {
49797
+ const style = styleOf$1(ctx, value.style);
49798
+ if (style.wrap === "box" && (value.box ?? 0) <= 0 && value.resolved !== "") warnOnce(ctx, `wrap-box:${value.style}`, "text: wrap \"box\" outside layout", { style: value.style });
49670
49799
  return {
49671
- style: styleOf$1(ctx, value.style),
49672
- layout: layoutFor(ctx, value.resolved, value.style),
49800
+ style,
49801
+ layout: layoutFor(ctx, value.resolved, value.style, value.box),
49673
49802
  generation: ctx.state.generation
49674
49803
  };
49675
49804
  }
@@ -49786,8 +49915,9 @@ function samePoint(first, second) {
49786
49915
  /**
49787
49916
  * Tells whether a container already shows a value: the string, the style name and the anchor it
49788
49917
  * was last updated with, drawn with the style object that is current and under the generation
49789
- * that is current. A replaced style is a new object under the same name. A font or an icon a dev
49790
- * hot swap replaced moves the generation, because the objects hold textures that are destroyed.
49918
+ * that is current, and for a `"box"` style at the same box. A replaced style is a new object under
49919
+ * the same name. A font or an icon a dev hot swap replaced moves the generation, because the
49920
+ * objects hold textures that are destroyed. The box of any other style changes nothing drawn.
49791
49921
  *
49792
49922
  * @param ctx - Domain context of the text plugin.
49793
49923
  * @param container - The container of this label.
@@ -49797,7 +49927,8 @@ function samePoint(first, second) {
49797
49927
  */
49798
49928
  function isCurrent(ctx, container, previous, next) {
49799
49929
  const drawn = ctx.state.drawn.get(container);
49800
- return previous.resolved === next.resolved && previous.style === next.style && samePoint(previous.anchor, next.anchor) && drawn?.style === styleOf$1(ctx, next.style) && drawn.generation === ctx.state.generation;
49930
+ const boxStands = previous.box === next.box || drawn?.style.wrap !== "box";
49931
+ return previous.resolved === next.resolved && previous.style === next.style && samePoint(previous.anchor, next.anchor) && drawn?.style === styleOf$1(ctx, next.style) && drawn.generation === ctx.state.generation && boxStands;
49801
49932
  }
49802
49933
  /**
49803
49934
  * Draws a changed value into a container that holds a label already. A ticking counter keeps its
@@ -50054,9 +50185,9 @@ function createTextApi(ctx) {
50054
50185
  return domain;
50055
50186
  };
50056
50187
  return {
50057
- measure: (content, style) => {
50188
+ measure: (content, style, width = 0) => {
50058
50189
  const tctx = text();
50059
- return sizeOf(layoutFor(tctx, sourceOf(tctx, content), style));
50190
+ return sizeOf(layoutFor(tctx, sourceOf(tctx, content), style, width));
50060
50191
  },
50061
50192
  hasGlyph: (char, style) => hasGlyphOf(text(), char, style),
50062
50193
  styles: () => Object.freeze([...ctx.state.styles.keys()]),
@@ -54789,6 +54920,16 @@ function entitySet(entities) {
54789
54920
  return new Set(entities);
54790
54921
  }
54791
54922
  /**
54923
+ * Tells whether an element whose `Box` just came arrives with an enter: new, and not a row a
54924
+ * range change scrolled in.
54925
+ *
54926
+ * @param element - The element, if it is still known.
54927
+ * @returns True when its enter hook is to play.
54928
+ */
54929
+ function arrives(element) {
54930
+ return element !== void 0 && !element.entered && !element.scrolledIn;
54931
+ }
54932
+ /**
54792
54933
  * Builds the reconcile half of the jsx module: the two systems of the frame and the root
54793
54934
  * bookkeeping around them.
54794
54935
  *
@@ -55729,7 +55870,8 @@ function createReconciler(ctx, modules, fields) {
55729
55870
  /**
55730
55871
  * Keeps an element whose spawn has just been applied: the world calls it from the `Box` hook, at
55731
55872
  * the flush of phase `layout`. Its enter hook waits for `playEntered`, so the game's systems see
55732
- * the entity first (F48).
55873
+ * the entity first (F48). The hook itself plays at once, so they see the start pose it writes (F49);
55874
+ * `ui.enter` plays it again when one of them changed what it read.
55733
55875
  *
55734
55876
  * @param entity - The entity the `Box` was attached to.
55735
55877
  */
@@ -55738,6 +55880,8 @@ function createReconciler(ctx, modules, fields) {
55738
55880
  if (entering.count < entering.list.length) entering.list[entering.count] = entity;
55739
55881
  else entering.list.push(entity);
55740
55882
  entering.count += 1;
55883
+ const element = state.elements.get(entity);
55884
+ if (arrives(element)) modules.layout.pose(element, overlayOf(element) === void 0 ? void 0 : createSpan());
55741
55885
  }
55742
55886
  /**
55743
55887
  * Plays the enter hooks queued since the last call, in the order the elements came. The `ui.enter`
@@ -56593,6 +56737,203 @@ function watched(handle, span, watch) {
56593
56737
  };
56594
56738
  }
56595
56739
  /**
56740
+ * A handle that keeps what an enter hook read and wrote: the components it read with `get`, the
56741
+ * components it wrote, and every `set` with the values it wrote over. Everything goes on to the
56742
+ * real handle.
56743
+ *
56744
+ * @param handle - The handle the hook plays on.
56745
+ * @param posed - Where it is kept.
56746
+ * @returns The recording handle.
56747
+ */
56748
+ function recording(handle, posed) {
56749
+ const wrote = (component) => {
56750
+ if (!posed.written.includes(component.componentName)) posed.written.push(component.componentName);
56751
+ };
56752
+ return {
56753
+ entity: handle.entity,
56754
+ key: handle.key,
56755
+ get: (component) => {
56756
+ if (!posed.reads.some((read) => read.component === component)) posed.reads.push({
56757
+ component: asAnyComponent(component),
56758
+ seen: {}
56759
+ });
56760
+ return handle.get(component);
56761
+ },
56762
+ rest: (component) => handle.rest(component),
56763
+ set: (component, patch) => {
56764
+ wrote(component);
56765
+ keepWrite(posed, asAnyComponent(component), patch, handle.get(component));
56766
+ handle.set(component, patch);
56767
+ },
56768
+ tween: (component, to, options) => {
56769
+ wrote(component);
56770
+ return handle.tween(component, to, options);
56771
+ },
56772
+ toRest: (component, options) => {
56773
+ wrote(component);
56774
+ return handle.toRest(component, options);
56775
+ },
56776
+ all: (handles) => handle.all(handles),
56777
+ peer: (key) => handle.peer(key)
56778
+ };
56779
+ }
56780
+ /**
56781
+ * Keeps one `set` of an enter hook with the values its fields had before it. A component the
56782
+ * entity does not carry yet has nothing to go back to: a replay writes it again.
56783
+ *
56784
+ * @param posed - Where the write is kept.
56785
+ * @param component - The component the hook wrote.
56786
+ * @param patch - The fields it wrote.
56787
+ * @param current - The component's value before the write, if the entity carries it.
56788
+ */
56789
+ function keepWrite(posed, component, patch, current) {
56790
+ if (current === void 0) return;
56791
+ const wrote = { ...patch };
56792
+ const was = current;
56793
+ const before = {};
56794
+ for (const field of Object.keys(wrote)) before[field] = was[field];
56795
+ posed.writes.push({
56796
+ component,
56797
+ wrote,
56798
+ before
56799
+ });
56800
+ }
56801
+ /**
56802
+ * A component type as the record of an enter play keeps it.
56803
+ *
56804
+ * @param component - The component type a hook used.
56805
+ * @returns The same type, read as a record of fields.
56806
+ */
56807
+ function asAnyComponent(component) {
56808
+ return component;
56809
+ }
56810
+ /**
56811
+ * Plays the enter hook of an element at its `Box`, before the game's systems of phase `sync` see
56812
+ * the element, so they see its start pose (F49). What the hook read and wrote is kept on the
56813
+ * element: `enterPosed` plays it again in `ui.enter` when a game system changed a component it
56814
+ * read (F48), and keeps this play otherwise.
56815
+ *
56816
+ * @param ctx - Domain context of the ui plugin.
56817
+ * @param element - The element whose `Box` was just attached.
56818
+ * @param span - Notes the longest finite track the hook starts, for the level of an overlay.
56819
+ */
56820
+ function poseEnter(ctx, element, span) {
56821
+ const hook = element.motion?.enter;
56822
+ if (hook === void 0) return;
56823
+ const posed = {
56824
+ motion: void 0,
56825
+ reads: [],
56826
+ written: [],
56827
+ writes: [],
56828
+ span
56829
+ };
56830
+ posed.motion = play(ctx, element, (view) => hook(recording(view, posed), element.node, void 0, element.cause), span);
56831
+ element.posed = posed;
56832
+ const ecs = ctx.deps.world.ecs;
56833
+ for (const read of posed.reads) {
56834
+ if (posed.written.includes(read.component.componentName)) continue;
56835
+ read.seen = { ...ecs.get(element.entity, read.component) };
56836
+ }
56837
+ }
56838
+ /**
56839
+ * Tells whether a game system changed a component the enter hook read at the `Box`.
56840
+ *
56841
+ * @param ctx - Domain context of the ui plugin.
56842
+ * @param element - The element.
56843
+ * @param posed - What the play at the `Box` kept.
56844
+ * @returns True when a field the hook could have read is not what it was.
56845
+ */
56846
+ function readChanged(ctx, element, posed) {
56847
+ const ecs = ctx.deps.world.ecs;
56848
+ for (const read of posed.reads) {
56849
+ if (posed.written.includes(read.component.componentName)) continue;
56850
+ if (fieldsDiffer(read.seen, ecs.get(element.entity, read.component))) return true;
56851
+ }
56852
+ return false;
56853
+ }
56854
+ /**
56855
+ * Tells whether a component's value is not the one a play saw: a field differs, one came or went,
56856
+ * or the component itself went.
56857
+ *
56858
+ * @param seen - The fields as the play saw them.
56859
+ * @param now - The value now, or nothing when the entity lost the component.
56860
+ * @returns True when they differ.
56861
+ * @example
56862
+ * ```ts
56863
+ * fieldsDiffer({ dy: 300 }, { dy: 180 }); // true
56864
+ * ```
56865
+ */
56866
+ function fieldsDiffer(seen, now) {
56867
+ if (now === void 0) return Object.keys(seen).length > 0;
56868
+ const value = now;
56869
+ const fields = /* @__PURE__ */ new Set([...Object.keys(seen), ...Object.keys(value)]);
56870
+ for (const field of fields) if (value[field] !== seen[field]) return true;
56871
+ return false;
56872
+ }
56873
+ /**
56874
+ * Undoes the play at the `Box` before the hook plays again: its motion is cancelled and forgotten,
56875
+ * and a field still holding what a `set` wrote gets its value from before. A field written since
56876
+ * keeps the new value. The later writes go back first, so the first one's value is what stays.
56877
+ *
56878
+ * @param ctx - Domain context of the ui plugin.
56879
+ * @param element - The element.
56880
+ * @param posed - What the play at the `Box` kept.
56881
+ */
56882
+ function undoPosed(ctx, element, posed) {
56883
+ forgetMotion(element, posed.motion);
56884
+ for (const write of posed.writes.toReversed()) revertWrite(ctx, element, write);
56885
+ }
56886
+ /**
56887
+ * Cancels the motion of a play that is undone and takes it out of the element's handles.
56888
+ *
56889
+ * @param element - The element.
56890
+ * @param motion - The motion the play returned, if any.
56891
+ */
56892
+ function forgetMotion(element, motion) {
56893
+ if (motion === void 0) return;
56894
+ motion.cancel();
56895
+ const at = element.handles.indexOf(motion);
56896
+ if (at !== -1) element.handles.splice(at, 1);
56897
+ element.endless?.delete(motion);
56898
+ }
56899
+ /**
56900
+ * Gives one `set` of an undone play back: each field still holding what it wrote gets the value it
56901
+ * had before.
56902
+ *
56903
+ * @param ctx - Domain context of the ui plugin.
56904
+ * @param element - The element.
56905
+ * @param write - The write to take back.
56906
+ */
56907
+ function revertWrite(ctx, element, write) {
56908
+ const ecs = ctx.deps.world.ecs;
56909
+ const current = ecs.get(element.entity, write.component);
56910
+ if (current === void 0) return;
56911
+ const kept = Object.keys(write.wrote).filter((field) => current[field] === write.wrote[field]);
56912
+ if (kept.length === 0) return;
56913
+ ecs.set(element.entity, write.component, Object.fromEntries(kept.map((field) => [field, write.before[field]])));
56914
+ }
56915
+ /**
56916
+ * Settles the enter hook an element played at its `Box`, in `ui.enter`: when a game system of
56917
+ * `sync` changed a component the hook read, the play is undone and the hook plays again on what
56918
+ * the system wrote (F48); otherwise the play stands, and an overlay's span gets what it noted.
56919
+ *
56920
+ * @param ctx - Domain context of the ui plugin.
56921
+ * @param element - The element whose enter plays now.
56922
+ * @param span - Where the longest finite track is noted, for the level of an overlay.
56923
+ * @returns True when the play at the `Box` stands; false when there was none, or it was undone.
56924
+ */
56925
+ function enterPosed(ctx, element, span) {
56926
+ const posed = element.posed;
56927
+ if (posed === void 0) return false;
56928
+ element.posed = void 0;
56929
+ if (readChanged(ctx, element, posed)) {
56930
+ undoPosed(ctx, element, posed);
56931
+ return false;
56932
+ }
56933
+ if (span !== void 0 && posed.span !== void 0) Object.assign(span, posed.span);
56934
+ return true;
56935
+ }
56936
+ /**
56596
56937
  * Runs one motion hook and keeps the handle it returned, so the exit sweep knows when the
56597
56938
  * element is still playing. A motion whose hook started a track that repeats forever is also
56598
56939
  * noted as endless: the sweep never waits for it. Without a hook the value the solve wrote stands;
@@ -57175,23 +57516,55 @@ function shownOf(element, source) {
57175
57516
  }
57176
57517
  /**
57177
57518
  * The size a text or an icon takes before Yoga clamps it: an icon is square on one line of its
57178
- * text style, a bound text measures the string it shows, any other text its content. The size is
57179
- * rounded up to whole units. Yoga rounds a node with a measure function outward but sizes its
57180
- * parent from the unrounded length, so a fractional size would run into the parent's padding.
57519
+ * text style, a bound text measures the string it shows, any other text its content, wrapped at
57520
+ * `width` when its style wraps at its box. The size is rounded up to whole units. Yoga rounds a
57521
+ * node with a measure function outward but sizes its parent from the unrounded length, so a
57522
+ * fractional size would run into the parent's padding.
57181
57523
  *
57182
57524
  * @param element - The text or icon element.
57183
57525
  * @param source - What it is measured through.
57526
+ * @param width - The width Yoga offers, or `undefined` when it bounds none.
57184
57527
  * @returns The width and the height, in whole units.
57185
57528
  */
57186
- function naturalSize(element, source) {
57529
+ function naturalSize(element, source, width) {
57187
57530
  const { content, style } = contentOf$1(element);
57188
- const size = element.type === "icon" ? squareOf(source.measure(LINE_SAMPLE, style)) : source.measure(shownOf(element, source) ?? content, style);
57531
+ const size = element.type === "icon" ? squareOf(source.measure(LINE_SAMPLE, style)) : source.measure(shownOf(element, source) ?? content, style, width);
57189
57532
  return {
57190
57533
  width: Math.ceil(size.width - WHOLE_EPSILON),
57191
57534
  height: Math.ceil(size.height - WHOLE_EPSILON)
57192
57535
  };
57193
57536
  }
57194
57537
  /**
57538
+ * The width Yoga bounds a measure with: what it offers in an exact or an at-most mode, nothing in
57539
+ * an undefined one, where a text is as wide as one line of it.
57540
+ *
57541
+ * @param yoga - The loaded Yoga module, for the measure modes.
57542
+ * @param width - What Yoga offered on the width.
57543
+ * @param mode - How to read that width.
57544
+ * @returns The width, or `undefined` when none is bounded.
57545
+ */
57546
+ function offeredWidth(yoga, width, mode) {
57547
+ if (mode === yoga.MEASURE_MODE_UNDEFINED || !Number.isFinite(width)) return void 0;
57548
+ return width;
57549
+ }
57550
+ /**
57551
+ * The width a text wrapped at: the width Yoga offered, when the text measures to another size
57552
+ * there than on one line, which only a style with `wrap: "box"` does. A text that fits, any other
57553
+ * style and an icon wrapped at none, so `ui` writes no box on them and a described or saved `Text`
57554
+ * carries none.
57555
+ *
57556
+ * @param element - The text or icon element.
57557
+ * @param source - What it is measured through.
57558
+ * @param offered - The width Yoga offered, or `undefined` when it bounded none.
57559
+ * @param size - What the element measured at `offered`.
57560
+ * @returns The width, or `undefined` when the text did not wrap at it.
57561
+ */
57562
+ function wrappedAt(element, source, offered, size) {
57563
+ if (offered === void 0 || element.type !== "text") return void 0;
57564
+ const line = naturalSize(element, source);
57565
+ return line.width !== size.width || line.height !== size.height ? offered : void 0;
57566
+ }
57567
+ /**
57195
57568
  * Clamps one axis of a measured size to what Yoga offered on that axis.
57196
57569
  *
57197
57570
  * @param yoga - The loaded Yoga module, for the three measure modes.
@@ -57227,7 +57600,8 @@ function clampToMode(yoga, measured, asked) {
57227
57600
  }
57228
57601
  /**
57229
57602
  * Installs the measure function of a text element, or drops it when the text is fixed. The
57230
- * function keeps the size it answered on the element, for `remeasureShown`.
57603
+ * function keeps the size it answered and the width it was offered on the element, for
57604
+ * `remeasureShown`.
57231
57605
  *
57232
57606
  * @param state - The layout state, for the Yoga module and the `measured` counter.
57233
57607
  * @param node - The node of the element.
@@ -57242,9 +57616,11 @@ function installMeasure(state, node, element, source) {
57242
57616
  return;
57243
57617
  }
57244
57618
  const measureFunction = (width, widthMode, height, heightMode) => {
57245
- const size = naturalSize(element, source);
57619
+ const offered = offeredWidth(yoga, width, widthMode);
57620
+ const size = naturalSize(element, source, offered);
57246
57621
  state.measured += 1;
57247
57622
  element.measuredSize = size;
57623
+ element.measuredWidth = wrappedAt(element, source, offered, size);
57248
57624
  if (element.type === "icon") state.iconLine = lineOf(source);
57249
57625
  return clampToMode(yoga, size, {
57250
57626
  width,
@@ -57266,12 +57642,29 @@ function markMeasured(state, element) {
57266
57642
  state.byEntity.get(element.entity)?.markDirty();
57267
57643
  }
57268
57644
  /**
57645
+ * The width a text that was measured inside a bound width is laid out at now: its node's content
57646
+ * width from the last solve. Yoga may serve a width it measured before from its own cache without
57647
+ * calling the measure function, so the width that function was offered last can be another one.
57648
+ *
57649
+ * @param state - The layout state, for the edge enums of Yoga.
57650
+ * @param node - The node of the text.
57651
+ * @param element - The text element.
57652
+ * @returns The content width, or `undefined` when the text was measured unbounded.
57653
+ */
57654
+ function laidOutWidth(state, node, element) {
57655
+ const yoga = state.yoga;
57656
+ if (element.measuredWidth === void 0 || yoga === void 0) return void 0;
57657
+ const padding = node.getComputedPadding(yoga.EDGE_LEFT) + node.getComputedPadding(yoga.EDGE_RIGHT);
57658
+ return node.getComputedWidth() - padding;
57659
+ }
57660
+ /**
57269
57661
  * Marks the node of a text dirty when what it draws now measures to another size than the node was
57270
57662
  * laid out with. `text` writes `Text` when the shown string of a bound label changes, and for
57271
57663
  * every label it resolves again: a message after a new locale, any label when its font arrives or
57272
57664
  * its style is swapped. So a text without `bind` is measured again too, and a counter that rolls
57273
- * through digits of one width asks for no solve. A text with a fixed size has no measure function
57274
- * and is left alone.
57665
+ * through digits of one width asks for no solve. A text whose width was bounded is measured at
57666
+ * the width it is laid out at now, so one that wraps at its box compares the same lines. A text
57667
+ * with a fixed size has no measure function and is left alone.
57275
57668
  *
57276
57669
  * @param state - The layout state, for the node of the element.
57277
57670
  * @param element - An element whose `Text` changed this frame.
@@ -57283,7 +57676,7 @@ function remeasureShown(state, element, source) {
57283
57676
  const before = element.measuredSize;
57284
57677
  if (node === void 0 || before === void 0) return false;
57285
57678
  if (!needsMeasure(element)) return false;
57286
- const size = naturalSize(element, source);
57679
+ const size = naturalSize(element, source, laidOutWidth(state, node, element));
57287
57680
  if (size.width === before.width && size.height === before.height) return false;
57288
57681
  node.markDirty();
57289
57682
  return true;
@@ -58139,7 +58532,7 @@ async function loadYogaModule() {
58139
58532
  */
58140
58533
  function textSourceOf(ctx) {
58141
58534
  return {
58142
- measure: (content, style) => ctx.deps.text.measure(content, style),
58535
+ measure: (content, style, width) => ctx.deps.text.measure(content, style, width),
58143
58536
  resolved: (entity) => ctx.deps.world.ecs.get(entity, Text)?.resolved,
58144
58537
  duration: (ms) => ctx.deps.i18n.duration(ms)
58145
58538
  };
@@ -58202,9 +58595,11 @@ function createLayoutApi(ctx) {
58202
58595
  },
58203
58596
  enter: (element, span) => {
58204
58597
  const hook = element.motion?.enter;
58205
- play(ctx, element, hook === void 0 ? void 0 : (view) => hook(view, element.node, void 0, element.cause), span);
58598
+ const played = hook === void 0 ? void 0 : (view) => hook(view, element.node, void 0, element.cause);
58599
+ if (!enterPosed(ctx, element, span)) play(ctx, element, played, span);
58206
58600
  startLoop(ctx, element);
58207
58601
  },
58602
+ pose: (element, span) => poseEnter(ctx, element, span),
58208
58603
  loop: (element) => startLoop(ctx, element),
58209
58604
  change: (element, previous, hint) => {
58210
58605
  const hook = element.motion?.change?.Box;
@@ -58787,8 +59182,9 @@ function registerComponents(ctx, jsx) {
58787
59182
  * control, so the cursor shows a hand over a local-state button. On an agent page also the tap
58788
59183
  * counter of `DebugTrigger` and its check of a view that takes no press.
58789
59184
  *
58790
- * The `Box` hook only queues an element; `ui.enter` plays its enter hook after the game's systems
58791
- * of the frame and before it is drawn (F48). Every remover goes into the state, so the teardown
59185
+ * The `Box` hook plays an element's enter hook and queues it, so the game's systems see its start
59186
+ * pose (F49); `ui.enter` starts its loop after them and before the frame is drawn, and plays the
59187
+ * hook again when a system changed what it read (F48). Every remover goes into the state, so the teardown
58792
59188
  * closes exactly these, and gives every hosted view its layer back before the ui entities go.
58793
59189
  *
58794
59190
  * @param ctx - Domain context of the ui plugin.