@getuserfeedback/protocol 3.0.11 → 3.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
- import type { WidgetLayoutSize, WidgetLayoutSizeUpdate, WidgetLayoutTimedSizeUpdate } from "./widget-layout-size-update.js";
1
+ import type { WidgetLayoutCubicBezier, WidgetLayoutSize, WidgetLayoutSizeUpdate, WidgetLayoutTimedSizeUpdate } from "./widget-layout-size-update.js";
2
2
  export type WidgetLayoutComparableSizeUpdate = WidgetLayoutSizeUpdate | WidgetLayoutTimedSizeUpdate;
3
- export type WidgetLayoutUnchangedTransitionPlan = {
3
+ export type WidgetLayoutUnchangedTransitionPlan<Update extends WidgetLayoutComparableSizeUpdate = WidgetLayoutComparableSizeUpdate> = {
4
4
  kind: "unchanged";
5
5
  size: WidgetLayoutSize;
6
- update: WidgetLayoutSizeUpdate;
6
+ update: Update;
7
7
  observedAtMs: number;
8
8
  };
9
9
  export type WidgetLayoutInstantTransitionPlan = {
@@ -13,12 +13,59 @@ export type WidgetLayoutInstantTransitionPlan = {
13
13
  startedAtMs: number;
14
14
  settledAtMs: number;
15
15
  };
16
- export type WidgetLayoutTransitionPlan = WidgetLayoutUnchangedTransitionPlan | WidgetLayoutInstantTransitionPlan;
16
+ export type WidgetLayoutTimedTransitionPlan = {
17
+ kind: "timed";
18
+ size: WidgetLayoutSize;
19
+ update: WidgetLayoutTimedSizeUpdate;
20
+ startedAtMs: number;
21
+ settledAtMs: number;
22
+ };
23
+ export type WidgetLayoutInstantOnlyTransitionPlan = WidgetLayoutUnchangedTransitionPlan<WidgetLayoutSizeUpdate> | WidgetLayoutInstantTransitionPlan;
24
+ export type WidgetLayoutTransitionPlan = WidgetLayoutInstantOnlyTransitionPlan;
25
+ export type WidgetLayoutTimedCapableTransitionPlan = WidgetLayoutUnchangedTransitionPlan | WidgetLayoutInstantTransitionPlan | WidgetLayoutTimedTransitionPlan;
26
+ export type WidgetLayoutInstantTransitionPolicy = {
27
+ kind: "instant";
28
+ durationMs: 0;
29
+ };
30
+ export type WidgetLayoutTimedTransitionPolicy = {
31
+ kind: "timed";
32
+ durationMs: number;
33
+ easing: WidgetLayoutCubicBezier;
34
+ };
35
+ export type WidgetLayoutTransitionPolicy = WidgetLayoutInstantTransitionPolicy | WidgetLayoutTimedTransitionPolicy;
36
+ export declare const INSTANT_WIDGET_LAYOUT_TRANSITION_POLICY: WidgetLayoutInstantTransitionPolicy;
17
37
  export declare function getWidgetLayoutPlanNowMs(): number;
18
38
  export declare function areWidgetLayoutSizeUpdatesEqual(left: WidgetLayoutComparableSizeUpdate | undefined, right: WidgetLayoutComparableSizeUpdate): boolean;
39
+ type WidgetLayoutInstantPolicyPlanInput = {
40
+ previous?: WidgetLayoutComparableSizeUpdate;
41
+ size: WidgetLayoutSize;
42
+ nowMs: number;
43
+ transitionPolicy?: WidgetLayoutInstantTransitionPolicy;
44
+ };
45
+ type WidgetLayoutTimedCapablePolicyPlanInput = {
46
+ previous?: WidgetLayoutComparableSizeUpdate;
47
+ size: WidgetLayoutSize;
48
+ nowMs: number;
49
+ transitionPolicy: WidgetLayoutTimedTransitionPolicy;
50
+ };
51
+ type WidgetLayoutOptionalPolicyPlanInput = {
52
+ previous?: WidgetLayoutComparableSizeUpdate;
53
+ size: WidgetLayoutSize;
54
+ nowMs: number;
55
+ transitionPolicy?: WidgetLayoutTransitionPolicy;
56
+ };
19
57
  export declare function planWidgetLayoutTransition(input: {
20
58
  previous?: WidgetLayoutSizeUpdate;
21
59
  next: WidgetLayoutSizeUpdate;
22
60
  nowMs: number;
23
- }): WidgetLayoutTransitionPlan;
61
+ }): WidgetLayoutInstantOnlyTransitionPlan;
62
+ export declare function planWidgetLayoutTransition(input: {
63
+ previous?: WidgetLayoutComparableSizeUpdate;
64
+ next: WidgetLayoutComparableSizeUpdate;
65
+ nowMs: number;
66
+ }): WidgetLayoutTimedCapableTransitionPlan;
67
+ export declare function planWidgetLayoutTransition(input: WidgetLayoutInstantPolicyPlanInput): WidgetLayoutInstantOnlyTransitionPlan;
68
+ export declare function planWidgetLayoutTransition(input: WidgetLayoutTimedCapablePolicyPlanInput): WidgetLayoutTimedCapableTransitionPlan;
69
+ export declare function planWidgetLayoutTransition(input: WidgetLayoutOptionalPolicyPlanInput): WidgetLayoutTimedCapableTransitionPlan;
70
+ export {};
24
71
  //# sourceMappingURL=widget-layout-transition-plan.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"widget-layout-transition-plan.d.ts","sourceRoot":"","sources":["../../src/host/widget-layout-transition-plan.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,gBAAgB,EAChB,sBAAsB,EACtB,2BAA2B,EAC3B,MAAM,gCAAgC,CAAC;AAExC,MAAM,MAAM,gCAAgC,GACzC,sBAAsB,GACtB,2BAA2B,CAAC;AAE/B,MAAM,MAAM,mCAAmC,GAAG;IACjD,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC/C,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GACnC,mCAAmC,GACnC,iCAAiC,CAAC;AAErC,wBAAgB,wBAAwB,IAAI,MAAM,CAQjD;AAED,wBAAgB,+BAA+B,CAC9C,IAAI,EAAE,gCAAgC,GAAG,SAAS,EAClD,KAAK,EAAE,gCAAgC,GACrC,OAAO,CAiCT;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE;IACjD,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd,GAAG,0BAA0B,CAyB7B"}
1
+ {"version":3,"file":"widget-layout-transition-plan.d.ts","sourceRoot":"","sources":["../../src/host/widget-layout-transition-plan.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,8 @@
1
+ import { toInstantWidgetLayoutSizeUpdate, toTimedWidgetLayoutSizeUpdate, } from "./widget-layout-size-update.js";
2
+ export const INSTANT_WIDGET_LAYOUT_TRANSITION_POLICY = {
3
+ kind: "instant",
4
+ durationMs: 0,
5
+ };
1
6
  export function getWidgetLayoutPlanNowMs() {
2
7
  const performanceNow = typeof globalThis.performance?.now === "function"
3
8
  ? globalThis.performance.now()
@@ -32,28 +37,75 @@ export function areWidgetLayoutSizeUpdatesEqual(left, right) {
32
37
  }
33
38
  }
34
39
  }
40
+ function cloneWidgetLayoutSizeUpdate(update) {
41
+ if (update.transition.kind === "timed") {
42
+ return {
43
+ size: { ...update.size },
44
+ transition: {
45
+ ...update.transition,
46
+ easing: {
47
+ ...update.transition.easing,
48
+ points: [...update.transition.easing.points],
49
+ },
50
+ },
51
+ };
52
+ }
53
+ return {
54
+ size: { ...update.size },
55
+ transition: { ...update.transition },
56
+ };
57
+ }
58
+ function isWidgetLayoutTimedSizeUpdate(update) {
59
+ return update.transition.kind === "timed";
60
+ }
61
+ function toWidgetLayoutSizeUpdateForPolicy(input) {
62
+ const policy = input.policy ?? INSTANT_WIDGET_LAYOUT_TRANSITION_POLICY;
63
+ switch (policy.kind) {
64
+ case "instant":
65
+ return toInstantWidgetLayoutSizeUpdate(input.size);
66
+ case "timed":
67
+ return toTimedWidgetLayoutSizeUpdate({
68
+ size: input.size,
69
+ durationMs: policy.durationMs,
70
+ easing: policy.easing,
71
+ });
72
+ default: {
73
+ const unreachable = policy;
74
+ throw new Error(`Unsupported widget layout transition policy: ${JSON.stringify(unreachable)}`);
75
+ }
76
+ }
77
+ }
35
78
  export function planWidgetLayoutTransition(input) {
36
79
  if (!Number.isFinite(input.nowMs)) {
37
80
  throw new RangeError("Widget layout transition plans require finite nowMs");
38
81
  }
39
- if (areWidgetLayoutSizeUpdatesEqual(input.previous, input.next)) {
82
+ const next = "next" in input
83
+ ? input.next
84
+ : toWidgetLayoutSizeUpdateForPolicy({
85
+ size: input.size,
86
+ policy: input.transitionPolicy,
87
+ });
88
+ if (areWidgetLayoutSizeUpdatesEqual(input.previous, next)) {
40
89
  return {
41
90
  kind: "unchanged",
42
- size: { ...input.next.size },
43
- update: {
44
- size: { ...input.next.size },
45
- transition: { ...input.next.transition },
46
- },
91
+ size: { ...next.size },
92
+ update: cloneWidgetLayoutSizeUpdate(next),
47
93
  observedAtMs: input.nowMs,
48
94
  };
49
95
  }
96
+ if (isWidgetLayoutTimedSizeUpdate(next)) {
97
+ return {
98
+ kind: "timed",
99
+ size: { ...next.size },
100
+ update: cloneWidgetLayoutSizeUpdate(next),
101
+ startedAtMs: input.nowMs,
102
+ settledAtMs: input.nowMs + next.transition.durationMs,
103
+ };
104
+ }
50
105
  return {
51
106
  kind: "instant",
52
- size: { ...input.next.size },
53
- update: {
54
- size: { ...input.next.size },
55
- transition: { ...input.next.transition },
56
- },
107
+ size: { ...next.size },
108
+ update: cloneWidgetLayoutSizeUpdate(next),
57
109
  startedAtMs: input.nowMs,
58
110
  settledAtMs: input.nowMs,
59
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getuserfeedback/protocol",
3
- "version": "3.0.11",
3
+ "version": "3.0.13",
4
4
  "description": "getuserfeedback widget protocol — host surface and (later) wire protocol",
5
5
  "keywords": [
6
6
  "getuserfeedback",
@@ -132,8 +132,8 @@
132
132
  }
133
133
  },
134
134
  "scripts": {
135
- "lint": "ultracite check .",
136
- "build": "bun ../../scripts/clean-build-output.ts dist tsconfig.tsbuildinfo && tsc -b tsconfig.json",
135
+ "lint": "biome check .",
136
+ "build": "bun ../../scripts/clean-build-output.ts dist tsconfig.tsbuildinfo && tsc -b tsconfig.json && bun scripts/verify-dist-js.ts",
137
137
  "types": "bun ../../scripts/clean-build-output.ts dist tsconfig.tsbuildinfo && tsc -b tsconfig.json --emitDeclarationOnly",
138
138
  "typecheck": "tsc -b tsconfig.json",
139
139
  "test": "bun test --dots",
@@ -6,10 +6,24 @@ import {
6
6
  import {
7
7
  areWidgetLayoutSizeUpdatesEqual,
8
8
  getWidgetLayoutPlanNowMs,
9
+ INSTANT_WIDGET_LAYOUT_TRANSITION_POLICY,
9
10
  planWidgetLayoutTransition,
11
+ type WidgetLayoutInstantOnlyTransitionPlan,
12
+ type WidgetLayoutTimedCapableTransitionPlan,
13
+ type WidgetLayoutTransitionPlan,
10
14
  } from "./widget-layout-transition-plan.js";
11
15
 
12
16
  describe("widget layout transition plan", () => {
17
+ function expectInstantOnlyPlan(
18
+ _plan: WidgetLayoutInstantOnlyTransitionPlan,
19
+ ): void {}
20
+ function expectLegacyTransitionPlan(
21
+ _plan: WidgetLayoutTransitionPlan,
22
+ ): void {}
23
+ function expectTimedCapablePlan(
24
+ _plan: WidgetLayoutTimedCapableTransitionPlan,
25
+ ): void {}
26
+
13
27
  test("uses the browser monotonic clock for layout plan timestamps", () => {
14
28
  const originalPerformanceDescriptor = Object.getOwnPropertyDescriptor(
15
29
  globalThis,
@@ -72,6 +86,19 @@ describe("widget layout transition plan", () => {
72
86
  test("plans the first instant layout update at the requested time", () => {
73
87
  const next = toInstantWidgetLayoutSizeUpdate({ width: 320, height: 240 });
74
88
 
89
+ expectInstantOnlyPlan(
90
+ planWidgetLayoutTransition({
91
+ next,
92
+ nowMs: 1_000,
93
+ }),
94
+ );
95
+ expectLegacyTransitionPlan(
96
+ planWidgetLayoutTransition({
97
+ next,
98
+ nowMs: 1_000,
99
+ }),
100
+ );
101
+
75
102
  expect(
76
103
  planWidgetLayoutTransition({
77
104
  next,
@@ -112,6 +139,44 @@ describe("widget layout transition plan", () => {
112
139
  expect(areWidgetLayoutSizeUpdatesEqual(previous, next)).toBe(true);
113
140
  });
114
141
 
142
+ test("plans size input as instant by default", () => {
143
+ const previous = toInstantWidgetLayoutSizeUpdate({
144
+ width: 320,
145
+ height: 240,
146
+ });
147
+
148
+ const plan = planWidgetLayoutTransition({
149
+ previous,
150
+ size: { width: 320, height: 360 },
151
+ nowMs: 1_500,
152
+ });
153
+ expectInstantOnlyPlan(plan);
154
+ expect(plan).toEqual({
155
+ kind: "instant",
156
+ size: { width: 320, height: 360 },
157
+ update: {
158
+ size: { width: 320, height: 360 },
159
+ transition: {
160
+ kind: "instant",
161
+ durationMs: 0,
162
+ },
163
+ },
164
+ startedAtMs: 1_500,
165
+ settledAtMs: 1_500,
166
+ });
167
+ });
168
+
169
+ test("keeps explicit instant transition policy instant-only", () => {
170
+ const plan = planWidgetLayoutTransition({
171
+ size: { width: 320, height: 360 },
172
+ nowMs: 1_500,
173
+ transitionPolicy: INSTANT_WIDGET_LAYOUT_TRANSITION_POLICY,
174
+ });
175
+
176
+ expectInstantOnlyPlan(plan);
177
+ expect(plan.kind).toBe("instant");
178
+ });
179
+
115
180
  test("compares the full timed transition shape before it is live", () => {
116
181
  const base = {
117
182
  size: { width: 320, height: 240 },
@@ -153,6 +218,82 @@ describe("widget layout transition plan", () => {
153
218
  });
154
219
  });
155
220
 
221
+ test("plans explicit timed updates with settled time from duration", () => {
222
+ const next = toTimedWidgetLayoutSizeUpdate({
223
+ size: { width: 320, height: 360 },
224
+ durationMs: 180,
225
+ easing: [0.2, 0, 0, 1],
226
+ });
227
+
228
+ expect(
229
+ planWidgetLayoutTransition({
230
+ next,
231
+ nowMs: 1_500,
232
+ }),
233
+ ).toEqual({
234
+ kind: "timed",
235
+ size: { width: 320, height: 360 },
236
+ update: {
237
+ size: { width: 320, height: 360 },
238
+ transition: {
239
+ kind: "timed",
240
+ durationMs: 180,
241
+ easing: {
242
+ kind: "cubic-bezier",
243
+ points: [0.2, 0, 0, 1],
244
+ },
245
+ },
246
+ },
247
+ startedAtMs: 1_500,
248
+ settledAtMs: 1_680,
249
+ });
250
+ });
251
+
252
+ test("plans timed transition policy from size input", () => {
253
+ const plan = planWidgetLayoutTransition({
254
+ size: { width: 320, height: 360 },
255
+ nowMs: 1_500,
256
+ transitionPolicy: {
257
+ kind: "timed",
258
+ durationMs: 180,
259
+ easing: [0.2, 0, 0, 1],
260
+ },
261
+ });
262
+ expectTimedCapablePlan(plan);
263
+
264
+ expect(plan).toEqual({
265
+ kind: "timed",
266
+ size: { width: 320, height: 360 },
267
+ update: {
268
+ size: { width: 320, height: 360 },
269
+ transition: {
270
+ kind: "timed",
271
+ durationMs: 180,
272
+ easing: {
273
+ kind: "cubic-bezier",
274
+ points: [0.2, 0, 0, 1],
275
+ },
276
+ },
277
+ },
278
+ startedAtMs: 1_500,
279
+ settledAtMs: 1_680,
280
+ });
281
+ });
282
+
283
+ test("rejects invalid timed transition policy through protocol helpers", () => {
284
+ expect(() =>
285
+ planWidgetLayoutTransition({
286
+ size: { width: 320, height: 360 },
287
+ nowMs: 1_500,
288
+ transitionPolicy: {
289
+ kind: "timed",
290
+ durationMs: 0,
291
+ easing: [0.2, 0, 0, 1],
292
+ },
293
+ }),
294
+ ).toThrow(RangeError);
295
+ });
296
+
156
297
  test("does not leak mutable references from the next update", () => {
157
298
  const next = {
158
299
  size: { width: 320, height: 240 },
@@ -171,6 +312,26 @@ describe("widget layout transition plan", () => {
171
312
  expect(plan.update.transition.durationMs).toBe(0);
172
313
  });
173
314
 
315
+ test("does not leak mutable timed transition points from the next update", () => {
316
+ const next = toTimedWidgetLayoutSizeUpdate({
317
+ size: { width: 320, height: 240 },
318
+ durationMs: 180,
319
+ easing: [0.2, 0, 0, 1],
320
+ });
321
+ const plan = planWidgetLayoutTransition({
322
+ next,
323
+ nowMs: 1_000,
324
+ });
325
+
326
+ (next.transition.easing.points as unknown as number[])[0] = 0.4;
327
+
328
+ expect(plan.update.transition.kind).toBe("timed");
329
+ if (plan.update.transition.kind !== "timed") {
330
+ throw new Error("expected timed transition");
331
+ }
332
+ expect(plan.update.transition.easing.points).toEqual([0.2, 0, 0, 1]);
333
+ });
334
+
174
335
  test("rejects non-finite plan time", () => {
175
336
  expect(() =>
176
337
  planWidgetLayoutTransition({
@@ -1,17 +1,25 @@
1
1
  import type {
2
+ WidgetLayoutCubicBezier,
2
3
  WidgetLayoutSize,
3
4
  WidgetLayoutSizeUpdate,
4
5
  WidgetLayoutTimedSizeUpdate,
5
6
  } from "./widget-layout-size-update.js";
7
+ import {
8
+ toInstantWidgetLayoutSizeUpdate,
9
+ toTimedWidgetLayoutSizeUpdate,
10
+ } from "./widget-layout-size-update.js";
6
11
 
7
12
  export type WidgetLayoutComparableSizeUpdate =
8
13
  | WidgetLayoutSizeUpdate
9
14
  | WidgetLayoutTimedSizeUpdate;
10
15
 
11
- export type WidgetLayoutUnchangedTransitionPlan = {
16
+ export type WidgetLayoutUnchangedTransitionPlan<
17
+ Update extends
18
+ WidgetLayoutComparableSizeUpdate = WidgetLayoutComparableSizeUpdate,
19
+ > = {
12
20
  kind: "unchanged";
13
21
  size: WidgetLayoutSize;
14
- update: WidgetLayoutSizeUpdate;
22
+ update: Update;
15
23
  observedAtMs: number;
16
24
  };
17
25
 
@@ -23,10 +31,46 @@ export type WidgetLayoutInstantTransitionPlan = {
23
31
  settledAtMs: number;
24
32
  };
25
33
 
26
- export type WidgetLayoutTransitionPlan =
27
- | WidgetLayoutUnchangedTransitionPlan
34
+ export type WidgetLayoutTimedTransitionPlan = {
35
+ kind: "timed";
36
+ size: WidgetLayoutSize;
37
+ update: WidgetLayoutTimedSizeUpdate;
38
+ startedAtMs: number;
39
+ settledAtMs: number;
40
+ };
41
+
42
+ export type WidgetLayoutInstantOnlyTransitionPlan =
43
+ | WidgetLayoutUnchangedTransitionPlan<WidgetLayoutSizeUpdate>
28
44
  | WidgetLayoutInstantTransitionPlan;
29
45
 
46
+ export type WidgetLayoutTransitionPlan = WidgetLayoutInstantOnlyTransitionPlan;
47
+
48
+ export type WidgetLayoutTimedCapableTransitionPlan =
49
+ | WidgetLayoutUnchangedTransitionPlan
50
+ | WidgetLayoutInstantTransitionPlan
51
+ | WidgetLayoutTimedTransitionPlan;
52
+
53
+ export type WidgetLayoutInstantTransitionPolicy = {
54
+ kind: "instant";
55
+ durationMs: 0;
56
+ };
57
+
58
+ export type WidgetLayoutTimedTransitionPolicy = {
59
+ kind: "timed";
60
+ durationMs: number;
61
+ easing: WidgetLayoutCubicBezier;
62
+ };
63
+
64
+ export type WidgetLayoutTransitionPolicy =
65
+ | WidgetLayoutInstantTransitionPolicy
66
+ | WidgetLayoutTimedTransitionPolicy;
67
+
68
+ export const INSTANT_WIDGET_LAYOUT_TRANSITION_POLICY: WidgetLayoutInstantTransitionPolicy =
69
+ {
70
+ kind: "instant",
71
+ durationMs: 0,
72
+ };
73
+
30
74
  export function getWidgetLayoutPlanNowMs(): number {
31
75
  const performanceNow =
32
76
  typeof globalThis.performance?.now === "function"
@@ -75,32 +119,156 @@ export function areWidgetLayoutSizeUpdatesEqual(
75
119
  }
76
120
  }
77
121
 
122
+ function cloneWidgetLayoutSizeUpdate(
123
+ update: WidgetLayoutSizeUpdate,
124
+ ): WidgetLayoutSizeUpdate;
125
+ function cloneWidgetLayoutSizeUpdate(
126
+ update: WidgetLayoutTimedSizeUpdate,
127
+ ): WidgetLayoutTimedSizeUpdate;
128
+ function cloneWidgetLayoutSizeUpdate(
129
+ update: WidgetLayoutComparableSizeUpdate,
130
+ ): WidgetLayoutComparableSizeUpdate;
131
+ function cloneWidgetLayoutSizeUpdate(
132
+ update: WidgetLayoutComparableSizeUpdate,
133
+ ): WidgetLayoutComparableSizeUpdate {
134
+ if (update.transition.kind === "timed") {
135
+ return {
136
+ size: { ...update.size },
137
+ transition: {
138
+ ...update.transition,
139
+ easing: {
140
+ ...update.transition.easing,
141
+ points: [...update.transition.easing.points],
142
+ },
143
+ },
144
+ };
145
+ }
146
+ return {
147
+ size: { ...update.size },
148
+ transition: { ...update.transition },
149
+ };
150
+ }
151
+
152
+ function isWidgetLayoutTimedSizeUpdate(
153
+ update: WidgetLayoutComparableSizeUpdate,
154
+ ): update is WidgetLayoutTimedSizeUpdate {
155
+ return update.transition.kind === "timed";
156
+ }
157
+
158
+ function toWidgetLayoutSizeUpdateForPolicy(input: {
159
+ policy: WidgetLayoutTransitionPolicy | undefined;
160
+ size: WidgetLayoutSize;
161
+ }): WidgetLayoutComparableSizeUpdate {
162
+ const policy = input.policy ?? INSTANT_WIDGET_LAYOUT_TRANSITION_POLICY;
163
+ switch (policy.kind) {
164
+ case "instant":
165
+ return toInstantWidgetLayoutSizeUpdate(input.size);
166
+ case "timed":
167
+ return toTimedWidgetLayoutSizeUpdate({
168
+ size: input.size,
169
+ durationMs: policy.durationMs,
170
+ easing: policy.easing,
171
+ });
172
+ default: {
173
+ const unreachable: never = policy;
174
+ throw new Error(
175
+ `Unsupported widget layout transition policy: ${JSON.stringify(unreachable)}`,
176
+ );
177
+ }
178
+ }
179
+ }
180
+
181
+ type WidgetLayoutInstantUpdatePlanInput = {
182
+ previous?: WidgetLayoutSizeUpdate;
183
+ next: WidgetLayoutSizeUpdate;
184
+ nowMs: number;
185
+ };
186
+
187
+ type WidgetLayoutTimedCapableUpdatePlanInput = {
188
+ previous?: WidgetLayoutComparableSizeUpdate;
189
+ next: WidgetLayoutComparableSizeUpdate;
190
+ nowMs: number;
191
+ };
192
+
193
+ type WidgetLayoutInstantPolicyPlanInput = {
194
+ previous?: WidgetLayoutComparableSizeUpdate;
195
+ size: WidgetLayoutSize;
196
+ nowMs: number;
197
+ transitionPolicy?: WidgetLayoutInstantTransitionPolicy;
198
+ };
199
+
200
+ type WidgetLayoutTimedCapablePolicyPlanInput = {
201
+ previous?: WidgetLayoutComparableSizeUpdate;
202
+ size: WidgetLayoutSize;
203
+ nowMs: number;
204
+ transitionPolicy: WidgetLayoutTimedTransitionPolicy;
205
+ };
206
+
207
+ type WidgetLayoutOptionalPolicyPlanInput = {
208
+ previous?: WidgetLayoutComparableSizeUpdate;
209
+ size: WidgetLayoutSize;
210
+ nowMs: number;
211
+ transitionPolicy?: WidgetLayoutTransitionPolicy;
212
+ };
213
+
78
214
  export function planWidgetLayoutTransition(input: {
79
215
  previous?: WidgetLayoutSizeUpdate;
80
216
  next: WidgetLayoutSizeUpdate;
81
217
  nowMs: number;
82
- }): WidgetLayoutTransitionPlan {
218
+ }): WidgetLayoutInstantOnlyTransitionPlan;
219
+ export function planWidgetLayoutTransition(input: {
220
+ previous?: WidgetLayoutComparableSizeUpdate;
221
+ next: WidgetLayoutComparableSizeUpdate;
222
+ nowMs: number;
223
+ }): WidgetLayoutTimedCapableTransitionPlan;
224
+ export function planWidgetLayoutTransition(
225
+ input: WidgetLayoutInstantPolicyPlanInput,
226
+ ): WidgetLayoutInstantOnlyTransitionPlan;
227
+ export function planWidgetLayoutTransition(
228
+ input: WidgetLayoutTimedCapablePolicyPlanInput,
229
+ ): WidgetLayoutTimedCapableTransitionPlan;
230
+ export function planWidgetLayoutTransition(
231
+ input: WidgetLayoutOptionalPolicyPlanInput,
232
+ ): WidgetLayoutTimedCapableTransitionPlan;
233
+ export function planWidgetLayoutTransition(
234
+ input:
235
+ | WidgetLayoutInstantUpdatePlanInput
236
+ | WidgetLayoutTimedCapableUpdatePlanInput
237
+ | WidgetLayoutInstantPolicyPlanInput
238
+ | WidgetLayoutTimedCapablePolicyPlanInput
239
+ | WidgetLayoutOptionalPolicyPlanInput,
240
+ ): WidgetLayoutTimedCapableTransitionPlan {
83
241
  if (!Number.isFinite(input.nowMs)) {
84
242
  throw new RangeError("Widget layout transition plans require finite nowMs");
85
243
  }
86
- if (areWidgetLayoutSizeUpdatesEqual(input.previous, input.next)) {
244
+ const next =
245
+ "next" in input
246
+ ? input.next
247
+ : toWidgetLayoutSizeUpdateForPolicy({
248
+ size: input.size,
249
+ policy: input.transitionPolicy,
250
+ });
251
+ if (areWidgetLayoutSizeUpdatesEqual(input.previous, next)) {
87
252
  return {
88
253
  kind: "unchanged",
89
- size: { ...input.next.size },
90
- update: {
91
- size: { ...input.next.size },
92
- transition: { ...input.next.transition },
93
- },
254
+ size: { ...next.size },
255
+ update: cloneWidgetLayoutSizeUpdate(next),
94
256
  observedAtMs: input.nowMs,
95
257
  };
96
258
  }
259
+ if (isWidgetLayoutTimedSizeUpdate(next)) {
260
+ return {
261
+ kind: "timed",
262
+ size: { ...next.size },
263
+ update: cloneWidgetLayoutSizeUpdate(next),
264
+ startedAtMs: input.nowMs,
265
+ settledAtMs: input.nowMs + next.transition.durationMs,
266
+ };
267
+ }
97
268
  return {
98
269
  kind: "instant",
99
- size: { ...input.next.size },
100
- update: {
101
- size: { ...input.next.size },
102
- transition: { ...input.next.transition },
103
- },
270
+ size: { ...next.size },
271
+ update: cloneWidgetLayoutSizeUpdate(next),
104
272
  startedAtMs: input.nowMs,
105
273
  settledAtMs: input.nowMs,
106
274
  };