diff --git a/packages/example/src/form/ControlledFormRoot.tsx b/packages/example/src/form/ControlledFormRoot.tsx index 21523b5..ceb0a09 100644 --- a/packages/example/src/form/ControlledFormRoot.tsx +++ b/packages/example/src/form/ControlledFormRoot.tsx @@ -14,15 +14,12 @@ const TextEdit = Component.fromClass(Godot.TextEdit) * A form UI where React controls the state */ export function ControlledFormRoot() { - const rootRef = CenterContainer.useRef() - Component.useSignal(rootRef, "ready", () => { - console.log("ready") - }) - return ( diff --git a/packages/react-godot-renderer/src/Component.ts b/packages/react-godot-renderer/src/Component.ts index 5996cff..d5d290b 100644 --- a/packages/react-godot-renderer/src/Component.ts +++ b/packages/react-godot-renderer/src/Component.ts @@ -10,6 +10,10 @@ export type Props> = { } & { // biome-ignore lint/complexity/noBannedTypes: using Function here is completely fine [K in keyof T as T[K] extends Function | Godot.Signal ? never : K]?: T[K] +} & { + [K in keyof T as T[K] extends Godot.Signal ? K : never]?: T[K] extends Godot.Signal + ? (this: T, ...args: Parameters) => ReturnType + : never } export interface Prototype> { diff --git a/packages/react-godot-renderer/src/Reconciler.ts b/packages/react-godot-renderer/src/Reconciler.ts index 8ca5f67..225f0da 100644 --- a/packages/react-godot-renderer/src/Reconciler.ts +++ b/packages/react-godot-renderer/src/Reconciler.ts @@ -1,4 +1,5 @@ -import { Control, Node, type NodePathMap, PackedScene, ResourceLoader } from "godot" +/** biome-ignore-all lint/complexity/noBannedTypes: "Function" is used as a type in GodotJS, keeping it for consistency */ +import { Callable, Control, Node, type NodePathMap, PackedScene, ResourceLoader, Signal } from "godot" import ReactReconciler, { type HostConfig } from "react-reconciler" import { hasProperty } from "./utils.js" @@ -225,20 +226,55 @@ export const make = () => { const applyNextProps = (instance: Node, nextProps: Record) => { Object.keys(nextProps).forEach(name => { - (instance as any)[name] = nextProps[name] - }) + if (!hasProperty(instance, name)) return - if (hasProperty(nextProps, "name")) { - if (typeof nextProps.name !== "string") - throw new Error("Prop 'name' should be a string") - instance.set_name(nextProps.name) - } - - if (instance instanceof Control) { - if (hasProperty(nextProps, "anchors_preset")) { - if (typeof nextProps.anchors_preset !== "number") - throw new Error("Prop 'anchors_preset' should be a number") - instance.set_anchors_preset(nextProps.anchors_preset) + if (name === "name") { + if (typeof nextProps[name] !== "string") + throw new Error("Prop 'name' should be a string") + instance.set_name(nextProps[name]) + return } - } + + if (instance[name] instanceof Signal) { + if ((typeof nextProps[name] !== "function") || nextProps[name] === undefined) + throw new Error(`Prop '${ name }' should be a function or undefined`) + instance[`${ name }_event`] = nextProps[name] + + if (!isNodeSignalRegistered(instance, name)) { + const callable = Callable.create(instance, function(this, ...args) { + if (this[`${ name }_event`]) + (this[`${ name }_event`] as Function)(...args) + }) + instance[`${ name }_callable`] = callable + instance.connect(name, callable) + } + + return + } + + if (instance instanceof Control) { + if (name === "anchors_preset") { + if (typeof nextProps[name] !== "number") + throw new Error("Prop 'anchors_preset' should be a number") + instance.set_anchors_preset(nextProps[name]) + return + } + } + + instance[name] = nextProps[name] + }) } + +type NodeWithSignalMetadata = Node & { + [K in `${ A }_callable`]: Callable +} & { + [K in `${ A }_event`]?: Function +} + +const isNodeSignalRegistered = ( + u: Node, + name: A, +): u is NodeWithSignalMetadata => ( + hasProperty(u, `${ name }_callable`) && + u[`${ name }_callable`] instanceof Callable +)