@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.
- package/dist/host/widget-layout-transition-plan.d.ts +52 -5
- package/dist/host/widget-layout-transition-plan.d.ts.map +1 -1
- package/dist/host/widget-layout-transition-plan.js +63 -11
- package/package.json +3 -3
- package/src/host/widget-layout-transition-plan.test.ts +161 -0
- package/src/host/widget-layout-transition-plan.ts +184 -16
|
@@ -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:
|
|
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
|
|
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
|
-
}):
|
|
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;
|
|
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
|
-
|
|
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: { ...
|
|
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: { ...
|
|
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.
|
|
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": "
|
|
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:
|
|
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
|
|
27
|
-
|
|
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
|
-
}):
|
|
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
|
-
|
|
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: { ...
|
|
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: { ...
|
|
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
|
};
|