@moku-labs/game 0.14.11 → 0.14.12

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.
@@ -1399,14 +1399,18 @@ function invoke(state, log, descriptor, signal) {
1399
1399
  }
1400
1400
  /**
1401
1401
  * Runs the handler of an awaited effect. A missing handler resolves `undefined` instantly, so a
1402
- * game without `audio` runs; a `cosmetic` descriptor swallows the failure the same way.
1402
+ * game without `audio` runs. A `cosmetic` descriptor swallows the failure the same way, after one
1403
+ * warn entry `flow:fx-cosmetic-failed`: a broken sound never fails the node, and it is never
1404
+ * silent either. A failure that arrives after the abort of the signal is no failure: the handler
1405
+ * was told to stop, so nothing is written.
1403
1406
  *
1404
1407
  * @param state - fx module state.
1408
+ * @param log - The engine log.
1405
1409
  * @param descriptor - The awaited descriptor.
1406
- * @param signal - The node's abort signal.
1410
+ * @param signal - The signal the handler is called with.
1407
1411
  * @returns What the handler resolved with.
1408
1412
  */
1409
- async function effectValue(state, descriptor, signal) {
1413
+ async function effectValue(state, log, descriptor, signal) {
1410
1414
  const entry = handlerFor(state, descriptor.kind);
1411
1415
  if (entry === void 0) return;
1412
1416
  try {
@@ -1415,8 +1419,12 @@ async function effectValue(state, descriptor, signal) {
1415
1419
  mode: state.mode
1416
1420
  });
1417
1421
  } catch (error) {
1418
- if (descriptor.cosmetic === true) return;
1419
- throw error;
1422
+ if (descriptor.cosmetic !== true) throw error;
1423
+ if (signal.aborted) return;
1424
+ log.warn("flow:fx-cosmetic-failed", {
1425
+ kind: descriptor.kind,
1426
+ message: asError$5(error).message
1427
+ });
1420
1428
  }
1421
1429
  }
1422
1430
  /**
@@ -1567,7 +1575,7 @@ function createFxApi(ctx, deps) {
1567
1575
  const answers = descriptor.answers;
1568
1576
  if (answers !== void 0) return openForAnswers(ctx, deps.gate, descriptor, answers, signal);
1569
1577
  const shown = descriptor.kind === "guide" ? beginGuide(state, signal) : signal;
1570
- const work = effectValue(state, descriptor, shown);
1578
+ const work = effectValue(state, ctx.log, descriptor, shown);
1571
1579
  if (state.mode === "fast" || !ctx.deps.time.isRunning()) return work;
1572
1580
  return enqueue(state, work);
1573
1581
  },
@@ -3340,6 +3348,25 @@ function refuseEffect(signal) {
3340
3348
  return refused;
3341
3349
  }
3342
3350
  /**
3351
+ * Refuses a descriptor that `fx.start` may not start: one with `answers`, and a `guide`. Both end
3352
+ * with an answer, and an answer has to be awaited. Every other descriptor passes. The one place
3353
+ * this rule and its message are written: the `fx` of a node context calls it, and so does
3354
+ * `fakeFx` of the testing entry, so a unit test gets the error the game would get.
3355
+ *
3356
+ * @param descriptor - The descriptor a body hands to `fx.start`.
3357
+ * @param path - Runtime path of the node, named in the message.
3358
+ * @throws {Error} When the descriptor has `answers` or is a `guide`.
3359
+ * @example
3360
+ * ```ts
3361
+ * refuseStart({ kind: "sfx" }, "board/place"); // undefined: a sound may be started
3362
+ * refuseStart({ kind: "popup", answers: ["ok"] }, "board/place"); // throws '[game] fx.start() got the effect "popup" in the node "board/place", …'
3363
+ * ```
3364
+ */
3365
+ function refuseStart(descriptor, path) {
3366
+ if (descriptor.answers === void 0 && descriptor.kind !== "guide") return;
3367
+ throw new Error(`[game] fx.start() got the effect "${descriptor.kind}" in the node "${path}", and its answer has to be awaited.\n Write await fx(...) for a descriptor with answers and for a guide.`);
3368
+ }
3369
+ /**
3343
3370
  * Builds the `fx` of a node context: awaited effects by call, effects the body does not wait for
3344
3371
  * by `start`, cosmetic hints by `emit`. A `guide` narrows the gate while it runs; the runner lifts
3345
3372
  * the narrow when the node is left. A body that was aborted keeps running, since the loop only
@@ -3373,8 +3400,9 @@ function nodeFx(modules, step) {
3373
3400
  * Starts one effect and does not wait for it. It takes the path of an awaited effect, so the
3374
3401
  * mode and the node's signal apply; only its end is nobody's wait. A failure is the handler's:
3375
3402
  * it goes to the log and never to the node. A `cosmetic` descriptor does not get that far: the
3376
- * gateway swallows its failure. After the abort of the node nothing is started and nothing is
3377
- * thrown, not even for a descriptor refused otherwise. An effect that ends with a rejection
3403
+ * gateway swallows its failure and writes the warn entry `flow:fx-cosmetic-failed` itself, so
3404
+ * no `flow:fx-start-failed` follows. After the abort of the node nothing is started and nothing
3405
+ * is thrown, not even for a descriptor refused otherwise. An effect that ends with a rejection
3378
3406
  * after the abort did not fail: its handler was told to stop, so nothing is logged.
3379
3407
  *
3380
3408
  * @param descriptor - The descriptor the node starts.
@@ -3382,7 +3410,7 @@ function nodeFx(modules, step) {
3382
3410
  */
3383
3411
  const start = (descriptor) => {
3384
3412
  if (step.signal.aborted) return;
3385
- if (descriptor.answers !== void 0 || descriptor.kind === "guide") throw new Error(`[game] fx.start() got the effect "${descriptor.kind}" in the node "${step.path}", and its answer has to be awaited.\n Write await fx(...) for a descriptor with answers and for a guide.`);
3413
+ refuseStart(descriptor, step.path);
3386
3414
  modules.fx.run(descriptor, step.signal).catch((error) => {
3387
3415
  if (step.signal.aborted) return;
3388
3416
  step.log.error("flow:fx-start-failed", {
@@ -28265,16 +28293,18 @@ const Covered = /*#__PURE__*/ tag("Covered");
28265
28293
  */
28266
28294
  const Escapable = /*#__PURE__*/ tag("Escapable");
28267
28295
  /**
28268
- * The three Yoga counters, the reconcile count of the frame and the range re-renders of the
28269
- * windowed lists. Written by the two systems of `ui`, read by the acceptance tests. Not part of
28270
- * `app.ui`.
28296
+ * The three Yoga counters, the reconcile count of the frame, the range re-renders of the windowed
28297
+ * lists, and the style and write passes of the reconcile. Written by the two systems of `ui`, read
28298
+ * by the acceptance tests. Not part of `app.ui`.
28271
28299
  */
28272
28300
  const UiCounters = /*#__PURE__*/ resource("UiCounters", {
28273
28301
  solves: 0,
28274
28302
  nodes: 0,
28275
28303
  measured: 0,
28276
28304
  reconciles: 0,
28277
- windowRenders: 0
28305
+ windowRenders: 0,
28306
+ styled: 0,
28307
+ written: 0
28278
28308
  });
28279
28309
  /**
28280
28310
  * Builds the effect a node awaits to show a popup. The gate opens for the outcomes of the
@@ -31010,6 +31040,19 @@ function createRange(ctx, layout, reconcile) {
31010
31040
  return scroll?.window === void 0 ? void 0 : scroll;
31011
31041
  }
31012
31042
  /**
31043
+ * The height an element is laid out at as a row of a windowed scroll, whatever its style says.
31044
+ * The reconcile keeps it with the resolved style: a row whose scroll names another row height
31045
+ * is laid out again, though its own description says the same.
31046
+ *
31047
+ * @param parent - The parent of the element.
31048
+ * @param node - The node of this render.
31049
+ * @returns The row height, or `undefined` for an element that is no row of a windowed scroll.
31050
+ */
31051
+ function rowHeightOf(parent, node) {
31052
+ const scroll = windowedScrollOf(parent);
31053
+ return scroll?.window === void 0 || isOwnKey(node.key) ? void 0 : scroll.window.rowHeight;
31054
+ }
31055
+ /**
31013
31056
  * The style an element is laid out with, from the style it resolved to. A row of a windowed
31014
31057
  * scroll is laid out at the row height, whatever its style says; a row style with another
31015
31058
  * height is one warning per scroll.
@@ -31192,6 +31235,7 @@ function createRange(ctx, layout, reconcile) {
31192
31235
  }
31193
31236
  return {
31194
31237
  rowStyle,
31238
+ rowHeightOf,
31195
31239
  slotsFor,
31196
31240
  step,
31197
31241
  dropRow,
@@ -31202,14 +31246,18 @@ function createRange(ctx, layout, reconcile) {
31202
31246
  //#endregion
31203
31247
  //#region src/plugins/ui/jsx/remeasure.ts
31204
31248
  /**
31205
- * @file ui/jsx — the bound-text remeasure step: a text whose shown string changed this frame is
31206
- * measured again, and its root solves only when the new string takes another size.
31249
+ * @file ui/jsx — the remeasure step: a text `text` wrote this frame is measured again, and its
31250
+ * root solves only when it takes another size. A bound text is written when the string it shows
31251
+ * changes; any text when `text` resolves it again, after a new locale or when its font arrives.
31252
+ * An icon without a size carries no `Text`: it is measured again when the line it is squared on
31253
+ * moved.
31207
31254
  */
31208
31255
  /**
31209
- * Asks for a solve of every root with a bound text whose shown string measures to another size
31210
- * now. `text` writes `resolved` earlier in this phase, and only when the shown string changed,
31211
- * so a counter that stands still walks nothing. A leaving text is out of the flow: its roll
31212
- * solves nothing.
31256
+ * Asks for a solve of every root with a text that measures to another size now. `text` writes
31257
+ * `resolved` earlier in this phase: for a bound text when the string it shows changed, so a
31258
+ * counter that stands still walks nothing, and for every label it resolved again, a message after
31259
+ * a new locale or any label whose font arrived. No root is reconciled for it and no view runs. A
31260
+ * leaving text is out of the flow: its roll solves nothing.
31213
31261
  *
31214
31262
  * @param ctx - Domain context of the ui plugin.
31215
31263
  * @param state - The jsx state: the elements and their roots.
@@ -31224,14 +31272,109 @@ function markResizedTexts(ctx, state, layout) {
31224
31272
  if (layout.remeasure(element)) root.needsSolve = true;
31225
31273
  }
31226
31274
  }
31275
+ /**
31276
+ * Asks for a solve of every root with an icon that takes another size now. An icon without a
31277
+ * size is square on one line of the `body` text style and carries no `Text`, so `text` never
31278
+ * names it. The layout reads that line once per call, and the icons are walked only when it
31279
+ * moved: the body font arrived, left or was replaced, with or without a text on the screen. A
31280
+ * leaving icon is out of the flow.
31281
+ *
31282
+ * @param ctx - Domain context of the ui plugin.
31283
+ * @param state - The jsx state: the elements and their roots.
31284
+ * @param layout - The layout module, which knows the line and measures the icon again.
31285
+ */
31286
+ function markResizedIcons(ctx, state, layout) {
31287
+ if (!layout.iconLineMoved()) return;
31288
+ const ecs = ctx.deps.world.ecs;
31289
+ for (const element of state.elements.values()) {
31290
+ const root = state.roots.get(element.root);
31291
+ if (!(element.type === "icon" && !ecs.has(element.entity, Exiting)) || root === void 0) continue;
31292
+ if (layout.remeasure(element)) root.needsSolve = true;
31293
+ }
31294
+ }
31295
+ //#endregion
31296
+ //#region src/plugins/ui/jsx/same.ts
31297
+ /**
31298
+ * @file ui/jsx — the comparison of two prop values of a description. A view builds its props
31299
+ * again at every render, so identity says nothing about plain data: a plain object and an array
31300
+ * are compared by value. Anything else is compared by identity, because only its owner knows what
31301
+ * it holds: a function, a `Set`, a `Map`, a `Date`, a class instance. Pure.
31302
+ */
31303
+ /**
31304
+ * Tells a plain object from any other: one written as a literal, or made without a prototype.
31305
+ *
31306
+ * @param value - An object that is no array.
31307
+ * @returns True when its own enumerable keys are all it holds.
31308
+ * @example
31309
+ * ```ts
31310
+ * isPlain(new Set(["a"])); // false
31311
+ * ```
31312
+ */
31313
+ function isPlain(value) {
31314
+ const prototype = Object.getPrototypeOf(value);
31315
+ return prototype === Object.prototype || prototype === null;
31316
+ }
31317
+ /**
31318
+ * Compares two lists by value: the same length and the same entry at every index.
31319
+ *
31320
+ * @param left - The list of the last render.
31321
+ * @param right - What this render wrote in its place.
31322
+ * @returns True when `right` is a list that says the same.
31323
+ * @example
31324
+ * ```ts
31325
+ * sameList(["a", { n: 1 }], ["a", { n: 1 }]); // true
31326
+ * ```
31327
+ */
31328
+ function sameList(left, right) {
31329
+ return Array.isArray(right) && left.length === right.length && left.every((entry, index) => sameValue(entry, right[index]));
31330
+ }
31331
+ /**
31332
+ * Compares two plain objects by value: the same own keys, each with the same value.
31333
+ *
31334
+ * @param left - The object of the last render.
31335
+ * @param right - The object of this render.
31336
+ * @returns True when both say the same.
31337
+ * @example
31338
+ * ```ts
31339
+ * sameFieldsDeep({ gap: 8, padding: { top: 4 } }, { gap: 8, padding: { top: 4 } }); // true
31340
+ * ```
31341
+ */
31342
+ function sameFieldsDeep(left, right) {
31343
+ const keys = Object.keys(left);
31344
+ return keys.length === Object.keys(right).length && keys.every((key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]));
31345
+ }
31346
+ /**
31347
+ * Tells whether two prop values say the same. Plain objects and arrays are compared by value,
31348
+ * recursing with this function; everything else by identity: a function, a `Set`, a `Map`, a
31349
+ * `Date`, a typed array, a class instance. So two sets are never the same, whatever they hold,
31350
+ * and a hook built once is the same in every new object around it. `NaN` is the same as `NaN`.
31351
+ *
31352
+ * @param left - The value of the last render.
31353
+ * @param right - The value of this render.
31354
+ * @returns True when nothing has to be done for the difference.
31355
+ * @example
31356
+ * ```ts
31357
+ * sameValue({ gap: 8, is: { hover: { gap: 4 } } }, { gap: 8, is: { hover: { gap: 4 } } }); // true
31358
+ * sameValue({ picked: new Set(["a"]) }, { picked: new Set(["a"]) }); // false
31359
+ * ```
31360
+ */
31361
+ function sameValue(left, right) {
31362
+ if (left === right) return true;
31363
+ if (!(typeof left === "object" && typeof right === "object" && left !== null && right !== null)) return Number.isNaN(left) && Number.isNaN(right);
31364
+ if (Array.isArray(left)) return sameList(left, right);
31365
+ if (Array.isArray(right) || !isPlain(left) || !isPlain(right)) return false;
31366
+ return sameFieldsDeep(left, right);
31367
+ }
31227
31368
  //#endregion
31228
31369
  //#region src/plugins/ui/jsx/reconcile.ts
31229
31370
  /**
31230
31371
  * @file ui/jsx — the reconcile: a description tree diffed by identity into the entities `ui`
31231
- * owns, and the solve that gives every one of them its rect before it is ever drawn. Beside it:
31372
+ * owns, and the solve that gives every one of them its rect before it is ever drawn. An element
31373
+ * that stays is compared prop by prop, by value: one that says what it said is neither styled nor
31374
+ * written. Beside it:
31232
31375
  * the range of a windowed scroll (`range.ts`), the rest and the motion of the extras
31233
- * (`extras.ts`), the hint routed to a keyed element (`hints.ts`) and the bound-text remeasure
31234
- * (`remeasure.ts`).
31376
+ * (`extras.ts`), the hint routed to a keyed element (`hints.ts`) and the remeasure of the texts
31377
+ * `text` resolved again (`remeasure.ts`).
31235
31378
  */
31236
31379
  /** The four components an element may be drawn with; a state change can trade one for another. */
31237
31380
  const VISUALS = [
@@ -31281,6 +31424,43 @@ const ELEMENT_OWNED = namesOf$1([
31281
31424
  /** The style of a scroll's content: one object, so a render never reads as a new style. */
31282
31425
  const CONTENT_STYLE = Object.freeze({ origin: "topLeft" });
31283
31426
  /**
31427
+ * The props, besides `style`, `state` and `content`, that a live element is written from: `bind`,
31428
+ * `texture`, `name` and `fit` reach its visual, `intent`, `payload`, `local` and `escape` its
31429
+ * input, `components` its extras and `motion` its hooks. Every other prop is read from the node
31430
+ * where it is used, at every patch: `hosts`, the props of a windowed scroll, the props of a text
31431
+ * field, and the children.
31432
+ */
31433
+ const WRITTEN = [
31434
+ "bind",
31435
+ "texture",
31436
+ "name",
31437
+ "fit",
31438
+ "components",
31439
+ "motion",
31440
+ "intent",
31441
+ "payload",
31442
+ "local",
31443
+ "escape"
31444
+ ];
31445
+ /**
31446
+ * Tells whether two nodes write the same visual, input and extras: every prop of `WRITTEN` says
31447
+ * the same, as `sameValue` reads it.
31448
+ *
31449
+ * @param previous - The node the element was last patched from.
31450
+ * @param next - The node of this render.
31451
+ * @returns True when no component of the element has to be written for these props.
31452
+ * @example
31453
+ * ```ts
31454
+ * writesSame(
31455
+ * { type: "button", props: { intent: "claim", payload: { id: 1 } }, children: [] },
31456
+ * { type: "button", props: { intent: "claim", payload: { id: 2 } }, children: [] }
31457
+ * ); // false
31458
+ * ```
31459
+ */
31460
+ function writesSame(previous, next) {
31461
+ return WRITTEN.every((name) => sameValue(previous.props[name], next.props[name]));
31462
+ }
31463
+ /**
31284
31464
  * The children one element is diffed against. A scroll container holds exactly one of them: the
31285
31465
  * content the finger moves, which keeps the rects of its own children and carries them along. Its
31286
31466
  * pivot is its top-left corner, so the scroll step writes its offset straight into `y`; it lays
@@ -31682,6 +31862,7 @@ function createReconciler(ctx, modules, fields) {
31682
31862
  warned: nameSet(),
31683
31863
  scrolledIn: range.isMoving()
31684
31864
  };
31865
+ state.styled += 1;
31685
31866
  state.elements.set(entity, element);
31686
31867
  state.byIdentity.set(identity, entity);
31687
31868
  trackHost(state, element);
@@ -31689,6 +31870,10 @@ function createReconciler(ctx, modules, fields) {
31689
31870
  if (element.type === "input") fields.enter(element);
31690
31871
  if (element.type === "scroll") state.scrolls.add(entity);
31691
31872
  modules.layout.attach(element);
31873
+ element.basis = {
31874
+ epoch: state.epoch,
31875
+ rowHeight: range.rowHeightOf(parent, node)
31876
+ };
31692
31877
  diffChildren(root, element, childrenOf(node, range.slotsFor(element)), instance);
31693
31878
  root.needsSolve = true;
31694
31879
  return entity;
@@ -31791,11 +31976,12 @@ function createReconciler(ctx, modules, fields) {
31791
31976
  * its offset belongs to the finger, and a new look or a new rect never resets it. The look
31792
31977
  * written is recorded as the rest of the visual, so the rest the world holds is always the one
31793
31978
  * the element is drawn with: a restore brings it back to exactly that. The extras of the
31794
- * `components` prop are diffed last.
31979
+ * `components` prop are diffed last. Every call is one write pass of `UiCounters`.
31795
31980
  *
31796
31981
  * @param element - The element that changed.
31797
31982
  */
31798
31983
  function writeLive(element) {
31984
+ state.written += 1;
31799
31985
  writeOrder(element);
31800
31986
  const visuals = visualOf$1(element);
31801
31987
  const values = [...visuals, ...inputOf(element)];
@@ -31813,41 +31999,99 @@ function createReconciler(ctx, modules, fields) {
31813
31999
  writeExtras(element);
31814
32000
  }
31815
32001
  /**
31816
- * Updates one live element from its new node.
32002
+ * Compares the node of this render with the one a live element was last patched from. The
32003
+ * resolved style is kept, and `styles.resolveElement` not called, while the raw `style` says the
32004
+ * same by value, the state flags are the same, and what a style is resolved on stands: the
32005
+ * epoch of the jsx state, which a new viewport ends, and the row height of a windowed row. The element is unchanged when its style is kept and its `content` and every
32006
+ * prop of `WRITTEN` say the same too. A plain object and an array are compared by value;
32007
+ * anything else by identity: a function, a `Set`, a `Map`, a `Date`, a class instance. The
32008
+ * same object always says the same: one changed in place is not seen.
31817
32009
  *
31818
- * @param root - The root being reconciled.
32010
+ * @param root - The root being reconciled: its covered flag is one of the state flags.
31819
32011
  * @param element - The element that stayed.
31820
32012
  * @param node - The node of this render.
31821
- * @param instance - The identity of the nearest component instance.
32013
+ * @returns The flags and the style of this render, and what they change.
31822
32014
  */
31823
- function patch(root, element, node, instance) {
31824
- checkWindow(node);
31825
- const previousNode = element.node;
32015
+ function compare(root, element, node) {
32016
+ const previous = element.node;
32017
+ const parent = parentOf(element);
31826
32018
  const is = isFlagsOf(node, {
31827
32019
  pressed: element.is.pressed,
31828
32020
  hover: element.is.hover,
31829
32021
  focus: element.is.focus,
31830
32022
  covered: root.covered
31831
32023
  });
31832
- const style = styleFor(parentOf(element), node, is);
31833
- const moved = modules.layout.affectsRect(element.style, style);
31834
- const contentChanged = previousNode.props.content !== node.props.content || previousNode.props.style !== node.props.style;
32024
+ const rowHeight = range.rowHeightOf(parent, node);
32025
+ const stands = element.basis?.epoch === state.epoch && element.basis.rowHeight === rowHeight;
32026
+ const sameStyle = sameValue(previous.props.style, node.props.style);
32027
+ const keepsStyle = stands && sameStyle && sameValue(element.is, is);
32028
+ const style = keepsStyle ? element.style : styleFor(parent, node, is);
32029
+ const sameContent = sameValue(previous.props.content, node.props.content);
32030
+ return {
32031
+ is,
32032
+ style,
32033
+ rowHeight,
32034
+ resolved: !keepsStyle,
32035
+ moved: !keepsStyle && modules.layout.affectsRect(element.style, style),
32036
+ measures: !stands || !sameStyle || !sameContent,
32037
+ unchanged: keepsStyle && sameContent && writesSame(previous, node)
32038
+ };
32039
+ }
32040
+ /**
32041
+ * Writes what a patch changed on one element: its style onto the layout node when a rect or a
32042
+ * measure may move, which asks the root for a solve; then, on a live element, its visual, its
32043
+ * input and its extras, and its rest pose when no rect moved.
32044
+ *
32045
+ * @param root - The root being reconciled.
32046
+ * @param element - The element, its node, flags and style of this render already set.
32047
+ * @param change - What the patch found.
32048
+ */
32049
+ function rewrite(root, element, change) {
32050
+ const applies = change.moved || change.measures;
32051
+ if (change.resolved || applies) state.styled += 1;
32052
+ if (applies) {
32053
+ modules.layout.applyStyle(element);
32054
+ root.needsSolve = true;
32055
+ }
32056
+ if (!element.live) return;
32057
+ writeLive(element);
32058
+ if (!change.moved) modules.layout.repose(element, parentRect(element), false, hintOf(state, element));
32059
+ }
32060
+ /**
32061
+ * Updates one live element from its new node. An element whose description says what it said is
32062
+ * neither styled nor written: no `styles.resolveElement`, no `layout.applyStyle`, no solve asked
32063
+ * for, no component set, no rest recorded, no rest pose moved. What still runs for it reads a
32064
+ * prop the comparison leaves out, or costs nothing: the check of a windowed scroll's props; the
32065
+ * node of this render kept on the element, which the hosts, the rows of a windowed scroll, a
32066
+ * text field and the measure of a text read later; its motion and its instance; the host set;
32067
+ * the props of a text field; the loop of a new motion; and the diff of its children.
32068
+ *
32069
+ * @param root - The root being reconciled.
32070
+ * @param element - The element that stayed.
32071
+ * @param node - The node of this render.
32072
+ * @param instance - The identity of the nearest component instance.
32073
+ */
32074
+ function patch(root, element, node, instance) {
32075
+ checkWindow(node);
32076
+ const change = compare(root, element, node);
31835
32077
  const motion = node.props.motion;
31836
32078
  const loopChanged = motion?.loop !== element.motion?.loop;
32079
+ if (!change.unchanged) delete element.basis;
31837
32080
  element.node = node;
31838
- element.is = is;
31839
- element.style = style;
32081
+ element.is = change.is;
32082
+ element.style = change.style;
31840
32083
  element.motion = motion;
31841
32084
  element.instance = instance;
31842
32085
  trackHost(state, element);
31843
32086
  if (element.type === "input") fields.patch(element);
31844
32087
  if (loopChanged && element.entered) modules.layout.loop(element);
31845
- if (moved || contentChanged) {
31846
- modules.layout.applyStyle(element);
31847
- root.needsSolve = true;
32088
+ if (!change.unchanged) {
32089
+ rewrite(root, element, change);
32090
+ element.basis = {
32091
+ epoch: state.epoch,
32092
+ rowHeight: change.rowHeight
32093
+ };
31848
32094
  }
31849
- if (element.live) writeLive(element);
31850
- if (element.live && !moved) modules.layout.repose(element, parentRect(element), false, hintOf(state, element));
31851
32095
  diffChildren(root, element, childrenOf(node, range.slotsFor(element)), instance);
31852
32096
  }
31853
32097
  /**
@@ -32208,7 +32452,9 @@ function createReconciler(ctx, modules, fields) {
32208
32452
  modules.layout.scroll(containers, lookup);
32209
32453
  range.step(containers);
32210
32454
  markResizedTexts(ctx, state, modules.layout);
32455
+ markResizedIcons(ctx, state, modules.layout);
32211
32456
  const viewportChanged = modules.styles.useViewport(ctx.deps.renderer.viewport.size());
32457
+ if (viewportChanged) state.epoch += 1;
32212
32458
  for (const root of dirtyRoots(viewportChanged)) reconcileDirtyRoot(root);
32213
32459
  hostViews(ctx, state);
32214
32460
  writeCounters();
@@ -32224,6 +32470,8 @@ function createReconciler(ctx, modules, fields) {
32224
32470
  counters.solves = solves;
32225
32471
  counters.reconciles = state.reconciles;
32226
32472
  counters.windowRenders = state.windowRenders;
32473
+ counters.styled = state.styled;
32474
+ counters.written = state.written;
32227
32475
  }
32228
32476
  /**
32229
32477
  * The second system of phase `layout`: one solve per marked root, then the rects, the rest
@@ -32325,6 +32573,7 @@ function createReconciler(ctx, modules, fields) {
32325
32573
  const root = state.roots.get(element.root);
32326
32574
  const moved = modules.layout.affectsRect(element.style, style);
32327
32575
  element.style = style;
32576
+ state.styled += 1;
32328
32577
  if (moved) {
32329
32578
  modules.layout.applyStyle(element);
32330
32579
  if (root !== void 0) root.needsSolve = true;
@@ -32453,6 +32702,7 @@ function createJsxApi(ctx, modules) {
32453
32702
  state.components.set(definition.name, definition);
32454
32703
  },
32455
32704
  refreshAll: () => {
32705
+ state.epoch += 1;
32456
32706
  for (const element of state.elements.values()) modules.layout.markMeasured(element);
32457
32707
  for (const root of state.roots.values()) {
32458
32708
  root.dirty = true;
@@ -33188,6 +33438,32 @@ const LINE_SAMPLE = "M";
33188
33438
  /** Below this a measured length counts as whole: the tolerance Yoga's own rounding uses. */
33189
33439
  const WHOLE_EPSILON = 1e-4;
33190
33440
  /**
33441
+ * One line of the text style an icon without a size is squared on.
33442
+ *
33443
+ * @param source - What the line is measured through.
33444
+ * @returns The height of that line, as `text` measures it now.
33445
+ */
33446
+ function lineOf(source) {
33447
+ return source.measure(LINE_SAMPLE, DEFAULT_TEXT_STYLE).height;
33448
+ }
33449
+ /**
33450
+ * Tells whether the line the icons are squared on moved since an icon was last measured: the
33451
+ * body font arrived, left or was replaced, or the style was swapped. Nothing is read before the
33452
+ * first icon without a size was measured, so a screen without one costs nothing; after that a
33453
+ * call is one read of the advance table. A move is told once.
33454
+ *
33455
+ * @param state - The layout state: the line the icons were last measured with.
33456
+ * @param source - What the line is measured through.
33457
+ * @returns True when every icon without a size has to be measured again.
33458
+ */
33459
+ function iconLineMoved(state, source) {
33460
+ if (state.iconLine === void 0) return false;
33461
+ const line = lineOf(source);
33462
+ if (line === state.iconLine) return false;
33463
+ state.iconLine = line;
33464
+ return true;
33465
+ }
33466
+ /**
33191
33467
  * Makes an icon square on the line height of the text next to it.
33192
33468
  *
33193
33469
  * @param line - What one line of that text style measures.
@@ -33353,6 +33629,7 @@ function installMeasure(state, node, element, source) {
33353
33629
  const size = naturalSize(element, source);
33354
33630
  state.measured += 1;
33355
33631
  element.measuredSize = size;
33632
+ if (element.type === "icon") state.iconLine = lineOf(source);
33356
33633
  return clampToMode(yoga, size, {
33357
33634
  width,
33358
33635
  widthMode,
@@ -33373,9 +33650,12 @@ function markMeasured(state, element) {
33373
33650
  state.byEntity.get(element.entity)?.markDirty();
33374
33651
  }
33375
33652
  /**
33376
- * Marks the node of a bound text dirty when the string `text` shows for it now measures to another
33377
- * size than the node was laid out with. `text` writes `resolved` only when the shown string
33378
- * changes, and a counter that rolls through digits of one width asks for no solve.
33653
+ * Marks the node of a text dirty when what it draws now measures to another size than the node was
33654
+ * laid out with. `text` writes `Text` when the shown string of a bound label changes, and for
33655
+ * every label it resolves again: a message after a new locale, any label when its font arrives or
33656
+ * its style is swapped. So a text without `bind` is measured again too, and a counter that rolls
33657
+ * through digits of one width asks for no solve. A text with a fixed size has no measure function
33658
+ * and is left alone.
33379
33659
  *
33380
33660
  * @param state - The layout state, for the node of the element.
33381
33661
  * @param element - An element whose `Text` changed this frame.
@@ -33386,7 +33666,7 @@ function remeasureShown(state, element, source) {
33386
33666
  const node = state.byEntity.get(element.entity);
33387
33667
  const before = element.measuredSize;
33388
33668
  if (node === void 0 || before === void 0) return false;
33389
- if (bindOf(element) === void 0 || !needsMeasure(element)) return false;
33669
+ if (!needsMeasure(element)) return false;
33390
33670
  const size = naturalSize(element, source);
33391
33671
  if (size.width === before.width && size.height === before.height) return false;
33392
33672
  node.markDirty();
@@ -33830,6 +34110,7 @@ function createLayoutApi(ctx) {
33830
34110
  markMeasured(state, element);
33831
34111
  },
33832
34112
  remeasure: (element) => remeasureShown(state, element, source),
34113
+ iconLineMoved: () => iconLineMoved(state, source),
33833
34114
  markMeasured: (element) => markMeasured(state, element),
33834
34115
  place: (parent, children) => placeChildren(state, parent, children),
33835
34116
  free: (element) => freeNode(state, element),
@@ -34389,6 +34670,7 @@ function stopUi(state) {
34389
34670
  state.layout.nodes = 0;
34390
34671
  state.layout.yoga = void 0;
34391
34672
  state.layout.scrolling = void 0;
34673
+ state.layout.iconLine = void 0;
34392
34674
  state.jsx.components.clear();
34393
34675
  state.jsx.roots.clear();
34394
34676
  state.jsx.elements.clear();
@@ -34744,7 +35026,10 @@ function createJsxState() {
34744
35026
  hints: [],
34745
35027
  scrolls: emptySet(),
34746
35028
  windowRenders: 0,
34747
- remount: false
35029
+ remount: false,
35030
+ epoch: 0,
35031
+ styled: 0,
35032
+ written: 0
34748
35033
  };
34749
35034
  }
34750
35035
  //#endregion
@@ -34779,6 +35064,7 @@ function createLayoutState() {
34779
35064
  pointerY: 0,
34780
35065
  offset: 0
34781
35066
  },
35067
+ iconLine: void 0,
34782
35068
  order: 0,
34783
35069
  cleanups: []
34784
35070
  };
@@ -36510,4 +36796,4 @@ const screen = [
36510
36796
  uiPlugin
36511
36797
  ];
36512
36798
  //#endregion
36513
- export { sfx as $, types_exports$13 as A, Noise as At, defineStyle as B, rendererPlugin as Bt, types_exports$6 as C, effectsPlugin as Ct, types_exports$10 as D, ColorMatrix as Dt, types_exports$9 as E, Blur as Et, resolve as F, assetsPlugin as Ft, external as G, to as Gt, platformPlugin as H, flowPlugin as Ht, Box as I, animPlugin as It, mark as J, SaveUnreadableError as Jt, frames as K, type as Kt, LocalWrite as L, AnimPlayer as Lt, types_exports$15 as M, defineFilter as Mt, types_exports$16 as N, audioPlugin as Nt, types_exports$11 as O, Displacement as Ot, uiPlugin as P, scenesPlugin as Pt, set as Q, popup as R, Animation as Rt, types_exports$5 as S, messageDuration as St, types_exports$8 as T, Alpha as Tt, HAPTIC_KINDS as U, exit as Ut, textPlugin as V, worldPlugin as Vt, defineAnimation as W, slot as Wt, play$1 as X, timePlugin as Xt, parallel as Y, lifecyclePlugin as Yt, sequence as Z, clockPlugin as Zt, types_exports as _, Touchable as _t, defineMotion as a, wait as at, types_exports$3 as b, i18nPlugin as bt, schedule as c, DropTarget as ct, tr as d, Pointer as dt, spawn as et, defineFeature as f, PointerOver as ft, load as g, Tappable as gt, defineBundles as h, Swipeable as ht, screen as i, use as it, types_exports$14 as j, Outline as jt, types_exports$12 as k, Glow as kt, projection as l, Held as lt, music as m, Pressed as mt, createPlugin as n, stagger as nt, guide as o, inputPlugin as ot, defineEmitter as p, Pressable as pt, haptic as q, modelPlugin as qt, defineGame as r, tween as rt, hint as s, Draggable as st, createApp as t, spawned as tt, defineScene as u, Hovered as ut, types_exports$1 as v, Traceable as vt, types_exports$7 as w, Emitter as wt, types_exports$4 as x, messageArgument as xt, types_exports$2 as y, Traced as yt, defineTokens as z, Frames as zt };
36799
+ export { sfx as $, types_exports$13 as A, Noise as At, defineStyle as B, rendererPlugin as Bt, types_exports$6 as C, effectsPlugin as Ct, types_exports$10 as D, ColorMatrix as Dt, types_exports$9 as E, Blur as Et, resolve as F, assetsPlugin as Ft, external as G, slot as Gt, platformPlugin as H, flowPlugin as Ht, Box as I, animPlugin as It, mark as J, modelPlugin as Jt, frames as K, to as Kt, LocalWrite as L, AnimPlayer as Lt, types_exports$15 as M, defineFilter as Mt, types_exports$16 as N, audioPlugin as Nt, types_exports$11 as O, Displacement as Ot, uiPlugin as P, scenesPlugin as Pt, set as Q, clockPlugin as Qt, popup as R, Animation as Rt, types_exports$5 as S, messageDuration as St, types_exports$8 as T, Alpha as Tt, HAPTIC_KINDS as U, refuseStart as Ut, textPlugin as V, worldPlugin as Vt, defineAnimation as W, exit as Wt, play$1 as X, lifecyclePlugin as Xt, parallel as Y, SaveUnreadableError as Yt, sequence as Z, timePlugin as Zt, types_exports as _, Touchable as _t, defineMotion as a, wait as at, types_exports$3 as b, i18nPlugin as bt, schedule as c, DropTarget as ct, tr as d, Pointer as dt, spawn as et, defineFeature as f, PointerOver as ft, load as g, Tappable as gt, defineBundles as h, Swipeable as ht, screen as i, use as it, types_exports$14 as j, Outline as jt, types_exports$12 as k, Glow as kt, projection as l, Held as lt, music as m, Pressed as mt, createPlugin as n, stagger as nt, guide as o, inputPlugin as ot, defineEmitter as p, Pressable as pt, haptic as q, type as qt, defineGame as r, tween as rt, hint as s, Draggable as st, createApp as t, spawned as tt, defineScene as u, Hovered as ut, types_exports$1 as v, Traceable as vt, types_exports$7 as w, Emitter as wt, types_exports$4 as x, messageArgument as xt, types_exports$2 as y, Traced as yt, defineTokens as z, Frames as zt };