diff --git a/.gitignore b/.gitignore index e1c5612..fd02e03 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ DerivedData/ .Ulysses-Group.plist /.docc-build /io.github.AparokshaUI.Generation.json -/.vscode \ No newline at end of file +/io.github.AparokshaUI.swiftlint.json +/.vscode diff --git a/Sources/Adwaita/Model/Data Flow/Idle.swift b/Sources/Adwaita/Model/Data Flow/Idle.swift new file mode 100644 index 0000000..7f41e3e --- /dev/null +++ b/Sources/Adwaita/Model/Data Flow/Idle.swift @@ -0,0 +1,110 @@ +// +// Idle.swift +// Adwaita +// +// Created by david-swift on 02.05.24. +// + +import CAdw + +/// Add a task to GLib's idle. +public struct Idle { + + /// The idle handler. + static let handler = IdleHandler() + + /// Run a function whenever there are no higher priority events pending to the default main loop. + /// - Parameters: + /// - priority: The task's priority, default priority by default. + /// - closure: The closure to run. + @discardableResult + public init( + priority: Priority = .defaultIdle, + closure: @escaping () -> Void + ) { + Self.handler.add({ closure(); return false }, priority: .init(priority.rawValue)) + } + + /// Repeat a function with a certain delay. + /// - Parameters: + /// - delay: The delay between the repetitions. + /// - priority: The task's priority, default priority by default. + /// - closure: The closure to run. Return if you want to exit the loop. + @discardableResult + public init( + delay: Duration, + priority: Priority = .defaultIdle, + closure: @escaping () -> Bool + ) { + Self.handler.add(closure, priority: .init(priority.rawValue), delay: delay) + } + + /// The priority of an idle task. + public enum Priority: Int { + + /// A very low priority background task. + case low = 300 + /// A high priority event source. + case high = -100 + /// A default priority event source. + case `default` = 0 + /// A high priority idle function. + case highIdle = 100 + /// A default priority idle function. + case defaultIdle = 200 + + } + + /// An idle handler. + class IdleHandler { + + /// Add a function to be called whenever there are no higher priority events pending to the default main loop. + /// - Parameter closure: The function. + func add(_ closure: @escaping () -> Bool, priority: Int32, delay: Duration? = nil) { + let context = UnsafeMutableRawPointer(Unmanaged.passRetained(ClosureContainer(closure: closure)).toOpaque()) + let secondsToMilliseconds: Int64 = 1_000 + let attosecondsToMilliseconds: Int64 = 1_000_000_000_000_000 + if let delay { + let milliseconds = delay.components.seconds * secondsToMilliseconds + + (delay.components.attoseconds / attosecondsToMilliseconds) + // swiftlint:disable prefer_self_in_static_references + g_timeout_add_full(priority, .init(milliseconds), { IdleHandler.run(pointer: $0) }, context, nil) + // swiftlint:enable prefer_self_in_static_references + } else { + // swiftlint:disable prefer_self_in_static_references + g_idle_add_full(priority, { IdleHandler.run(pointer: $0) }, context, nil) + // swiftlint:enable prefer_self_in_static_references + } + } + + /// Execute the function. + /// - Parameter pointer: The closure wrapper's pointer. + static func run(pointer: gpointer?) -> Int32 { + if let pointer { + let container = Unmanaged.fromOpaque(pointer).takeUnretainedValue() + let result = container.closure() + if !result { + Unmanaged.fromOpaque(pointer).release() + } + return result.cBool + } + return G_SOURCE_REMOVE + } + + } + + /// A reference type holding a closure. + class ClosureContainer { + + /// The closure. + var closure: () -> Bool + + /// Initialize an object. + /// - Parameter closure: The closure. + init(closure: @escaping () -> Bool) { + self.closure = closure + } + + } + +} diff --git a/Sources/Adwaita/Window/Window.swift b/Sources/Adwaita/Window/Window.swift index 9090c27..5576124 100644 --- a/Sources/Adwaita/Window/Window.swift +++ b/Sources/Adwaita/Window/Window.swift @@ -110,7 +110,7 @@ public struct Window: WindowScene { updateAppShortcuts(app: app) } for signal in signals where signal.update { - Task { + Idle { app.showWindow(signal.id.uuidString) } } diff --git a/Tests/IdleDemo.swift b/Tests/IdleDemo.swift new file mode 100644 index 0000000..f7fb500 --- /dev/null +++ b/Tests/IdleDemo.swift @@ -0,0 +1,50 @@ +// +// IdleDemo.swift +// Adwaita +// +// Created by david-swift on 05.05.24. +// + +// swiftlint:disable missing_docs + +import Adwaita + +struct IdleDemo: View { + + @State private var progress = 0.0 + @State private var activeProcess = false + let max = 500.0 + let delayFactor = 5.0 + let maxWidth = 300 + + var view: Body { + ProgressBar(value: progress, total: max) + .vexpand() + .valign(.center) + .frame(maxWidth: maxWidth) + Button("Play") { + Task { + Idle { + activeProcess = true + progress = 0 + } + Idle(delay: .seconds(delayFactor / max)) { + progress += 1 + let done = progress == max + if done { + activeProcess = false + } + return !done + } + } + } + .padding() + .style("pill") + .hexpand() + .halign(.center) + .insensitive(activeProcess) + } + +} + +// swiftlint:enable missing_docs diff --git a/Tests/Page.swift b/Tests/Page.swift index bf91ffc..be63880 100644 --- a/Tests/Page.swift +++ b/Tests/Page.swift @@ -29,6 +29,7 @@ enum Page: String, Identifiable, CaseIterable, Codable { case flowBox case navigationView case picture + case idle var id: Self { self @@ -94,6 +95,8 @@ enum Page: String, Identifiable, CaseIterable, Codable { return "A page-based navigation container" case .picture: return "Display an image" + case .idle: + return "Update UI from an asynchronous context" } } @@ -135,6 +138,8 @@ enum Page: String, Identifiable, CaseIterable, Codable { NavigationViewDemo(app: app) case .picture: PictureDemo(url: pictureURL, app: app, window: window) + case .idle: + IdleDemo() } } // swiftlint:enable cyclomatic_complexity