@gtkx/animated 1.3.0
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/LICENSE +373 -0
- package/README.md +173 -0
- package/dist/animated.d.ts +20 -0
- package/dist/animated.d.ts.map +1 -0
- package/dist/animated.js +44 -0
- package/dist/animated.js.map +1 -0
- package/dist/apply-animated-values.d.ts +4 -0
- package/dist/apply-animated-values.d.ts.map +1 -0
- package/dist/apply-animated-values.js +84 -0
- package/dist/apply-animated-values.js.map +1 -0
- package/dist/bootstrap.d.ts +2 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +11 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/reduced-motion.d.ts +13 -0
- package/dist/reduced-motion.d.ts.map +1 -0
- package/dist/reduced-motion.js +59 -0
- package/dist/reduced-motion.js.map +1 -0
- package/dist/request-frame.d.ts +4 -0
- package/dist/request-frame.d.ts.map +1 -0
- package/dist/request-frame.js +155 -0
- package/dist/request-frame.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/with-animated.d.ts +6 -0
- package/dist/with-animated.d.ts.map +1 -0
- package/dist/with-animated.js +181 -0
- package/dist/with-animated.js.map +1 -0
- package/package.json +77 -0
- package/src/animated.ts +62 -0
- package/src/apply-animated-values.ts +116 -0
- package/src/bootstrap.ts +12 -0
- package/src/index.ts +110 -0
- package/src/reduced-motion.ts +82 -0
- package/src/request-frame.ts +214 -0
- package/src/types.ts +64 -0
- package/src/with-animated.tsx +254 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as GLib from "@gtkx/gi/glib";
|
|
2
|
+
import * as Gtk from "@gtkx/gi/gtk";
|
|
3
|
+
|
|
4
|
+
type FrameCallback = () => void;
|
|
5
|
+
type Timer = ReturnType<typeof setTimeout>;
|
|
6
|
+
type Driver = { widget: Gtk.Widget; tickId: number; stallSource: number };
|
|
7
|
+
|
|
8
|
+
type Scheduler = {
|
|
9
|
+
callbacks: FrameCallback[];
|
|
10
|
+
driver: Driver | null;
|
|
11
|
+
fallbackTimer: Timer | null;
|
|
12
|
+
flushedAt: number;
|
|
13
|
+
isTicking: boolean;
|
|
14
|
+
stalledUntil: WeakMap<Gtk.Widget, number>;
|
|
15
|
+
ticks: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const FALLBACK_FRAME_MS = 16;
|
|
19
|
+
const MIN_FRAME_MS = 1;
|
|
20
|
+
const STALL_MS = 250;
|
|
21
|
+
const STALL_COOLDOWN_MS = 1000;
|
|
22
|
+
|
|
23
|
+
const scheduler: Scheduler = {
|
|
24
|
+
callbacks: [],
|
|
25
|
+
driver: null,
|
|
26
|
+
fallbackTimer: null,
|
|
27
|
+
flushedAt: 0,
|
|
28
|
+
isTicking: false,
|
|
29
|
+
stalledUntil: new WeakMap(),
|
|
30
|
+
ticks: 0,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const isSuspended = (widget: Gtk.Widget): boolean => widget instanceof Gtk.Window && widget.isSuspended();
|
|
34
|
+
|
|
35
|
+
const hasClock = (widget: Gtk.Widget): boolean =>
|
|
36
|
+
widget.getMapped() && widget.getFrameClock() !== null && !isSuspended(widget);
|
|
37
|
+
|
|
38
|
+
const isCoolingDown = (widget: Gtk.Widget): boolean => {
|
|
39
|
+
const until = scheduler.stalledUntil.get(widget);
|
|
40
|
+
|
|
41
|
+
if (until === undefined) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (performance.now() < until) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
scheduler.stalledUntil.delete(widget);
|
|
50
|
+
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const findDriverWidget = (): Gtk.Widget | null => {
|
|
55
|
+
const candidates = Gtk.Window.listToplevels().filter((widget) => hasClock(widget));
|
|
56
|
+
|
|
57
|
+
return candidates.find((widget) => !scheduler.stalledUntil.has(widget)) ??
|
|
58
|
+
candidates.find((widget) => !isCoolingDown(widget)) ??
|
|
59
|
+
null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const flush = (): void => {
|
|
63
|
+
const pending = scheduler.callbacks;
|
|
64
|
+
scheduler.callbacks = [];
|
|
65
|
+
scheduler.isTicking = true;
|
|
66
|
+
scheduler.flushedAt = performance.now();
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
for (const callback of pending) {
|
|
70
|
+
callback();
|
|
71
|
+
}
|
|
72
|
+
} finally {
|
|
73
|
+
scheduler.isTicking = false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const armStallSource = (): number => GLib.timeoutAdd(GLib.PRIORITY_DEFAULT_IDLE, STALL_MS, shouldRepeatStallCheck);
|
|
78
|
+
|
|
79
|
+
const cancelStallSource = (driver: Driver): void => {
|
|
80
|
+
if (driver.stallSource === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
GLib.Source.remove(driver.stallSource);
|
|
85
|
+
driver.stallSource = 0;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const finishDriver = (driver: Driver): void => {
|
|
89
|
+
cancelStallSource(driver);
|
|
90
|
+
driver.widget.off("unmap", onDriverUnmapped);
|
|
91
|
+
scheduler.driver = null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const releaseDriver = (driver: Driver): void => {
|
|
95
|
+
finishDriver(driver);
|
|
96
|
+
driver.widget.removeTickCallback(driver.tickId);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const flushThenArm = (): void => {
|
|
100
|
+
try {
|
|
101
|
+
flush();
|
|
102
|
+
} finally {
|
|
103
|
+
arm();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const shouldContinueTicking = (driver: Driver): boolean => {
|
|
108
|
+
if (scheduler.driver !== driver) {
|
|
109
|
+
arm();
|
|
110
|
+
|
|
111
|
+
return GLib.SOURCE_REMOVE;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (scheduler.callbacks.length === 0) {
|
|
115
|
+
finishDriver(driver);
|
|
116
|
+
|
|
117
|
+
return GLib.SOURCE_REMOVE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
driver.stallSource = armStallSource();
|
|
121
|
+
|
|
122
|
+
return GLib.SOURCE_CONTINUE;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const shouldKeepTicking = (): boolean => {
|
|
126
|
+
const { driver } = scheduler;
|
|
127
|
+
|
|
128
|
+
if (driver === null) {
|
|
129
|
+
return GLib.SOURCE_REMOVE;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (performance.now() - scheduler.flushedAt < MIN_FRAME_MS) {
|
|
133
|
+
return GLib.SOURCE_CONTINUE;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
cancelStallSource(driver);
|
|
137
|
+
scheduler.ticks += 1;
|
|
138
|
+
scheduler.stalledUntil.delete(driver.widget);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
flush();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
finishDriver(driver);
|
|
144
|
+
arm();
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return shouldContinueTicking(driver);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const onFallbackFrame = (): void => {
|
|
152
|
+
scheduler.fallbackTimer = null;
|
|
153
|
+
flushThenArm();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const armDriver = (widget: Gtk.Widget): void => {
|
|
157
|
+
const tickId = widget.addTickCallback(shouldKeepTicking);
|
|
158
|
+
scheduler.driver = { widget, tickId, stallSource: armStallSource() };
|
|
159
|
+
widget.on("unmap", onDriverUnmapped);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
function shouldRepeatStallCheck(): boolean {
|
|
163
|
+
const { driver } = scheduler;
|
|
164
|
+
|
|
165
|
+
if (driver !== null) {
|
|
166
|
+
driver.stallSource = 0;
|
|
167
|
+
releaseDriver(driver);
|
|
168
|
+
scheduler.stalledUntil.set(driver.widget, performance.now() + STALL_COOLDOWN_MS);
|
|
169
|
+
flushThenArm();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return GLib.SOURCE_REMOVE;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function onDriverUnmapped(): void {
|
|
176
|
+
const { driver } = scheduler;
|
|
177
|
+
|
|
178
|
+
if (driver === null) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
scheduler.stalledUntil.delete(driver.widget);
|
|
183
|
+
releaseDriver(driver);
|
|
184
|
+
|
|
185
|
+
if (!scheduler.isTicking) {
|
|
186
|
+
arm();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function arm(): void {
|
|
191
|
+
if (scheduler.callbacks.length === 0 || scheduler.driver !== null || scheduler.fallbackTimer !== null) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const widget = findDriverWidget();
|
|
196
|
+
|
|
197
|
+
if (widget === null) {
|
|
198
|
+
scheduler.fallbackTimer = setTimeout(onFallbackFrame, FALLBACK_FRAME_MS);
|
|
199
|
+
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
armDriver(widget);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const requestFrame = (callback: FrameCallback): void => {
|
|
207
|
+
scheduler.callbacks.push(callback);
|
|
208
|
+
|
|
209
|
+
if (!scheduler.isTicking) {
|
|
210
|
+
arm();
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export { requestFrame };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type * as Gtk from "@gtkx/gi/gtk";
|
|
2
|
+
import type * as elements from "@gtkx/jsx";
|
|
3
|
+
import type { FluidValue } from "@react-spring/shared";
|
|
4
|
+
import type { ComponentPropsWithRef, ElementType, FunctionComponent, Ref } from "react";
|
|
5
|
+
|
|
6
|
+
/** An array-valued prop whose items may each be a spring or an interpolation, such as mixed text children. */
|
|
7
|
+
type AnimatedItems<T> = [Exclude<Extract<T, Iterable<unknown>>, string>] extends [never]
|
|
8
|
+
? never
|
|
9
|
+
: Exclude<Extract<T, Iterable<unknown>>, string> extends Iterable<infer Item>
|
|
10
|
+
? Iterable<AnimatedProp<Item>>
|
|
11
|
+
: never;
|
|
12
|
+
|
|
13
|
+
/** A prop value that an animated component also accepts as a spring or an interpolation. */
|
|
14
|
+
type AnimatedProp<T> = T | FluidValue<Exclude<T, undefined>> | AnimatedItems<Exclude<T, undefined>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `style` object whose declarations may each be a spring or an interpolation, nested blocks
|
|
18
|
+
* included, so the object a spring hook returns can be handed to `style` as it is.
|
|
19
|
+
*/
|
|
20
|
+
type AnimatedStyle<T> = {
|
|
21
|
+
[K in keyof T]: T[K] extends string | number | undefined | null
|
|
22
|
+
? AnimatedProp<T[K]>
|
|
23
|
+
: AnimatedProp<T[K]> | AnimatedStyle<NonNullable<T[K]>>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The props of an animated component: every prop of the wrapped component, each also accepting a
|
|
28
|
+
* {@link FluidValue} such as a `SpringValue` or an `Interpolation`, while `ref` and `key` keep
|
|
29
|
+
* their original types.
|
|
30
|
+
*/
|
|
31
|
+
type AnimatedProps<Props extends object> = {
|
|
32
|
+
[P in keyof Props]: P extends "key" | "ref"
|
|
33
|
+
? Props[P]
|
|
34
|
+
: P extends "style"
|
|
35
|
+
? AnimatedProp<Props[P]> | AnimatedStyle<NonNullable<Props[P]>>
|
|
36
|
+
: AnimatedProp<Props[P]>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** A component returned by {@link animated}: the wrapped component with animated props. */
|
|
40
|
+
type AnimatedComponent<T extends Exclude<ElementType, string>> = FunctionComponent<
|
|
41
|
+
AnimatedProps<ComponentPropsWithRef<T>>
|
|
42
|
+
>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The widget components of the generated `@gtkx/jsx` store, keyed by element name, each wrapped as
|
|
46
|
+
* an {@link AnimatedComponent}. Only components whose `ref` exposes a `Gtk.Widget` subclass are
|
|
47
|
+
* included, so `animated.GtkLabel` is available while non-widget elements such as `GtkAdjustment`
|
|
48
|
+
* are wrapped explicitly through the `animated(...)` call instead.
|
|
49
|
+
*/
|
|
50
|
+
type AnimatedElements = {
|
|
51
|
+
readonly [K in keyof typeof elements as (typeof elements)[K] extends Exclude<ElementType, string>
|
|
52
|
+
? ComponentPropsWithRef<(typeof elements)[K]> extends { ref?: Ref<infer Instance> | undefined }
|
|
53
|
+
? [NonNullable<Instance>] extends [never]
|
|
54
|
+
? never
|
|
55
|
+
: NonNullable<Instance> extends Gtk.Widget
|
|
56
|
+
? K
|
|
57
|
+
: never
|
|
58
|
+
: never
|
|
59
|
+
: never]: (typeof elements)[K] extends Exclude<ElementType, string>
|
|
60
|
+
? AnimatedComponent<(typeof elements)[K]>
|
|
61
|
+
: never;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type { AnimatedComponent, AnimatedElements, AnimatedItems, AnimatedProp, AnimatedProps, AnimatedStyle };
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Lookup } from "@react-spring/types";
|
|
2
|
+
import { useMergedRef } from "@gtkx/react/internal";
|
|
3
|
+
import {
|
|
4
|
+
addFluidObserver,
|
|
5
|
+
type FluidEvent,
|
|
6
|
+
type FluidValue,
|
|
7
|
+
getFluidValue,
|
|
8
|
+
hasFluidValue,
|
|
9
|
+
raf,
|
|
10
|
+
removeFluidObserver,
|
|
11
|
+
useForceUpdate,
|
|
12
|
+
} from "@react-spring/shared";
|
|
13
|
+
import { type ElementType, type ReactNode, type Ref, type RefObject, useLayoutEffect, useRef } from "react";
|
|
14
|
+
import type { AnimatedComponent } from "./types.js";
|
|
15
|
+
import { didApplyAnimatedValues } from "./apply-animated-values.js";
|
|
16
|
+
import { trackReducedMotion } from "./reduced-motion.js";
|
|
17
|
+
|
|
18
|
+
type AnimatedInput = { ref?: Ref<object> | undefined; [key: string]: unknown };
|
|
19
|
+
type Wrappable = Exclude<ElementType, string>;
|
|
20
|
+
type ObserverRef = { current: PropsObserver | null };
|
|
21
|
+
|
|
22
|
+
const STYLE_PROP = "style";
|
|
23
|
+
const cache: WeakMap<object, AnimatedComponent<Wrappable>> = new WeakMap();
|
|
24
|
+
|
|
25
|
+
const getDisplayName = (component: Wrappable): string => {
|
|
26
|
+
const { displayName, name } = component as { displayName?: unknown; name?: unknown };
|
|
27
|
+
|
|
28
|
+
if (typeof displayName === "string" && displayName !== "") {
|
|
29
|
+
return displayName;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return typeof name === "string" && name !== "" ? name : "Anonymous";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isFluidProp = (value: unknown): boolean => hasFluidValue(value) || isFluidArray(value);
|
|
36
|
+
|
|
37
|
+
const isBlock = (value: unknown): value is Lookup =>
|
|
38
|
+
typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
|
|
39
|
+
|
|
40
|
+
function isFluidArray(value: unknown): value is unknown[] {
|
|
41
|
+
return Array.isArray(value) && value.some((item) => isFluidProp(item));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isFluidStyle(value: unknown): value is Lookup {
|
|
45
|
+
return isBlock(value) && Object.values(value).some((item) => hasFluidValue(item) || isFluidStyle(item));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveStyle(style: Lookup): Lookup {
|
|
49
|
+
const resolved: Lookup = {};
|
|
50
|
+
|
|
51
|
+
for (const name in style) {
|
|
52
|
+
const value: unknown = style[name];
|
|
53
|
+
const next: unknown = isFluidStyle(value) ? resolveStyle(value) : getFluidValue(value);
|
|
54
|
+
resolved[name] = next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return resolved;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveValue(value: unknown): unknown {
|
|
61
|
+
return isFluidArray(value) ? value.map((item) => resolveValue(item)) : getFluidValue(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const isFluidNamed = (name: string, value: unknown): boolean =>
|
|
65
|
+
isFluidProp(value) || (name === STYLE_PROP && isFluidStyle(value));
|
|
66
|
+
|
|
67
|
+
const resolveNamed = (name: string, value: unknown): unknown =>
|
|
68
|
+
name === STYLE_PROP && isFluidStyle(value) ? resolveStyle(value) : resolveValue(value);
|
|
69
|
+
|
|
70
|
+
const resolveProps = (props: Lookup, isAnimatedOnly: boolean): Lookup => {
|
|
71
|
+
const resolved: Lookup = {};
|
|
72
|
+
|
|
73
|
+
for (const name in props) {
|
|
74
|
+
const value: unknown = props[name];
|
|
75
|
+
|
|
76
|
+
if (isFluidNamed(name, value)) {
|
|
77
|
+
resolved[name] = resolveNamed(name, value);
|
|
78
|
+
} else if (!isAnimatedOnly) {
|
|
79
|
+
resolved[name] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return resolved;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function collectEach(items: unknown[], dependencies: Set<FluidValue>): void {
|
|
87
|
+
for (const item of items) {
|
|
88
|
+
collectDependencies(item, dependencies);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectDependencies(value: unknown, dependencies: Set<FluidValue>): void {
|
|
93
|
+
if (hasFluidValue(value)) {
|
|
94
|
+
dependencies.add(value);
|
|
95
|
+
} else if (isFluidArray(value)) {
|
|
96
|
+
collectEach(value, dependencies);
|
|
97
|
+
} else if (isFluidStyle(value)) {
|
|
98
|
+
collectEach(Object.values(value), dependencies);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const collectNamed = (name: string, value: unknown, dependencies: Set<FluidValue>): void => {
|
|
103
|
+
if (isFluidNamed(name, value)) {
|
|
104
|
+
collectDependencies(value, dependencies);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const getDependencies = (props: Lookup): Set<FluidValue> => {
|
|
109
|
+
const dependencies: Set<FluidValue> = new Set();
|
|
110
|
+
|
|
111
|
+
for (const name in props) {
|
|
112
|
+
collectNamed(name, props[name], dependencies);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return dependencies;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const getFluidNames = (props: Lookup): Set<string> => {
|
|
119
|
+
const names: Set<string> = new Set();
|
|
120
|
+
|
|
121
|
+
for (const name in props) {
|
|
122
|
+
if (isFluidNamed(name, props[name])) {
|
|
123
|
+
names.add(name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return names;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const observe = (observer: PropsObserver): void => {
|
|
131
|
+
for (const dependency of observer.dependencies) {
|
|
132
|
+
addFluidObserver(dependency, observer);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const unobserve = (observer: PropsObserver): void => {
|
|
137
|
+
for (const dependency of observer.dependencies) {
|
|
138
|
+
removeFluidObserver(dependency, observer);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
raf.cancel(observer.update);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const useObserver = (observer: PropsObserver): void => {
|
|
145
|
+
const observerRef: ObserverRef = useRef<PropsObserver | null>(null);
|
|
146
|
+
|
|
147
|
+
useLayoutEffect(() => {
|
|
148
|
+
observerRef.current = observer;
|
|
149
|
+
observe(observer);
|
|
150
|
+
|
|
151
|
+
return () => {
|
|
152
|
+
if (observerRef.current === null) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
unobserve(observerRef.current);
|
|
157
|
+
observerRef.current = null;
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const collectStaticReplacements = (
|
|
163
|
+
props: Lookup,
|
|
164
|
+
previous: Set<string>,
|
|
165
|
+
current: Set<string>,
|
|
166
|
+
values: Lookup,
|
|
167
|
+
): void => {
|
|
168
|
+
for (const name of previous) {
|
|
169
|
+
const value: unknown = props[name];
|
|
170
|
+
|
|
171
|
+
if (value !== undefined && !current.has(name)) {
|
|
172
|
+
values[name] = value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const useCommitSync = (instanceRef: RefObject<object | null>, props: Lookup): void => {
|
|
178
|
+
const fluidNamesRef = useRef<Set<string>>(new Set());
|
|
179
|
+
|
|
180
|
+
useLayoutEffect(() => {
|
|
181
|
+
const previous = fluidNamesRef.current;
|
|
182
|
+
const current = getFluidNames(props);
|
|
183
|
+
fluidNamesRef.current = current;
|
|
184
|
+
trackReducedMotion();
|
|
185
|
+
const instance = instanceRef.current;
|
|
186
|
+
|
|
187
|
+
if (instance === null) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const values = resolveProps(props, true);
|
|
192
|
+
collectStaticReplacements(props, previous, current, values);
|
|
193
|
+
didApplyAnimatedValues(instance, values);
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const useAnimatedUpdate = (instanceRef: RefObject<object | null>, props: Lookup): (() => void) => {
|
|
198
|
+
const forceUpdate = useForceUpdate();
|
|
199
|
+
|
|
200
|
+
return () => {
|
|
201
|
+
const instance = instanceRef.current;
|
|
202
|
+
const isApplied = instance !== null && didApplyAnimatedValues(instance, resolveProps(props, true));
|
|
203
|
+
|
|
204
|
+
if (!isApplied) {
|
|
205
|
+
forceUpdate();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const createAnimatedComponent = (Component: Wrappable): AnimatedComponent<Wrappable> => {
|
|
211
|
+
const Animated = ({ ref: givenRef, ...props }: AnimatedInput): ReactNode => {
|
|
212
|
+
const instanceRef = useRef<object | null>(null);
|
|
213
|
+
const ref = useMergedRef(givenRef, instanceRef);
|
|
214
|
+
const update = useAnimatedUpdate(instanceRef, props);
|
|
215
|
+
useObserver(new PropsObserver(update, getDependencies(props)));
|
|
216
|
+
useCommitSync(instanceRef, props);
|
|
217
|
+
|
|
218
|
+
return <Component {...resolveProps(props, false)} ref={ref} />;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
Animated.displayName = `Animated(${getDisplayName(Component)})`;
|
|
222
|
+
|
|
223
|
+
return Animated as AnimatedComponent<Wrappable>;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
function withAnimated<T extends Wrappable>(component: T): AnimatedComponent<T> {
|
|
227
|
+
let cached = cache.get(component);
|
|
228
|
+
|
|
229
|
+
if (cached === undefined) {
|
|
230
|
+
cached = createAnimatedComponent(component);
|
|
231
|
+
cache.set(component, cached);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return cached as AnimatedComponent<T>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
class PropsObserver {
|
|
238
|
+
readonly update: () => void;
|
|
239
|
+
|
|
240
|
+
readonly dependencies: Set<FluidValue>;
|
|
241
|
+
|
|
242
|
+
constructor(update: () => void, dependencies: Set<FluidValue>) {
|
|
243
|
+
this.update = update;
|
|
244
|
+
this.dependencies = dependencies;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
eventObserved(event: FluidEvent): void {
|
|
248
|
+
if (event.type === "change") {
|
|
249
|
+
raf.write(this.update);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export { withAnimated };
|