@_spi(SGTKInternal) import GLib // MARK: - Callback box /// Retained owner of a GLib source callback. GLib holds exactly one strong /// reference to this box, released by the source's `GDestroyNotify`. @MainActor final class SourceCallbackBox { private let body: @MainActor (SourceId) -> Void private let repeats: Bool private let owner: SourceId init(owner: SourceId, repeats: Bool, body: @escaping @MainActor (SourceId) -> Void) { self.owner = owner self.repeats = repeats self.body = body } /// Runs the callback. Returns `true` for `G_SOURCE_CONTINUE`, `false` for /// `G_SOURCE_REMOVE`. /// /// Pins locals before calling `body` so that a body which removes its own /// source (triggering synchronous `g_source_destroy` -> `GDestroyNotify` -> /// box deallocation) does not cause use-after-free when this method reads /// `repeats` and `owner.isRemoved` afterward. func invoke() -> Bool { let owner = self.owner // local strong ref survives box dealloc let repeats = self.repeats body(owner) return repeats && !owner.isRemoved } } // MARK: - Trampolines /// GLib `GSourceFunc` trampoline. Bridges the C callback back to the main /// actor via `MainActor.assumeIsolated`. Dispatch runs on the thread pumping /// the GLib main context, which must be the process's main thread. @_cdecl("portico_source_dispatch") nonisolated func portico_source_dispatch(_ data: UnsafeMutableRawPointer?) -> Int32 { guard let data else { return 0 } let box = Unmanaged.fromOpaque(data).takeUnretainedValue() return withExtendedLifetime(box) { MainActor.assumeIsolated { box.invoke() ? 1 : 0 } } } /// GLib `GDestroyNotify` trampoline. Releases the +1 retain that GLib holds /// on the callback box. /// /// Destroy-notify may run on any thread, so this is a plain nonisolated /// refcount release rather than a `MainActor.assumeIsolated` call. @_cdecl("portico_source_destroy_notify") nonisolated func portico_source_destroy_notify(_ data: UnsafeMutableRawPointer?) { guard let data else { return } Unmanaged.fromOpaque(data).release() } // MARK: - Duration conversion /// Converts a `Duration` to whole GLib milliseconds. Negative durations clamp /// to `0`; a non-zero sub-millisecond remainder rounds up to `1`; values beyond /// `UInt32.max` milliseconds clamp to `UInt32.max`. @_spi(Portico) public func _clampedMilliseconds(_ duration: Duration) -> UInt32 { let c = duration.components if c.seconds < 0 || c.attoseconds < 0 { return 0 } let attosecondsPerMillisecond: Int64 = 1_000_000_000_000_000 guard c.seconds <= Int64(UInt32.max) / 1000 else { return UInt32.max } var total = c.seconds * 1000 + c.attoseconds / attosecondsPerMillisecond if c.attoseconds % attosecondsPerMillisecond > 0 { total += 1 } return total > Int64(UInt32.max) ? UInt32.max : UInt32(total) } // MARK: - Attach helper /// Sets the priority and callback on `source`, attaches it to the default main /// context, and returns its handle. @MainActor private func attachSource( _ source: GLib.Source, priority: SourcePriority, repeats: Bool, body: @escaping @MainActor (SourceId) -> Void ) -> SourceId { let id = SourceId(source: source) let box = SourceCallbackBox(owner: id, repeats: repeats, body: body) let dispatch: @convention(c) (UnsafeMutableRawPointer?) -> Int32 = portico_source_dispatch let notify: @convention(c) (UnsafeMutableRawPointer?) -> Void = portico_source_destroy_notify let ptr = source.pointer portico_g_source_set_priority(ptr, priority.rawValue) portico_g_source_set_callback(ptr, dispatch, Unmanaged.passRetained(box).toOpaque(), notify) id.rawValue = portico_g_source_attach(ptr, nil) return id } // MARK: - Idle /// Runs `body` when the GLib main loop has no higher-priority work pending. /// /// Attaches a `g_idle_source_new` source to the default main context. The /// closure receives the source's own ``SourceId`` so it can stop itself. /// /// - Parameters: /// - priority: Source priority. Defaults to ``SourcePriority/defaultIdle``. /// - repeats: When `true`, `body` runs on every idle iteration until removed. /// Defaults to `false` (run once). A repeating idle source keeps the main /// loop from sleeping - prefer `Timeout` for periodic work. /// - body: The work to perform on the main thread. /// - Returns: A handle that removes the source; discardable. @MainActor @discardableResult public func Idle( priority: SourcePriority = .defaultIdle, repeats: Bool = false, _ body: @escaping @MainActor (SourceId) -> Void ) -> SourceId { attachSource(GLib.idleSourceNew(), priority: priority, repeats: repeats, body: body) } /// Runs `body` when the GLib main loop has no higher-priority work pending. /// /// Convenience overload for closures that do not need the source handle. /// /// - Parameters: /// - priority: Source priority. Defaults to ``SourcePriority/defaultIdle``. /// - repeats: When `true`, `body` runs on every idle iteration until removed. /// Defaults to `false` (run once). /// - body: The work to perform on the main thread. /// - Returns: A handle that removes the source; discardable. @MainActor @discardableResult public func Idle( priority: SourcePriority = .defaultIdle, repeats: Bool = false, _ body: @escaping @MainActor () -> Void ) -> SourceId { Idle(priority: priority, repeats: repeats) { _ in body() } } // MARK: - Timeout /// Runs `body` repeatedly at `interval` on the GLib main loop. /// /// Attaches a `g_timeout_source_new` source to the default main context. The /// interval is monotonic and millisecond-granular; GLib may delay a fire when /// higher-priority sources are busy and does not try to catch up lost time, so /// do not rely on this for precise timing. The first fire is one `interval` /// after attach. /// /// - Parameters: /// - interval: Time between fires. Rounded up to whole milliseconds; negative /// values clamp to `0`. /// - priority: Source priority. Defaults to ``SourcePriority/default``. /// - repeats: When `false`, the source is removed after the first fire. /// Defaults to `true`. /// - body: The work to perform on the main thread; receives the source's own /// ``SourceId`` so it can stop itself. /// - Returns: A handle that removes the source; discardable. @MainActor @discardableResult public func Timeout( interval: Duration, priority: SourcePriority = .default, repeats: Bool = true, _ body: @escaping @MainActor (SourceId) -> Void ) -> SourceId { attachSource( GLib.timeoutSourceNew(interval: _clampedMilliseconds(interval)), priority: priority, repeats: repeats, body: body ) } /// Runs `body` repeatedly at `interval` on the GLib main loop. /// /// Convenience overload for closures that do not need the source handle. /// /// - Parameters: /// - interval: Time between fires. See ``Timeout(interval:priority:repeats:_:)`` /// for granularity and drift behavior. /// - priority: Source priority. Defaults to ``SourcePriority/default``. /// - repeats: When `false`, the source is removed after the first fire. /// Defaults to `true`. /// - body: The work to perform on the main thread. /// - Returns: A handle that removes the source; discardable. @MainActor @discardableResult public func Timeout( interval: Duration, priority: SourcePriority = .default, repeats: Bool = true, _ body: @escaping @MainActor () -> Void ) -> SourceId { Timeout(interval: interval, priority: priority, repeats: repeats) { _ in body() } }