-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathbinding.ts
155 lines (136 loc) · 4.9 KB
/
binding.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { Binding, createBinding, joinBindings } from "@rbxts/react";
import { lerp } from "./math";
export interface BindingApi<T> {
subscribe: (callback: (newValue: T) => void) => () => void;
update: (newValue: T) => void;
getValue: () => T;
}
export interface Lerpable<T> {
Lerp: (this: T, to: T, alpha: number) => T;
}
export type BindingOrValue<T> = Binding<T> | T;
type Bindable<T = unknown> = Binding<T> | NonNullable<T>;
type ComposeBindings<T extends Bindable[]> = {
[K in keyof T]: T[K] extends Bindable<infer U> ? U : T[K];
};
type BindingCombiner<T extends Bindable[], U> = (...values: ComposeBindings<T>) => U;
/**
* Returns whether the given value is a binding.
* @param value The value to check.
* @returns Whether the value is a binding.
*/
export function isBinding<T>(value: T | Binding<T>): value is Binding<T>;
export function isBinding<T = unknown>(value: unknown): value is Binding<T>;
export function isBinding(value: unknown): value is Binding<unknown> {
return typeIs(value, "table") && "getValue" in value && "map" in value;
}
/**
* Converts a value to a binding. If the given value is already a binding, it
* will be returned as-is.
* @param value The value to convert.
* @returns The converted binding.
*/
export function toBinding<T>(value: T | Binding<T>): Binding<T> {
if (isBinding(value)) {
return value;
} else {
const [result] = createBinding(value);
return result;
}
}
/**
* Returns the value of a binding. If the given value is not a binding, it will
* be returned as-is.
* @param binding The binding to get the value of.
* @returns The value of the binding.
*/
export function getBindingValue<T>(binding: T | Binding<T>): T {
if (isBinding(binding)) {
return binding.getValue();
} else {
return binding;
}
}
/**
* Maps a binding to a new binding. If the given value is not a binding, it will
* be passed to the mapper function and returned as a new binding.
* @param binding The binding to map.
* @param callback The mapper function.
* @returns The mapped binding.
*/
export function mapBinding<T, U>(binding: T | Binding<T>, callback: (value: T) => U): Binding<U> {
if (isBinding(binding)) {
return binding.map(callback);
} else {
const [result] = createBinding(callback(binding as T));
return result;
}
}
/**
* Joins a map of bindings into a single binding. If any of the given values
* are not bindings, they will be wrapped in a new binding.
* @param bindings The bindings to join.
* @returns The joined binding.
*/
export function joinAnyBindings<T extends Readonly<Record<string, unknown>>>(
bindings: T,
): Binding<{ [K in keyof T]: T[K] extends BindingOrValue<infer U> ? U : T[K] }>;
export function joinAnyBindings<T extends readonly unknown[]>(
bindings: readonly [...T],
): Binding<{ [K in keyof T]: T[K] extends BindingOrValue<infer U> ? U : T[K] }>;
export function joinAnyBindings(bindings: object): Binding<unknown> {
const bindingsToMap = {} as Record<string | number, Binding<unknown>>;
for (const [k, v] of pairs(bindings)) {
bindingsToMap[k as keyof object] = toBinding(v);
}
return joinBindings(bindingsToMap);
}
/**
* Gets the internal API of a binding. This is a hacky way to get access to the
* `BindingInternalApi` object of a binding, which is not exposed by React.
* @param binding The binding to get the internal API of.
* @returns The binding's API.
*/
export function getBindingApi<T>(binding: Binding<T>) {
for (const [key, value] of pairs(binding)) {
const name = tostring(key);
if (name === "Symbol(BindingImpl)" || name.sub(1, 12) === "RoactBinding") {
return value as unknown as BindingApi<T>;
}
}
}
/**
* Returns a binding that lerps between two values using the given binding as
* the alpha.
* @param binding The binding to use as the alpha.
* @param from The value to lerp from.
* @param to The value to lerp to.
* @returns A binding that lerps between two values.
*/
export function lerpBinding<T extends number | Lerpable<any>>(
binding: Binding<number> | number,
from: T,
to: T,
): Binding<T> {
return mapBinding(binding, (alpha) => {
if (typeIs(from, "number")) {
return lerp(from, to as number, alpha);
} else {
return from.Lerp(to, alpha);
}
});
}
/**
* Composes multiple bindings or values together into a single binding.
* Calls the combiner function with the values of the bindings when any
* of the bindings change.
* @param ...bindings A list of bindings or values.
* @param combiner The function that maps the bindings to a new value.
* @returns A binding that returns the result of the combiner.
*/
export function composeBindings<T extends Bindable[], U>(...bindings: [...T, BindingCombiner<T, U>]): Binding<U>;
export function composeBindings<T>(...values: [...Bindable[], BindingCombiner<Bindable[], T>]): Binding<T> {
const combiner = values.pop() as BindingCombiner<Bindable[], T>;
const bindings = values.map(toBinding);
return joinBindings(bindings).map((bindings) => combiner(...bindings));
}