Add signal support
Some checks failed
Lint / lint (push) Failing after 9s

This commit is contained in:
Julien Valverdé
2026-01-04 18:13:09 +01:00
parent 7f1be2cd1d
commit eae6647b7e
3 changed files with 58 additions and 21 deletions

View File

@@ -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 (
<CenterContainer
ref={rootRef}
anchors_preset={Godot.Control.LayoutPreset.PRESET_FULL_RECT}
ready={function(this) {
console.log("ready")
}}
>
<PanelContainer>
<MarginContainer>

View File

@@ -10,6 +10,10 @@ export type Props<T extends Godot.Node<Godot.NodePathMap>> = {
} & {
// 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<infer F>
? (this: T, ...args: Parameters<F>) => ReturnType<F>
: never
}
export interface Prototype<T extends Godot.Node<Godot.NodePathMap>> {

View File

@@ -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<NodePathMap>, nextProps: Record<string, unknown>) => {
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")
if (name === "name") {
if (typeof nextProps[name] !== "string")
throw new Error("Prop 'name' should be a string")
instance.set_name(nextProps.name)
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 (hasProperty(nextProps, "anchors_preset")) {
if (typeof nextProps.anchors_preset !== "number")
if (name === "anchors_preset") {
if (typeof nextProps[name] !== "number")
throw new Error("Prop 'anchors_preset' should be a number")
instance.set_anchors_preset(nextProps.anchors_preset)
instance.set_anchors_preset(nextProps[name])
return
}
}
instance[name] = nextProps[name]
})
}
type NodeWithSignalMetadata<A extends string> = Node<NodePathMap> & {
[K in `${ A }_callable`]: Callable
} & {
[K in `${ A }_event`]?: Function
}
const isNodeSignalRegistered = <A extends string>(
u: Node<NodePathMap>,
name: A,
): u is NodeWithSignalMetadata<A> => (
hasProperty(u, `${ name }_callable`) &&
u[`${ name }_callable`] instanceof Callable
)