@ringozz/react-godot 1.0.0-2 → 1.0.0-3

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/README.md CHANGED
@@ -1,94 +1,94 @@
1
- # @ringozz/react-godot
2
-
3
- React bindings for the Godot Engine, built on [`@ringozz/godot`](../godot/README.md). A custom `react-reconciler` host config lets you build Godot scenes declaratively with JSX.
4
-
5
- ## Installation
6
-
7
- ```sh
8
- bun add @ringozz/react-godot @ringozz/godot react
9
- ```
10
-
11
- `@ringozz/godot` and `react` (`^19.2`) are peer dependencies. Configure the JSX runtime in `tsconfig.json`:
12
-
13
- ```json
14
- {
15
- "compilerOptions": {
16
- "jsx": "react-jsx",
17
- "jsxImportSource": "@ringozz/react-godot"
18
- }
19
- }
20
- ```
21
-
22
- ## API
23
-
24
- ```ts
25
- import { createRoot, createPortal, useSignal, useMutableCallback, type ComponentProps } from '@ringozz/react-godot';
26
- ```
27
-
28
- - **`createRoot(parent: Node)`** — mounts a React tree into a Godot `Node` (e.g. `SceneTree.root`). Returns `{ render, unmount }`; `render` resolves when the commit completes. StrictMode is enabled only when `NODE_ENV === 'development'`.
29
- - **`createPortal(children, container)`** — renders into another Godot `Node` (a `CanvasLayer` for overlays, a `SubViewport`, a dedicated parent node). The container must be a `Node` — portals cannot target `Resource`s. Unmounting frees the portal's children.
30
- - **`useSignal<T extends (...args: any[]) => any>(signal, handler)`** — connects a Godot `Signal` for the component's lifetime, disconnecting on unmount. Uses a mutable callback ref, so the handler always sees fresh state.
31
- - **`useMutableCallback<T>(fn)`** — returns a `RefObject<T>` whose `.current` always holds the latest `fn`.
32
- - **`ComponentProps<typeof ClassName>`** — full prop validation for an element (Godot properties + `children`/`ref` + `object`/`attach`).
33
-
34
- ## Example
35
-
36
- ```tsx
37
- import { Engine } from '@ringozz/godot/Engine';
38
- import { SceneTree } from '@ringozz/godot/SceneTree';
39
- import { Label } from '@ringozz/godot/Label';
40
- import { createRoot, useSignal } from '@ringozz/react-godot';
41
- import { useRef } from 'react';
42
-
43
- const tree = Engine.getMainLoop() as SceneTree;
44
- const { render } = createRoot(tree.root);
45
-
46
- function Hud() {
47
- const label = useRef<Label>(null);
48
- useSignal(tree.processFrame, () => {
49
- if (label.current) label.current.text = String(Math.floor(performance.now() / 1000));
50
- });
51
- return <Label ref={label} position={[16, 16]} text="ticking" />;
52
- }
53
-
54
- render(<Hud />);
55
- ```
56
-
57
- ### Render a component
58
-
59
- ```tsx
60
- function App() {
61
- return (
62
- <WorldEnvironment>
63
- <Environment attach="environment" backgroundColor={[0.05, 0.1, 0.25]} />
64
- </WorldEnvironment>
65
- );
66
- }
67
- ```
68
-
69
- ## How elements map to Godot
70
-
71
- - Each element instantiates its class via `ClassDB.instantiate(type)` (or uses an existing instance passed through the `object` prop, which bypasses instantiation and `.reference()`s `RefCounted` objects).
72
- - **`attach="prop"`** sets the element as a named property of its parent instead of adding it as a child — use it for resources. A physics body's shape goes on the **shape resource element**, never on the `CollisionShape3D` node:
73
-
74
- ```tsx
75
- <StaticBody3D>
76
- <CollisionShape3D>
77
- <BoxShape3D attach="shape" size={[1, 1, 1]} />
78
- </CollisionShape3D>
79
- <MeshInstance3D>
80
- <BoxMesh attach="mesh" />
81
- <StandardMaterial3D attach="materialOverride" />
82
- </MeshInstance3D>
83
- </StaticBody3D>
84
- ```
85
-
86
- The `attach` value may not change across updates.
87
- - **`object` prop**: pass an existing Godot instance (nodes or resources) to render rather than create one.
88
- - **Text**: standalone text nodes are unsupported. A single string child is mapped to the node's `text` property — fine for `Label`/`Label3D` — but arrays/mixed string children throw; use a `text` prop instead.
89
-
90
- ## Notes
91
-
92
- - `Node` children are added/moved with `addChild`/`moveChild`/`removeChild`; unmounting a subtree calls `free()` on each deleted instance.
93
- - Portals and other non-React children of a root container are never touched by the reconciler.
94
- - See the demo in the repo (`dev/DemoApp.tsx`) for physics, camera controls, and a portal HUD, and `AGENTS.md` for reconciler internals.
1
+ # @ringozz/react-godot
2
+
3
+ React bindings for the Godot Engine, built on [`@ringozz/godot`](../godot/README.md). A custom `react-reconciler` host config lets you build Godot scenes declaratively with JSX.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ bun add @ringozz/react-godot @ringozz/godot react
9
+ ```
10
+
11
+ `@ringozz/godot` and `react` (`^19.2`) are peer dependencies. Configure the JSX runtime in `tsconfig.json`:
12
+
13
+ ```json
14
+ {
15
+ "compilerOptions": {
16
+ "jsx": "react-jsx",
17
+ "jsxImportSource": "@ringozz/react-godot"
18
+ }
19
+ }
20
+ ```
21
+
22
+ ## API
23
+
24
+ ```ts
25
+ import { createRoot, createPortal, useSignal, useMutableCallback, type ComponentProps } from '@ringozz/react-godot';
26
+ ```
27
+
28
+ - **`createRoot(parent: Node)`** — mounts a React tree into a Godot `Node` (e.g. `SceneTree.root`). Returns `{ render, unmount }`; `render` resolves when the commit completes. StrictMode is enabled only when `NODE_ENV === 'development'`.
29
+ - **`createPortal(children, container)`** — renders into another Godot `Node` (a `CanvasLayer` for overlays, a `SubViewport`, a dedicated parent node). The container must be a `Node` — portals cannot target `Resource`s. Unmounting frees the portal's children.
30
+ - **`useSignal<T extends (...args: any[]) => any>(signal, handler)`** — connects a Godot `Signal` for the component's lifetime, disconnecting on unmount. Uses a mutable callback ref, so the handler always sees fresh state.
31
+ - **`useMutableCallback<T>(fn)`** — returns a `RefObject<T>` whose `.current` always holds the latest `fn`.
32
+ - **`ComponentProps<typeof ClassName>`** — full prop validation for an element (Godot properties + `children`/`ref` + `object`/`attach`).
33
+
34
+ ## Example
35
+
36
+ ```tsx
37
+ import { Engine } from '@ringozz/godot/Engine';
38
+ import { SceneTree } from '@ringozz/godot/SceneTree';
39
+ import { Label } from '@ringozz/godot/Label';
40
+ import { createRoot, useSignal } from '@ringozz/react-godot';
41
+ import { useRef } from 'react';
42
+
43
+ const tree = Engine.getMainLoop() as SceneTree;
44
+ const { render } = createRoot(tree.root);
45
+
46
+ function Hud() {
47
+ const label = useRef<Label>(null);
48
+ useSignal(tree.processFrame, () => {
49
+ if (label.current) label.current.text = String(Math.floor(performance.now() / 1000));
50
+ });
51
+ return <Label ref={label} position={[16, 16]} text="ticking" />;
52
+ }
53
+
54
+ render(<Hud />);
55
+ ```
56
+
57
+ ### Render a component
58
+
59
+ ```tsx
60
+ function App() {
61
+ return (
62
+ <WorldEnvironment>
63
+ <Environment attach="environment" backgroundColor={[0.05, 0.1, 0.25]} />
64
+ </WorldEnvironment>
65
+ );
66
+ }
67
+ ```
68
+
69
+ ## How elements map to Godot
70
+
71
+ - Each element instantiates its class via `ClassDB.instantiate(type)` (or uses an existing instance passed through the `object` prop, which bypasses instantiation and `.reference()`s `RefCounted` objects).
72
+ - **`attach="prop"`** sets the element as a named property of its parent instead of adding it as a child — use it for resources. A physics body's shape goes on the **shape resource element**, never on the `CollisionShape3D` node:
73
+
74
+ ```tsx
75
+ <StaticBody3D>
76
+ <CollisionShape3D>
77
+ <BoxShape3D attach="shape" size={[1, 1, 1]} />
78
+ </CollisionShape3D>
79
+ <MeshInstance3D>
80
+ <BoxMesh attach="mesh" />
81
+ <StandardMaterial3D attach="materialOverride" />
82
+ </MeshInstance3D>
83
+ </StaticBody3D>
84
+ ```
85
+
86
+ The `attach` value may not change across updates.
87
+ - **`object` prop**: pass an existing Godot instance (nodes or resources) to render rather than create one.
88
+ - **Text**: standalone text nodes are unsupported. A single string child is mapped to the node's `text` property — fine for `Label`/`Label3D` — but arrays/mixed string children throw; use a `text` prop instead.
89
+
90
+ ## Notes
91
+
92
+ - `Node` children are added/moved with `addChild`/`moveChild`/`removeChild`; unmounting a subtree calls `free()` on each deleted instance.
93
+ - Portals and other non-React children of a root container are never touched by the reconciler.
94
+ - See the demo in the repo (`dev/DemoApp.tsx`) for physics, camera controls, and a portal HUD, and `AGENTS.md` for reconciler internals.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ringozz/react-godot",
3
3
  "author": "Vladimir Davidovich",
4
- "version": "1.0.0-2",
4
+ "version": "1.0.0-3",
5
5
  "description": "A React renderer for Godot Engine via @ringozz/godot",
6
6
  "type": "module",
7
7
  "main": "./src/index.ts",
@@ -20,7 +20,7 @@
20
20
  "react-reconciler": "^0.33.0"
21
21
  },
22
22
  "peerDependencies": {
23
- "@ringozz/godot": "^4.7.1-3",
23
+ "@ringozz/godot": "^4.7.1-6",
24
24
  "@types/react": "^19.2.0",
25
25
  "react": "^19.2.0"
26
26
  }
package/src/index.ts CHANGED
@@ -1,58 +1,58 @@
1
- /**********************************************************************
2
- Copyright (c) Vladimir Davidovich. All rights reserved.
3
- ***********************************************************************/
4
-
5
- import Reconciler from 'react-reconciler';
6
- import Constants from 'react-reconciler/constants.js';
7
- import type { Node } from '@ringozz/godot/Node';
8
- import * as FiberConfig from './react-fiber.ts';
9
- export * from './react-hooks.ts';
10
- export type * from './react-types.ts';
11
-
12
- const reconciler = Reconciler(FiberConfig);
13
- reconciler.injectIntoDevTools(undefined as never);
14
-
15
- export function createRoot(parent: Node) {
16
- const isStrictMode = process.env.NODE_ENV === 'development';
17
- const concurrentUpdatesByDefaultOverride = false;
18
- const identifierPrefix = '';
19
- /* eslint-disable @typescript-eslint/no-explicit-any */
20
- const onUncaughtError = (reconciler as any).defaultOnUncaughtError;
21
- const onCaughtError = (reconciler as any).defaultOnCaughtError;
22
- const onRecoverableError = (reconciler as any).defaultOnRecoverableError;
23
- const onDefaultTransitionIndicator = () => { };
24
- const root = reconciler.createContainer(
25
- parent,
26
- Constants.ConcurrentRoot,
27
- null,
28
- isStrictMode,
29
- concurrentUpdatesByDefaultOverride,
30
- identifierPrefix,
31
- onUncaughtError,
32
- onCaughtError,
33
- onRecoverableError,
34
- onDefaultTransitionIndicator
35
- );
36
- return {
37
- render: (component: React.ReactNode) => new Promise<void>((resolve, reject) => {
38
- try {
39
- reconciler.updateContainer(component, root, null, resolve);
40
- } catch (e) {
41
- reject(e);
42
- }
43
- }),
44
- unmount: () => {
45
- try {
46
- reconciler.updateContainerSync(null, root, null);
47
- reconciler.flushSyncWork();
48
- return Promise.resolve();
49
- } catch (e) {
50
- return Promise.reject(e);
51
- }
52
- }
53
- };
54
- }
55
-
56
- export function createPortal(children: React.ReactNode, container: Node, key?: string | null): React.ReactPortal {
57
- return reconciler.createPortal(children, container, null, key) as unknown as React.ReactPortal;
58
- }
1
+ /**********************************************************************
2
+ Copyright (c) Vladimir Davidovich. All rights reserved.
3
+ ***********************************************************************/
4
+
5
+ import Reconciler from 'react-reconciler';
6
+ import Constants from 'react-reconciler/constants.js';
7
+ import type { Node } from '@ringozz/godot/Node';
8
+ import * as FiberConfig from './react-fiber.ts';
9
+ export * from './react-hooks.ts';
10
+ export type * from './react-types.ts';
11
+
12
+ const reconciler = Reconciler(FiberConfig);
13
+ reconciler.injectIntoDevTools(undefined as never);
14
+
15
+ export function createRoot(parent: Node) {
16
+ const isStrictMode = !!import.meta.env?.DEV;
17
+ const concurrentUpdatesByDefaultOverride = false;
18
+ const identifierPrefix = '';
19
+ /* eslint-disable @typescript-eslint/no-explicit-any */
20
+ const onUncaughtError = (reconciler as any).defaultOnUncaughtError;
21
+ const onCaughtError = (reconciler as any).defaultOnCaughtError;
22
+ const onRecoverableError = (reconciler as any).defaultOnRecoverableError;
23
+ const onDefaultTransitionIndicator = () => { };
24
+ const root = reconciler.createContainer(
25
+ parent,
26
+ Constants.ConcurrentRoot,
27
+ null,
28
+ isStrictMode,
29
+ concurrentUpdatesByDefaultOverride,
30
+ identifierPrefix,
31
+ onUncaughtError,
32
+ onCaughtError,
33
+ onRecoverableError,
34
+ onDefaultTransitionIndicator
35
+ );
36
+ return {
37
+ render: (component: React.ReactNode) => new Promise<void>((resolve, reject) => {
38
+ try {
39
+ reconciler.updateContainer(component, root, null, resolve);
40
+ } catch (e) {
41
+ reject(e);
42
+ }
43
+ }),
44
+ unmount: () => {
45
+ try {
46
+ reconciler.updateContainerSync(null, root, null);
47
+ reconciler.flushSyncWork();
48
+ return Promise.resolve();
49
+ } catch (e) {
50
+ return Promise.reject(e);
51
+ }
52
+ }
53
+ };
54
+ }
55
+
56
+ export function createPortal(children: React.ReactNode, container: Node, key?: string | null): React.ReactPortal {
57
+ return reconciler.createPortal(children, container, null, key) as unknown as React.ReactPortal;
58
+ }
@@ -1,307 +1,307 @@
1
- /**********************************************************************
2
- Copyright (c) Vladimir Davidovich. All rights reserved.
3
- ***********************************************************************/
4
-
5
- import { ClassDB } from '@ringozz/godot/ClassDB';
6
- import type { Node } from '@ringozz/godot/Node';
7
- import { Object as Instance } from '@ringozz/godot/Object';
8
- import { RefCounted } from '@ringozz/godot/RefCounted';
9
- import type Reconciler from 'react-reconciler';
10
- import Constants from 'react-reconciler/constants.js';
11
- import pkg from '../package.json' with { type: 'json' };
12
- import type { InstanceProps } from './react-types.ts';
13
-
14
- /* eslint-disable @typescript-eslint/no-unused-vars */
15
-
16
- export const rendererPackageName = pkg.name;
17
- export const rendererVersion = pkg.version;
18
- export const supportsMutation = true;
19
- export const supportsPersistence = false;
20
- export const supportsHydration = false;
21
- export const isPrimaryRenderer = true;
22
-
23
- type Type = string;
24
- type Props = InstanceProps & Record<string, unknown>;
25
- type Container = Instance;
26
- type TextInstance = Instance;
27
- type SuspenseInstance = Instance;
28
- type PublicInstance = Instance;
29
- type HostContext = unknown;
30
-
31
- const EMPTY = Object.freeze({});
32
- const attachments = new WeakMap<Instance, string>();
33
-
34
- export function createInstance<T extends Instance>(type: Type, props: Props, root: Container, hostContext: HostContext, internalHandle: Reconciler.OpaqueHandle): T {
35
- const { attach, object, ...rest } = props;
36
- if (object instanceof RefCounted)
37
- object.reference();
38
-
39
- const instance = (object ?? ClassDB.instantiate(type)) as T;
40
- if (attach)
41
- attachments.set(instance, attach);
42
-
43
- commitUpdate(instance, type, EMPTY, rest, internalHandle);
44
- return instance;
45
- }
46
-
47
- export function createTextInstance(text: string, root: Container, hostContext: HostContext, internalHandle: Reconciler.OpaqueHandle): TextInstance {
48
- throw new Error('Text nodes are not supported: use a text prop, or a single string/all-string children (they are flattened into the node\'s `text` property).');
49
- }
50
-
51
- export const appendInitialChild = appendChild;
52
-
53
- export function finalizeInitialChildren(instance: Instance, type: Type, props: Props, root: Container, hostContext: HostContext): boolean {
54
- return false;
55
- }
56
-
57
- export function shouldSetTextContent(type: Type, props: Props): boolean {
58
- const c = props.children;
59
- if (typeof c === 'string' || typeof c === 'number')
60
- return true;
61
- return Array.isArray(c) && c.every((v) => typeof v === 'string' || typeof v === 'number');
62
- }
63
-
64
- export function getRootHostContext(root: Container): HostContext | null {
65
- return EMPTY;
66
- }
67
-
68
- export function getChildHostContext(parentHostContext: HostContext, type: Type, root: Container): HostContext {
69
- return parentHostContext;
70
- }
71
-
72
- export function getPublicInstance(instance: Instance | TextInstance): PublicInstance {
73
- return instance;
74
- }
75
-
76
- export function prepareForCommit(containerInfo: Container): Props | null {
77
- return null;
78
- }
79
-
80
- export function resetAfterCommit(containerInfo: Container): void {
81
- }
82
-
83
- export function preparePortalMount(containerInfo: Container): void {
84
- // No-op: portal children attach to their target Node via appendChildToContainer.
85
- // Mirrors ReactDOM's empty implementation.
86
- }
87
-
88
- export const noTimeout = -1;
89
- export const scheduleTimeout = setTimeout;
90
- export const cancelTimeout = clearTimeout;
91
- export const supportsMicrotasks = true;
92
- export const scheduleMicrotask = queueMicrotask;
93
-
94
- let currentUpdatePriority = Constants.NoEventPriority;
95
- export function setCurrentUpdatePriority(newPriority: Reconciler.EventPriority) {
96
- currentUpdatePriority = newPriority;
97
- }
98
-
99
- export function getCurrentUpdatePriority(): Reconciler.EventPriority {
100
- return currentUpdatePriority;
101
- }
102
-
103
- const currentEvent = () => globalThis.window?.event;
104
-
105
- export function resolveUpdatePriority(): Reconciler.EventPriority {
106
- if (currentUpdatePriority)
107
- return currentUpdatePriority;
108
- const event = currentEvent();
109
- switch (event?.type) {
110
- case 'click':
111
- case 'contextmenu':
112
- case 'keydown':
113
- case 'keypress':
114
- case 'keyup':
115
- case 'pointercancel':
116
- case 'pointerdown':
117
- case 'pointerup':
118
- return Constants.DiscreteEventPriority;
119
- case 'pointerenter':
120
- case 'pointerleave':
121
- case 'pointermove':
122
- case 'pointerout':
123
- case 'pointerover':
124
- case 'resize':
125
- case 'wheel':
126
- return Constants.ContinuousEventPriority;
127
- default:
128
- return Constants.DefaultEventPriority;
129
- }
130
- }
131
-
132
- let schedulerEvent: Event | undefined = undefined;
133
- export function trackSchedulerEvent() {
134
- schedulerEvent = currentEvent();
135
- }
136
-
137
- export function resolveEventType(): null | string {
138
- const event = currentEvent();
139
- return event && event !== schedulerEvent ? event.type : null;
140
- }
141
-
142
- export function resolveEventTimeStamp(): number {
143
- const event = currentEvent();
144
- return event && event !== schedulerEvent ? event.timeStamp : -1.1;
145
- }
146
-
147
- export function requestPostPaintCallback() {
148
- }
149
-
150
- export function resetFormInstance() {
151
- }
152
-
153
- export const NotPendingTransition = null;
154
- export const HostTransitionContext = null as never;
155
-
156
- export function shouldAttemptEagerTransition() {
157
- return false;
158
- }
159
-
160
- export function getInstanceFromNode(node: unknown): Reconciler.Fiber | null | undefined {
161
- return null;
162
- }
163
-
164
- export function beforeActiveInstanceBlur(): void {
165
- }
166
-
167
- export function afterActiveInstanceBlur(): void {
168
- }
169
-
170
- export function prepareScopeUpdate(scopeInstance: unknown, instance: unknown): void {
171
- }
172
-
173
- export function getInstanceFromScope(scopeInstance: unknown): Instance | null {
174
- return null;
175
- }
176
-
177
- export function detachDeletedInstance(node: Instance): void {
178
- node.free();
179
- }
180
-
181
- export function maySuspendCommit(type: Type, props: Props) {
182
- return false;
183
- }
184
-
185
- export function preloadInstance(type: Type, props: Props) {
186
- return true; // Return true to indicate it's already loaded
187
- }
188
-
189
- export function startSuspendingCommit() {
190
- }
191
-
192
- export function suspendInstance(type: Type, props: Props) {
193
- }
194
-
195
- export function waitForCommitToBeReady() {
196
- return null;
197
- }
198
-
199
- export function appendChild(parentInstance: Instance, child: Instance | TextInstance): void {
200
- const attached = attachments.get(child);
201
- if (attached) {
202
- Instance.assign(parentInstance, { [attached]: child });
203
- return;
204
- }
205
- (parentInstance as Node).addChild(child as Node);
206
- }
207
-
208
- export function appendChildToContainer(container: Container, child: Instance | TextInstance): void {
209
- appendChild(container, child);
210
- }
211
-
212
- export function insertBefore(parentInstance: Instance, child: Instance | TextInstance, beforeChild: Instance | TextInstance | SuspenseInstance): void {
213
- const attached = attachments.get(child);
214
- if (attached) {
215
- Instance.assign(parentInstance, { [attached]: child });
216
- return;
217
- }
218
- if ((child as Node).getParent() !== null) {
219
- (parentInstance as Node).removeChild(child as Node);
220
- }
221
- (parentInstance as Node).addChild(child as Node);
222
- (parentInstance as Node).moveChild(child as Node, (beforeChild as Node).getIndex());
223
- }
224
-
225
- export function insertInContainerBefore(container: Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance | SuspenseInstance): void {
226
- insertBefore(container, child, beforeChild);
227
- }
228
-
229
- export function removeChild(parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance): void {
230
- const attached = attachments.get(child);
231
- if (attached) {
232
- Instance.assign(parentInstance, { [attached]: undefined });
233
- return;
234
- }
235
- (parentInstance as Node).removeChild(child as Node);
236
- }
237
-
238
- export function removeChildFromContainer(container: Container, child: Instance | TextInstance | SuspenseInstance): void {
239
- removeChild(container, child);
240
- }
241
-
242
- export function resetTextContent(instance: Instance): void {
243
- instance.set('text', undefined);
244
- }
245
-
246
- export function commitTextUpdate(instance: TextInstance, oldText: string, newText: string): void {
247
- instance.set('text', newText);
248
- }
249
-
250
- export function commitMount(instance: Instance, type: Type, props: Props, internalInstanceHandle: Reconciler.OpaqueHandle): void {
251
- }
252
-
253
- function isEqual(a: unknown, b: unknown) {
254
- if (a === b) return true;
255
-
256
- const i1 = (a as Iterable<unknown> | undefined)?.[Symbol.iterator]?.();
257
- const i2 = (b as Iterable<unknown> | undefined)?.[Symbol.iterator]?.();
258
- while (i1 && i2) {
259
- const n1 = i1.next();
260
- const n2 = i2.next();
261
- if (n1.done !== n2.done) return false; // Different lengths
262
- if (n1.done) return true; // Both finished
263
- if (n1.value !== n2.value) return false; // Values mismatch
264
- }
265
- }
266
-
267
- export function commitUpdate(instance: Instance, type: Type, prevProps: Props, nextProps: Props, internalHandle: Reconciler.OpaqueHandle): void {
268
- const { attach: attachOld, ref: refOld, children: childrenOld, ...restOld } = prevProps;
269
- const { attach: attachNew, ref: refNew, children: childrenNew, ...restNew } = nextProps;
270
- if (attachOld !== attachNew)
271
- throw new Error(`Cannot change attachment ${attachOld} to ${attachNew}`);
272
-
273
- const toText = (c: unknown) => Array.isArray(c) ? c.join('') : c as string;
274
- if (shouldSetTextContent(type, prevProps)) restOld['text'] = toText(childrenOld);
275
- if (shouldSetTextContent(type, nextProps)) restNew['text'] = toText(childrenNew);
276
-
277
- for (const [key, oldVal] of Object.entries(restOld)) {
278
- const newVal = restNew[key];
279
- if (isEqual(oldVal, newVal))
280
- delete restNew[key];
281
- else if (newVal === undefined)
282
- restNew[key] = undefined;
283
- }
284
-
285
- Instance.assign(instance, restNew);
286
- }
287
-
288
- export function hideInstance(instance: Instance | TextInstance): void {
289
- instance.set('visible', false);
290
- }
291
-
292
- export const hideTextInstance = hideInstance;
293
-
294
- export function unhideInstance(instance: Instance | TextInstance): void {
295
- instance.set('visible', true);
296
- }
297
-
298
- export const unhideTextInstance = unhideInstance;
299
-
300
- export function clearContainer(container: Container): void {
301
- // No-op: Godot containers (e.g. the SceneTree root) can hold non-React
302
- // children such as portal targets. The reconciler calls this on every
303
- // initial mount; queue-freeing the container's children here would destroy
304
- // those foreign nodes and leave React fibers referencing freed instances.
305
- // React-managed children are removed by the reconciler's deletion path.
306
- }
307
-
1
+ /**********************************************************************
2
+ Copyright (c) Vladimir Davidovich. All rights reserved.
3
+ ***********************************************************************/
4
+
5
+ import { ClassDB } from '@ringozz/godot/ClassDB';
6
+ import type { Node } from '@ringozz/godot/Node';
7
+ import { Object as Instance } from '@ringozz/godot/Object';
8
+ import { RefCounted } from '@ringozz/godot/RefCounted';
9
+ import type Reconciler from 'react-reconciler';
10
+ import Constants from 'react-reconciler/constants.js';
11
+ import pkg from '../package.json' with { type: 'json' };
12
+ import type { InstanceProps } from './react-types.ts';
13
+
14
+ /* eslint-disable @typescript-eslint/no-unused-vars */
15
+
16
+ export const rendererPackageName = pkg.name;
17
+ export const rendererVersion = pkg.version;
18
+ export const supportsMutation = true;
19
+ export const supportsPersistence = false;
20
+ export const supportsHydration = false;
21
+ export const isPrimaryRenderer = true;
22
+
23
+ type Type = string;
24
+ type Props = InstanceProps & Record<string, unknown>;
25
+ type Container = Instance;
26
+ type TextInstance = Instance;
27
+ type SuspenseInstance = Instance;
28
+ type PublicInstance = Instance;
29
+ type HostContext = unknown;
30
+
31
+ const EMPTY = Object.freeze({});
32
+ const attachments = new WeakMap<Instance, string>();
33
+
34
+ export function createInstance<T extends Instance>(type: Type, props: Props, root: Container, hostContext: HostContext, internalHandle: Reconciler.OpaqueHandle): T {
35
+ const { attach, object, ...rest } = props;
36
+ if (object instanceof RefCounted)
37
+ object.reference();
38
+
39
+ const instance = (object ?? ClassDB.instantiate(type)) as T;
40
+ if (attach)
41
+ attachments.set(instance, attach);
42
+
43
+ commitUpdate(instance, type, EMPTY, rest, internalHandle);
44
+ return instance;
45
+ }
46
+
47
+ export function createTextInstance(text: string, root: Container, hostContext: HostContext, internalHandle: Reconciler.OpaqueHandle): TextInstance {
48
+ throw new Error('Text nodes are not supported: use a text prop, or a single string/all-string children (they are flattened into the node\'s `text` property).');
49
+ }
50
+
51
+ export const appendInitialChild = appendChild;
52
+
53
+ export function finalizeInitialChildren(instance: Instance, type: Type, props: Props, root: Container, hostContext: HostContext): boolean {
54
+ return false;
55
+ }
56
+
57
+ export function shouldSetTextContent(type: Type, props: Props): boolean {
58
+ const c = props.children;
59
+ if (typeof c === 'string' || typeof c === 'number')
60
+ return true;
61
+ return Array.isArray(c) && c.every((v) => typeof v === 'string' || typeof v === 'number');
62
+ }
63
+
64
+ export function getRootHostContext(root: Container): HostContext | null {
65
+ return EMPTY;
66
+ }
67
+
68
+ export function getChildHostContext(parentHostContext: HostContext, type: Type, root: Container): HostContext {
69
+ return parentHostContext;
70
+ }
71
+
72
+ export function getPublicInstance(instance: Instance | TextInstance): PublicInstance {
73
+ return instance;
74
+ }
75
+
76
+ export function prepareForCommit(containerInfo: Container): Props | null {
77
+ return null;
78
+ }
79
+
80
+ export function resetAfterCommit(containerInfo: Container): void {
81
+ }
82
+
83
+ export function preparePortalMount(containerInfo: Container): void {
84
+ // No-op: portal children attach to their target Node via appendChildToContainer.
85
+ // Mirrors ReactDOM's empty implementation.
86
+ }
87
+
88
+ export const noTimeout = -1;
89
+ export const scheduleTimeout = setTimeout;
90
+ export const cancelTimeout = clearTimeout;
91
+ export const supportsMicrotasks = true;
92
+ export const scheduleMicrotask = queueMicrotask;
93
+
94
+ let currentUpdatePriority = Constants.NoEventPriority;
95
+ export function setCurrentUpdatePriority(newPriority: Reconciler.EventPriority) {
96
+ currentUpdatePriority = newPriority;
97
+ }
98
+
99
+ export function getCurrentUpdatePriority(): Reconciler.EventPriority {
100
+ return currentUpdatePriority;
101
+ }
102
+
103
+ const currentEvent = () => globalThis.window?.event;
104
+
105
+ export function resolveUpdatePriority(): Reconciler.EventPriority {
106
+ if (currentUpdatePriority)
107
+ return currentUpdatePriority;
108
+ const event = currentEvent();
109
+ switch (event?.type) {
110
+ case 'click':
111
+ case 'contextmenu':
112
+ case 'keydown':
113
+ case 'keypress':
114
+ case 'keyup':
115
+ case 'pointercancel':
116
+ case 'pointerdown':
117
+ case 'pointerup':
118
+ return Constants.DiscreteEventPriority;
119
+ case 'pointerenter':
120
+ case 'pointerleave':
121
+ case 'pointermove':
122
+ case 'pointerout':
123
+ case 'pointerover':
124
+ case 'resize':
125
+ case 'wheel':
126
+ return Constants.ContinuousEventPriority;
127
+ default:
128
+ return Constants.DefaultEventPriority;
129
+ }
130
+ }
131
+
132
+ let schedulerEvent: Event | undefined = undefined;
133
+ export function trackSchedulerEvent() {
134
+ schedulerEvent = currentEvent();
135
+ }
136
+
137
+ export function resolveEventType(): null | string {
138
+ const event = currentEvent();
139
+ return event && event !== schedulerEvent ? event.type : null;
140
+ }
141
+
142
+ export function resolveEventTimeStamp(): number {
143
+ const event = currentEvent();
144
+ return event && event !== schedulerEvent ? event.timeStamp : -1.1;
145
+ }
146
+
147
+ export function requestPostPaintCallback() {
148
+ }
149
+
150
+ export function resetFormInstance() {
151
+ }
152
+
153
+ export const NotPendingTransition = null;
154
+ export const HostTransitionContext = null as never;
155
+
156
+ export function shouldAttemptEagerTransition() {
157
+ return false;
158
+ }
159
+
160
+ export function getInstanceFromNode(node: unknown): Reconciler.Fiber | null | undefined {
161
+ return null;
162
+ }
163
+
164
+ export function beforeActiveInstanceBlur(): void {
165
+ }
166
+
167
+ export function afterActiveInstanceBlur(): void {
168
+ }
169
+
170
+ export function prepareScopeUpdate(scopeInstance: unknown, instance: unknown): void {
171
+ }
172
+
173
+ export function getInstanceFromScope(scopeInstance: unknown): Instance | null {
174
+ return null;
175
+ }
176
+
177
+ export function detachDeletedInstance(node: Instance): void {
178
+ node.free();
179
+ }
180
+
181
+ export function maySuspendCommit(type: Type, props: Props) {
182
+ return false;
183
+ }
184
+
185
+ export function preloadInstance(type: Type, props: Props) {
186
+ return true; // Return true to indicate it's already loaded
187
+ }
188
+
189
+ export function startSuspendingCommit() {
190
+ }
191
+
192
+ export function suspendInstance(type: Type, props: Props) {
193
+ }
194
+
195
+ export function waitForCommitToBeReady() {
196
+ return null;
197
+ }
198
+
199
+ export function appendChild(parentInstance: Instance, child: Instance | TextInstance): void {
200
+ const attached = attachments.get(child);
201
+ if (attached) {
202
+ Instance.assign(parentInstance, { [attached]: child });
203
+ return;
204
+ }
205
+ (parentInstance as Node).addChild(child as Node);
206
+ }
207
+
208
+ export function appendChildToContainer(container: Container, child: Instance | TextInstance): void {
209
+ appendChild(container, child);
210
+ }
211
+
212
+ export function insertBefore(parentInstance: Instance, child: Instance | TextInstance, beforeChild: Instance | TextInstance | SuspenseInstance): void {
213
+ const attached = attachments.get(child);
214
+ if (attached) {
215
+ Instance.assign(parentInstance, { [attached]: child });
216
+ return;
217
+ }
218
+ if ((child as Node).getParent() !== null) {
219
+ (parentInstance as Node).removeChild(child as Node);
220
+ }
221
+ (parentInstance as Node).addChild(child as Node);
222
+ (parentInstance as Node).moveChild(child as Node, (beforeChild as Node).getIndex());
223
+ }
224
+
225
+ export function insertInContainerBefore(container: Container, child: Instance | TextInstance, beforeChild: Instance | TextInstance | SuspenseInstance): void {
226
+ insertBefore(container, child, beforeChild);
227
+ }
228
+
229
+ export function removeChild(parentInstance: Instance, child: Instance | TextInstance | SuspenseInstance): void {
230
+ const attached = attachments.get(child);
231
+ if (attached) {
232
+ Instance.assign(parentInstance, { [attached]: undefined });
233
+ return;
234
+ }
235
+ (parentInstance as Node).removeChild(child as Node);
236
+ }
237
+
238
+ export function removeChildFromContainer(container: Container, child: Instance | TextInstance | SuspenseInstance): void {
239
+ removeChild(container, child);
240
+ }
241
+
242
+ export function resetTextContent(instance: Instance): void {
243
+ instance.set('text', undefined);
244
+ }
245
+
246
+ export function commitTextUpdate(instance: TextInstance, oldText: string, newText: string): void {
247
+ instance.set('text', newText);
248
+ }
249
+
250
+ export function commitMount(instance: Instance, type: Type, props: Props, internalInstanceHandle: Reconciler.OpaqueHandle): void {
251
+ }
252
+
253
+ function isEqual(a: unknown, b: unknown) {
254
+ if (a === b) return true;
255
+
256
+ const i1 = (a as Iterable<unknown> | undefined)?.[Symbol.iterator]?.();
257
+ const i2 = (b as Iterable<unknown> | undefined)?.[Symbol.iterator]?.();
258
+ while (i1 && i2) {
259
+ const n1 = i1.next();
260
+ const n2 = i2.next();
261
+ if (n1.done !== n2.done) return false; // Different lengths
262
+ if (n1.done) return true; // Both finished
263
+ if (n1.value !== n2.value) return false; // Values mismatch
264
+ }
265
+ }
266
+
267
+ export function commitUpdate(instance: Instance, type: Type, prevProps: Props, nextProps: Props, internalHandle: Reconciler.OpaqueHandle): void {
268
+ const { attach: attachOld, ref: refOld, children: childrenOld, ...restOld } = prevProps;
269
+ const { attach: attachNew, ref: refNew, children: childrenNew, ...restNew } = nextProps;
270
+ if (attachOld !== attachNew)
271
+ throw new Error(`Cannot change attachment ${attachOld} to ${attachNew}`);
272
+
273
+ const toText = (c: unknown) => Array.isArray(c) ? c.join('') : c as string;
274
+ if (shouldSetTextContent(type, prevProps)) restOld['text'] = toText(childrenOld);
275
+ if (shouldSetTextContent(type, nextProps)) restNew['text'] = toText(childrenNew);
276
+
277
+ for (const [key, oldVal] of Object.entries(restOld)) {
278
+ const newVal = restNew[key];
279
+ if (isEqual(oldVal, newVal))
280
+ delete restNew[key];
281
+ else if (newVal === undefined)
282
+ restNew[key] = undefined;
283
+ }
284
+
285
+ Instance.assign(instance, restNew);
286
+ }
287
+
288
+ export function hideInstance(instance: Instance | TextInstance): void {
289
+ instance.set('visible', false);
290
+ }
291
+
292
+ export const hideTextInstance = hideInstance;
293
+
294
+ export function unhideInstance(instance: Instance | TextInstance): void {
295
+ instance.set('visible', true);
296
+ }
297
+
298
+ export const unhideTextInstance = unhideInstance;
299
+
300
+ export function clearContainer(container: Container): void {
301
+ // No-op: Godot containers (e.g. the SceneTree root) can hold non-React
302
+ // children such as portal targets. The reconciler calls this on every
303
+ // initial mount; queue-freeing the container's children here would destroy
304
+ // those foreign nodes and leave React fibers referencing freed instances.
305
+ // React-managed children are removed by the reconciler's deletion path.
306
+ }
307
+
@@ -1,22 +1,22 @@
1
- /**********************************************************************
2
- Copyright (c) Vladimir Davidovich. All rights reserved.
3
- ***********************************************************************/
4
-
5
- import type { Signal } from '@ringozz/godot';
6
- import type React from 'react';
7
- import { useEffect, useRef } from 'react';
8
-
9
- export function useMutableCallback<T>(fn: T): React.RefObject<T> {
10
- const ref = useRef<T>(fn);
11
- useEffect(() => void (ref.current = fn), [fn]);
12
- return ref;
13
- }
14
-
15
- export function useSignal<T extends (...args: any[]) => any>(signal: Signal<T>, handler: T): void {
16
- const ref = useMutableCallback(handler);
17
- useEffect(() => {
18
- const fn = ((...args: any[]) => ref.current(...args)) as T;
19
- signal.connect(fn);
20
- return () => signal.disconnect(fn);
21
- }, [signal.getObjectId(), signal.getName()]);
22
- }
1
+ /**********************************************************************
2
+ Copyright (c) Vladimir Davidovich. All rights reserved.
3
+ ***********************************************************************/
4
+
5
+ import type { Signal } from '@ringozz/godot';
6
+ import type React from 'react';
7
+ import { useEffect, useRef } from 'react';
8
+
9
+ export function useMutableCallback<T>(fn: T): React.RefObject<T> {
10
+ const ref = useRef<T>(fn);
11
+ useEffect(() => void (ref.current = fn), [fn]);
12
+ return ref;
13
+ }
14
+
15
+ export function useSignal<T extends (...args: any[]) => any>(signal: Signal<T>, handler: T): void {
16
+ const ref = useMutableCallback(handler);
17
+ useEffect(() => {
18
+ const fn = ((...args: any[]) => ref.current(...args)) as T;
19
+ signal.connect(fn);
20
+ return () => signal.disconnect(fn);
21
+ }, [signal.getObjectId(), signal.getName()]);
22
+ }
package/src/react-jsx.ts CHANGED
@@ -1,41 +1,41 @@
1
- /**********************************************************************
2
- Copyright (c) Vladimir Davidovich. All rights reserved.
3
- ***********************************************************************/
4
-
5
- import { jsxDEV as reactJsxDEV } from 'react/jsx-dev-runtime';
6
- import type * as ReactJSX from 'react/jsx-runtime';
7
- import { Fragment, jsx as reactJsx, jsxs as reactJsxs } from 'react/jsx-runtime';
8
- import type { ComponentProps, Instance } from './react-types.ts';
9
- import { GodotVar } from '@ringozz/godot/runtime';
10
-
11
- export function jsx(type: any, props?: any, key?: any) {
12
- return reactJsx(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
13
- }
14
-
15
- export function jsxs(type: any, props?: any, key?: any) {
16
- return reactJsxs(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
17
- }
18
-
19
- export function jsxDEV(type: any, props?: any, key?: any, isStatic?: any, source?: any, self?: any) {
20
- return reactJsxDEV(GodotVar.isPrototypeOf(type) ? type.name : type, props, key, isStatic, source, self);
21
- }
22
-
23
- export { Fragment };
24
-
25
- declare module '@ringozz/react-godot/jsx-runtime' {
26
- export namespace JSX {
27
- interface IntrinsicElements extends ReactJSX.JSX.IntrinsicElements { }
28
- interface Element extends ReactJSX.JSX.Element { }
29
-
30
- type GodotConstructor = { new(...args: any[]): Instance } & Function;
31
-
32
- type ElementType =
33
- | ReactJSX.JSX.ElementType
34
- | GodotConstructor;
35
-
36
- type LibraryManagedAttributes<C, P> =
37
- C extends GodotConstructor
38
- ? ComponentProps<C>
39
- : ReactJSX.JSX.LibraryManagedAttributes<C, P>;
40
- }
41
- }
1
+ /**********************************************************************
2
+ Copyright (c) Vladimir Davidovich. All rights reserved.
3
+ ***********************************************************************/
4
+
5
+ import { jsxDEV as reactJsxDEV } from 'react/jsx-dev-runtime';
6
+ import type * as ReactJSX from 'react/jsx-runtime';
7
+ import { Fragment, jsx as reactJsx, jsxs as reactJsxs } from 'react/jsx-runtime';
8
+ import type { ComponentProps, Instance } from './react-types.ts';
9
+ import { GodotVar } from '@ringozz/godot/runtime';
10
+
11
+ export function jsx(type: any, props?: any, key?: any) {
12
+ return reactJsx(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
13
+ }
14
+
15
+ export function jsxs(type: any, props?: any, key?: any) {
16
+ return reactJsxs(GodotVar.isPrototypeOf(type) ? type.name : type, props, key);
17
+ }
18
+
19
+ export function jsxDEV(type: any, props?: any, key?: any, isStatic?: any, source?: any, self?: any) {
20
+ return reactJsxDEV(GodotVar.isPrototypeOf(type) ? type.name : type, props, key, isStatic, source, self);
21
+ }
22
+
23
+ export { Fragment };
24
+
25
+ declare module '@ringozz/react-godot/jsx-runtime' {
26
+ export namespace JSX {
27
+ interface IntrinsicElements extends ReactJSX.JSX.IntrinsicElements { }
28
+ interface Element extends ReactJSX.JSX.Element { }
29
+
30
+ type GodotConstructor = { new(...args: any[]): Instance } & Function;
31
+
32
+ type ElementType =
33
+ | ReactJSX.JSX.ElementType
34
+ | GodotConstructor;
35
+
36
+ type LibraryManagedAttributes<C, P> =
37
+ C extends GodotConstructor
38
+ ? ComponentProps<C>
39
+ : ReactJSX.JSX.LibraryManagedAttributes<C, P>;
40
+ }
41
+ }
@@ -1,36 +1,36 @@
1
- /**********************************************************************
2
- Copyright (c) Vladimir Davidovich. All rights reserved.
3
- ***********************************************************************/
4
-
5
- import type React from 'react';
6
- import type { Object } from '@ringozz/godot/Object';
7
- import type { ValueTypes } from '@ringozz/godot';
8
-
9
- type FunctionKeys<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
10
- type Properties<T> = Omit<T, FunctionKeys<T>>;
11
- type Overwrite<P, O> = Properties<P> & O;
12
- type Mutable<P> = { [K in keyof P]: P[K] | Readonly<P[K]> };
13
- type ConstructorRepresentation<T = any> = new (...args: any[]) => T;
14
-
15
- export type Instance = Object;
16
-
17
- export type InstanceProps<T extends Instance = Instance> = {
18
- /** An existing instance to render instead of creating a new one. */
19
- object?: T;
20
- /** Attaches the element to a named property of the parent instead of adding it as a child. */
21
- attach?: string;
22
- };
23
-
24
- type ReactProps<P> = React.PropsWithChildren<React.RefAttributes<P>>;
25
-
26
- type WidenVT<T> = {
27
- [K in keyof T]: T[K] extends ValueTypes ? T[K] | number[] : T[K];
28
- };
29
-
30
- type ElementProps<T extends ConstructorRepresentation, P = InstanceType<T>> = Partial<
31
- Overwrite<WidenVT<P>, ReactProps<P>>
32
- >;
33
-
34
- export type ComponentProps<T extends ConstructorRepresentation> = Mutable<
35
- Overwrite<ElementProps<T>, Omit<InstanceProps<InstanceType<T>>, 'object'>>
36
- >;
1
+ /**********************************************************************
2
+ Copyright (c) Vladimir Davidovich. All rights reserved.
3
+ ***********************************************************************/
4
+
5
+ import type React from 'react';
6
+ import type { Object } from '@ringozz/godot/Object';
7
+ import type { ValueTypes } from '@ringozz/godot';
8
+
9
+ type FunctionKeys<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
10
+ type Properties<T> = Omit<T, FunctionKeys<T>>;
11
+ type Overwrite<P, O> = Properties<P> & O;
12
+ type Mutable<P> = { [K in keyof P]: P[K] | Readonly<P[K]> };
13
+ type ConstructorRepresentation<T = any> = new (...args: any[]) => T;
14
+
15
+ export type Instance = Object;
16
+
17
+ export type InstanceProps<T extends Instance = Instance> = {
18
+ /** An existing instance to render instead of creating a new one. */
19
+ object?: T;
20
+ /** Attaches the element to a named property of the parent instead of adding it as a child. */
21
+ attach?: string;
22
+ };
23
+
24
+ type ReactProps<P> = React.PropsWithChildren<React.RefAttributes<P>>;
25
+
26
+ type WidenVT<T> = {
27
+ [K in keyof T]: T[K] extends ValueTypes ? T[K] | number[] : T[K];
28
+ };
29
+
30
+ type ElementProps<T extends ConstructorRepresentation, P = InstanceType<T>> = Partial<
31
+ Overwrite<WidenVT<P>, ReactProps<P>>
32
+ >;
33
+
34
+ export type ComponentProps<T extends ConstructorRepresentation> = Mutable<
35
+ Overwrite<ElementProps<T>, Omit<InstanceProps<InstanceType<T>>, 'object'>>
36
+ >;