From de1fae4ae6197a874dc05004aab2c74de44e9c85 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 31 Oct 2025 19:56:21 -0400 Subject: [PATCH 01/27] Initial DatePicker implementation in UIKit --- .github/workflows/build-test-and-docs.yml | 4 +- Examples/Bundler.toml | 5 + Examples/Package.swift | 4 + .../DatePickerExample/DatePickerApp.swift | 47 +++++++ Sources/SwiftCrossUI/Backend/AppBackend.swift | 21 +++ .../Environment/EnvironmentValues.swift | 15 +++ Sources/SwiftCrossUI/Views/DatePicker.swift | 126 ++++++++++++++++++ .../Modifiers/DatePickerStyleModifier.swift | 8 ++ .../UIKitBackend/UIKitBackend+Control.swift | 73 ++++++++++ 9 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 Examples/Sources/DatePickerExample/DatePickerApp.swift create mode 100644 Sources/SwiftCrossUI/Views/DatePicker.swift create mode 100644 Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift diff --git a/.github/workflows/build-test-and-docs.yml b/.github/workflows/build-test-and-docs.yml index 287d40c417..0dfa5ccb80 100644 --- a/.github/workflows/build-test-and-docs.yml +++ b/.github/workflows/build-test-and-docs.yml @@ -106,9 +106,10 @@ jobs: buildtarget PathsExample if [ $device_type != TV ]; then - # Slider is not implemented for tvOS + # Slider and DatePicker are not implemented for tvOS buildtarget ControlsExample buildtarget RandomNumberGeneratorExample + buildtarget DatePickerExample fi if [ $device_type = iPad ]; then @@ -165,6 +166,7 @@ jobs: buildtarget PathsExample buildtarget ControlsExample buildtarget RandomNumberGeneratorExample + buildtarget DatePickerExample # TODO test whether this works on Catalyst # buildtarget SplitExample diff --git a/Examples/Bundler.toml b/Examples/Bundler.toml index 3b96dc6128..79a006f096 100644 --- a/Examples/Bundler.toml +++ b/Examples/Bundler.toml @@ -64,3 +64,8 @@ version = '0.1.0' identifier = 'dev.swiftcrossui.HoverExample' product = 'HoverExample' version = '0.1.0' + +[apps.DatePickerExample] +identifier = 'dev.swiftcrossui.DatePickerExample' +product = 'DatePickerExample' +version = '0.1.0' diff --git a/Examples/Package.swift b/Examples/Package.swift index 1735fc7675..f6446740a5 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -76,6 +76,10 @@ let package = Package( .executableTarget( name: "HoverExample", dependencies: exampleDependencies + ), + .executableTarget( + name: "DatePickerExample", + dependencies: exampleDependencies ) ] ) diff --git a/Examples/Sources/DatePickerExample/DatePickerApp.swift b/Examples/Sources/DatePickerExample/DatePickerApp.swift new file mode 100644 index 0000000000..4047b67445 --- /dev/null +++ b/Examples/Sources/DatePickerExample/DatePickerApp.swift @@ -0,0 +1,47 @@ +import DefaultBackend +import Foundation +import SwiftCrossUI + +#if canImport(SwiftBundlerRuntime) + import SwiftBundlerRuntime +#endif + +@main +@HotReloadable +struct DatePickerApp: App { + @State var date = Date() + @State var style: DatePickerStyle? = DatePickerStyle.automatic + + var allStyles: [DatePickerStyle] + + init() { + allStyles = [.automatic] + + if #available(iOS 14, macCatalyst 14, *) { + allStyles.append(.graphical) + } + + if #available(iOS 13.4, macCatalyst 13.4, *) { + allStyles.append(.compact) + #if os(iOS) || os(visionOS) + allStyles.append(.wheel) + #endif + } + } + + var body: some Scene { + WindowGroup("Date Picker") { + VStack { + Text("Selected date: \(date)") + + Picker(of: allStyles, selection: $style) + + DatePicker( + "Test Picker", + selection: $date + ) + .datePickerStyle(style ?? .automatic) + } + } + } +} diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index 31a412cf2f..c06f43a873 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -705,6 +705,17 @@ public protocol AppBackend: Sendable { ) /// Navigates a web view to a given URL. func navigateWebView(_ webView: Widget, to url: URL) + + // MARK: Date picker + func createDatePicker() -> Widget + func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) } extension AppBackend { @@ -1162,4 +1173,14 @@ extension AppBackend { ) { todo() } + + public func createDatePicker() -> Widget { todo() } + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { todo() } } diff --git a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift index ffb03d5d94..80e70c7d6f 100644 --- a/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift +++ b/Sources/SwiftCrossUI/Environment/EnvironmentValues.swift @@ -190,6 +190,16 @@ public struct EnvironmentValues { ) } + /// The current calendar that views should use when handling dates. + public var calendar: Calendar + + /// The current time zone that views should use when handling dates. + public var timeZone: TimeZone + + #if !os(tvOS) + public var datePickerStyle: DatePickerStyle + #endif + /// Creates the default environment. init(backend: Backend) { self.backend = backend @@ -212,6 +222,11 @@ public struct EnvironmentValues { isEnabled = true scrollDismissesKeyboardMode = .automatic isTextSelectionEnabled = false + calendar = .autoupdatingCurrent + timeZone = .autoupdatingCurrent + #if !os(tvOS) + datePickerStyle = .automatic + #endif } /// Returns a copy of the environment with the specified property set to the diff --git a/Sources/SwiftCrossUI/Views/DatePicker.swift b/Sources/SwiftCrossUI/Views/DatePicker.swift new file mode 100644 index 0000000000..44330ffcb2 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/DatePicker.swift @@ -0,0 +1,126 @@ +import Foundation + +@available(tvOS, unavailable) +public struct DatePickerComponents: OptionSet, Sendable { + public let rawValue: UInt + + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + public init() { + self.rawValue = 0 + } + + public static let date = DatePickerComponents(rawValue: 0x1C) + public static let hourAndMinute = DatePickerComponents(rawValue: 0x60) + + @available(iOS, unavailable) + @available(visionOS, unavailable) + @available(macCatalyst, unavailable) + public static let hourMinuteAndSecond = DatePickerComponents(rawValue: 0xE0) +} + +@available(tvOS, unavailable) +public enum DatePickerStyle: Sendable, Hashable { + case automatic + + @available(iOS 14, macCatalyst 14, *) + case graphical + + @available(iOS 13.4, macCatalyst 13.4, *) + case compact + + @available(iOS 13.4, macCatalyst 13.4, *) + @available(macOS, unavailable) + case wheel +} + +@available(tvOS, unavailable) +public struct DatePicker { + private var label: Label + private var selection: Binding + private var range: ClosedRange + private var components: DatePickerComponents + private var style: DatePickerStyle = .automatic + + public nonisolated init( + selection: Binding, + range: ClosedRange = Date.distantPast...Date.distantFuture, + components: DatePickerComponents = [.hourAndMinute, .date], + @ViewBuilder label: () -> Label + ) { + self.label = label() + self.selection = selection + self.range = range + self.components = components + } + + public nonisolated init( + _ label: String, + selection: Binding, + range: ClosedRange = Date.distantPast...Date.distantFuture, + components: DatePickerComponents = [.hourAndMinute, .date] + ) where Label == Text { + self.label = Text(label) + self.selection = selection + self.range = range + self.components = components + } + + public typealias Components = DatePickerComponents +} + +extension DatePicker: View { + public var body: some View { + HStack { + label + + DatePickerImplementation(selection: selection, range: range, components: components) + } + } +} + +internal struct DatePickerImplementation: ElementaryView { + @Binding private var selection: Date + private var range: ClosedRange + private var components: DatePickerComponents + + init(selection: Binding, range: ClosedRange, components: DatePickerComponents) { + self._selection = selection + self.range = range + self.components = components + } + + let body = EmptyView() + + func asWidget(backend: Backend) -> Backend.Widget { + backend.createDatePicker() + } + + func update( + _ widget: Backend.Widget, + proposedSize: SIMD2, + environment: EnvironmentValues, + backend: Backend, + dryRun: Bool + ) -> ViewUpdateResult { + if !dryRun { + backend.updateDatePicker( + widget, + environment: environment, + date: selection, + range: range, + components: components, + onChange: { selection = $0 } + ) + } + + // I reject your proposedSize and substitute my own + let naturalSize = backend.naturalSize(of: widget) + if !dryRun { + backend.setSize(of: widget, to: naturalSize) + } + return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize)) + } +} diff --git a/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift new file mode 100644 index 0000000000..89a6858772 --- /dev/null +++ b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift @@ -0,0 +1,8 @@ +extension View { + @available(tvOS, unavailable) + public func datePickerStyle(_ style: DatePickerStyle) -> some View { + EnvironmentModifier(self) { environment in + environment.with(\.datePickerStyle, style) + } + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 238a97bdc1..4d4139cb2a 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -239,6 +239,26 @@ final class SliderWidget: WrapperWidget { } } +@available(tvOS, unavailable) +final class DatePickerWidget: WrapperWidget { + var onChange: ((Date) -> Void)? { + didSet { + if oldValue == nil { + child.addTarget(self, action: #selector(dateChanged), for: .valueChanged) + } + } + } + + @objc + func dateChanged(sender: UIDatePicker) { + onChange?(sender.date) + } + + override var intrinsicContentSize: CGSize { + return child.sizeThatFits(UIView.layoutFittingCompressedSize) + } +} + extension UIKitBackend { public func createButton() -> Widget { ButtonWidget() @@ -501,5 +521,58 @@ extension UIKitBackend { let sliderWidget = slider as! SliderWidget sliderWidget.child.setValue(Float(value), animated: true) } + + public func createDatePicker() -> Widget { + DatePickerWidget() + } + + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + let datePickerWidget = datePicker as! DatePickerWidget + + datePickerWidget.child.date = date + datePickerWidget.onChange = onChange + + datePickerWidget.child.calendar = environment.calendar + datePickerWidget.child.timeZone = environment.timeZone + datePickerWidget.child.minimumDate = range.lowerBound + datePickerWidget.child.maximumDate = range.upperBound + + datePickerWidget.child.datePickerMode = + switch components { + case [.date, .hourAndMinute]: + .dateAndTime + case .date: + .date + case .hourAndMinute: + .time + default: + // Crashing upon receiving [] is consistent with SwiftUI. + fatalError("Unexpected Components: \(components)") + } + + if #available(iOS 13.4, macCatalyst 13.4, *) { + switch environment.datePickerStyle { + case .automatic: + datePickerWidget.child.preferredDatePickerStyle = .automatic + case .compact: + datePickerWidget.child.preferredDatePickerStyle = .compact + case .graphical: + guard #available(iOS 14, macCatalyst 14, *) else { + preconditionFailure( + "DatePickerStyle.graphical is only available on iOS 14 or newer") + } + datePickerWidget.child.preferredDatePickerStyle = .inline + case .wheel: + datePickerWidget.child.preferredDatePickerStyle = .wheels + } + } + } #endif } From 2b757de9002f6c6a82d9b021cd0de4a50c5e334d Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 31 Oct 2025 22:30:22 -0400 Subject: [PATCH 02/27] Add DatePicker for AppKitBackend --- .../DatePickerExample/DatePickerApp.swift | 2 +- Sources/AppKitBackend/AppKitBackend.swift | 74 +++++++++++++++++++ .../UIKitBackend/UIKitBackend+Control.swift | 1 + 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/Examples/Sources/DatePickerExample/DatePickerApp.swift b/Examples/Sources/DatePickerExample/DatePickerApp.swift index 4047b67445..e8392b0d78 100644 --- a/Examples/Sources/DatePickerExample/DatePickerApp.swift +++ b/Examples/Sources/DatePickerExample/DatePickerApp.swift @@ -10,7 +10,7 @@ import SwiftCrossUI @HotReloadable struct DatePickerApp: App { @State var date = Date() - @State var style: DatePickerStyle? = DatePickerStyle.automatic + @State var style: DatePickerStyle? = .automatic var allStyles: [DatePickerStyle] diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index f0f3fa4817..feeaf27777 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1689,6 +1689,64 @@ public final class AppKitBackend: AppBackend { let request = URLRequest(url: url) webView.load(request) } + + public func createDatePicker() -> NSView { + let datePicker = CustomDatePicker() + datePicker.delegate = datePicker.strongDelegate + return datePicker + } + + public func updateDatePicker( + _ datePicker: NSView, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + let datePicker = datePicker as! CustomDatePicker + + datePicker.isEnabled = environment.isEnabled + datePicker.textColor = environment.suggestedForegroundColor.nsColor + + // If the time zone is set to autoupdatingCurrent, then the cursor position is reset after + // every keystroke. Thanks Apple + datePicker.timeZone = + environment.timeZone == .autoupdatingCurrent ? .current : environment.timeZone + // Changing the calendar also resets the cursor position, so avoid triggering the setter when possible + if datePicker.calendar != environment.calendar { + datePicker.calendar = environment.calendar + } + + datePicker.dateValue = date + datePicker.strongDelegate.onChange = onChange + + datePicker.minDate = range.lowerBound + datePicker.maxDate = range.upperBound + + datePicker.datePickerStyle = + switch environment.datePickerStyle { + case .automatic, .compact: + .textFieldAndStepper + case .graphical: + .clockAndCalendar + } + + var elementFlags: NSDatePicker.ElementFlags = [] + if components.contains(.date) { + elementFlags.insert(.yearMonthDay) + + // Trying to add .era when the calendar doesn't support it prevents input from occurring at all. + // FIXME: This works fine for Japanese but locks up for Gregorian + // elementFlags.insert(.era) + } + if components.contains(.hourMinuteAndSecond) { + elementFlags.insert(.hourMinuteSecond) + } else { + elementFlags.insert(.hourMinute) + } + datePicker.datePickerElements = elementFlags + } } final class NSCustomTapGestureTarget: NSView { @@ -2191,3 +2249,19 @@ final class CustomWKNavigationDelegate: NSObject, WKNavigationDelegate { onNavigate?(url) } } + +final class CustomDatePicker: NSDatePicker { + var strongDelegate = CustomDatePickerDelegate() +} + +final class CustomDatePickerDelegate: NSObject, NSDatePickerCellDelegate { + var onChange: ((Date) -> Void)? + + func datePickerCell( + _: NSDatePickerCell, + validateProposedDateValue proposedDateValue: AutoreleasingUnsafeMutablePointer, + timeInterval _: UnsafeMutablePointer? + ) { + onChange?(proposedDateValue.pointee as Date) + } +} diff --git a/Sources/UIKitBackend/UIKitBackend+Control.swift b/Sources/UIKitBackend/UIKitBackend+Control.swift index 4d4139cb2a..6c31e865e4 100644 --- a/Sources/UIKitBackend/UIKitBackend+Control.swift +++ b/Sources/UIKitBackend/UIKitBackend+Control.swift @@ -539,6 +539,7 @@ extension UIKitBackend { datePickerWidget.child.date = date datePickerWidget.onChange = onChange + datePickerWidget.child.isEnabled = environment.isEnabled datePickerWidget.child.calendar = environment.calendar datePickerWidget.child.timeZone = environment.timeZone datePickerWidget.child.minimumDate = range.lowerBound From 3f1bfcb9a900fad9ca71156f156405724531b770 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 31 Oct 2025 22:31:21 -0400 Subject: [PATCH 03/27] Add DatePickerExample to macOS CI --- .github/workflows/build-test-and-docs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test-and-docs.yml b/.github/workflows/build-test-and-docs.yml index 0dfa5ccb80..5558e9865a 100644 --- a/.github/workflows/build-test-and-docs.yml +++ b/.github/workflows/build-test-and-docs.yml @@ -52,7 +52,8 @@ jobs: swift build --target SpreadsheetExample && \ swift build --target NotesExample && \ swift build --target GtkExample && \ - swift build --target PathsExample + swift build --target PathsExample && \ + swift build --target DatePickerExample - name: Test run: swift test --test-product swift-cross-uiPackageTests From fd0172516c212703eadcc87e5210d660b9335762 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 31 Oct 2025 23:35:11 -0400 Subject: [PATCH 04/27] Fix DatePicker update logic for AppKitBackend --- Sources/AppKitBackend/AppKitBackend.swift | 50 +++++++++++++++-------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index feeaf27777..37f62169dd 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1696,6 +1696,15 @@ public final class AppKitBackend: AppBackend { return datePicker } + // Depending on the calendar, era is either necessary or must be omitted. Making the wrong + // choice for the current calendar means the cursor position is reset after every keystroke. I + // know of no simple way to tell whether NSDatePicker requires or forbids eras for a given + // calendar, so in lieu of that I have hardcoded the calendar identifiers. It's shorter to list + // the calendars that forbid eras than the calendars that require them. + private let calendarsWithoutEras: Set = [ + .chinese, .gregorian, .hebrew, .iso8601, + ] + public func updateDatePicker( _ datePicker: NSView, environment: EnvironmentValues, @@ -1713,12 +1722,34 @@ public final class AppKitBackend: AppBackend { // every keystroke. Thanks Apple datePicker.timeZone = environment.timeZone == .autoupdatingCurrent ? .current : environment.timeZone - // Changing the calendar also resets the cursor position, so avoid triggering the setter when possible + + // A couple properties cause infinite update loops if we assign to them on every update, so + // check their values first. if datePicker.calendar != environment.calendar { datePicker.calendar = environment.calendar } - datePicker.dateValue = date + if datePicker.dateValue != date { + datePicker.dateValue = date + } + + var elementFlags: NSDatePicker.ElementFlags = [] + if components.contains(.date) { + elementFlags.insert(.yearMonthDay) + if !calendarsWithoutEras.contains(environment.calendar.identifier) { + elementFlags.insert(.era) + } + } + if components.contains(.hourMinuteAndSecond) { + elementFlags.insert(.hourMinuteSecond) + } else { + elementFlags.insert(.hourMinute) + } + + if datePicker.datePickerElements != elementFlags { + datePicker.datePickerElements = elementFlags + } + datePicker.strongDelegate.onChange = onChange datePicker.minDate = range.lowerBound @@ -1731,21 +1762,6 @@ public final class AppKitBackend: AppBackend { case .graphical: .clockAndCalendar } - - var elementFlags: NSDatePicker.ElementFlags = [] - if components.contains(.date) { - elementFlags.insert(.yearMonthDay) - - // Trying to add .era when the calendar doesn't support it prevents input from occurring at all. - // FIXME: This works fine for Japanese but locks up for Gregorian - // elementFlags.insert(.era) - } - if components.contains(.hourMinuteAndSecond) { - elementFlags.insert(.hourMinuteSecond) - } else { - elementFlags.insert(.hourMinute) - } - datePicker.datePickerElements = elementFlags } } From 51066382640a0925d5d1f4c8466ae3926e51075b Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 31 Oct 2025 23:47:19 -0400 Subject: [PATCH 05/27] Update argument name to match SwiftUI --- Sources/SwiftCrossUI/Views/DatePicker.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/DatePicker.swift b/Sources/SwiftCrossUI/Views/DatePicker.swift index 44330ffcb2..c44d8929bb 100644 --- a/Sources/SwiftCrossUI/Views/DatePicker.swift +++ b/Sources/SwiftCrossUI/Views/DatePicker.swift @@ -47,25 +47,25 @@ public struct DatePicker { public nonisolated init( selection: Binding, range: ClosedRange = Date.distantPast...Date.distantFuture, - components: DatePickerComponents = [.hourAndMinute, .date], + displayedComponents: DatePickerComponents = [.hourAndMinute, .date], @ViewBuilder label: () -> Label ) { self.label = label() self.selection = selection self.range = range - self.components = components + self.components = displayedComponents } public nonisolated init( _ label: String, selection: Binding, range: ClosedRange = Date.distantPast...Date.distantFuture, - components: DatePickerComponents = [.hourAndMinute, .date] + displayedComponents: DatePickerComponents = [.hourAndMinute, .date] ) where Label == Text { self.label = Text(label) self.selection = selection self.range = range - self.components = components + self.components = displayedComponents } public typealias Components = DatePickerComponents From 1d4073dbce0c584d81712adb18e6b76f28207859 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 31 Oct 2025 23:49:36 -0400 Subject: [PATCH 06/27] Add more availability annotations --- Sources/SwiftCrossUI/Backend/AppBackend.swift | 27 +++++++++++-------- Sources/SwiftCrossUI/Views/DatePicker.swift | 2 ++ 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index c06f43a873..d3caeaf8a1 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -538,6 +538,19 @@ public protocol AppBackend: Sendable { /// Sets the index of the selected option of a picker. func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) + @available(tvOS, unavailable) + func createDatePicker() -> Widget + + @available(tvOS, unavailable) + func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) + /// Creates an indeterminate progress spinner. func createProgressSpinner() -> Widget @@ -705,17 +718,6 @@ public protocol AppBackend: Sendable { ) /// Navigates a web view to a given URL. func navigateWebView(_ webView: Widget, to url: URL) - - // MARK: Date picker - func createDatePicker() -> Widget - func updateDatePicker( - _ datePicker: Widget, - environment: EnvironmentValues, - date: Date, - range: ClosedRange, - components: DatePickerComponents, - onChange: @escaping (Date) -> Void - ) } extension AppBackend { @@ -1174,7 +1176,10 @@ extension AppBackend { todo() } + @available(tvOS, unavailable) public func createDatePicker() -> Widget { todo() } + + @available(tvOS, unavailable) public func updateDatePicker( _ datePicker: Widget, environment: EnvironmentValues, diff --git a/Sources/SwiftCrossUI/Views/DatePicker.swift b/Sources/SwiftCrossUI/Views/DatePicker.swift index c44d8929bb..763d0c6b38 100644 --- a/Sources/SwiftCrossUI/Views/DatePicker.swift +++ b/Sources/SwiftCrossUI/Views/DatePicker.swift @@ -71,6 +71,7 @@ public struct DatePicker { public typealias Components = DatePickerComponents } +@available(tvOS, unavailable) extension DatePicker: View { public var body: some View { HStack { @@ -81,6 +82,7 @@ extension DatePicker: View { } } +@available(tvOS, unavailable) internal struct DatePickerImplementation: ElementaryView { @Binding private var selection: Date private var range: ClosedRange From 9823cf1cdc5bd9a52c8bdf2cffe04949be89cf21 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 31 Oct 2025 23:53:57 -0400 Subject: [PATCH 07/27] Shut up tvOS let me see if the iOS CI will pass --- .../Views/Modifiers/DatePickerStyleModifier.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift index 89a6858772..d3971d69f1 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift @@ -1,8 +1,12 @@ extension View { @available(tvOS, unavailable) public func datePickerStyle(_ style: DatePickerStyle) -> some View { - EnvironmentModifier(self) { environment in - environment.with(\.datePickerStyle, style) - } + #if os(tvOS) + preconditionFailure() + #else + EnvironmentModifier(self) { environment in + environment.with(\.datePickerStyle, style) + } + #endif } } From 6f276fe30a586e7b5b06a5b0ba99f363d9dba7fc Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Sat, 1 Nov 2025 00:01:36 -0400 Subject: [PATCH 08/27] please work. --- Sources/SwiftCrossUI/Backend/AppBackend.swift | 40 +++++++++---------- Sources/SwiftCrossUI/Views/DatePicker.swift | 38 ++++++++++-------- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Sources/SwiftCrossUI/Backend/AppBackend.swift b/Sources/SwiftCrossUI/Backend/AppBackend.swift index d3caeaf8a1..f0fede9cc0 100644 --- a/Sources/SwiftCrossUI/Backend/AppBackend.swift +++ b/Sources/SwiftCrossUI/Backend/AppBackend.swift @@ -538,18 +538,18 @@ public protocol AppBackend: Sendable { /// Sets the index of the selected option of a picker. func setSelectedOption(ofPicker picker: Widget, to selectedOption: Int?) - @available(tvOS, unavailable) func createDatePicker() -> Widget - @available(tvOS, unavailable) - func updateDatePicker( - _ datePicker: Widget, - environment: EnvironmentValues, - date: Date, - range: ClosedRange, - components: DatePickerComponents, - onChange: @escaping (Date) -> Void - ) + #if !os(tvOS) + func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) + #endif /// Creates an indeterminate progress spinner. func createProgressSpinner() -> Widget @@ -1176,16 +1176,16 @@ extension AppBackend { todo() } - @available(tvOS, unavailable) public func createDatePicker() -> Widget { todo() } - @available(tvOS, unavailable) - public func updateDatePicker( - _ datePicker: Widget, - environment: EnvironmentValues, - date: Date, - range: ClosedRange, - components: DatePickerComponents, - onChange: @escaping (Date) -> Void - ) { todo() } + #if !os(tvOS) + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { todo() } + #endif } diff --git a/Sources/SwiftCrossUI/Views/DatePicker.swift b/Sources/SwiftCrossUI/Views/DatePicker.swift index 763d0c6b38..44979575a2 100644 --- a/Sources/SwiftCrossUI/Views/DatePicker.swift +++ b/Sources/SwiftCrossUI/Views/DatePicker.swift @@ -107,22 +107,26 @@ internal struct DatePickerImplementation: ElementaryView { backend: Backend, dryRun: Bool ) -> ViewUpdateResult { - if !dryRun { - backend.updateDatePicker( - widget, - environment: environment, - date: selection, - range: range, - components: components, - onChange: { selection = $0 } - ) - } - - // I reject your proposedSize and substitute my own - let naturalSize = backend.naturalSize(of: widget) - if !dryRun { - backend.setSize(of: widget, to: naturalSize) - } - return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize)) + #if os(tvOS) + preconditionFailure() + #else + if !dryRun { + backend.updateDatePicker( + widget, + environment: environment, + date: selection, + range: range, + components: components, + onChange: { selection = $0 } + ) + } + + // I reject your proposedSize and substitute my own + let naturalSize = backend.naturalSize(of: widget) + if !dryRun { + backend.setSize(of: widget, to: naturalSize) + } + return ViewUpdateResult.leafView(size: ViewSize(fixedSize: naturalSize)) + #endif } } From a5392299bc8604f65bceaf73c346b651eb7f41a8 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Sat, 1 Nov 2025 00:07:26 -0400 Subject: [PATCH 09/27] Fine, here's your view --- .../SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift index d3971d69f1..6b1407da13 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/DatePickerStyleModifier.swift @@ -2,7 +2,8 @@ extension View { @available(tvOS, unavailable) public func datePickerStyle(_ style: DatePickerStyle) -> some View { #if os(tvOS) - preconditionFailure() + assertionFailure() + return EmptyView() #else EnvironmentModifier(self) { environment in environment.with(\.datePickerStyle, style) From 096cd5850234bbe38d45601128a8317501d3e6a5 Mon Sep 17 00:00:00 2001 From: bbrk24 Date: Sun, 2 Nov 2025 13:44:27 -0500 Subject: [PATCH 10/27] Initial WinUI implementation --- Examples/Package.resolved | 12 +- .../DatePickerExample/DatePickerApp.swift | 2 +- Package.resolved | 12 +- Package.swift | 4 +- Sources/WinUIBackend/WinUIBackend.swift | 297 ++++++++++++++++++ 5 files changed, 312 insertions(+), 15 deletions(-) diff --git a/Examples/Package.resolved b/Examples/Package.resolved index 3842945e2a..a0d340fafb 100644 --- a/Examples/Package.resolved +++ b/Examples/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "6977ba851e440a7fbdfc7cb46441e32853dc2ba48ba34fe702e6784699d08682", + "originHash" : "e48163963f1b0c0a4a505d749632d1c23f3997e66caf3ede5961e2d8b49fd2bb", "pins" : [ { "identity" : "aexml", @@ -283,7 +283,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-uwp", "state" : { - "revision" : "e3ff9c195775e16b404b82cf6886c5e81d73b6c1" + "revision" : "8128f6615b7c5b46ada289ab6d49d871ca1e13a5" } }, { @@ -291,7 +291,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-webview2core", "state" : { - "revision" : "c9911ca23455b9fcdb2429e98baa6f4d003b381c" + "revision" : "4396f5d94d6dfd1f95ab25e79de98141b7f4f183" } }, { @@ -299,7 +299,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-windowsappsdk", "state" : { - "revision" : "ba6f0ec377b70d8be835d253102ff665a0e47d99" + "revision" : "f1c50892f10c0f7f635d3c7a3d728fd634ad001a" } }, { @@ -315,7 +315,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-winui", "state" : { - "revision" : "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86" + "revision" : "42c47f4e4129c8b5a5d9912f05e1168c924ac180" } }, { @@ -401,4 +401,4 @@ } ], "version" : 3 -} +} \ No newline at end of file diff --git a/Examples/Sources/DatePickerExample/DatePickerApp.swift b/Examples/Sources/DatePickerExample/DatePickerApp.swift index e8392b0d78..8ca80119f0 100644 --- a/Examples/Sources/DatePickerExample/DatePickerApp.swift +++ b/Examples/Sources/DatePickerExample/DatePickerApp.swift @@ -23,7 +23,7 @@ struct DatePickerApp: App { if #available(iOS 13.4, macCatalyst 13.4, *) { allStyles.append(.compact) - #if os(iOS) || os(visionOS) + #if os(iOS) || os(visionOS) || canImport(WinUIBackend) allStyles.append(.wheel) #endif } diff --git a/Package.resolved b/Package.resolved index 173c2d3001..44f5f7caa3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3b87bbc3d0f0110380f592dc86a1c8c65c20f5a326f484bdbe2f6ef5e357840d", + "originHash" : "f2f19baaeb2fc5d982c6991eea69319a9a441fcad3461d5899e9f29943737a99", "pins" : [ { "identity" : "jpeg", @@ -86,7 +86,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-uwp", "state" : { - "revision" : "e3ff9c195775e16b404b82cf6886c5e81d73b6c1" + "revision" : "8128f6615b7c5b46ada289ab6d49d871ca1e13a5" } }, { @@ -94,7 +94,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-webview2core", "state" : { - "revision" : "c9911ca23455b9fcdb2429e98baa6f4d003b381c" + "revision" : "4396f5d94d6dfd1f95ab25e79de98141b7f4f183" } }, { @@ -102,7 +102,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-windowsappsdk", "state" : { - "revision" : "ba6f0ec377b70d8be835d253102ff665a0e47d99" + "revision" : "f1c50892f10c0f7f635d3c7a3d728fd634ad001a" } }, { @@ -118,7 +118,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stackotter/swift-winui", "state" : { - "revision" : "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86" + "revision" : "42c47f4e4129c8b5a5d9912f05e1168c924ac180" } }, { @@ -141,4 +141,4 @@ } ], "version" : 3 -} +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index d854305ca1..0636fef00f 100644 --- a/Package.swift +++ b/Package.swift @@ -100,7 +100,7 @@ let package = Package( ), .package( url: "https://github.com/stackotter/swift-windowsappsdk", - revision: "ba6f0ec377b70d8be835d253102ff665a0e47d99" + revision: "f1c50892f10c0f7f635d3c7a3d728fd634ad001a" ), .package( url: "https://github.com/stackotter/swift-windowsfoundation", @@ -108,7 +108,7 @@ let package = Package( ), .package( url: "https://github.com/stackotter/swift-winui", - revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86" + revision: "42c47f4e4129c8b5a5d9912f05e1168c924ac180" ), // .package( // url: "https://github.com/stackotter/TermKit", diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 882285118a..15013bf3e3 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -463,6 +463,8 @@ public final class WinUIBackend: AppBackend { // the defaults set in the following code from the WinUI repository: // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/ProgressRing/ProgressRing.xaml#L12 return SIMD2(32, 32) + } else if let datePicker = widget as? CustomDatePicker { + return datePicker.naturalSize(in: self) } let oldWidth = widget.width @@ -1704,6 +1706,51 @@ public final class WinUIBackend: AppBackend { winUiPath.data = path.group } + public func createDatePicker() -> Widget { + return CustomDatePicker() + } + + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + let customDatePicker = datePicker as! CustomDatePicker + + if components.contains(.hourMinuteAndSecond) { + print( + "DatePickerComponents.hourMinuteAndSecond is not supported in WinUIBackend. Falling back to .hourAndMinute." + ) + } + + customDatePicker.toggleTimeView(shown: components.contains(.hourAndMinute)) + + if environment.timeZone != .autoupdatingCurrent { + print("environment.timeZone is has no effect in WinUIBackend.") + } + + let dateViewType: CustomDatePicker.DateViewType.Discriminator? = + if components.contains(.date) { + switch environment.datePickerStyle { + case .automatic, .wheel: + .datePicker + case .compact: + .calendarDatePicker + case .graphical: + .calendarView + } + } else { nil } + + customDatePicker.onChange = onChange + customDatePicker.changeDateView(to: dateViewType) + customDatePicker.updateIfNeeded(date: date, calendar: environment.calendar) + customDatePicker.setDateRange(to: range) + customDatePicker.setEnabled(to: environment.isEnabled) + } + // public func createTable(rows: Int, columns: Int) -> Widget { // let grid = Grid() // grid.columnSpacing = 10 @@ -1940,3 +1987,253 @@ public final class GeometryGroupHolder { var group = GeometryGroup() var strokeStyle: StrokeStyle? } + +@MainActor +final class CustomDatePicker: StackPanel { + override init() { + super.init() + self.spacing = 10 + } + + deinit { + timeChangedEvent?.dispose() + dateChangedEvent?.dispose() + } + + enum DateViewType { + case calendarView(CalendarView) + case calendarDatePicker(CalendarDatePicker) + case datePicker(WinUI.DatePicker) + + var asControl: Control { + switch self { + case .calendarView(let calendarView): calendarView + case .calendarDatePicker(let calendarDatePicker): calendarDatePicker + case .datePicker(let datePicker): datePicker + } + } + + enum Discriminator { + case calendarView, calendarDatePicker, datePicker + } + + var discriminator: Discriminator { + switch self { + case .calendarView(_): .calendarView + case .calendarDatePicker(_): .calendarDatePicker + case .datePicker(_): .datePicker + } + } + } + + private var dateView: DateViewType? + private var timeView: TimePicker? + private var date = Date() + private var calendar = Calendar.current + private var needsUpdate = false + var onChange: ((Date) -> Void)? + private var timeChangedEvent: EventCleanup? + private var dateChangedEvent: EventCleanup? + + func toggleTimeView(shown: Bool) { + guard shown != (self.timeView != nil) else { return } + + if shown { + let timeView = TimePicker() + children.append(timeView) + self.timeView = timeView + timeChangedEvent = timeView.timeChanged.addHandler { [unowned self] _, change in + guard let change else { return } + self.date = calendar.startOfDay(for: date) + Double(change.newTime.duration) / ticksPerSecond + self.onChange?(self.date) + } + needsUpdate = true + } else { + timeChangedEvent?.dispose() + timeChangedEvent = nil + children.removeAtEnd() + self.timeView = nil + } + } + + func setEnabled(to isEnabled: Bool) { + dateView?.asControl.isEnabled = isEnabled + timeView?.isEnabled = isEnabled + } + + func changeDateView(to newDiscriminator: DateViewType.Discriminator?) { + guard newDiscriminator != dateView?.discriminator else { return } + + dateChangedEvent?.dispose() + if dateView != nil { + children.removeAt(0) + } + + switch newDiscriminator { + case .calendarView: + let calendarView = CalendarView() + dateView = .calendarView(calendarView) + children.insertAt(0, calendarView) + orientation = .vertical + dateChangedEvent = calendarView.selectedDatesChanged.addHandler { [unowned self] _, _ in + guard calendarView.selectedDates.size > 0 else { return } + + self.date = componentsToFoundationDate(dateTime: calendarView.selectedDates.getAt(0), timeSpan: timeView?.selectedTime) + + if calendarView.selectedDates.size > 1 { + self.needsUpdate = true + } + + self.onChange?(self.date) + } + needsUpdate = true + case .calendarDatePicker: + let calendarDatePicker = CalendarDatePicker() + dateView = .calendarDatePicker(calendarDatePicker) + children.insertAt(0, calendarDatePicker) + orientation = .horizontal + dateChangedEvent = calendarDatePicker.dateChanged.addHandler { [unowned self] _, change in + guard let newDate = change?.newDate else { return } + self.date = componentsToFoundationDate(dateTime: newDate, timeSpan: timeView?.selectedTime) + self.onChange?(self.date) + } + needsUpdate = true + case .datePicker: + let datePicker = WinUI.DatePicker() + dateView = .datePicker(datePicker) + children.insertAt(0, datePicker) + orientation = .horizontal + dateChangedEvent = datePicker.selectedDateChanged.addHandler { [unowned self] _, _ in + guard let selectedDate = datePicker.selectedDate else { return } + self.date = componentsToFoundationDate(dateTime: selectedDate, timeSpan: timeView?.selectedTime) + self.onChange?(self.date) + } + needsUpdate = true + case nil: + break + } + } + + func setDateRange(to range: ClosedRange) { + guard let dateView else { return } + + let (startDate, _) = foundationDateToComponents(range.lowerBound) + let (endDate, _) = foundationDateToComponents(range.upperBound) + + switch dateView { + case .calendarView(let calendarView): + calendarView.minDate = startDate + calendarView.maxDate = endDate + case .calendarDatePicker(let calendarDatePicker): + calendarDatePicker.minDate = startDate + calendarDatePicker.maxDate = endDate + case .datePicker(let datePicker): + // FIXME: For some reason it says these properties don't exist? + // datePicker.displayDateStart = startDate + // datePicker.displayDateEnd = endDate + break + } + } + + func updateIfNeeded(date: Date, calendar: Calendar) { + if !needsUpdate && date == self.date && calendar == self.calendar { return } + defer { needsUpdate = false } + + self.date = date + self.calendar = calendar + + let (dateTime, timeSpan) = foundationDateToComponents(date) + + switch dateView { + case .calendarView(let calendarView): + calendarView.calendarIdentifier = identifier(for: calendar) + switch calendarView.selectedDates.size { + case 0: + calendarView.selectedDates.append(dateTime) + case 1: + calendarView.selectedDates.setAt(0, dateTime) + default: + calendarView.selectedDates.clear() + calendarView.selectedDates.setAt(0, dateTime) + } + case .calendarDatePicker(let calendarDatePicker): + calendarDatePicker.calendarIdentifier = identifier(for: calendar) + calendarDatePicker.date = dateTime + case .datePicker(let datePicker): + datePicker.selectedDate = dateTime + case nil: + break + } + + if let timeView { + timeView.selectedTime = timeSpan + } + } + + private func identifier(for calendar: Calendar) -> String { + switch calendar.identifier { + case .chinese: "ChineseLunarCalendar" + case .gregorian, .iso8601: "GregorianCalendar" + case .hebrew: "HebrewCalendar" + case .islamicTabular: "HijriCalendar" + case .islamicUmmAlQura: "UmAlQuraCalendar" + case .japanese: "JapaneseCalendar" + case .persian: "PersianCalendar" + case .republicOfChina: "TaiwanCalendar" + case let id: fatalError("Unsupported calendar identifier \(id)") + } + } + + // Magic numbers taken from https://stackoverflow.com/a/5471380/6253337 + private let ticksPerSecond: Double = 10000000 + private let unixEpochInUniversalTime: Int64 = 116444736000000000 + + private func foundationDateToComponents(_ date: Date) -> (DateTime, TimeSpan) { + let timeInterval = date.timeIntervalSince(calendar.startOfDay(for: date)) + + return ( + DateTime(universalTime: Int64(date.timeIntervalSince1970 * ticksPerSecond + Double(unixEpochInUniversalTime))), + TimeSpan(duration: Int64(timeInterval * ticksPerSecond)) + ) + } + + private func componentsToFoundationDate(dateTime: DateTime, timeSpan: TimeSpan?) -> Date { + let baseDate = Date(timeIntervalSince1970: Double(dateTime.universalTime - unixEpochInUniversalTime) / ticksPerSecond) + + if let timeSpan { + let time = Double(timeSpan.duration) / ticksPerSecond + return calendar.startOfDay(for: baseDate) + time + } else { + return baseDate + } + } + + func naturalSize(in backend: WinUIBackend) -> SIMD2 { + // FIXME: TimePicker and DatePicker both report their natural size as 0 x 0 before initial render + // FIXME: CalendarView reports its natural size incorrectly + + let timeViewSize = if let timeView { + backend.naturalSize(of: timeView) + } else { + SIMD2.zero + } + + let dateViewSize = if let dateControl = dateView?.asControl { + backend.naturalSize(of: dateControl) + } else { + SIMD2.zero + } + + if orientation == .horizontal { + return SIMD2( + x: timeViewSize.x + dateViewSize.x + Int(self.spacing), + y: max(timeViewSize.y, dateViewSize.y) + ) + } else { + return SIMD2( + x: max(timeViewSize.x, dateViewSize.x), + y: timeViewSize.y + dateViewSize.y + Int(self.spacing) + ) + } + } +} From d019996f92727f1c78722d3a5a67e683b3d9ce82 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Sun, 2 Nov 2025 13:48:11 -0500 Subject: [PATCH 11/27] Reformat WinUI code --- Sources/WinUIBackend/WinUIBackend.swift | 221 +++++++++++++----------- 1 file changed, 121 insertions(+), 100 deletions(-) diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 15013bf3e3..1d8e14a13f 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -1733,16 +1733,18 @@ public final class WinUIBackend: AppBackend { } let dateViewType: CustomDatePicker.DateViewType.Discriminator? = - if components.contains(.date) { - switch environment.datePickerStyle { - case .automatic, .wheel: - .datePicker - case .compact: - .calendarDatePicker - case .graphical: - .calendarView + if components.contains(.date) { + switch environment.datePickerStyle { + case .automatic, .wheel: + .datePicker + case .compact: + .calendarDatePicker + case .graphical: + .calendarView + } + } else { + nil } - } else { nil } customDatePicker.onChange = onChange customDatePicker.changeDateView(to: dateViewType) @@ -2007,9 +2009,9 @@ final class CustomDatePicker: StackPanel { var asControl: Control { switch self { - case .calendarView(let calendarView): calendarView - case .calendarDatePicker(let calendarDatePicker): calendarDatePicker - case .datePicker(let datePicker): datePicker + case .calendarView(let calendarView): calendarView + case .calendarDatePicker(let calendarDatePicker): calendarDatePicker + case .datePicker(let datePicker): datePicker } } @@ -2019,9 +2021,9 @@ final class CustomDatePicker: StackPanel { var discriminator: Discriminator { switch self { - case .calendarView(_): .calendarView - case .calendarDatePicker(_): .calendarDatePicker - case .datePicker(_): .datePicker + case .calendarView(_): .calendarView + case .calendarDatePicker(_): .calendarDatePicker + case .datePicker(_): .datePicker } } } @@ -2044,7 +2046,9 @@ final class CustomDatePicker: StackPanel { self.timeView = timeView timeChangedEvent = timeView.timeChanged.addHandler { [unowned self] _, change in guard let change else { return } - self.date = calendar.startOfDay(for: date) + Double(change.newTime.duration) / ticksPerSecond + self.date = + calendar.startOfDay(for: date) + + Double(change.newTime.duration) / ticksPerSecond self.onChange?(self.date) } needsUpdate = true @@ -2070,47 +2074,57 @@ final class CustomDatePicker: StackPanel { } switch newDiscriminator { - case .calendarView: - let calendarView = CalendarView() - dateView = .calendarView(calendarView) - children.insertAt(0, calendarView) - orientation = .vertical - dateChangedEvent = calendarView.selectedDatesChanged.addHandler { [unowned self] _, _ in - guard calendarView.selectedDates.size > 0 else { return } - - self.date = componentsToFoundationDate(dateTime: calendarView.selectedDates.getAt(0), timeSpan: timeView?.selectedTime) - - if calendarView.selectedDates.size > 1 { - self.needsUpdate = true - } + case .calendarView: + let calendarView = CalendarView() + dateView = .calendarView(calendarView) + children.insertAt(0, calendarView) + orientation = .vertical + dateChangedEvent = calendarView.selectedDatesChanged.addHandler { + [unowned self] _, _ in + + guard calendarView.selectedDates.size > 0 else { return } + + self.date = componentsToFoundationDate( + dateTime: calendarView.selectedDates.getAt(0), + timeSpan: timeView?.selectedTime) + + if calendarView.selectedDates.size > 1 { + self.needsUpdate = true + } - self.onChange?(self.date) - } - needsUpdate = true - case .calendarDatePicker: - let calendarDatePicker = CalendarDatePicker() - dateView = .calendarDatePicker(calendarDatePicker) - children.insertAt(0, calendarDatePicker) - orientation = .horizontal - dateChangedEvent = calendarDatePicker.dateChanged.addHandler { [unowned self] _, change in - guard let newDate = change?.newDate else { return } - self.date = componentsToFoundationDate(dateTime: newDate, timeSpan: timeView?.selectedTime) - self.onChange?(self.date) - } - needsUpdate = true - case .datePicker: - let datePicker = WinUI.DatePicker() - dateView = .datePicker(datePicker) - children.insertAt(0, datePicker) - orientation = .horizontal - dateChangedEvent = datePicker.selectedDateChanged.addHandler { [unowned self] _, _ in - guard let selectedDate = datePicker.selectedDate else { return } - self.date = componentsToFoundationDate(dateTime: selectedDate, timeSpan: timeView?.selectedTime) - self.onChange?(self.date) - } - needsUpdate = true - case nil: - break + self.onChange?(self.date) + } + needsUpdate = true + case .calendarDatePicker: + let calendarDatePicker = CalendarDatePicker() + dateView = .calendarDatePicker(calendarDatePicker) + children.insertAt(0, calendarDatePicker) + orientation = .horizontal + dateChangedEvent = calendarDatePicker.dateChanged.addHandler { + [unowned self] _, change in + + guard let newDate = change?.newDate else { return } + self.date = componentsToFoundationDate( + dateTime: newDate, timeSpan: timeView?.selectedTime) + self.onChange?(self.date) + } + needsUpdate = true + case .datePicker: + let datePicker = WinUI.DatePicker() + dateView = .datePicker(datePicker) + children.insertAt(0, datePicker) + orientation = .horizontal + dateChangedEvent = datePicker.selectedDateChanged.addHandler { + [unowned self] _, _ in + + guard let selectedDate = datePicker.selectedDate else { return } + self.date = componentsToFoundationDate( + dateTime: selectedDate, timeSpan: timeView?.selectedTime) + self.onChange?(self.date) + } + needsUpdate = true + case nil: + break } } @@ -2121,17 +2135,17 @@ final class CustomDatePicker: StackPanel { let (endDate, _) = foundationDateToComponents(range.upperBound) switch dateView { - case .calendarView(let calendarView): - calendarView.minDate = startDate - calendarView.maxDate = endDate - case .calendarDatePicker(let calendarDatePicker): - calendarDatePicker.minDate = startDate - calendarDatePicker.maxDate = endDate - case .datePicker(let datePicker): - // FIXME: For some reason it says these properties don't exist? - // datePicker.displayDateStart = startDate - // datePicker.displayDateEnd = endDate - break + case .calendarView(let calendarView): + calendarView.minDate = startDate + calendarView.maxDate = endDate + case .calendarDatePicker(let calendarDatePicker): + calendarDatePicker.minDate = startDate + calendarDatePicker.maxDate = endDate + case .datePicker(let datePicker): + // FIXME: For some reason it says these properties don't exist? + // datePicker.displayDateStart = startDate + // datePicker.displayDateEnd = endDate + break } } @@ -2145,24 +2159,24 @@ final class CustomDatePicker: StackPanel { let (dateTime, timeSpan) = foundationDateToComponents(date) switch dateView { - case .calendarView(let calendarView): - calendarView.calendarIdentifier = identifier(for: calendar) - switch calendarView.selectedDates.size { - case 0: - calendarView.selectedDates.append(dateTime) - case 1: - calendarView.selectedDates.setAt(0, dateTime) - default: - calendarView.selectedDates.clear() - calendarView.selectedDates.setAt(0, dateTime) - } - case .calendarDatePicker(let calendarDatePicker): - calendarDatePicker.calendarIdentifier = identifier(for: calendar) - calendarDatePicker.date = dateTime - case .datePicker(let datePicker): - datePicker.selectedDate = dateTime - case nil: - break + case .calendarView(let calendarView): + calendarView.calendarIdentifier = identifier(for: calendar) + switch calendarView.selectedDates.size { + case 0: + calendarView.selectedDates.append(dateTime) + case 1: + calendarView.selectedDates.setAt(0, dateTime) + default: + calendarView.selectedDates.clear() + calendarView.selectedDates.setAt(0, dateTime) + } + case .calendarDatePicker(let calendarDatePicker): + calendarDatePicker.calendarIdentifier = identifier(for: calendar) + calendarDatePicker.date = dateTime + case .datePicker(let datePicker): + datePicker.selectedDate = dateTime + case nil: + break } if let timeView { @@ -2183,22 +2197,27 @@ final class CustomDatePicker: StackPanel { case let id: fatalError("Unsupported calendar identifier \(id)") } } - + // Magic numbers taken from https://stackoverflow.com/a/5471380/6253337 - private let ticksPerSecond: Double = 10000000 - private let unixEpochInUniversalTime: Int64 = 116444736000000000 + private let ticksPerSecond: Double = 10_000_000 + private let unixEpochInUniversalTime: Int64 = 116_444_736_000_000_000 private func foundationDateToComponents(_ date: Date) -> (DateTime, TimeSpan) { let timeInterval = date.timeIntervalSince(calendar.startOfDay(for: date)) return ( - DateTime(universalTime: Int64(date.timeIntervalSince1970 * ticksPerSecond + Double(unixEpochInUniversalTime))), + DateTime( + universalTime: Int64( + date.timeIntervalSince1970 * ticksPerSecond + Double(unixEpochInUniversalTime))), TimeSpan(duration: Int64(timeInterval * ticksPerSecond)) ) } private func componentsToFoundationDate(dateTime: DateTime, timeSpan: TimeSpan?) -> Date { - let baseDate = Date(timeIntervalSince1970: Double(dateTime.universalTime - unixEpochInUniversalTime) / ticksPerSecond) + let baseDate = Date( + timeIntervalSince1970: Double(dateTime.universalTime - unixEpochInUniversalTime) + / ticksPerSecond + ) if let timeSpan { let time = Double(timeSpan.duration) / ticksPerSecond @@ -2212,17 +2231,19 @@ final class CustomDatePicker: StackPanel { // FIXME: TimePicker and DatePicker both report their natural size as 0 x 0 before initial render // FIXME: CalendarView reports its natural size incorrectly - let timeViewSize = if let timeView { - backend.naturalSize(of: timeView) - } else { - SIMD2.zero - } + let timeViewSize = + if let timeView { + backend.naturalSize(of: timeView) + } else { + SIMD2.zero + } - let dateViewSize = if let dateControl = dateView?.asControl { - backend.naturalSize(of: dateControl) - } else { - SIMD2.zero - } + let dateViewSize = + if let dateControl = dateView?.asControl { + backend.naturalSize(of: dateControl) + } else { + SIMD2.zero + } if orientation == .horizontal { return SIMD2( From 890f1c1b7f4d88c929f3d1c0530eb73e1249faab Mon Sep 17 00:00:00 2001 From: bbrk24 Date: Sun, 2 Nov 2025 15:48:12 -0500 Subject: [PATCH 12/27] Implement minYear/maxYear for DatePicker --- Sources/SwiftCrossUI/Views/DatePicker.swift | 22 +++++++++++++++++++++ Sources/WinUIBackend/WinUIBackend.swift | 10 +++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftCrossUI/Views/DatePicker.swift b/Sources/SwiftCrossUI/Views/DatePicker.swift index 44979575a2..7e1dc40a0f 100644 --- a/Sources/SwiftCrossUI/Views/DatePicker.swift +++ b/Sources/SwiftCrossUI/Views/DatePicker.swift @@ -23,14 +23,18 @@ public struct DatePickerComponents: OptionSet, Sendable { @available(tvOS, unavailable) public enum DatePickerStyle: Sendable, Hashable { + /// A date input chosen by the backend. case automatic + /// A date input that shows a calendar grid. @available(iOS 14, macCatalyst 14, *) case graphical + /// A smaller date input. This may be a text field, or a button that opens a calendar pop-up. @available(iOS 13.4, macCatalyst 13.4, *) case compact + /// A set of scrollable inputs that can be used to select a date. @available(iOS 13.4, macCatalyst 13.4, *) @available(macOS, unavailable) case wheel @@ -44,6 +48,15 @@ public struct DatePicker { private var components: DatePickerComponents private var style: DatePickerStyle = .automatic + /// Displays a date input. + /// - Parameters: + /// - selection: The currently-selected date. + /// - range: The range of dates to display. The backend takes this as a hint but it is not + /// necessarily enforced; in particular, a backend may be able to limit the date but not the + /// time, or only the year and not the month and day. As such this parameter should be + /// treated as an aid to validation rather than a replacement for it. + /// - displayedComponents: What parts of the date/time to display in the input. + /// - label: The view to be shown next to the date input. public nonisolated init( selection: Binding, range: ClosedRange = Date.distantPast...Date.distantFuture, @@ -56,6 +69,15 @@ public struct DatePicker { self.components = displayedComponents } + /// Displays a date input. + /// - Parameters: + /// - label: The text to be shown next to the date input. + /// - selection: The currently-selected date. + /// - range: The range of dates to display. The backend takes this as a hint but it is not + /// necessarily enforced; in particular, a backend may be able to limit the date but not the + /// time, or only the year and not the month and day. As such this parameter should be + /// treated as an aid to validation rather than a replacement for it. + /// - displayedComponents: What parts of the date/time to display in the input. public nonisolated init( _ label: String, selection: Binding, diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 1d8e14a13f..a2e333592b 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -2142,10 +2142,8 @@ final class CustomDatePicker: StackPanel { calendarDatePicker.minDate = startDate calendarDatePicker.maxDate = endDate case .datePicker(let datePicker): - // FIXME: For some reason it says these properties don't exist? - // datePicker.displayDateStart = startDate - // datePicker.displayDateEnd = endDate - break + datePicker.minYear = startDate + datePicker.maxYear = endDate } } @@ -2208,7 +2206,9 @@ final class CustomDatePicker: StackPanel { return ( DateTime( universalTime: Int64( - date.timeIntervalSince1970 * ticksPerSecond + Double(unixEpochInUniversalTime))), + date.timeIntervalSince1970 * ticksPerSecond + Double(unixEpochInUniversalTime) + ) + ), TimeSpan(duration: Int64(timeInterval * ticksPerSecond)) ) } From 2bfe6bb549c01e1d91f295cf070f2c043906ed0b Mon Sep 17 00:00:00 2001 From: bbrk24 Date: Sun, 2 Nov 2025 18:42:04 -0500 Subject: [PATCH 13/27] Improve WinUI sizing code --- Sources/WinUIBackend/WinUIBackend.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index a2e333592b..0b51b27d45 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -464,7 +464,15 @@ public final class WinUIBackend: AppBackend { // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/ProgressRing/ProgressRing.xaml#L12 return SIMD2(32, 32) } else if let datePicker = widget as? CustomDatePicker { + // CustomDatePicker is a StackPanel whose individual subviews need to be manually sized + // and then added together. Its naturalSize(in:) method dispatches back here once for + // each of its children. return datePicker.naturalSize(in: self) + } else if widget is WinUI.DatePicker { + // Width is 296: + // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/CommonStyles/DatePicker_themeresources.xaml#L261 + // Height is experimentally 29 which I don't see anywhere in that file. + return SIMD2(296, 29) } let oldWidth = widget.width @@ -528,6 +536,9 @@ public final class WinUIBackend: AppBackend { 64, 32 ) + } else if widget is CalendarView { + // I don't actually know why this is necessary. Value was derived by trial and error. + adjustment = SIMD2(20, 0) } else { adjustment = .zero } @@ -2228,12 +2239,14 @@ final class CustomDatePicker: StackPanel { } func naturalSize(in backend: WinUIBackend) -> SIMD2 { - // FIXME: TimePicker and DatePicker both report their natural size as 0 x 0 before initial render - // FIXME: CalendarView reports its natural size incorrectly + // FIXME: CalendarView and CalendarDatePicker report their size incorrectly on first render let timeViewSize = if let timeView { - backend.naturalSize(of: timeView) + // Width is 242, as shown in the WinUI repository: + // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/CommonStyles/TimePicker_themeresources.xaml#L116 + // Height is experimentally 29 which I don't see anywhere in that file. + SIMD2(242, 29) } else { SIMD2.zero } From 03ad175b501cf05a1951e8065b233bfe0cf9e708 Mon Sep 17 00:00:00 2001 From: bbrk24 Date: Sun, 2 Nov 2025 23:23:10 -0500 Subject: [PATCH 14/27] Fix CalendarDatePicker size --- Sources/WinUIBackend/WinUIBackend.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 0b51b27d45..b3dc6e19b2 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -537,8 +537,15 @@ public final class WinUIBackend: AppBackend { 32 ) } else if widget is CalendarView { - // I don't actually know why this is necessary. Value was derived by trial and error. + // I don't actually know why this is necessary, but without it the abbreviations for the + // weekdays wrap, making it taller than it says it is. Value was derived by trial and + // error. adjustment = SIMD2(20, 0) + } else if computedSize.width == 0 && computedSize.width == 0 && widget is CalendarDatePicker { + // I can't find any source on what the size of CalendarDatePicker is, but it reports 0x0 + // in at least some cases before initial render. In these cases, use a size derived + // experimentally. + adjustment = SIMD2(116, 32) } else { adjustment = .zero } @@ -2239,8 +2246,6 @@ final class CustomDatePicker: StackPanel { } func naturalSize(in backend: WinUIBackend) -> SIMD2 { - // FIXME: CalendarView and CalendarDatePicker report their size incorrectly on first render - let timeViewSize = if let timeView { // Width is 242, as shown in the WinUI repository: From b05eecbb7101db34e254bfa02747b062ba140dad Mon Sep 17 00:00:00 2001 From: bbrk24 Date: Mon, 3 Nov 2025 18:32:53 -0500 Subject: [PATCH 15/27] Minor cleanup --- Sources/AppKitBackend/AppKitBackend.swift | 10 +++++----- Sources/WinUIBackend/WinUIBackend.swift | 12 ++++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 37f62169dd..cf447d22ae 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1699,10 +1699,10 @@ public final class AppKitBackend: AppBackend { // Depending on the calendar, era is either necessary or must be omitted. Making the wrong // choice for the current calendar means the cursor position is reset after every keystroke. I // know of no simple way to tell whether NSDatePicker requires or forbids eras for a given - // calendar, so in lieu of that I have hardcoded the calendar identifiers. It's shorter to list - // the calendars that forbid eras than the calendars that require them. - private let calendarsWithoutEras: Set = [ - .chinese, .gregorian, .hebrew, .iso8601, + // calendar, so in lieu of that I have hardcoded the calendar identifiers. + private let calendarsWithEras: Set = [ + .Buddhist, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .indian, .islamic, + .islamicCivil, .islamicTabular, .islamicUmmAlQura, .japanese, .persian, .republicOfChina, ] public func updateDatePicker( @@ -1736,7 +1736,7 @@ public final class AppKitBackend: AppBackend { var elementFlags: NSDatePicker.ElementFlags = [] if components.contains(.date) { elementFlags.insert(.yearMonthDay) - if !calendarsWithoutEras.contains(environment.calendar.identifier) { + if calendarsWithEras.contains(environment.calendar.identifier) { elementFlags.insert(.era) } } diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index b3dc6e19b2..7c0315d66f 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -1769,6 +1769,10 @@ public final class WinUIBackend: AppBackend { customDatePicker.updateIfNeeded(date: date, calendar: environment.calendar) customDatePicker.setDateRange(to: range) customDatePicker.setEnabled(to: environment.isEnabled) + + // TODO(parity): foreground color ignored + // Setting foreground like for other views works for TimePicker and DatePicker but not for + // CalendarView or CalendarDatePicker. } // public func createTable(rows: Int, columns: Int) -> Widget { @@ -2104,7 +2108,8 @@ final class CustomDatePicker: StackPanel { self.date = componentsToFoundationDate( dateTime: calendarView.selectedDates.getAt(0), - timeSpan: timeView?.selectedTime) + timeSpan: timeView?.selectedTime + ) if calendarView.selectedDates.size > 1 { self.needsUpdate = true @@ -2210,6 +2215,9 @@ final class CustomDatePicker: StackPanel { case .japanese: "JapaneseCalendar" case .persian: "PersianCalendar" case .republicOfChina: "TaiwanCalendar" + #if compiler(>=6.2) + case .vietnamese: "VietnameseLunarCalendar" + #endif case let id: fatalError("Unsupported calendar identifier \(id)") } } @@ -2247,7 +2255,7 @@ final class CustomDatePicker: StackPanel { func naturalSize(in backend: WinUIBackend) -> SIMD2 { let timeViewSize = - if let timeView { + if timeView != nil { // Width is 242, as shown in the WinUI repository: // https://github.com/marcelwgn/microsoft-ui-xaml/blob/ff21f9b212cea2191b959649e45e52486c8465aa/src/controls/dev/CommonStyles/TimePicker_themeresources.xaml#L116 // Height is experimentally 29 which I don't see anywhere in that file. From b8c5fd25fe9c70b4e5c26568efcdac82bc435986 Mon Sep 17 00:00:00 2001 From: William Baker Date: Mon, 3 Nov 2025 21:08:26 -0500 Subject: [PATCH 16/27] Generate GTK classes and improve manual type conversion --- Sources/Gtk/Generated/Calendar.swift | 216 ++++++++ Sources/Gtk/Generated/SpinButton.swift | 669 ++++++++++++++++++++++++ Sources/Gtk/Widgets/Calendar.swift | 18 - Sources/Gtk3/Generated/Calendar.swift | 232 ++++++++ Sources/Gtk3/Generated/SpinButton.swift | 363 +++++++++++++ Sources/Gtk3/Widgets/Calendar.swift | 16 - Sources/GtkCodeGen/GtkCodeGen.swift | 12 +- Sources/WinUIBackend/WinUIBackend.swift | 5 +- 8 files changed, 1494 insertions(+), 37 deletions(-) create mode 100644 Sources/Gtk/Generated/Calendar.swift create mode 100644 Sources/Gtk/Generated/SpinButton.swift delete mode 100644 Sources/Gtk/Widgets/Calendar.swift create mode 100644 Sources/Gtk3/Generated/Calendar.swift create mode 100644 Sources/Gtk3/Generated/SpinButton.swift delete mode 100644 Sources/Gtk3/Widgets/Calendar.swift diff --git a/Sources/Gtk/Generated/Calendar.swift b/Sources/Gtk/Generated/Calendar.swift new file mode 100644 index 0000000000..5e20955cf9 --- /dev/null +++ b/Sources/Gtk/Generated/Calendar.swift @@ -0,0 +1,216 @@ +import CGtk + +/// `GtkCalendar` is a widget that displays a Gregorian calendar, one month +/// at a time. +/// +/// ![An example GtkCalendar](calendar.png) +/// +/// A `GtkCalendar` can be created with [ctor@Gtk.Calendar.new]. +/// +/// The date that is currently displayed can be altered with +/// [method@Gtk.Calendar.select_day]. +/// +/// To place a visual marker on a particular day, use +/// [method@Gtk.Calendar.mark_day] and to remove the marker, +/// [method@Gtk.Calendar.unmark_day]. Alternative, all +/// marks can be cleared with [method@Gtk.Calendar.clear_marks]. +/// +/// The selected date can be retrieved from a `GtkCalendar` using +/// [method@Gtk.Calendar.get_date]. +/// +/// Users should be aware that, although the Gregorian calendar is the +/// legal calendar in most countries, it was adopted progressively +/// between 1582 and 1929. Display before these dates is likely to be +/// historically incorrect. +/// +/// # Shortcuts and Gestures +/// +/// `GtkCalendar` supports the following gestures: +/// +/// - Scrolling up or down will switch to the previous or next month. +/// - Date strings can be dropped for setting the current day. +/// +/// # CSS nodes +/// +/// ``` +/// calendar.view +/// ├── header +/// │ ├── button +/// │ ├── stack.month +/// │ ├── button +/// │ ├── button +/// │ ├── label.year +/// │ ╰── button +/// ╰── grid +/// ╰── label[.day-name][.week-number][.day-number][.other-month][.today] +/// ``` +/// +/// `GtkCalendar` has a main node with name calendar. It contains a subnode +/// called header containing the widgets for switching between years and months. +/// +/// The grid subnode contains all day labels, including week numbers on the left +/// (marked with the .week-number css class) and day names on top (marked with the +/// .day-name css class). +/// +/// Day labels that belong to the previous or next month get the .other-month +/// style class. The label of the current day get the .today style class. +/// +/// Marked day labels get the :selected state assigned. +open class Calendar: Widget { + /// Creates a new calendar, with the current date being selected. + public convenience init() { + self.init( + gtk_calendar_new() + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + addSignal(name: "day-selected") { [weak self] () in + guard let self = self else { return } + self.daySelected?(self) + } + + addSignal(name: "next-month") { [weak self] () in + guard let self = self else { return } + self.nextMonth?(self) + } + + addSignal(name: "next-year") { [weak self] () in + guard let self = self else { return } + self.nextYear?(self) + } + + addSignal(name: "prev-month") { [weak self] () in + guard let self = self else { return } + self.prevMonth?(self) + } + + addSignal(name: "prev-year") { [weak self] () in + guard let self = self else { return } + self.prevYear?(self) + } + + let handler5: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::day", handler: gCallback(handler5)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDay?(self, param0) + } + + let handler6: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::month", handler: gCallback(handler6)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyMonth?(self, param0) + } + + let handler7: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-day-names", handler: gCallback(handler7)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowDayNames?(self, param0) + } + + let handler8: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-heading", handler: gCallback(handler8)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowHeading?(self, param0) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-week-numbers", handler: gCallback(handler9)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowWeekNumbers?(self, param0) + } + + let handler10: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::year", handler: gCallback(handler10)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyYear?(self, param0) + } + } + + /// The selected day (as a number between 1 and 31). + @GObjectProperty(named: "day") public var day: Int + + /// The selected month (as a number between 0 and 11). + /// + /// This property gets initially set to the current month. + @GObjectProperty(named: "month") public var month: Int + + /// Determines whether day names are displayed. + @GObjectProperty(named: "show-day-names") public var showDayNames: Bool + + /// Determines whether a heading is displayed. + @GObjectProperty(named: "show-heading") public var showHeading: Bool + + /// Determines whether week numbers are displayed. + @GObjectProperty(named: "show-week-numbers") public var showWeekNumbers: Bool + + /// The selected year. + /// + /// This property gets initially set to the current year. + @GObjectProperty(named: "year") public var year: Int + + /// Emitted when the user selects a day. + public var daySelected: ((Calendar) -> Void)? + + /// Emitted when the user switched to the next month. + public var nextMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the next year. + public var nextYear: ((Calendar) -> Void)? + + /// Emitted when the user switched to the previous month. + public var prevMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the previous year. + public var prevYear: ((Calendar) -> Void)? + + public var notifyDay: ((Calendar, OpaquePointer) -> Void)? + + public var notifyMonth: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowDayNames: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowHeading: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowWeekNumbers: ((Calendar, OpaquePointer) -> Void)? + + public var notifyYear: ((Calendar, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk/Generated/SpinButton.swift b/Sources/Gtk/Generated/SpinButton.swift new file mode 100644 index 0000000000..b20ebe5a9a --- /dev/null +++ b/Sources/Gtk/Generated/SpinButton.swift @@ -0,0 +1,669 @@ +import CGtk + +/// A `GtkSpinButton` is an ideal way to allow the user to set the +/// value of some attribute. +/// +/// ![An example GtkSpinButton](spinbutton.png) +/// +/// Rather than having to directly type a number into a `GtkEntry`, +/// `GtkSpinButton` allows the user to click on one of two arrows +/// to increment or decrement the displayed value. A value can still be +/// typed in, with the bonus that it can be checked to ensure it is in a +/// given range. +/// +/// The main properties of a `GtkSpinButton` are through an adjustment. +/// See the [class@Gtk.Adjustment] documentation for more details about +/// an adjustment's properties. +/// +/// Note that `GtkSpinButton` will by default make its entry large enough +/// to accommodate the lower and upper bounds of the adjustment. If this +/// is not desired, the automatic sizing can be turned off by explicitly +/// setting [property@Gtk.Editable:width-chars] to a value != -1. +/// +/// ## Using a GtkSpinButton to get an integer +/// +/// ```c +/// // Provides a function to retrieve an integer value from a GtkSpinButton +/// // and creates a spin button to model percentage values. +/// +/// int +/// grab_int_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value_as_int (button); +/// } +/// +/// void +/// create_integer_spin_button (void) +/// { +/// +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (50.0, 0.0, 100.0, 1.0, 5.0, 0.0); +/// +/// window = gtk_window_new (); +/// +/// // creates the spinbutton, with no decimal places +/// button = gtk_spin_button_new (adjustment, 1.0, 0); +/// gtk_window_set_child (GTK_WINDOW (window), button); +/// +/// gtk_window_present (GTK_WINDOW (window)); +/// } +/// ``` +/// +/// ## Using a GtkSpinButton to get a floating point value +/// +/// ```c +/// // Provides a function to retrieve a floating point value from a +/// // GtkSpinButton, and creates a high precision spin button. +/// +/// float +/// grab_float_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value (button); +/// } +/// +/// void +/// create_floating_spin_button (void) +/// { +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (2.500, 0.0, 5.0, 0.001, 0.1, 0.0); +/// +/// window = gtk_window_new (); +/// +/// // creates the spinbutton, with three decimal places +/// button = gtk_spin_button_new (adjustment, 0.001, 3); +/// gtk_window_set_child (GTK_WINDOW (window), button); +/// +/// gtk_window_present (GTK_WINDOW (window)); +/// } +/// ``` +/// +/// # Shortcuts and Gestures +/// +/// The following signals have default keybindings: +/// +/// - [signal@Gtk.SpinButton::change-value] +/// +/// # CSS nodes +/// +/// ``` +/// spinbutton.horizontal +/// ├── text +/// │ ├── undershoot.left +/// │ ╰── undershoot.right +/// ├── button.down +/// ╰── button.up +/// ``` +/// +/// ``` +/// spinbutton.vertical +/// ├── button.up +/// ├── text +/// │ ├── undershoot.left +/// │ ╰── undershoot.right +/// ╰── button.down +/// ``` +/// +/// `GtkSpinButton`s main CSS node has the name spinbutton. It creates subnodes +/// for the entry and the two buttons, with these names. The button nodes have +/// the style classes .up and .down. The `GtkText` subnodes (if present) are put +/// below the text node. The orientation of the spin button is reflected in +/// the .vertical or .horizontal style class on the main node. +/// +/// # Accessibility +/// +/// `GtkSpinButton` uses the %GTK_ACCESSIBLE_ROLE_SPIN_BUTTON role. +open class SpinButton: Widget, CellEditable, Editable, Orientable { + /// Creates a new `GtkSpinButton`. + public convenience init( + adjustment: UnsafeMutablePointer!, climbRate: Double, digits: UInt + ) { + self.init( + gtk_spin_button_new(adjustment, climbRate, guint(digits)) + ) + } + + /// Creates a new `GtkSpinButton` with the given properties. + /// + /// This is a convenience constructor that allows creation + /// of a numeric `GtkSpinButton` without manually creating + /// an adjustment. The value is initially set to the minimum + /// value and a page increment of 10 * @step is the default. + /// The precision of the spin button is equivalent to the + /// precision of @step. + /// + /// Note that the way in which the precision is derived works + /// best if @step is a power of ten. If the resulting precision + /// is not suitable for your needs, use + /// [method@Gtk.SpinButton.set_digits] to correct it. + public convenience init(range min: Double, max: Double, step: Double) { + self.init( + gtk_spin_button_new_with_range(min, max, step) + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + addSignal(name: "activate") { [weak self] () in + guard let self = self else { return } + self.activate?(self) + } + + let handler1: + @convention(c) (UnsafeMutableRawPointer, GtkScrollType, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "change-value", handler: gCallback(handler1)) { + [weak self] (param0: GtkScrollType) in + guard let self = self else { return } + self.changeValue?(self, param0) + } + + let handler2: + @convention(c) (UnsafeMutableRawPointer, gpointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "input", handler: gCallback(handler2)) { [weak self] (param0: gpointer) in + guard let self = self else { return } + self.input?(self, param0) + } + + addSignal(name: "output") { [weak self] () in + guard let self = self else { return } + self.output?(self) + } + + addSignal(name: "value-changed") { [weak self] () in + guard let self = self else { return } + self.valueChanged?(self) + } + + addSignal(name: "wrapped") { [weak self] () in + guard let self = self else { return } + self.wrapped?(self) + } + + addSignal(name: "editing-done") { [weak self] () in + guard let self = self else { return } + self.editingDone?(self) + } + + addSignal(name: "remove-widget") { [weak self] () in + guard let self = self else { return } + self.removeWidget?(self) + } + + addSignal(name: "changed") { [weak self] () in + guard let self = self else { return } + self.changed?(self) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, Int, Int, UnsafeMutableRawPointer) -> Void = + { _, value1, value2, data in + SignalBox2.run(data, value1, value2) + } + + addSignal(name: "delete-text", handler: gCallback(handler9)) { + [weak self] (param0: Int, param1: Int) in + guard let self = self else { return } + self.deleteText?(self, param0, param1) + } + + let handler10: + @convention(c) ( + UnsafeMutableRawPointer, UnsafePointer, Int, gpointer, + UnsafeMutableRawPointer + ) -> Void = + { _, value1, value2, value3, data in + SignalBox3, Int, gpointer>.run( + data, value1, value2, value3) + } + + addSignal(name: "insert-text", handler: gCallback(handler10)) { + [weak self] (param0: UnsafePointer, param1: Int, param2: gpointer) in + guard let self = self else { return } + self.insertText?(self, param0, param1, param2) + } + + let handler11: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::activates-default", handler: gCallback(handler11)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyActivatesDefault?(self, param0) + } + + let handler12: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::adjustment", handler: gCallback(handler12)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyAdjustment?(self, param0) + } + + let handler13: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::climb-rate", handler: gCallback(handler13)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyClimbRate?(self, param0) + } + + let handler14: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::digits", handler: gCallback(handler14)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDigits?(self, param0) + } + + let handler15: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::numeric", handler: gCallback(handler15)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyNumeric?(self, param0) + } + + let handler16: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::snap-to-ticks", handler: gCallback(handler16)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifySnapToTicks?(self, param0) + } + + let handler17: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::update-policy", handler: gCallback(handler17)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyUpdatePolicy?(self, param0) + } + + let handler18: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::value", handler: gCallback(handler18)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyValue?(self, param0) + } + + let handler19: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::wrap", handler: gCallback(handler19)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyWrap?(self, param0) + } + + let handler20: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::editing-canceled", handler: gCallback(handler20)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyEditingCanceled?(self, param0) + } + + let handler21: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::cursor-position", handler: gCallback(handler21)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyCursorPosition?(self, param0) + } + + let handler22: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::editable", handler: gCallback(handler22)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyEditable?(self, param0) + } + + let handler23: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::enable-undo", handler: gCallback(handler23)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyEnableUndo?(self, param0) + } + + let handler24: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::max-width-chars", handler: gCallback(handler24)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyMaxWidthChars?(self, param0) + } + + let handler25: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::selection-bound", handler: gCallback(handler25)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifySelectionBound?(self, param0) + } + + let handler26: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::text", handler: gCallback(handler26)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyText?(self, param0) + } + + let handler27: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::width-chars", handler: gCallback(handler27)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyWidthChars?(self, param0) + } + + let handler28: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::xalign", handler: gCallback(handler28)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyXalign?(self, param0) + } + + let handler29: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::orientation", handler: gCallback(handler29)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyOrientation?(self, param0) + } + } + + /// The acceleration rate when you hold down a button or key. + @GObjectProperty(named: "climb-rate") public var climbRate: Double + + /// The number of decimal places to display. + @GObjectProperty(named: "digits") public var digits: UInt + + /// Whether non-numeric characters should be ignored. + @GObjectProperty(named: "numeric") public var numeric: Bool + + /// Whether erroneous values are automatically changed to the spin buttons + /// nearest step increment. + @GObjectProperty(named: "snap-to-ticks") public var snapToTicks: Bool + + /// Whether the spin button should update always, or only when the value + /// is acceptable. + @GObjectProperty(named: "update-policy") public var updatePolicy: SpinButtonUpdatePolicy + + /// The current value. + @GObjectProperty(named: "value") public var value: Double + + /// Whether a spin button should wrap upon reaching its limits. + @GObjectProperty(named: "wrap") public var wrap: Bool + + /// The current position of the insertion cursor in chars. + @GObjectProperty(named: "cursor-position") public var cursorPosition: Int + + /// Whether the entry contents can be edited. + @GObjectProperty(named: "editable") public var editable: Bool + + /// If undo/redo should be enabled for the editable. + @GObjectProperty(named: "enable-undo") public var enableUndo: Bool + + /// The desired maximum width of the entry, in characters. + @GObjectProperty(named: "max-width-chars") public var maxWidthChars: Int + + /// The contents of the entry. + @GObjectProperty(named: "text") public var text: String + + /// Number of characters to leave space for in the entry. + @GObjectProperty(named: "width-chars") public var widthChars: Int + + /// The horizontal alignment, from 0 (left) to 1 (right). + /// + /// Reversed for RTL layouts. + @GObjectProperty(named: "xalign") public var xalign: Float + + /// The orientation of the orientable. + @GObjectProperty(named: "orientation") public var orientation: Orientation + + /// Emitted when the spin button is activated. + /// + /// The keybindings for this signal are all forms of the Enter key. + /// + /// If the Enter key results in the value being committed to the + /// spin button, then activation does not occur until Enter is + /// pressed again. + public var activate: ((SpinButton) -> Void)? + + /// Emitted when the user initiates a value change. + /// + /// This is a [keybinding signal](class.SignalAction.html). + /// + /// Applications should not connect to it, but may emit it with + /// g_signal_emit_by_name() if they need to control the cursor + /// programmatically. + /// + /// The default bindings for this signal are Up/Down and PageUp/PageDown. + public var changeValue: ((SpinButton, GtkScrollType) -> Void)? + + /// Emitted to convert the users input into a double value. + /// + /// The signal handler is expected to use [method@Gtk.Editable.get_text] + /// to retrieve the text of the spinbutton and set @new_value to the + /// new value. + /// + /// The default conversion uses g_strtod(). + public var input: ((SpinButton, gpointer) -> Void)? + + /// Emitted to tweak the formatting of the value for display. + /// + /// ```c + /// // show leading zeros + /// static gboolean + /// on_output (GtkSpinButton *spin, + /// gpointer data) + /// { + /// char *text; + /// int value; + /// + /// value = gtk_spin_button_get_value_as_int (spin); + /// text = g_strdup_printf ("%02d", value); + /// gtk_editable_set_text (GTK_EDITABLE (spin), text): + /// g_free (text); + /// + /// return TRUE; + /// } + /// ``` + public var output: ((SpinButton) -> Void)? + + /// Emitted when the value is changed. + /// + /// Also see the [signal@Gtk.SpinButton::output] signal. + public var valueChanged: ((SpinButton) -> Void)? + + /// Emitted right after the spinbutton wraps from its maximum + /// to its minimum value or vice-versa. + public var wrapped: ((SpinButton) -> Void)? + + /// This signal is a sign for the cell renderer to update its + /// value from the @cell_editable. + /// + /// Implementations of `GtkCellEditable` are responsible for + /// emitting this signal when they are done editing, e.g. + /// `GtkEntry` emits this signal when the user presses Enter. Typical things to + /// do in a handler for ::editing-done are to capture the edited value, + /// disconnect the @cell_editable from signals on the `GtkCellRenderer`, etc. + /// + /// gtk_cell_editable_editing_done() is a convenience method + /// for emitting `GtkCellEditable::editing-done`. + public var editingDone: ((SpinButton) -> Void)? + + /// This signal is meant to indicate that the cell is finished + /// editing, and the @cell_editable widget is being removed and may + /// subsequently be destroyed. + /// + /// Implementations of `GtkCellEditable` are responsible for + /// emitting this signal when they are done editing. It must + /// be emitted after the `GtkCellEditable::editing-done` signal, + /// to give the cell renderer a chance to update the cell's value + /// before the widget is removed. + /// + /// gtk_cell_editable_remove_widget() is a convenience method + /// for emitting `GtkCellEditable::remove-widget`. + public var removeWidget: ((SpinButton) -> Void)? + + /// Emitted at the end of a single user-visible operation on the + /// contents. + /// + /// E.g., a paste operation that replaces the contents of the + /// selection will cause only one signal emission (even though it + /// is implemented by first deleting the selection, then inserting + /// the new content, and may cause multiple ::notify::text signals + /// to be emitted). + public var changed: ((SpinButton) -> Void)? + + /// Emitted when text is deleted from the widget by the user. + /// + /// The default handler for this signal will normally be responsible for + /// deleting the text, so by connecting to this signal and then stopping + /// the signal with g_signal_stop_emission(), it is possible to modify the + /// range of deleted text, or prevent it from being deleted entirely. + /// + /// The @start_pos and @end_pos parameters are interpreted as for + /// [method@Gtk.Editable.delete_text]. + public var deleteText: ((SpinButton, Int, Int) -> Void)? + + /// Emitted when text is inserted into the widget by the user. + /// + /// The default handler for this signal will normally be responsible + /// for inserting the text, so by connecting to this signal and then + /// stopping the signal with g_signal_stop_emission(), it is possible + /// to modify the inserted text, or prevent it from being inserted entirely. + public var insertText: ((SpinButton, UnsafePointer, Int, gpointer) -> Void)? + + public var notifyActivatesDefault: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyAdjustment: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyClimbRate: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyDigits: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyNumeric: ((SpinButton, OpaquePointer) -> Void)? + + public var notifySnapToTicks: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyUpdatePolicy: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyValue: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyWrap: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyEditingCanceled: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyCursorPosition: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyEditable: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyEnableUndo: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyMaxWidthChars: ((SpinButton, OpaquePointer) -> Void)? + + public var notifySelectionBound: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyText: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyWidthChars: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyXalign: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyOrientation: ((SpinButton, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk/Widgets/Calendar.swift b/Sources/Gtk/Widgets/Calendar.swift deleted file mode 100644 index de8215a136..0000000000 --- a/Sources/Gtk/Widgets/Calendar.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright © 2016 Tomas Linhart. All rights reserved. -// - -import CGtk - -public class Calendar: Widget { - public convenience init() { - self.init( - gtk_calendar_new() - ) - } - - /// The selected year. This property gets initially set to the current year. - @GObjectProperty(named: "year") public var year: Int - /// Determines whether a heading is displayed. - @GObjectProperty(named: "show-heading") public var showHeading: Bool -} diff --git a/Sources/Gtk3/Generated/Calendar.swift b/Sources/Gtk3/Generated/Calendar.swift new file mode 100644 index 0000000000..0d23f2d51c --- /dev/null +++ b/Sources/Gtk3/Generated/Calendar.swift @@ -0,0 +1,232 @@ +import CGtk3 + +/// #GtkCalendar is a widget that displays a Gregorian calendar, one month +/// at a time. It can be created with gtk_calendar_new(). +/// +/// The month and year currently displayed can be altered with +/// gtk_calendar_select_month(). The exact day can be selected from the +/// displayed month using gtk_calendar_select_day(). +/// +/// To place a visual marker on a particular day, use gtk_calendar_mark_day() +/// and to remove the marker, gtk_calendar_unmark_day(). Alternative, all +/// marks can be cleared with gtk_calendar_clear_marks(). +/// +/// The way in which the calendar itself is displayed can be altered using +/// gtk_calendar_set_display_options(). +/// +/// The selected date can be retrieved from a #GtkCalendar using +/// gtk_calendar_get_date(). +/// +/// Users should be aware that, although the Gregorian calendar is the +/// legal calendar in most countries, it was adopted progressively +/// between 1582 and 1929. Display before these dates is likely to be +/// historically incorrect. +open class Calendar: Widget { + /// Creates a new calendar, with the current date being selected. + public convenience init() { + self.init( + gtk_calendar_new() + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + addSignal(name: "day-selected") { [weak self] () in + guard let self = self else { return } + self.daySelected?(self) + } + + addSignal(name: "day-selected-double-click") { [weak self] () in + guard let self = self else { return } + self.daySelectedDoubleClick?(self) + } + + addSignal(name: "month-changed") { [weak self] () in + guard let self = self else { return } + self.monthChanged?(self) + } + + addSignal(name: "next-month") { [weak self] () in + guard let self = self else { return } + self.nextMonth?(self) + } + + addSignal(name: "next-year") { [weak self] () in + guard let self = self else { return } + self.nextYear?(self) + } + + addSignal(name: "prev-month") { [weak self] () in + guard let self = self else { return } + self.prevMonth?(self) + } + + addSignal(name: "prev-year") { [weak self] () in + guard let self = self else { return } + self.prevYear?(self) + } + + let handler7: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::day", handler: gCallback(handler7)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDay?(self, param0) + } + + let handler8: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::detail-height-rows", handler: gCallback(handler8)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDetailHeightRows?(self, param0) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::detail-width-chars", handler: gCallback(handler9)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDetailWidthChars?(self, param0) + } + + let handler10: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::month", handler: gCallback(handler10)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyMonth?(self, param0) + } + + let handler11: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::no-month-change", handler: gCallback(handler11)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyNoMonthChange?(self, param0) + } + + let handler12: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-day-names", handler: gCallback(handler12)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowDayNames?(self, param0) + } + + let handler13: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-details", handler: gCallback(handler13)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowDetails?(self, param0) + } + + let handler14: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-heading", handler: gCallback(handler14)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowHeading?(self, param0) + } + + let handler15: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::show-week-numbers", handler: gCallback(handler15)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyShowWeekNumbers?(self, param0) + } + + let handler16: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::year", handler: gCallback(handler16)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyYear?(self, param0) + } + } + + /// Emitted when the user selects a day. + public var daySelected: ((Calendar) -> Void)? + + /// Emitted when the user double-clicks a day. + public var daySelectedDoubleClick: ((Calendar) -> Void)? + + /// Emitted when the user clicks a button to change the selected month on a + /// calendar. + public var monthChanged: ((Calendar) -> Void)? + + /// Emitted when the user switched to the next month. + public var nextMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the next year. + public var nextYear: ((Calendar) -> Void)? + + /// Emitted when the user switched to the previous month. + public var prevMonth: ((Calendar) -> Void)? + + /// Emitted when user switched to the previous year. + public var prevYear: ((Calendar) -> Void)? + + public var notifyDay: ((Calendar, OpaquePointer) -> Void)? + + public var notifyDetailHeightRows: ((Calendar, OpaquePointer) -> Void)? + + public var notifyDetailWidthChars: ((Calendar, OpaquePointer) -> Void)? + + public var notifyMonth: ((Calendar, OpaquePointer) -> Void)? + + public var notifyNoMonthChange: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowDayNames: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowDetails: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowHeading: ((Calendar, OpaquePointer) -> Void)? + + public var notifyShowWeekNumbers: ((Calendar, OpaquePointer) -> Void)? + + public var notifyYear: ((Calendar, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk3/Generated/SpinButton.swift b/Sources/Gtk3/Generated/SpinButton.swift new file mode 100644 index 0000000000..0e07eafef1 --- /dev/null +++ b/Sources/Gtk3/Generated/SpinButton.swift @@ -0,0 +1,363 @@ +import CGtk3 + +/// A #GtkSpinButton is an ideal way to allow the user to set the value of +/// some attribute. Rather than having to directly type a number into a +/// #GtkEntry, GtkSpinButton allows the user to click on one of two arrows +/// to increment or decrement the displayed value. A value can still be +/// typed in, with the bonus that it can be checked to ensure it is in a +/// given range. +/// +/// The main properties of a GtkSpinButton are through an adjustment. +/// See the #GtkAdjustment section for more details about an adjustment's +/// properties. Note that GtkSpinButton will by default make its entry +/// large enough to accomodate the lower and upper bounds of the adjustment, +/// which can lead to surprising results. Best practice is to set both +/// the #GtkEntry:width-chars and #GtkEntry:max-width-chars poperties +/// to the desired number of characters to display in the entry. +/// +/// # CSS nodes +/// +/// |[ +/// spinbutton.horizontal +/// ├── undershoot.left +/// ├── undershoot.right +/// ├── entry +/// │ ╰── ... +/// ├── button.down +/// ╰── button.up +/// ]| +/// +/// |[ +/// spinbutton.vertical +/// ├── undershoot.left +/// ├── undershoot.right +/// ├── button.up +/// ├── entry +/// │ ╰── ... +/// ╰── button.down +/// ]| +/// +/// GtkSpinButtons main CSS node has the name spinbutton. It creates subnodes +/// for the entry and the two buttons, with these names. The button nodes have +/// the style classes .up and .down. The GtkEntry subnodes (if present) are put +/// below the entry node. The orientation of the spin button is reflected in +/// the .vertical or .horizontal style class on the main node. +/// +/// ## Using a GtkSpinButton to get an integer +/// +/// |[ +/// // Provides a function to retrieve an integer value from a GtkSpinButton +/// // and creates a spin button to model percentage values. +/// +/// gint +/// grab_int_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value_as_int (button); +/// } +/// +/// void +/// create_integer_spin_button (void) +/// { +/// +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (50.0, 0.0, 100.0, 1.0, 5.0, 0.0); +/// +/// window = gtk_window_new (GTK_WINDOW_TOPLEVEL); +/// gtk_container_set_border_width (GTK_CONTAINER (window), 5); +/// +/// // creates the spinbutton, with no decimal places +/// button = gtk_spin_button_new (adjustment, 1.0, 0); +/// gtk_container_add (GTK_CONTAINER (window), button); +/// +/// gtk_widget_show_all (window); +/// } +/// ]| +/// +/// ## Using a GtkSpinButton to get a floating point value +/// +/// |[ +/// // Provides a function to retrieve a floating point value from a +/// // GtkSpinButton, and creates a high precision spin button. +/// +/// gfloat +/// grab_float_value (GtkSpinButton *button, +/// gpointer user_data) +/// { +/// return gtk_spin_button_get_value (button); +/// } +/// +/// void +/// create_floating_spin_button (void) +/// { +/// GtkWidget *window, *button; +/// GtkAdjustment *adjustment; +/// +/// adjustment = gtk_adjustment_new (2.500, 0.0, 5.0, 0.001, 0.1, 0.0); +/// +/// window = gtk_window_new (GTK_WINDOW_TOPLEVEL); +/// gtk_container_set_border_width (GTK_CONTAINER (window), 5); +/// +/// // creates the spinbutton, with three decimal places +/// button = gtk_spin_button_new (adjustment, 0.001, 3); +/// gtk_container_add (GTK_CONTAINER (window), button); +/// +/// gtk_widget_show_all (window); +/// } +/// ]| +open class SpinButton: Entry, Orientable { + /// Creates a new #GtkSpinButton. + public convenience init( + adjustment: UnsafeMutablePointer!, climbRate: Double, digits: UInt + ) { + self.init( + gtk_spin_button_new(adjustment, climbRate, guint(digits)) + ) + } + + /// This is a convenience constructor that allows creation of a numeric + /// #GtkSpinButton without manually creating an adjustment. The value is + /// initially set to the minimum value and a page increment of 10 * @step + /// is the default. The precision of the spin button is equivalent to the + /// precision of @step. + /// + /// Note that the way in which the precision is derived works best if @step + /// is a power of ten. If the resulting precision is not suitable for your + /// needs, use gtk_spin_button_set_digits() to correct it. + public convenience init(range min: Double, max: Double, step: Double) { + self.init( + gtk_spin_button_new_with_range(min, max, step) + ) + } + + override func didMoveToParent() { + super.didMoveToParent() + + let handler0: + @convention(c) (UnsafeMutableRawPointer, GtkScrollType, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "change-value", handler: gCallback(handler0)) { + [weak self] (param0: GtkScrollType) in + guard let self = self else { return } + self.changeValue?(self, param0) + } + + let handler1: + @convention(c) (UnsafeMutableRawPointer, gpointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "input", handler: gCallback(handler1)) { [weak self] (param0: gpointer) in + guard let self = self else { return } + self.input?(self, param0) + } + + addSignal(name: "output") { [weak self] () in + guard let self = self else { return } + self.output?(self) + } + + addSignal(name: "value-changed") { [weak self] () in + guard let self = self else { return } + self.valueChanged?(self) + } + + addSignal(name: "wrapped") { [weak self] () in + guard let self = self else { return } + self.wrapped?(self) + } + + let handler5: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::adjustment", handler: gCallback(handler5)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyAdjustment?(self, param0) + } + + let handler6: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::climb-rate", handler: gCallback(handler6)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyClimbRate?(self, param0) + } + + let handler7: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::digits", handler: gCallback(handler7)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyDigits?(self, param0) + } + + let handler8: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::numeric", handler: gCallback(handler8)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyNumeric?(self, param0) + } + + let handler9: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::snap-to-ticks", handler: gCallback(handler9)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifySnapToTicks?(self, param0) + } + + let handler10: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::update-policy", handler: gCallback(handler10)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyUpdatePolicy?(self, param0) + } + + let handler11: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::value", handler: gCallback(handler11)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyValue?(self, param0) + } + + let handler12: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::wrap", handler: gCallback(handler12)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyWrap?(self, param0) + } + + let handler13: + @convention(c) (UnsafeMutableRawPointer, OpaquePointer, UnsafeMutableRawPointer) -> Void = + { _, value1, data in + SignalBox1.run(data, value1) + } + + addSignal(name: "notify::orientation", handler: gCallback(handler13)) { + [weak self] (param0: OpaquePointer) in + guard let self = self else { return } + self.notifyOrientation?(self, param0) + } + } + + @GObjectProperty(named: "digits") public var digits: UInt + + @GObjectProperty(named: "numeric") public var numeric: Bool + + @GObjectProperty(named: "snap-to-ticks") public var snapToTicks: Bool + + @GObjectProperty(named: "update-policy") public var updatePolicy: SpinButtonUpdatePolicy + + @GObjectProperty(named: "value") public var value: Double + + @GObjectProperty(named: "wrap") public var wrap: Bool + + /// The ::change-value signal is a [keybinding signal][GtkBindingSignal] + /// which gets emitted when the user initiates a value change. + /// + /// Applications should not connect to it, but may emit it with + /// g_signal_emit_by_name() if they need to control the cursor + /// programmatically. + /// + /// The default bindings for this signal are Up/Down and PageUp and/PageDown. + public var changeValue: ((SpinButton, GtkScrollType) -> Void)? + + /// The ::input signal can be used to influence the conversion of + /// the users input into a double value. The signal handler is + /// expected to use gtk_entry_get_text() to retrieve the text of + /// the entry and set @new_value to the new value. + /// + /// The default conversion uses g_strtod(). + public var input: ((SpinButton, gpointer) -> Void)? + + /// The ::output signal can be used to change to formatting + /// of the value that is displayed in the spin buttons entry. + /// |[ + /// // show leading zeros + /// static gboolean + /// on_output (GtkSpinButton *spin, + /// gpointer data) + /// { + /// GtkAdjustment *adjustment; + /// gchar *text; + /// int value; + /// + /// adjustment = gtk_spin_button_get_adjustment (spin); + /// value = (int)gtk_adjustment_get_value (adjustment); + /// text = g_strdup_printf ("%02d", value); + /// gtk_entry_set_text (GTK_ENTRY (spin), text); + /// g_free (text); + /// + /// return TRUE; + /// } + /// ]| + public var output: ((SpinButton) -> Void)? + + /// The ::value-changed signal is emitted when the value represented by + /// @spinbutton changes. Also see the #GtkSpinButton::output signal. + public var valueChanged: ((SpinButton) -> Void)? + + /// The ::wrapped signal is emitted right after the spinbutton wraps + /// from its maximum to minimum value or vice-versa. + public var wrapped: ((SpinButton) -> Void)? + + public var notifyAdjustment: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyClimbRate: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyDigits: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyNumeric: ((SpinButton, OpaquePointer) -> Void)? + + public var notifySnapToTicks: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyUpdatePolicy: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyValue: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyWrap: ((SpinButton, OpaquePointer) -> Void)? + + public var notifyOrientation: ((SpinButton, OpaquePointer) -> Void)? +} diff --git a/Sources/Gtk3/Widgets/Calendar.swift b/Sources/Gtk3/Widgets/Calendar.swift deleted file mode 100644 index d1141f6b35..0000000000 --- a/Sources/Gtk3/Widgets/Calendar.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright © 2016 Tomas Linhart. All rights reserved. -// - -import CGtk3 - -public class Calendar: Widget { - public convenience init() { - self.init(gtk_calendar_new()) - } - - /// The selected year. This property gets initially set to the current year. - @GObjectProperty(named: "year") public var year: Int - /// Determines whether a heading is displayed. - @GObjectProperty(named: "show-heading") public var showHeading: Bool -} diff --git a/Sources/GtkCodeGen/GtkCodeGen.swift b/Sources/GtkCodeGen/GtkCodeGen.swift index 921f604305..35ccb1a2fb 100644 --- a/Sources/GtkCodeGen/GtkCodeGen.swift +++ b/Sources/GtkCodeGen/GtkCodeGen.swift @@ -27,6 +27,12 @@ struct GtkCodeGen { "GtkSelectionModel*": "OpaquePointer?", "GtkListItemFactory*": "OpaquePointer?", "GtkTextTagTable*": "OpaquePointer?", + "int": "Int", + ] + + static let cTypesManuallyConverted: [String: String] = [ + "guint": "guint", + "int": "CInt", ] /// Problematic signals which are excluded from the generated Swift @@ -110,7 +116,7 @@ struct GtkCodeGen { "Button", "Entry", "Label", "Range", "Scale", "Image", "Switch", "Spinner", "ProgressBar", "FileChooserNative", "NativeDialog", "GestureClick", "GestureSingle", "Gesture", "EventController", "GestureLongPress", "GLArea", "DrawingArea", - "CheckButton", + "CheckButton", "Calendar", "SpinButton", "Box", ] let gtk3AllowListedClasses = ["MenuShell", "EventBox"] let gtk4AllowListedClasses = [ @@ -805,6 +811,10 @@ struct GtkCodeGen { .unsafeCopy() .baseAddress! """ + } else if let type = parameter.type?.cType, + let destinationType = cTypesManuallyConverted[type] + { + return "\(destinationType)(\(argument))" } return argument diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 7c0315d66f..cd408336d1 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -541,7 +541,8 @@ public final class WinUIBackend: AppBackend { // weekdays wrap, making it taller than it says it is. Value was derived by trial and // error. adjustment = SIMD2(20, 0) - } else if computedSize.width == 0 && computedSize.width == 0 && widget is CalendarDatePicker { + } else if computedSize.width == 0 && computedSize.width == 0 && widget is CalendarDatePicker + { // I can't find any source on what the size of CalendarDatePicker is, but it reports 0x0 // in at least some cases before initial render. In these cases, use a size derived // experimentally. @@ -2216,7 +2217,7 @@ final class CustomDatePicker: StackPanel { case .persian: "PersianCalendar" case .republicOfChina: "TaiwanCalendar" #if compiler(>=6.2) - case .vietnamese: "VietnameseLunarCalendar" + case .vietnamese: "VietnameseLunarCalendar" #endif case let id: fatalError("Unsupported calendar identifier \(id)") } From c454cdf68b878f1f43e20f1c111eb50b114b9c08 Mon Sep 17 00:00:00 2001 From: William Baker Date: Mon, 3 Nov 2025 21:38:38 -0500 Subject: [PATCH 17/27] oops --- Sources/GtkCodeGen/GtkCodeGen.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GtkCodeGen/GtkCodeGen.swift b/Sources/GtkCodeGen/GtkCodeGen.swift index 35ccb1a2fb..ecf30734d8 100644 --- a/Sources/GtkCodeGen/GtkCodeGen.swift +++ b/Sources/GtkCodeGen/GtkCodeGen.swift @@ -116,7 +116,7 @@ struct GtkCodeGen { "Button", "Entry", "Label", "Range", "Scale", "Image", "Switch", "Spinner", "ProgressBar", "FileChooserNative", "NativeDialog", "GestureClick", "GestureSingle", "Gesture", "EventController", "GestureLongPress", "GLArea", "DrawingArea", - "CheckButton", "Calendar", "SpinButton", "Box", + "CheckButton", "Calendar", "SpinButton", ] let gtk3AllowListedClasses = ["MenuShell", "EventBox"] let gtk4AllowListedClasses = [ From 34fe6a41ec91a83d95cb37e9a6b86311a8e83ed4 Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Tue, 4 Nov 2025 08:28:47 -0500 Subject: [PATCH 18/27] Fix casing of calendar name --- Sources/AppKitBackend/AppKitBackend.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index cf447d22ae..27325bc439 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -1701,7 +1701,7 @@ public final class AppKitBackend: AppBackend { // know of no simple way to tell whether NSDatePicker requires or forbids eras for a given // calendar, so in lieu of that I have hardcoded the calendar identifiers. private let calendarsWithEras: Set = [ - .Buddhist, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .indian, .islamic, + .buddhist, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .indian, .islamic, .islamicCivil, .islamicTabular, .islamicUmmAlQura, .japanese, .persian, .republicOfChina, ] From 21a0a9a2062b5e7f5f277f754954b314f9ff3497 Mon Sep 17 00:00:00 2001 From: William Baker Date: Tue, 4 Nov 2025 18:07:51 -0500 Subject: [PATCH 19/27] Saving partial work on GtkBackend --- Sources/Gtk/Widgets/Box.swift | 4 ++ Sources/GtkBackend/GtkBackend.swift | 90 +++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/Sources/Gtk/Widgets/Box.swift b/Sources/Gtk/Widgets/Box.swift index 2111d4f1a2..73f2aa2abe 100644 --- a/Sources/Gtk/Widgets/Box.swift +++ b/Sources/Gtk/Widgets/Box.swift @@ -39,6 +39,10 @@ open class Box: Widget, Orientable { children = [] } + public func insert(child: Widget, after sibling: Widget) { + gtk_box_insert_child_after(castedPointer(), child.widgetPointer, sibling.widgetPointer) + } + @GObjectProperty(named: "spacing") open var spacing: Int @GObjectProperty(named: "orientation") open var orientation: Orientation diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 418014c1c0..da98221688 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1581,3 +1581,93 @@ extension UnsafeMutablePointer { class CustomListBox: ListBox { var cachedSelection: Int? = nil } + +final class TimePicker: Box { + private var hourCycle: Locale.HourCycle + private let hourPicker: SpinButton + private let hourMinuteSeparator = Label(string: ":") + private let minutePicker = SpinButton(range: 0, max: 59, step: 1) + private var minuteSecondSeparator: Label? + private var secondPicker: SpinButton? + private var amPmPicker: DropDown? + + init() { + let hourCycle = Locale.current.hourCycle + + self.hourCycle = hourCycle + self.hourPicker = SpinButton( + range: TimePicker.minHour(for: hourCycle), + max: TimePicker.maxHour(for: hourCycle), + step: 1 + ) + + super.init(orientation: .horizontal, spacing: 0) + + self.hourPicker.wrap = true + self.hourPicker.orientation = .vertical + self.minutePicker.wrap = true + self.minutePicker.orientation = .vertical + + self.orientation = .horizontal + + self.add(self.hourPicker) + self.add(self.hourMinuteSeparator) + self.add(self.minutePicker) + } + + func setEnabled(to isEnabled: Bool) { + hourPicker.sensitive = isEnabled + } + + private static func minHour(for hourCycle: Locale.HourCycle) -> Double { + switch hourCycle { + case .zeroToEleven, .zeroToTwentyThree: 0 + case .oneToTwelve, .oneToTwentyFour: 1 + } + } + + private static func maxHour(for hourCycle: Locale.HourCycle) -> Double { + switch hourCycle { + case .zeroToEleven: 11 + case .oneToTwelve: 12 + case .zeroToTwentyThree: 23 + case .oneToTwentyFour: 24 + } + } + + func update(calendar: Calendar, date: Date, showSeconds: Bool) { + let components = calendar.dateComponents([.hour, .minute, .second], from: date) + + if showSeconds { + if let secondsPicker { + // TODO update range + } else { + minuteSecondSeparator = Label(string: ":") + let secondsRange = calendar.range(of: .second, in: .minute, for: date) ?? 0..<60 + secondsPicker = SpinButton( + range: Double(secondsRange.lowerBound), + max: Double(secondsRange.upperBound - 1), + step: 1 + ) + secondsPicker.value = Double(components.second!) + insert(child: minuteSecondSeparator!, after: minutePicker) + insert(child: secondsPicker!, after: minuteSecondSeparator!) + } + } else { + if let minuteSecondSeparator { + remove(minuteSecondSeparator) + self.minuteSecondSeparator = nil + } + if let secondsPicker { + remove(secondsPicker) + self.secondsPicker = nil + } + } + + minutePicker.value = Double(components.minute!) + // TODO update minutePicker's range and everything about the hour + } +} + +final class DatePickerWidget: Box { +} From d763c22f387a43e4762c693f010fcdb50cd3988f Mon Sep 17 00:00:00 2001 From: William Baker Date: Tue, 4 Nov 2025 19:25:03 -0500 Subject: [PATCH 20/27] More partial work --- .../Widgets/SpinButton+ManualAdditions.swift | 7 ++ Sources/GtkBackend/GtkBackend.swift | 85 ++++++++++++++++--- 2 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 Sources/Gtk/Widgets/SpinButton+ManualAdditions.swift diff --git a/Sources/Gtk/Widgets/SpinButton+ManualAdditions.swift b/Sources/Gtk/Widgets/SpinButton+ManualAdditions.swift new file mode 100644 index 0000000000..1980000d06 --- /dev/null +++ b/Sources/Gtk/Widgets/SpinButton+ManualAdditions.swift @@ -0,0 +1,7 @@ +import CGtk + +extension SpinButton { + public func setRange(min: Double, max: Double) { + gtk_spin_button_set_range(opaquePointer, min, max) + } +} diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index da98221688..9cb368cf7d 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1500,6 +1500,26 @@ public final class GtkBackend: AppBackend { } } + public func createDatePicker() -> Widget { + return TimePicker() + } + + public func updateDatePicker( + _ datePicker: Widget, + environment: EnvironmentValues, + date: Date, + range: ClosedRange, + components: DatePickerComponents, + onChange: @escaping (Date) -> Void + ) { + let timePicker = datePicker as! TimePicker + timePicker.update( + calendar: environment.calendar, + date: date, + showSeconds: components.contains(.hourMinuteAndSecond) + ) + } + // MARK: Helpers private func wrapInCustomRootContainer(_ widget: Widget) -> Widget { @@ -1635,37 +1655,80 @@ final class TimePicker: Box { } } - func update(calendar: Calendar, date: Date, showSeconds: Bool) { + func update(calendar: Foundation.Calendar, date: Date, showSeconds: Bool) { let components = calendar.dateComponents([.hour, .minute, .second], from: date) if showSeconds { - if let secondsPicker { - // TODO update range + let secondsRange = calendar.range(of: .second, in: .minute, for: date) ?? 0..<60 + if let secondPicker { + secondPicker.setRange( + min: Double(secondsRange.lowerBound), + max: Double(secondsRange.upperBound - 1) + ) } else { minuteSecondSeparator = Label(string: ":") - let secondsRange = calendar.range(of: .second, in: .minute, for: date) ?? 0..<60 - secondsPicker = SpinButton( + secondPicker = SpinButton( range: Double(secondsRange.lowerBound), max: Double(secondsRange.upperBound - 1), step: 1 ) - secondsPicker.value = Double(components.second!) + secondPicker!.value = Double(components.second!) insert(child: minuteSecondSeparator!, after: minutePicker) - insert(child: secondsPicker!, after: minuteSecondSeparator!) + insert(child: secondPicker!, after: minuteSecondSeparator!) } } else { if let minuteSecondSeparator { remove(minuteSecondSeparator) self.minuteSecondSeparator = nil } - if let secondsPicker { - remove(secondsPicker) - self.secondsPicker = nil + if let secondPicker { + remove(secondPicker) + self.secondPicker = nil } } + let minutesRange = calendar.range(of: .minute, in: .hour, for: date) ?? 0..<60 minutePicker.value = Double(components.minute!) - // TODO update minutePicker's range and everything about the hour + minutePicker.setRange( + min: Double(minutesRange.lowerBound), + max: Double(minutesRange.upperBound - 1) + ) + + let hoursRange = calendar.range(of: .hour, in: .day, for: date) + self.hourCycle = (calendar.locale ?? .current).hourCycle + let effectiveHours = hoursRange?.map { + TimePicker.transformToRange($0, hourCycle: self.hourCycle) + } + + hourPicker.setRange( + min: effectiveHours?.min().map(Double.init(_:)) + ?? TimePicker.minHour(for: self.hourCycle), + max: effectiveHours?.max().map(Double.init(_:)) + ?? TimePicker.maxHour(for: self.hourCycle) + ) + + if self.hourCycle == .oneToTwelve || self.hourCycle == .zeroToEleven { + if let amPmPicker { + // update strings if necessary + } else { + amPmPicker = DropDown(strings: [calendar.amSymbol, calendar.pmSymbol]) + add(amPmPicker!) + } + } else { + if let amPmPicker { + remove(amPmPicker) + self.amPmPicker = nil + } + } + } + + private static func transformToRange(_ value: Int, hourCycle: Locale.HourCycle) -> Int { + switch hourCycle { + case .zeroToEleven: value % 12 + case .oneToTwelve: (value + 11) % 12 + 1 + case .zeroToTwentyThree: value % 24 + case .oneToTwentyFour: (value + 23) % 24 + 1 + } } } From 12265d2138558982a0619df10cd154d3a1203384 Mon Sep 17 00:00:00 2001 From: William Baker Date: Wed, 5 Nov 2025 23:26:49 -0500 Subject: [PATCH 21/27] Use Gtk.Calendar --- .../DatePickerExample/DatePickerApp.swift | 18 +++-- Sources/Gtk/Utility/GDateTime.swift | 55 +++++++++++++++ Sources/Gtk/Utility/GTimeZone.swift | 19 +++++ .../Widgets/Calendar+ManualAdditions.swift | 15 ++++ Sources/GtkBackend/GtkBackend.swift | 70 +++++++++++++++---- Sources/SwiftCrossUI/Views/DatePicker.swift | 10 ++- 6 files changed, 160 insertions(+), 27 deletions(-) create mode 100644 Sources/Gtk/Utility/GDateTime.swift create mode 100644 Sources/Gtk/Utility/GTimeZone.swift create mode 100644 Sources/Gtk/Widgets/Calendar+ManualAdditions.swift diff --git a/Examples/Sources/DatePickerExample/DatePickerApp.swift b/Examples/Sources/DatePickerExample/DatePickerApp.swift index 8ca80119f0..d27562ce15 100644 --- a/Examples/Sources/DatePickerExample/DatePickerApp.swift +++ b/Examples/Sources/DatePickerExample/DatePickerApp.swift @@ -21,12 +21,14 @@ struct DatePickerApp: App { allStyles.append(.graphical) } - if #available(iOS 13.4, macCatalyst 13.4, *) { - allStyles.append(.compact) - #if os(iOS) || os(visionOS) || canImport(WinUIBackend) - allStyles.append(.wheel) - #endif - } + #if !canImport(GtkBackend) + if #available(iOS 13.4, macCatalyst 13.4, *) { + allStyles.append(.compact) + #if os(iOS) || os(visionOS) || canImport(WinUIBackend) + allStyles.append(.wheel) + #endif + } + #endif } var body: some Scene { @@ -41,6 +43,10 @@ struct DatePickerApp: App { selection: $date ) .datePickerStyle(style ?? .automatic) + + Button("Reset date") { + date = Date() + } } } } diff --git a/Sources/Gtk/Utility/GDateTime.swift b/Sources/Gtk/Utility/GDateTime.swift new file mode 100644 index 0000000000..a4d61cb666 --- /dev/null +++ b/Sources/Gtk/Utility/GDateTime.swift @@ -0,0 +1,55 @@ +import CGtk +import Foundation + +public class GDateTime { + public let pointer: OpaquePointer + + public init(_ pointer: OpaquePointer) { + self.pointer = pointer + } + + public init?(_ pointer: OpaquePointer?) { + guard let pointer else { return nil } + self.pointer = pointer + } + + public convenience init?(unixEpoch: TimeInterval) { + // g_date_time_new_from_unix_utc_usec appears to be too new + self.init(g_date_time_new_from_unix_utc(gint64(unixEpoch))) + } + + public convenience init?( + timeZone: GTimeZone, + year: Int, + month: Int, + day: Int, + hour: Int, + minute: Int, + second: Double + ) { + self.init( + g_date_time_new( + timeZone.pointer, + gint(year), + gint(month), + gint(day), + gint(hour), + gint(minute), + second + ) + ) + } + + public convenience init!(_ date: Date) { + self.init(unixEpoch: date.timeIntervalSince1970) + } + + deinit { + g_date_time_unref(pointer) + } + + public func toDate() -> Date { + let offset = g_date_time_to_unix(pointer) + return Date(timeIntervalSince1970: Double(offset)) + } +} diff --git a/Sources/Gtk/Utility/GTimeZone.swift b/Sources/Gtk/Utility/GTimeZone.swift new file mode 100644 index 0000000000..7190315e07 --- /dev/null +++ b/Sources/Gtk/Utility/GTimeZone.swift @@ -0,0 +1,19 @@ +import CGtk +import Foundation + +public final class GTimeZone { + public let pointer: OpaquePointer + + public init?(identifier: String) { + guard let pointer = g_time_zone_new_identifier(identifier) else { return nil } + self.pointer = pointer + } + + public convenience init?(_ timeZone: TimeZone) { + self.init(identifier: timeZone.identifier) + } + + deinit { + g_time_zone_unref(pointer) + } +} diff --git a/Sources/Gtk/Widgets/Calendar+ManualAdditions.swift b/Sources/Gtk/Widgets/Calendar+ManualAdditions.swift new file mode 100644 index 0000000000..bfc400c1dc --- /dev/null +++ b/Sources/Gtk/Widgets/Calendar+ManualAdditions.swift @@ -0,0 +1,15 @@ +import CGtk +import Foundation + +extension Calendar { + public var date: Date { + get { + GDateTime(gtk_calendar_get_date(opaquePointer)).toDate() + } + set { + withExtendedLifetime(GDateTime(newValue)) { gDateTime in + gtk_calendar_select_day(opaquePointer, gDateTime.pointer) + } + } + } +} diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 9cb368cf7d..bf13f31311 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1501,7 +1501,9 @@ public final class GtkBackend: AppBackend { } public func createDatePicker() -> Widget { - return TimePicker() + let widget = Gtk.Calendar() + widget.date = Date() + return widget } public func updateDatePicker( @@ -1512,12 +1514,18 @@ public final class GtkBackend: AppBackend { components: DatePickerComponents, onChange: @escaping (Date) -> Void ) { - let timePicker = datePicker as! TimePicker - timePicker.update( - calendar: environment.calendar, - date: date, - showSeconds: components.contains(.hourMinuteAndSecond) - ) + if components.contains(.hourAndMinute) { + print("Warning: time picker is unimplemented on GtkBackend") + } + if environment.datePickerStyle == .wheel || environment.datePickerStyle == .compact { + print("Warning: only datePickerStyle.graphical is implemented in GtkBackend") + } + + let calendarWidget = datePicker as! Gtk.Calendar + calendarWidget.date = date + calendarWidget.daySelected = { calendarWidget in + onChange(calendarWidget.date) + } } // MARK: Helpers @@ -1602,6 +1610,9 @@ class CustomListBox: ListBox { var cachedSelection: Int? = nil } +// This kinda sorta works. Beyond the fact that it never shows the AM/PM picker, the SpinButtons +// don't behave correctly on change, and calendar.date(bySetting:value:of:) doesn't do what we need +// it to do. final class TimePicker: Box { private var hourCycle: Locale.HourCycle private let hourPicker: SpinButton @@ -1611,6 +1622,8 @@ final class TimePicker: Box { private var secondPicker: SpinButton? private var amPmPicker: DropDown? + var onChange: ((Date) -> Void)? + init() { let hourCycle = Locale.current.hourCycle @@ -1621,14 +1634,14 @@ final class TimePicker: Box { step: 1 ) - super.init(orientation: .horizontal, spacing: 0) + super.init(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0)) self.hourPicker.wrap = true self.hourPicker.orientation = .vertical + self.hourPicker.numeric = true self.minutePicker.wrap = true self.minutePicker.orientation = .vertical - - self.orientation = .horizontal + self.minutePicker.numeric = true self.add(self.hourPicker) self.add(self.hourMinuteSeparator) @@ -1643,6 +1656,9 @@ final class TimePicker: Box { switch hourCycle { case .zeroToEleven, .zeroToTwentyThree: 0 case .oneToTwelve, .oneToTwentyFour: 1 + #if os(macOS) + @unknown default: fatalError() + #endif } } @@ -1652,6 +1668,9 @@ final class TimePicker: Box { case .oneToTwelve: 12 case .zeroToTwentyThree: 23 case .oneToTwentyFour: 24 + #if os(macOS) + @unknown default: fatalError() + #endif } } @@ -1672,7 +1691,9 @@ final class TimePicker: Box { max: Double(secondsRange.upperBound - 1), step: 1 ) - secondPicker!.value = Double(components.second!) + secondPicker!.numeric = true + secondPicker!.wrap = true + secondPicker!.text = "\(components.second!)" insert(child: minuteSecondSeparator!, after: minutePicker) insert(child: secondPicker!, after: minuteSecondSeparator!) } @@ -1688,11 +1709,19 @@ final class TimePicker: Box { } let minutesRange = calendar.range(of: .minute, in: .hour, for: date) ?? 0..<60 - minutePicker.value = Double(components.minute!) minutePicker.setRange( min: Double(minutesRange.lowerBound), max: Double(minutesRange.upperBound - 1) ) + minutePicker.text = "\(components.minute!)" + minutePicker.valueChanged = { [unowned self] minutePicker in + guard let value = Int(exactly: minutePicker.value), + let newDate = calendar.date(bySetting: .minute, value: value, of: date) + else { + return + } + self.onChange?(newDate) + } let hoursRange = calendar.range(of: .hour, in: .day, for: date) self.hourCycle = (calendar.locale ?? .current).hourCycle @@ -1720,6 +1749,17 @@ final class TimePicker: Box { self.amPmPicker = nil } } + + hourPicker.text = + "\(TimePicker.transformToRange(components.hour!, hourCycle: self.hourCycle))" + hourPicker.valueChanged = { [unowned self] hourPicker in + guard let value = Int(exactly: hourPicker.value), + let newDate = calendar.date(bySetting: .hour, value: value, of: date) + else { + return + } + self.onChange?(newDate) + } } private static func transformToRange(_ value: Int, hourCycle: Locale.HourCycle) -> Int { @@ -1728,9 +1768,9 @@ final class TimePicker: Box { case .oneToTwelve: (value + 11) % 12 + 1 case .zeroToTwentyThree: value % 24 case .oneToTwentyFour: (value + 23) % 24 + 1 + #if os(macOS) + @unknown default: fatalError() + #endif } } } - -final class DatePickerWidget: Box { -} diff --git a/Sources/SwiftCrossUI/Views/DatePicker.swift b/Sources/SwiftCrossUI/Views/DatePicker.swift index 7e1dc40a0f..7d4a7dd2d1 100644 --- a/Sources/SwiftCrossUI/Views/DatePicker.swift +++ b/Sources/SwiftCrossUI/Views/DatePicker.swift @@ -52,9 +52,8 @@ public struct DatePicker { /// - Parameters: /// - selection: The currently-selected date. /// - range: The range of dates to display. The backend takes this as a hint but it is not - /// necessarily enforced; in particular, a backend may be able to limit the date but not the - /// time, or only the year and not the month and day. As such this parameter should be - /// treated as an aid to validation rather than a replacement for it. + /// necessarily enforced. As such this parameter should be treated as an aid to validation + /// rather than a replacement for it. /// - displayedComponents: What parts of the date/time to display in the input. /// - label: The view to be shown next to the date input. public nonisolated init( @@ -74,9 +73,8 @@ public struct DatePicker { /// - label: The text to be shown next to the date input. /// - selection: The currently-selected date. /// - range: The range of dates to display. The backend takes this as a hint but it is not - /// necessarily enforced; in particular, a backend may be able to limit the date but not the - /// time, or only the year and not the month and day. As such this parameter should be - /// treated as an aid to validation rather than a replacement for it. + /// necessarily enforced. As such this parameter should be treated as an aid to validation + /// rather than a replacement for it. /// - displayedComponents: What parts of the date/time to display in the input. public nonisolated init( _ label: String, From 9b591442085fc8db48a2fc8c41aa08788416acff Mon Sep 17 00:00:00 2001 From: William Baker Date: Wed, 5 Nov 2025 23:35:06 -0500 Subject: [PATCH 22/27] Add missing parts to GtkBackend.updateDatePicker --- Sources/GtkBackend/GtkBackend.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index bf13f31311..6e6730f697 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1526,6 +1526,9 @@ public final class GtkBackend: AppBackend { calendarWidget.daySelected = { calendarWidget in onChange(calendarWidget.date) } + calendarWidget.sensitive = environment.isEnabled + calendarWidget.css.clear() + calendarWidget.css.set(properties: Self.cssProperties(for: environment, isControl: true)) } // MARK: Helpers From 597c64e8042c90a5128db225eb5c037420fdb6f6 Mon Sep 17 00:00:00 2001 From: William Baker Date: Wed, 5 Nov 2025 23:41:51 -0500 Subject: [PATCH 23/27] Add DatePickerExample to Linux CI --- .github/workflows/build-test-and-docs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test-and-docs.yml b/.github/workflows/build-test-and-docs.yml index 5558e9865a..1386767cfd 100644 --- a/.github/workflows/build-test-and-docs.yml +++ b/.github/workflows/build-test-and-docs.yml @@ -298,7 +298,8 @@ jobs: swift build --target StressTestExample && \ swift build --target SpreadsheetExample && \ swift build --target NotesExample && \ - swift build --target GtkExample + swift build --target GtkExample && \ + swift build --target DatePickerExample - name: Test run: swift test --test-product swift-cross-uiPackageTests From fee1fca5aa7bf2d018ae2cbce0ccce88dfd445e6 Mon Sep 17 00:00:00 2001 From: William Baker Date: Thu, 6 Nov 2025 00:41:46 -0500 Subject: [PATCH 24/27] Fix one Mac availability error --- Sources/GtkBackend/GtkBackend.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 6e6730f697..f4528adead 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1517,8 +1517,8 @@ public final class GtkBackend: AppBackend { if components.contains(.hourAndMinute) { print("Warning: time picker is unimplemented on GtkBackend") } - if environment.datePickerStyle == .wheel || environment.datePickerStyle == .compact { - print("Warning: only datePickerStyle.graphical is implemented in GtkBackend") + if environment.datePickerStyle != .automatic && environment.datePickerStyle != .graphical { + print("Warning: only DatePickerStyle.graphical is implemented in GtkBackend") } let calendarWidget = datePicker as! Gtk.Calendar From 8d7c88873c39693c73fd354a4cd1309aca60bc8a Mon Sep 17 00:00:00 2001 From: William Baker Date: Thu, 6 Nov 2025 07:59:19 -0500 Subject: [PATCH 25/27] Add availability annotation on unused widget --- Sources/GtkBackend/GtkBackend.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index f4528adead..2753cb9141 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -1616,6 +1616,7 @@ class CustomListBox: ListBox { // This kinda sorta works. Beyond the fact that it never shows the AM/PM picker, the SpinButtons // don't behave correctly on change, and calendar.date(bySetting:value:of:) doesn't do what we need // it to do. +@available(macOS 13, *) final class TimePicker: Box { private var hourCycle: Locale.HourCycle private let hourPicker: SpinButton From 00205990880d11eed2b2f31e065c124dae6a96fc Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 7 Nov 2025 18:23:24 -0500 Subject: [PATCH 26/27] Add time zone listener for UIKitBackend --- Sources/UIKitBackend/UIKitBackend.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Sources/UIKitBackend/UIKitBackend.swift b/Sources/UIKitBackend/UIKitBackend.swift index f7580a6a06..0e385649fe 100644 --- a/Sources/UIKitBackend/UIKitBackend.swift +++ b/Sources/UIKitBackend/UIKitBackend.swift @@ -10,6 +10,8 @@ public final class UIKitBackend: AppBackend { static var mainWindow: UIWindow? static var hasReturnedAWindow = false + private var timeZoneObserver: NSObjectProtocol? + public let scrollBarWidth = 0 public let defaultPaddingAmount = 15 public let requiresToggleSwitchSpacer = true @@ -87,6 +89,7 @@ public final class UIKitBackend: AppBackend { var environment = defaultEnvironment environment.toggleStyle = .switch + environment.timeZone = .current switch UITraitCollection.current.userInterfaceStyle { case .light: @@ -102,6 +105,17 @@ public final class UIKitBackend: AppBackend { public func setRootEnvironmentChangeHandler(to action: @escaping () -> Void) { onTraitCollectionChange = action + if timeZoneObserver == nil { + timeZoneObserver = NotificationCenter.default.addObserver( + forName: .NSSystemTimeZoneDidChange, + object: nil, + queue: .main + ) { [unowned self] _ in + MainActor.assumeIsolated { + self.onTraitCollectionChange?() + } + } + } } public func computeWindowEnvironment( From 4df76941b212de74aa51daea33018dd1d861962c Mon Sep 17 00:00:00 2001 From: Riley Baker Date: Fri, 7 Nov 2025 18:30:13 -0500 Subject: [PATCH 27/27] Add listener for AppKitBackend --- Sources/AppKitBackend/AppKitBackend.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index 27325bc439..dec3c38ddb 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -349,6 +349,14 @@ public final class AppKitBackend: AppBackend { // Self.scrollBarWidth has changed action() } + + NotificationCenter.default.addObserver( + forName: .NSSystemTimeZoneDidChange, + object: nil, + queue: .main + ) { _ in + action() + } } public func computeWindowEnvironment(