diff --git a/Generator/Generator/ClassGen.swift b/Generator/Generator/ClassGen.swift index ce5822f00..05abc4d9d 100644 --- a/Generator/Generator/ClassGen.swift +++ b/Generator/Generator/ClassGen.swift @@ -518,98 +518,11 @@ func generateClasses (values: [JGodotExtensionAPIClass], outputDir: String?) asy } } -func generateSignalType (_ p: Printer, _ cdef: JGodotExtensionAPIClass, _ signal: JGodotSignal, _ name: String) -> String { - doc (p, cdef, "Signal support.\n") - doc (p, cdef, "Use the ``\(name)/connect(flags:_:)`` method to connect to the signal on the container object, and ``\(name)/disconnect(_:)`` to drop the connection.\nYou can also await the ``\(name)/emitted`` property for waiting for a single emission of the signal.") - - var lambdaFull = "" - p ("public class \(name)") { - p ("var target: Object") - p ("var signalName: StringName") - p ("init (target: Object, signalName: StringName)") { - p ("self.target = target") - p ("self.signalName = signalName") - } - doc (p, cdef, "Connects the signal to the specified callback\n\nTo disconnect, call the disconnect method, with the returned token on success\n - Parameters:\n - callback: the method to invoke when this signal is raised\n - flags: Optional, can be also added to configure the connection's behavior (see ``Object/ConnectFlags`` constants).\n - Returns: an object token that can be used to disconnect the object from the target on success, or the error produced by Godot.") - - p ("@discardableResult /* \(name) */") - var args = "" - var argUnwrap = "" - var callArgs = "" - var argIdx = 0 - var lambdaIgnore = "" - for arg in signal.arguments ?? [] { - if args != "" { - args += ", " - callArgs += ", " - lambdaIgnore += ", " - lambdaFull += ", " - } - args += getArgumentDeclaration(arg, omitLabel: true, isOptional: arg.type == "Variant") - let construct: String - - if let _ = classMap [arg.type] { - argUnwrap += "var ptr_\(argIdx): UnsafeMutableRawPointer?\n" - argUnwrap += "args [\(argIdx)]!.toType (Variant.GType.object, dest: &ptr_\(argIdx))\n" - let handleResolver: String - if hasSubclasses.contains(cdef.name) { - // If the type we are bubbling up has subclasses, we want to create the most - // derived type if possible, so we perform the longer lookup - handleResolver = "lookupObject (nativeHandle: ptr_\(argIdx)!) ?? " - } else { - handleResolver = "" - } - - construct = "lookupLiveObject (handleAddress: ptr_\(argIdx)!) as? \(arg.type) ?? \(handleResolver)\(arg.type) (nativeHandle: ptr_\(argIdx)!)" - } else if arg.type == "String" { - construct = "\(mapTypeName(arg.type)) (args [\(argIdx)]!)!.description" - } else if arg.type == "Variant" { - construct = "args [\(argIdx)]" - } else { - construct = "\(getGodotType(arg)) (args [\(argIdx)]!)!" - } - argUnwrap += "let arg_\(argIdx) = \(construct)\n" - callArgs += "arg_\(argIdx)" - lambdaIgnore += "_" - lambdaFull += escapeSwift (snakeToCamel (arg.name)) - argIdx += 1 - } - p ("public func connect (flags: Object.ConnectFlags = [], _ callback: @escaping (\(args)) -> ()) -> Object") { - p ("let signalProxy = SignalProxy()") - p ("signalProxy.proxy = ") { - p ("args in") - p (argUnwrap) - p ("callback (\(callArgs))") - } - p ("let callable = Callable(object: signalProxy, method: SignalProxy.proxyName)") - p ("let r = target.connect(signal: signalName, callable: callable, flags: UInt32 (flags.rawValue))") - p ("if r != .ok { print (\"Warning, error connecting to signal, code: \\(r)\") }") - p ("return signalProxy") - } - - doc (p, cdef, "Disconnects a signal that was previously connected, the return value from calling ``connect(flags:_:)``") - p ("public func disconnect (_ token: Object)") { - p ("target.disconnect(signal: signalName, callable: Callable (object: token, method: SignalProxy.proxyName))") - } - doc (p, cdef, "You can await this property to wait for the signal to be emitted once") - p ("public var emitted: Void "){ - p ("get async") { - p ("await withCheckedContinuation") { - p ("c in") - p ("connect (flags: .oneShot) { \(lambdaIgnore) in c.resume () }") - } - } - } - } - return lambdaFull -} - func generateSignals (_ p: Printer, cdef: JGodotExtensionAPIClass, signals: [JGodotSignal]) { p ("// Signals ") var parameterSignals: [JGodotSignal] = [] - var sidx = 0 for signal in signals { let signalProxyType: String @@ -617,11 +530,10 @@ func generateSignals (_ p: Printer, if signal.arguments != nil { parameterSignals.append (signal) - sidx += 1 - signalProxyType = "Signal\(sidx)" - lambdaSig = " " + generateSignalType (p, cdef, signal, signalProxyType) + " in" + signalProxyType = getGenericSignalType(signal) + lambdaSig = " \(getGenericSignalLambdaArgs(signal)) in" } else { - signalProxyType = "SimpleSignal" + signalProxyType = "GenericSignal< /* no args */ >" lambdaSig = "" } let signalName = godotMethodToSwift (signal.name) @@ -639,6 +551,30 @@ func generateSignals (_ p: Printer, } } +/// Return the type of a signal's parameters. +func getGenericSignalType(_ signal: JGodotSignal) -> String { + var argTypes: [String] = [] + for signalArgument in signal.arguments ?? [] { + let godotType = getGodotType(signalArgument) + if !godotType.isEmpty && godotType != "Variant" { + argTypes.append(godotType) + } + } + + return argTypes.isEmpty ? "GenericSignal< /* no args */ >" : "GenericSignal<\(argTypes.joined(separator: ", "))>" + } + +/// Return the names of a signal's parameters, +/// for use in documenting the corresponding lambda. +func getGenericSignalLambdaArgs(_ signal: JGodotSignal) -> String { + var argNames: [String] = [] + for signalArgument in signal.arguments ?? [] { + argNames.append(escapeSwift(snakeToCamel(signalArgument.name))) + } + + return argNames.joined(separator: ", ") +} + func generateSignalDocAppendix (_ p: Printer, cdef: JGodotExtensionAPIClass, signals: [JGodotSignal]?) { guard let signals = signals, signals.count > 0 else { return diff --git a/Package.swift b/Package.swift index 3313fa0e2..4d8034d8a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -231,7 +231,7 @@ targets.append(contentsOf: [ let package = Package( name: "SwiftGodot", platforms: [ - .macOS(.v13), + .macOS(.v14), .iOS (.v15) ], products: products, diff --git a/Sources/SwiftGodot/Core/GenericSignal.swift b/Sources/SwiftGodot/Core/GenericSignal.swift new file mode 100644 index 000000000..2309384a4 --- /dev/null +++ b/Sources/SwiftGodot/Core/GenericSignal.swift @@ -0,0 +1,99 @@ +// +// Created by Sam Deane on 25/10/2024. +// + +/// Signal support. +/// Use the ``GenericSignal/connect(flags:_:)`` method to connect to the signal on the container object, +/// and ``GenericSignal/disconnect(_:)`` to drop the connection. +/// +/// Use the ``GenericSignal/emit(...)`` method to emit a signal. +/// +/// You can also await the ``Signal1/emitted`` property for waiting for a single emission of the signal. +/// +public class GenericSignal { + var target: Object + var signalName: StringName + public init(target: Object, signalName: StringName) { + self.target = target + self.signalName = signalName + } + + /// Connects the signal to the specified callback + /// To disconnect, call the disconnect method, with the returned token on success + /// + /// - Parameters: + /// - callback: the method to invoke when this signal is raised + /// - flags: Optional, can be also added to configure the connection's behavior (see ``Object/ConnectFlags`` constants). + /// - Returns: an object token that can be used to disconnect the object from the target on success, or the error produced by Godot. + /// + @discardableResult /* Signal1 */ + public func connect(flags: Object.ConnectFlags = [], _ callback: @escaping (_ t: repeat each T) -> Void) -> Object { + let signalProxy = SignalProxy() + signalProxy.proxy = { args in + var index = 0 + do { + callback(repeat try args.unpack(as: (each T).self, index: &index)) + } catch { + print("Error unpacking signal arguments: \(error)") + } + } + + let callable = Callable(object: signalProxy, method: SignalProxy.proxyName) + let r = target.connect(signal: signalName, callable: callable, flags: UInt32(flags.rawValue)) + if r != .ok { print("Warning, error connecting to signal, code: \(r)") } + return signalProxy + } + + /// Disconnects a signal that was previously connected, the return value from calling + /// ``connect(flags:_:)`` + public func disconnect(_ token: Object) { + target.disconnect(signal: signalName, callable: Callable(object: token, method: SignalProxy.proxyName)) + } + + /// You can await this property to wait for the signal to be emitted once. + public var emitted: Void { + get async { + await withCheckedContinuation { c in + let signalProxy = SignalProxy() + signalProxy.proxy = { _ in c.resume() } + let callable = Callable(object: signalProxy, method: SignalProxy.proxyName) + let r = target.connect(signal: signalName, callable: callable, flags: UInt32(Object.ConnectFlags.oneShot.rawValue)) + if r != .ok { print("Warning, error connecting to signal, code: \(r)") } + } + + } + + } + +} + + +extension Arguments { + enum UnpackError: Error { + case typeMismatch + case missingArgument + } + + /// Unpack an argument as a specific type. + /// We throw a runtime error if the argument is not of the expected type, + /// or if there are not enough arguments to unpack. + func unpack(as type: T.Type, index: inout Int) throws -> T { + if index >= count { + throw UnpackError.missingArgument + } + let argument = self[index] + index += 1 + let value: T? + if argument.gtype == .object { + value = T.Representable.godotType == .object ? argument.asObject(Object.self) as? T : nil + } else { + value = T(argument) + } + + guard let value else { + throw UnpackError.typeMismatch + } + + return value + } +} diff --git a/Sources/SwiftGodot/Core/SignalSupport.swift b/Sources/SwiftGodot/Core/SignalSupport.swift index f5a0c7716..1408aa283 100644 --- a/Sources/SwiftGodot/Core/SignalSupport.swift +++ b/Sources/SwiftGodot/Core/SignalSupport.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Miguel de Icaza on 4/30/23. // @@ -18,129 +18,31 @@ /// invokeScript ("myDemo.proxy ()", params: ["myDemo", demo]) /// ``` public class SignalProxy: Object { - public static var proxyName = StringName ("proxy") + public static var proxyName = StringName("proxy") static var initClass: Bool = { register(type: SignalProxy.self) - + let s = ClassInfo(name: "SignalProxy") - + s.registerMethod(name: SignalProxy.proxyName, flags: .default, returnValue: nil, arguments: [], function: SignalProxy.proxyFunc) return true - } () - + }() + /// The code invoked when Godot invokes the `proxy` method on this object. public typealias Proxy = (borrowing Arguments) -> () public var proxy: Proxy? - - public required init () { + + public required init() { let _ = SignalProxy.initClass super.init() } - - public required init (nativeHandle: UnsafeRawPointer) { - super.init (nativeHandle: nativeHandle) - } - - func proxyFunc (args: borrowing Arguments) -> Variant? { - proxy? (args) - return nil - } -} -/// The simple signal is used to raise signals that take no arguments and return no values. -/// -/// To connect, you access the connect method, and pass you callback function, like this: -/// ``` -/// let myClass = MyClass () -/// let token = myClass.wakeup.connect { -/// print ("wakeup triggered") -/// } -/// ``` -/// -/// If you want to disconnect, you call: -/// ``` -/// myClass.wakeup.disconnect (token) -/// ``` -/// -/// To merely wait for one emission of the signal you can await the ``emitted`` property. -/// -/// Subclasses that use this, implement signals like this: -/// -/// ``` -/// class MyClass: Object { -/// // the `wakeup` signal -/// public var wakeup: SimpleSignal { SimpleSignal (self, "wakeup") } -/// } -/// ``` -/// -public class SimpleSignal { - var target: Object - var signalName: StringName - - /// - Parameters: - /// - target: the object where we will be operating on, to connect or disconnect the signal - /// - name: the name of the signal - public init (target: Object, signalName: StringName) { - self.target = target - self.signalName = signalName - } - - /// Connects the signal to the specified callback. - /// - /// To disconnect, call the disconnect method, with the returned token on success - /// - /// Example: - /// ```swift - /// node.ready.connect { - /// print ("Node is ready") - /// } - /// ``` - /// - /// - Parameters: - /// - callback: the method to invoke when the signal is raised - /// - flags: Optional, can be also added to configure the connection's behavior (see ``Object/ConnectFlags`` constants). - /// - Returns: an object token that can be used to disconnect the object from the target. - @discardableResult - public func connect (flags: Object.ConnectFlags = [], _ callback: @escaping () -> ()) -> Object { - let signalProxy = SignalProxy() - if flags.contains(.oneShot) { - signalProxy.proxy = { [weak signalProxy] args in - callback () - guard let signalProxy else { return } - signalProxy.proxy = nil - _ = signalProxy.callDeferred(method: "free") - } - } else { - signalProxy.proxy = { args in - callback () - } - } - - let callable = Callable(object: signalProxy, method: SignalProxy.proxyName) - let r = target.connect(signal: signalName, callable: callable, flags: UInt32 (flags.rawValue)) - if r != .ok { - print ("Warning, error connecting to signal \(signalName.description): \(r)") - } - return signalProxy + public required init(nativeHandle: UnsafeRawPointer) { + super.init(nativeHandle: nativeHandle) } - - /// Disconnects a signal that was previously connected, the return value from calling ``connect(flags:_:)`` - public func disconnect (_ token: Object) { - guard let signalProxy = token as? SignalProxy else { return } - signalProxy.proxy = nil - _ = signalProxy.callDeferred(method: "free") - target.disconnect(signal: signalName, callable: Callable (object: token, method: SignalProxy.proxyName)) - } - - /// You can await this property to wait for the signal to be emitted once - @MainActor - public var emitted: Void { - get async { - await withCheckedContinuation { c in - connect (flags: .oneShot) { - c.resume() - } - } - } + + func proxyFunc(args: borrowing Arguments) -> Variant? { + proxy?(args) + return nil } } diff --git a/Tests/SwiftGodotTests/MarshalTests.swift b/Tests/SwiftGodotTests/MarshalTests.swift index edc84fafb..2dfc682a8 100644 --- a/Tests/SwiftGodotTests/MarshalTests.swift +++ b/Tests/SwiftGodotTests/MarshalTests.swift @@ -33,7 +33,17 @@ final class MarshalTests: GodotTestCase { XCTAssertEqual (node.receivedInt, 22, "Integers should have been the same") XCTAssertEqual (node.receivedString, "Joey", "Strings should have been the same") } - + + func testBuiltInSignals() { + let node = TestNode() + var signalReceived = false + node.ready.connect { + signalReceived = true + } + node.emitSignal("ready") + XCTAssertTrue (signalReceived, "ready signal should have been received") + } + func testClassesMethodsPerformance() { let node = TestNode() let child = TestNode()