@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.
Files changed (43) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +173 -0
  3. package/dist/animated.d.ts +20 -0
  4. package/dist/animated.d.ts.map +1 -0
  5. package/dist/animated.js +44 -0
  6. package/dist/animated.js.map +1 -0
  7. package/dist/apply-animated-values.d.ts +4 -0
  8. package/dist/apply-animated-values.d.ts.map +1 -0
  9. package/dist/apply-animated-values.js +84 -0
  10. package/dist/apply-animated-values.js.map +1 -0
  11. package/dist/bootstrap.d.ts +2 -0
  12. package/dist/bootstrap.d.ts.map +1 -0
  13. package/dist/bootstrap.js +11 -0
  14. package/dist/bootstrap.js.map +1 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +5 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/reduced-motion.d.ts +13 -0
  20. package/dist/reduced-motion.d.ts.map +1 -0
  21. package/dist/reduced-motion.js +59 -0
  22. package/dist/reduced-motion.js.map +1 -0
  23. package/dist/request-frame.d.ts +4 -0
  24. package/dist/request-frame.d.ts.map +1 -0
  25. package/dist/request-frame.js +155 -0
  26. package/dist/request-frame.js.map +1 -0
  27. package/dist/types.d.ts +38 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +2 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/with-animated.d.ts +6 -0
  32. package/dist/with-animated.d.ts.map +1 -0
  33. package/dist/with-animated.js +181 -0
  34. package/dist/with-animated.js.map +1 -0
  35. package/package.json +77 -0
  36. package/src/animated.ts +62 -0
  37. package/src/apply-animated-values.ts +116 -0
  38. package/src/bootstrap.ts +12 -0
  39. package/src/index.ts +110 -0
  40. package/src/reduced-motion.ts +82 -0
  41. package/src/request-frame.ts +214 -0
  42. package/src/types.ts +64 -0
  43. package/src/with-animated.tsx +254 -0
@@ -0,0 +1,181 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMergedRef } from "@gtkx/react/internal";
3
+ import { addFluidObserver, getFluidValue, hasFluidValue, raf, removeFluidObserver, useForceUpdate, } from "@react-spring/shared";
4
+ import { useLayoutEffect, useRef } from "react";
5
+ import { didApplyAnimatedValues } from "./apply-animated-values.js";
6
+ import { trackReducedMotion } from "./reduced-motion.js";
7
+ const STYLE_PROP = "style";
8
+ const cache = new WeakMap();
9
+ const getDisplayName = (component) => {
10
+ const { displayName, name } = component;
11
+ if (typeof displayName === "string" && displayName !== "") {
12
+ return displayName;
13
+ }
14
+ return typeof name === "string" && name !== "" ? name : "Anonymous";
15
+ };
16
+ const isFluidProp = (value) => hasFluidValue(value) || isFluidArray(value);
17
+ const isBlock = (value) => typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype;
18
+ function isFluidArray(value) {
19
+ return Array.isArray(value) && value.some((item) => isFluidProp(item));
20
+ }
21
+ function isFluidStyle(value) {
22
+ return isBlock(value) && Object.values(value).some((item) => hasFluidValue(item) || isFluidStyle(item));
23
+ }
24
+ function resolveStyle(style) {
25
+ const resolved = {};
26
+ for (const name in style) {
27
+ const value = style[name];
28
+ const next = isFluidStyle(value) ? resolveStyle(value) : getFluidValue(value);
29
+ resolved[name] = next;
30
+ }
31
+ return resolved;
32
+ }
33
+ function resolveValue(value) {
34
+ return isFluidArray(value) ? value.map((item) => resolveValue(item)) : getFluidValue(value);
35
+ }
36
+ const isFluidNamed = (name, value) => isFluidProp(value) || (name === STYLE_PROP && isFluidStyle(value));
37
+ const resolveNamed = (name, value) => name === STYLE_PROP && isFluidStyle(value) ? resolveStyle(value) : resolveValue(value);
38
+ const resolveProps = (props, isAnimatedOnly) => {
39
+ const resolved = {};
40
+ for (const name in props) {
41
+ const value = props[name];
42
+ if (isFluidNamed(name, value)) {
43
+ resolved[name] = resolveNamed(name, value);
44
+ }
45
+ else if (!isAnimatedOnly) {
46
+ resolved[name] = value;
47
+ }
48
+ }
49
+ return resolved;
50
+ };
51
+ function collectEach(items, dependencies) {
52
+ for (const item of items) {
53
+ collectDependencies(item, dependencies);
54
+ }
55
+ }
56
+ function collectDependencies(value, dependencies) {
57
+ if (hasFluidValue(value)) {
58
+ dependencies.add(value);
59
+ }
60
+ else if (isFluidArray(value)) {
61
+ collectEach(value, dependencies);
62
+ }
63
+ else if (isFluidStyle(value)) {
64
+ collectEach(Object.values(value), dependencies);
65
+ }
66
+ }
67
+ const collectNamed = (name, value, dependencies) => {
68
+ if (isFluidNamed(name, value)) {
69
+ collectDependencies(value, dependencies);
70
+ }
71
+ };
72
+ const getDependencies = (props) => {
73
+ const dependencies = new Set();
74
+ for (const name in props) {
75
+ collectNamed(name, props[name], dependencies);
76
+ }
77
+ return dependencies;
78
+ };
79
+ const getFluidNames = (props) => {
80
+ const names = new Set();
81
+ for (const name in props) {
82
+ if (isFluidNamed(name, props[name])) {
83
+ names.add(name);
84
+ }
85
+ }
86
+ return names;
87
+ };
88
+ const observe = (observer) => {
89
+ for (const dependency of observer.dependencies) {
90
+ addFluidObserver(dependency, observer);
91
+ }
92
+ };
93
+ const unobserve = (observer) => {
94
+ for (const dependency of observer.dependencies) {
95
+ removeFluidObserver(dependency, observer);
96
+ }
97
+ raf.cancel(observer.update);
98
+ };
99
+ const useObserver = (observer) => {
100
+ const observerRef = useRef(null);
101
+ useLayoutEffect(() => {
102
+ observerRef.current = observer;
103
+ observe(observer);
104
+ return () => {
105
+ if (observerRef.current === null) {
106
+ return;
107
+ }
108
+ unobserve(observerRef.current);
109
+ observerRef.current = null;
110
+ };
111
+ });
112
+ };
113
+ const collectStaticReplacements = (props, previous, current, values) => {
114
+ for (const name of previous) {
115
+ const value = props[name];
116
+ if (value !== undefined && !current.has(name)) {
117
+ values[name] = value;
118
+ }
119
+ }
120
+ };
121
+ const useCommitSync = (instanceRef, props) => {
122
+ const fluidNamesRef = useRef(new Set());
123
+ useLayoutEffect(() => {
124
+ const previous = fluidNamesRef.current;
125
+ const current = getFluidNames(props);
126
+ fluidNamesRef.current = current;
127
+ trackReducedMotion();
128
+ const instance = instanceRef.current;
129
+ if (instance === null) {
130
+ return;
131
+ }
132
+ const values = resolveProps(props, true);
133
+ collectStaticReplacements(props, previous, current, values);
134
+ didApplyAnimatedValues(instance, values);
135
+ });
136
+ };
137
+ const useAnimatedUpdate = (instanceRef, props) => {
138
+ const forceUpdate = useForceUpdate();
139
+ return () => {
140
+ const instance = instanceRef.current;
141
+ const isApplied = instance !== null && didApplyAnimatedValues(instance, resolveProps(props, true));
142
+ if (!isApplied) {
143
+ forceUpdate();
144
+ }
145
+ };
146
+ };
147
+ const createAnimatedComponent = (Component) => {
148
+ const Animated = ({ ref: givenRef, ...props }) => {
149
+ const instanceRef = useRef(null);
150
+ const ref = useMergedRef(givenRef, instanceRef);
151
+ const update = useAnimatedUpdate(instanceRef, props);
152
+ useObserver(new PropsObserver(update, getDependencies(props)));
153
+ useCommitSync(instanceRef, props);
154
+ return _jsx(Component, { ...resolveProps(props, false), ref: ref });
155
+ };
156
+ Animated.displayName = `Animated(${getDisplayName(Component)})`;
157
+ return Animated;
158
+ };
159
+ function withAnimated(component) {
160
+ let cached = cache.get(component);
161
+ if (cached === undefined) {
162
+ cached = createAnimatedComponent(component);
163
+ cache.set(component, cached);
164
+ }
165
+ return cached;
166
+ }
167
+ class PropsObserver {
168
+ update;
169
+ dependencies;
170
+ constructor(update, dependencies) {
171
+ this.update = update;
172
+ this.dependencies = dependencies;
173
+ }
174
+ eventObserved(event) {
175
+ if (event.type === "change") {
176
+ raf.write(this.update);
177
+ }
178
+ }
179
+ }
180
+ export { withAnimated };
181
+ //# sourceMappingURL=with-animated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with-animated.js","sourceRoot":"","sources":["../src/with-animated.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { Lookup } from \"@react-spring/types\";\nimport { useMergedRef } from \"@gtkx/react/internal\";\nimport {\n addFluidObserver,\n type FluidEvent,\n type FluidValue,\n getFluidValue,\n hasFluidValue,\n raf,\n removeFluidObserver,\n useForceUpdate,\n} from \"@react-spring/shared\";\nimport { type ElementType, type ReactNode, type Ref, type RefObject, useLayoutEffect, useRef } from \"react\";\nimport type { AnimatedComponent } from \"./types.js\";\nimport { didApplyAnimatedValues } from \"./apply-animated-values.js\";\nimport { trackReducedMotion } from \"./reduced-motion.js\";\n\ntype AnimatedInput = { ref?: Ref<object> | undefined; [key: string]: unknown };\ntype Wrappable = Exclude<ElementType, string>;\ntype ObserverRef = { current: PropsObserver | null };\n\nconst STYLE_PROP = \"style\";\nconst cache: WeakMap<object, AnimatedComponent<Wrappable>> = new WeakMap();\n\nconst getDisplayName = (component: Wrappable): string => {\n const { displayName, name } = component as { displayName?: unknown; name?: unknown };\n\n if (typeof displayName === \"string\" && displayName !== \"\") {\n return displayName;\n }\n\n return typeof name === \"string\" && name !== \"\" ? name : \"Anonymous\";\n};\n\nconst isFluidProp = (value: unknown): boolean => hasFluidValue(value) || isFluidArray(value);\n\nconst isBlock = (value: unknown): value is Lookup =>\n typeof value === \"object\" && value !== null && Object.getPrototypeOf(value) === Object.prototype;\n\nfunction isFluidArray(value: unknown): value is unknown[] {\n return Array.isArray(value) && value.some((item) => isFluidProp(item));\n}\n\nfunction isFluidStyle(value: unknown): value is Lookup {\n return isBlock(value) && Object.values(value).some((item) => hasFluidValue(item) || isFluidStyle(item));\n}\n\nfunction resolveStyle(style: Lookup): Lookup {\n const resolved: Lookup = {};\n\n for (const name in style) {\n const value: unknown = style[name];\n const next: unknown = isFluidStyle(value) ? resolveStyle(value) : getFluidValue(value);\n resolved[name] = next;\n }\n\n return resolved;\n}\n\nfunction resolveValue(value: unknown): unknown {\n return isFluidArray(value) ? value.map((item) => resolveValue(item)) : getFluidValue(value);\n}\n\nconst isFluidNamed = (name: string, value: unknown): boolean =>\n isFluidProp(value) || (name === STYLE_PROP && isFluidStyle(value));\n\nconst resolveNamed = (name: string, value: unknown): unknown =>\n name === STYLE_PROP && isFluidStyle(value) ? resolveStyle(value) : resolveValue(value);\n\nconst resolveProps = (props: Lookup, isAnimatedOnly: boolean): Lookup => {\n const resolved: Lookup = {};\n\n for (const name in props) {\n const value: unknown = props[name];\n\n if (isFluidNamed(name, value)) {\n resolved[name] = resolveNamed(name, value);\n } else if (!isAnimatedOnly) {\n resolved[name] = value;\n }\n }\n\n return resolved;\n};\n\nfunction collectEach(items: unknown[], dependencies: Set<FluidValue>): void {\n for (const item of items) {\n collectDependencies(item, dependencies);\n }\n}\n\nfunction collectDependencies(value: unknown, dependencies: Set<FluidValue>): void {\n if (hasFluidValue(value)) {\n dependencies.add(value);\n } else if (isFluidArray(value)) {\n collectEach(value, dependencies);\n } else if (isFluidStyle(value)) {\n collectEach(Object.values(value), dependencies);\n }\n}\n\nconst collectNamed = (name: string, value: unknown, dependencies: Set<FluidValue>): void => {\n if (isFluidNamed(name, value)) {\n collectDependencies(value, dependencies);\n }\n};\n\nconst getDependencies = (props: Lookup): Set<FluidValue> => {\n const dependencies: Set<FluidValue> = new Set();\n\n for (const name in props) {\n collectNamed(name, props[name], dependencies);\n }\n\n return dependencies;\n};\n\nconst getFluidNames = (props: Lookup): Set<string> => {\n const names: Set<string> = new Set();\n\n for (const name in props) {\n if (isFluidNamed(name, props[name])) {\n names.add(name);\n }\n }\n\n return names;\n};\n\nconst observe = (observer: PropsObserver): void => {\n for (const dependency of observer.dependencies) {\n addFluidObserver(dependency, observer);\n }\n};\n\nconst unobserve = (observer: PropsObserver): void => {\n for (const dependency of observer.dependencies) {\n removeFluidObserver(dependency, observer);\n }\n\n raf.cancel(observer.update);\n};\n\nconst useObserver = (observer: PropsObserver): void => {\n const observerRef: ObserverRef = useRef<PropsObserver | null>(null);\n\n useLayoutEffect(() => {\n observerRef.current = observer;\n observe(observer);\n\n return () => {\n if (observerRef.current === null) {\n return;\n }\n\n unobserve(observerRef.current);\n observerRef.current = null;\n };\n });\n};\n\nconst collectStaticReplacements = (\n props: Lookup,\n previous: Set<string>,\n current: Set<string>,\n values: Lookup,\n): void => {\n for (const name of previous) {\n const value: unknown = props[name];\n\n if (value !== undefined && !current.has(name)) {\n values[name] = value;\n }\n }\n};\n\nconst useCommitSync = (instanceRef: RefObject<object | null>, props: Lookup): void => {\n const fluidNamesRef = useRef<Set<string>>(new Set());\n\n useLayoutEffect(() => {\n const previous = fluidNamesRef.current;\n const current = getFluidNames(props);\n fluidNamesRef.current = current;\n trackReducedMotion();\n const instance = instanceRef.current;\n\n if (instance === null) {\n return;\n }\n\n const values = resolveProps(props, true);\n collectStaticReplacements(props, previous, current, values);\n didApplyAnimatedValues(instance, values);\n });\n};\n\nconst useAnimatedUpdate = (instanceRef: RefObject<object | null>, props: Lookup): (() => void) => {\n const forceUpdate = useForceUpdate();\n\n return () => {\n const instance = instanceRef.current;\n const isApplied = instance !== null && didApplyAnimatedValues(instance, resolveProps(props, true));\n\n if (!isApplied) {\n forceUpdate();\n }\n };\n};\n\nconst createAnimatedComponent = (Component: Wrappable): AnimatedComponent<Wrappable> => {\n const Animated = ({ ref: givenRef, ...props }: AnimatedInput): ReactNode => {\n const instanceRef = useRef<object | null>(null);\n const ref = useMergedRef(givenRef, instanceRef);\n const update = useAnimatedUpdate(instanceRef, props);\n useObserver(new PropsObserver(update, getDependencies(props)));\n useCommitSync(instanceRef, props);\n\n return <Component {...resolveProps(props, false)} ref={ref} />;\n };\n\n Animated.displayName = `Animated(${getDisplayName(Component)})`;\n\n return Animated as AnimatedComponent<Wrappable>;\n};\n\nfunction withAnimated<T extends Wrappable>(component: T): AnimatedComponent<T> {\n let cached = cache.get(component);\n\n if (cached === undefined) {\n cached = createAnimatedComponent(component);\n cache.set(component, cached);\n }\n\n return cached as AnimatedComponent<T>;\n}\n\nclass PropsObserver {\n readonly update: () => void;\n\n readonly dependencies: Set<FluidValue>;\n\n constructor(update: () => void, dependencies: Set<FluidValue>) {\n this.update = update;\n this.dependencies = dependencies;\n }\n\n eventObserved(event: FluidEvent): void {\n if (event.type === \"change\") {\n raf.write(this.update);\n }\n }\n}\n\nexport { withAnimated };\n"]}
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@gtkx/animated",
3
+ "version": "1.3.0",
4
+ "description": "React Spring animations for GTKX: animated widget components driven by the GTK frame clock",
5
+ "keywords": [
6
+ "gtkx",
7
+ "gtk",
8
+ "gtk4",
9
+ "react",
10
+ "react-spring",
11
+ "animation",
12
+ "spring",
13
+ "linux",
14
+ "desktop"
15
+ ],
16
+ "homepage": "https://gtkx.dev",
17
+ "bugs": {
18
+ "url": "https://github.com/gtkx-org/gtkx/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/gtkx-org/gtkx.git",
23
+ "directory": "packages/animated"
24
+ },
25
+ "license": "MPL-2.0",
26
+ "author": "Eugenio Depalo <eugeniodepalo@gmail.com>",
27
+ "type": "module",
28
+ "exports": {
29
+ "./package.json": "./package.json",
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "default": "./dist/index.js"
33
+ }
34
+ },
35
+ "sideEffects": [
36
+ "./src/index.ts",
37
+ "./dist/index.js",
38
+ "./src/bootstrap.ts",
39
+ "./dist/bootstrap.js"
40
+ ],
41
+ "files": [
42
+ "dist",
43
+ "src"
44
+ ],
45
+ "nx": {
46
+ "targets": {
47
+ "lint": {
48
+ "options": {
49
+ "cwd": ".",
50
+ "command": "eslint {projectRoot}"
51
+ }
52
+ }
53
+ }
54
+ },
55
+ "engines": {
56
+ "node": ">=24"
57
+ },
58
+ "dependencies": {
59
+ "@react-spring/core": "^10.1.2",
60
+ "@react-spring/shared": "^10.1.2",
61
+ "@react-spring/types": "^10.1.2",
62
+ "@gtkx/react": "1.3.0",
63
+ "@gtkx/runtime": "1.3.0"
64
+ },
65
+ "peerDependencies": {
66
+ "react": "^19.2"
67
+ },
68
+ "devDependencies": {
69
+ "vitest": "^4.1.11",
70
+ "@gtkx/config": "1.3.0",
71
+ "@gtkx/testing": "1.3.0",
72
+ "@gtkx/vitest": "1.3.0"
73
+ },
74
+ "scripts": {
75
+ "release": "tsx ../../scripts/release-package.ts"
76
+ }
77
+ }
@@ -0,0 +1,62 @@
1
+ import type { ElementType } from "react";
2
+ import * as elements from "@gtkx/jsx";
3
+ import type { AnimatedComponent, AnimatedElements } from "./types.js";
4
+ import { withAnimated } from "./with-animated.js";
5
+
6
+ type Wrappable = Exclude<ElementType, string>;
7
+ /**
8
+ * The `animated` entrypoint: callable to wrap any component, and exposing every generated widget
9
+ * component as a property, so `animated.GtkLabel` is the animated form of `GtkLabel`.
10
+ */
11
+ type Animated = (<T extends Exclude<ElementType, string>>(component: T) => AnimatedComponent<T>) & AnimatedElements;
12
+
13
+ /**
14
+ * Wraps a component so that its props accept springs and interpolations: `animated(GtkLabel)` is
15
+ * `GtkLabel` with every prop also taking a `SpringValue` or an `Interpolation`. Animated values are
16
+ * written to the widget on each frame without a React render. The wrapper of a given component is
17
+ * created once and reused, so repeated lookups render the same component.
18
+ *
19
+ * Every widget component of the generated `@gtkx/jsx` store is also available as a property:
20
+ * `animated.GtkLabel` is `animated(GtkLabel)` without the import. Elements whose `ref` does not
21
+ * expose a `Gtk.Widget` subclass, such as `GtkAdjustment`, are wrapped through the call form.
22
+ */
23
+ const animated: Animated = new Proxy(wrapComponent, {
24
+ get(target, property, receiver): unknown {
25
+ const component = componentFor(property);
26
+
27
+ return component === null ? Reflect.get(target, property, receiver) : withAnimated(component);
28
+ },
29
+ getOwnPropertyDescriptor(target, property): PropertyDescriptor | undefined {
30
+ const component = componentFor(property);
31
+
32
+ if (component === null) {
33
+ return Reflect.getOwnPropertyDescriptor(target, property);
34
+ }
35
+
36
+ return { configurable: true, enumerable: true, writable: false, value: withAnimated(component) };
37
+ },
38
+ has(target, property): boolean {
39
+ return componentFor(property) !== null || Reflect.has(target, property);
40
+ },
41
+ ownKeys(target): (string | symbol)[] {
42
+ const names = Object.keys(elements).filter((name) => componentFor(name) !== null);
43
+
44
+ return [...new Set([...Reflect.ownKeys(target), ...names])];
45
+ },
46
+ }) as Animated;
47
+
48
+ function componentFor(property: string | symbol): Wrappable | null {
49
+ const value: unknown = Reflect.get(elements, property);
50
+
51
+ return isComponent(value) ? value : null;
52
+ }
53
+
54
+ function isComponent(value: unknown): value is Wrappable {
55
+ return typeof value === "function";
56
+ }
57
+
58
+ function wrapComponent<T extends Wrappable>(component: T): AnimatedComponent<T> {
59
+ return withAnimated(component);
60
+ }
61
+
62
+ export { animated, type Animated };
@@ -0,0 +1,116 @@
1
+ import type { Lookup } from "@react-spring/types";
2
+ import * as Gtk from "@gtkx/gi/gtk";
3
+ import { applyStyle, applyWrite } from "@gtkx/react/internal";
4
+ import { coerceObjectProperty } from "@gtkx/runtime";
5
+
6
+ const setterCache: WeakMap<object, Map<string, boolean>> = new WeakMap();
7
+
8
+ const hasSetterInChain = (prototype: object | null, name: string): boolean => {
9
+ for (let current = prototype; current !== null; current = Object.getPrototypeOf(current) as object | null) {
10
+ const descriptor = Object.getOwnPropertyDescriptor(current, name);
11
+
12
+ if (descriptor !== undefined) {
13
+ return descriptor.set !== undefined;
14
+ }
15
+ }
16
+
17
+ return false;
18
+ };
19
+
20
+ const hasSetter = (instance: object, name: string): boolean => {
21
+ const prototype = Object.getPrototypeOf(instance) as object | null;
22
+
23
+ if (prototype === null) {
24
+ return false;
25
+ }
26
+
27
+ let known = setterCache.get(prototype);
28
+
29
+ if (known === undefined) {
30
+ known = new Map();
31
+ setterCache.set(prototype, known);
32
+ }
33
+
34
+ let isWritable = known.get(name);
35
+
36
+ if (isWritable === undefined) {
37
+ isWritable = hasSetterInChain(prototype, name);
38
+ known.set(name, isWritable);
39
+ }
40
+
41
+ return isWritable;
42
+ };
43
+
44
+ const isText = (value: unknown): value is string | number => typeof value === "string" || typeof value === "number";
45
+
46
+ const getText = (value: unknown): string | null => {
47
+ if (isText(value)) {
48
+ return String(value);
49
+ }
50
+
51
+ if (!Array.isArray(value)) {
52
+ return null;
53
+ }
54
+
55
+ const texts = value.map((item) => getText(item));
56
+
57
+ return texts.every((item) => item !== null) ? texts.join("") : null;
58
+ };
59
+
60
+ const didApplyText = (instance: object, value: unknown): boolean => {
61
+ const text = getText(value);
62
+
63
+ if (text === null || !(instance instanceof Gtk.Label)) {
64
+ return false;
65
+ }
66
+
67
+ applyWrite(() => {
68
+ instance.setLabel(text);
69
+ });
70
+
71
+ return true;
72
+ };
73
+
74
+ const didApplyStyle = (instance: object, value: unknown): boolean => {
75
+ if (!(instance instanceof Gtk.Widget)) {
76
+ return false;
77
+ }
78
+
79
+ applyStyle(instance, value);
80
+
81
+ return true;
82
+ };
83
+
84
+ const didApplyValue = (instance: object, name: string, value: unknown): boolean => {
85
+ if (name === "children") {
86
+ return didApplyText(instance, value);
87
+ }
88
+
89
+ if (name === "style") {
90
+ return didApplyStyle(instance, value);
91
+ }
92
+
93
+ if (!hasSetter(instance, name)) {
94
+ return false;
95
+ }
96
+
97
+ applyWrite(() => {
98
+ Reflect.set(instance, name, coerceObjectProperty(instance, name, value));
99
+ });
100
+
101
+ return true;
102
+ };
103
+
104
+ const didApplyAnimatedValues = (instance: object, values: Lookup): boolean => {
105
+ let isApplied = true;
106
+
107
+ for (const name in values) {
108
+ if (!didApplyValue(instance, name, values[name])) {
109
+ isApplied = false;
110
+ }
111
+ }
112
+
113
+ return isApplied;
114
+ };
115
+
116
+ export { didApplyAnimatedValues };
@@ -0,0 +1,12 @@
1
+ import { Globals } from "@react-spring/core";
2
+ import { colors, createStringInterpolator } from "@react-spring/shared";
3
+ import { trackReducedMotion } from "./reduced-motion.js";
4
+ import { requestFrame } from "./request-frame.js";
5
+
6
+ Globals.assign({
7
+ colors,
8
+ createStringInterpolator,
9
+ requestAnimationFrame: requestFrame,
10
+ });
11
+
12
+ trackReducedMotion();
package/src/index.ts ADDED
@@ -0,0 +1,110 @@
1
+ import "./bootstrap.js";
2
+
3
+ export { animated, type Animated } from "./animated.js";
4
+ export { useReducedMotion } from "./reduced-motion.js";
5
+ export type {
6
+ AnimatedComponent,
7
+ AnimatedElements,
8
+ AnimatedItems,
9
+ AnimatedProp,
10
+ AnimatedProps,
11
+ AnimatedStyle,
12
+ } from "./types.js";
13
+ export {
14
+ BailSignal,
15
+ config,
16
+ Controller,
17
+ createInterpolator,
18
+ easings,
19
+ FrameValue,
20
+ Globals,
21
+ inferTo,
22
+ Interpolation,
23
+ Spring,
24
+ SpringContext,
25
+ SpringRef,
26
+ SpringValue,
27
+ to,
28
+ Trail,
29
+ Transition,
30
+ update,
31
+ useChain,
32
+ useSpring,
33
+ useSpringRef,
34
+ useSprings,
35
+ useSpringValue,
36
+ useTrail,
37
+ useTransition,
38
+ } from "@react-spring/core";
39
+ export type {
40
+ AnimationConfig,
41
+ AnimationProps,
42
+ AnimationResult,
43
+ AsyncResult,
44
+ Change,
45
+ ControllerFlushFn,
46
+ ControllerProps,
47
+ ControllerUpdate,
48
+ EventProp,
49
+ ForwardProps,
50
+ GoalProp,
51
+ GoalValue,
52
+ GoalValues,
53
+ InferTo,
54
+ InlineToProps,
55
+ Interpolated,
56
+ Interpolator,
57
+ ItemKeys,
58
+ LoopProp,
59
+ MatchProp,
60
+ OnChange,
61
+ OnPause,
62
+ OnProps,
63
+ OnResolve,
64
+ OnRest,
65
+ OnResume,
66
+ OnStart,
67
+ PickAnimated,
68
+ ReservedEventProps,
69
+ ReservedProps,
70
+ SpringChain,
71
+ SpringComponentProps,
72
+ SpringConfig,
73
+ SpringProps,
74
+ SpringsUpdate,
75
+ SpringTo,
76
+ SpringToFn,
77
+ SpringUpdate,
78
+ SpringUpdateFn,
79
+ SpringValues,
80
+ Springify,
81
+ ToProps,
82
+ ToValues,
83
+ TrailComponentProps,
84
+ TransitionComponentProps,
85
+ TransitionFn,
86
+ TransitionFrom,
87
+ TransitionKey,
88
+ TransitionRenderFn,
89
+ TransitionState,
90
+ TransitionTo,
91
+ TransitionValues,
92
+ UseSpringProps,
93
+ UseSpringsProps,
94
+ UseTrailProps,
95
+ UseTransitionProps,
96
+ VelocityProp,
97
+ } from "@react-spring/core";
98
+ export type { FluidValue } from "@react-spring/shared";
99
+ export type {
100
+ Animatable,
101
+ EasingFunction,
102
+ ExtrapolateType,
103
+ InterpolatorArgs,
104
+ InterpolatorConfig,
105
+ InterpolatorFactory,
106
+ InterpolatorFn,
107
+ Lookup,
108
+ OneOrMore,
109
+ UnknownProps,
110
+ } from "@react-spring/types";
@@ -0,0 +1,82 @@
1
+ import * as Gtk from "@gtkx/gi/gtk";
2
+ import { Globals } from "@react-spring/core";
3
+ import { useSyncExternalStore } from "react";
4
+
5
+ type Listener = () => void;
6
+ type Tracker = { settings: Gtk.Settings | null; isReduced: boolean | null };
7
+
8
+ const REDUCED_MOTION_MINOR = 22;
9
+ const tracker: Tracker = { settings: null, isReduced: null };
10
+ const listeners: Set<Listener> = new Set();
11
+
12
+ const hasReducedMotionSetting = (): boolean => Gtk.checkVersion(4, REDUCED_MOTION_MINOR, 0) === null;
13
+
14
+ const isReduceMotionPreferred = (settings: Gtk.Settings): boolean =>
15
+ hasReducedMotionSetting() && settings.gtkInterfaceReducedMotion === Gtk.ReducedMotion.REDUCE;
16
+
17
+ const sync = (): void => {
18
+ const { settings } = tracker;
19
+
20
+ if (settings === null) {
21
+ return;
22
+ }
23
+
24
+ const isReduced = !settings.gtkEnableAnimations || isReduceMotionPreferred(settings);
25
+ Globals.assign({ skipAnimation: !settings.gtkEnableAnimations });
26
+
27
+ if (isReduced === tracker.isReduced) {
28
+ return;
29
+ }
30
+
31
+ tracker.isReduced = isReduced;
32
+
33
+ for (const listener of listeners) {
34
+ listener();
35
+ }
36
+ };
37
+
38
+ const watch = (settings: Gtk.Settings): void => {
39
+ tracker.settings = settings;
40
+ settings.on("notify::gtk-enable-animations", sync);
41
+
42
+ if (hasReducedMotionSetting()) {
43
+ settings.on("notify::gtk-interface-reduced-motion", sync);
44
+ }
45
+
46
+ sync();
47
+ };
48
+
49
+ const trackReducedMotion = (): boolean | null => {
50
+ if (tracker.settings === null) {
51
+ const settings = Gtk.Settings.getDefault();
52
+
53
+ if (settings === null) {
54
+ return null;
55
+ }
56
+
57
+ watch(settings);
58
+ }
59
+
60
+ return tracker.isReduced;
61
+ };
62
+
63
+ const subscribe = (listener: Listener): (() => void) => {
64
+ listeners.add(listener);
65
+
66
+ return () => {
67
+ listeners.delete(listener);
68
+ };
69
+ };
70
+
71
+ /**
72
+ * Reports whether the desktop asks for less motion: `true` while GTK's `gtk-enable-animations`
73
+ * setting is off, which is when every spring already jumps straight to its target, and also while
74
+ * the `gtk-interface-reduced-motion` setting of GTK 4.22 asks to reduce motion, the preference
75
+ * behind the `prefers-reduced-motion` media query, which leaves springs running so that components
76
+ * can trade a slide for a fade themselves. Returns `null` until a display is open.
77
+ *
78
+ * @returns `true` when motion should be reduced, `false` when it should not, `null` when unknown.
79
+ */
80
+ const useReducedMotion = (): boolean | null => useSyncExternalStore(subscribe, trackReducedMotion);
81
+
82
+ export { trackReducedMotion, useReducedMotion };