Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/build-test-and-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -106,9 +107,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
Expand Down Expand Up @@ -165,6 +167,7 @@ jobs:
buildtarget PathsExample
buildtarget ControlsExample
buildtarget RandomNumberGeneratorExample
buildtarget DatePickerExample
# TODO test whether this works on Catalyst
# buildtarget SplitExample

Expand Down Expand Up @@ -305,7 +308,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
Expand Down
5 changes: 5 additions & 0 deletions Examples/Bundler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
12 changes: 6 additions & 6 deletions Examples/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Examples/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ let package = Package(
.executableTarget(
name: "HoverExample",
dependencies: exampleDependencies
),
.executableTarget(
name: "DatePickerExample",
dependencies: exampleDependencies
)
]
)
38 changes: 38 additions & 0 deletions Examples/Sources/DatePickerExample/DatePickerApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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? = .automatic

@Environment(\.supportedDatePickerStyles) var allStyles: [DatePickerStyle]

var body: some Scene {
WindowGroup("Date Picker") {
#hotReloadable {
VStack {
Text("Selected date: \(date)")

Picker(of: allStyles, selection: $style)

DatePicker(
"Test Picker",
selection: $date
)
.datePickerStyle(style ?? .automatic)

Button("Reset date to now") {
date = Date()
}
}
}
}
}
}
12 changes: 6 additions & 6 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ let package = Package(
),
.package(
url: "https://github.com/stackotter/swift-windowsappsdk",
revision: "ba6f0ec377b70d8be835d253102ff665a0e47d99"
revision: "f1c50892f10c0f7f635d3c7a3d728fd634ad001a"
),
.package(
url: "https://github.com/stackotter/swift-windowsfoundation",
revision: "4ad57d20553514bcb23724bdae9121569b19f172"
),
.package(
url: "https://github.com/stackotter/swift-winui",
revision: "1695ee3ea2b7a249f6504c7f1759e7ec7a38eb86"
revision: "42c47f4e4129c8b5a5d9912f05e1168c924ac180"
),
// .package(
// url: "https://github.com/stackotter/TermKit",
Expand Down
99 changes: 99 additions & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public final class AppKitBackend: AppBackend {
public let menuImplementationStyle = MenuImplementationStyle.dynamicPopover
public let canRevealFiles = true
public let deviceClass = DeviceClass.desktop
public let supportedDatePickerStyles: [DatePickerStyle] = [.automatic, .graphical, .compact]

public var scrollBarWidth: Int {
// We assume that all scrollers have their controlSize set to `.regular` by default.
Expand Down Expand Up @@ -350,6 +351,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(
Expand Down Expand Up @@ -1788,6 +1797,80 @@ public final class AppKitBackend: AppBackend {
parent.endSheet(sheet)
parent.nestedSheet = nil
}

public func createDatePicker() -> NSView {
let datePicker = CustomDatePicker()
datePicker.delegate = datePicker.strongDelegate
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.
private let calendarsRequiringEra: Set<Calendar.Identifier> = [
.buddhist, .coptic, .ethiopicAmeteAlem, .ethiopicAmeteMihret, .indian, .islamic,
.islamicCivil, .islamicTabular, .islamicUmmAlQura, .japanese, .persian, .republicOfChina,
]

public func updateDatePicker(
_ datePicker: NSView,
environment: EnvironmentValues,
date: Date,
range: ClosedRange<Date>,
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

// 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
}

if datePicker.dateValue != date {
datePicker.dateValue = date
}

var elementFlags: NSDatePicker.ElementFlags = []
if components.contains(.date) {
elementFlags.insert(.yearMonthDay)
if calendarsRequiringEra.contains(environment.calendar.identifier) {
elementFlags.insert(.era)
}
}
if components.contains(.hourMinuteAndSecond) {
elementFlags.insert(.hourMinuteSecond)
} else if components.contains(.hourAndMinute) {
elementFlags.insert(.hourMinute)
}

if datePicker.datePickerElements != elementFlags {
datePicker.datePickerElements = elementFlags
}

datePicker.strongDelegate.onChange = onChange

datePicker.minDate = range.lowerBound
datePicker.maxDate = range.upperBound

datePicker.datePickerStyle =
switch environment.datePickerStyle {
case .automatic, .compact:
.textFieldAndStepper
case .graphical:
.clockAndCalendar
}
}
}

public final class NSCustomSheet: NSCustomWindow, NSWindowDelegate {
Expand Down Expand Up @@ -2310,3 +2393,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<NSDate>,
timeInterval _: UnsafeMutablePointer<TimeInterval>?
) {
onChange?(proposedDateValue.pointee as Date)
}
}
Loading
Loading