Skip to content

Commit 12265d2

Browse files
committed
Use Gtk.Calendar
1 parent d763c22 commit 12265d2

File tree

6 files changed

+160
-27
lines changed

6 files changed

+160
-27
lines changed

Examples/Sources/DatePickerExample/DatePickerApp.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ struct DatePickerApp: App {
2121
allStyles.append(.graphical)
2222
}
2323

24-
if #available(iOS 13.4, macCatalyst 13.4, *) {
25-
allStyles.append(.compact)
26-
#if os(iOS) || os(visionOS) || canImport(WinUIBackend)
27-
allStyles.append(.wheel)
28-
#endif
29-
}
24+
#if !canImport(GtkBackend)
25+
if #available(iOS 13.4, macCatalyst 13.4, *) {
26+
allStyles.append(.compact)
27+
#if os(iOS) || os(visionOS) || canImport(WinUIBackend)
28+
allStyles.append(.wheel)
29+
#endif
30+
}
31+
#endif
3032
}
3133

3234
var body: some Scene {
@@ -41,6 +43,10 @@ struct DatePickerApp: App {
4143
selection: $date
4244
)
4345
.datePickerStyle(style ?? .automatic)
46+
47+
Button("Reset date") {
48+
date = Date()
49+
}
4450
}
4551
}
4652
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import CGtk
2+
import Foundation
3+
4+
public class GDateTime {
5+
public let pointer: OpaquePointer
6+
7+
public init(_ pointer: OpaquePointer) {
8+
self.pointer = pointer
9+
}
10+
11+
public init?(_ pointer: OpaquePointer?) {
12+
guard let pointer else { return nil }
13+
self.pointer = pointer
14+
}
15+
16+
public convenience init?(unixEpoch: TimeInterval) {
17+
// g_date_time_new_from_unix_utc_usec appears to be too new
18+
self.init(g_date_time_new_from_unix_utc(gint64(unixEpoch)))
19+
}
20+
21+
public convenience init?(
22+
timeZone: GTimeZone,
23+
year: Int,
24+
month: Int,
25+
day: Int,
26+
hour: Int,
27+
minute: Int,
28+
second: Double
29+
) {
30+
self.init(
31+
g_date_time_new(
32+
timeZone.pointer,
33+
gint(year),
34+
gint(month),
35+
gint(day),
36+
gint(hour),
37+
gint(minute),
38+
second
39+
)
40+
)
41+
}
42+
43+
public convenience init!(_ date: Date) {
44+
self.init(unixEpoch: date.timeIntervalSince1970)
45+
}
46+
47+
deinit {
48+
g_date_time_unref(pointer)
49+
}
50+
51+
public func toDate() -> Date {
52+
let offset = g_date_time_to_unix(pointer)
53+
return Date(timeIntervalSince1970: Double(offset))
54+
}
55+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import CGtk
2+
import Foundation
3+
4+
public final class GTimeZone {
5+
public let pointer: OpaquePointer
6+
7+
public init?(identifier: String) {
8+
guard let pointer = g_time_zone_new_identifier(identifier) else { return nil }
9+
self.pointer = pointer
10+
}
11+
12+
public convenience init?(_ timeZone: TimeZone) {
13+
self.init(identifier: timeZone.identifier)
14+
}
15+
16+
deinit {
17+
g_time_zone_unref(pointer)
18+
}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import CGtk
2+
import Foundation
3+
4+
extension Calendar {
5+
public var date: Date {
6+
get {
7+
GDateTime(gtk_calendar_get_date(opaquePointer)).toDate()
8+
}
9+
set {
10+
withExtendedLifetime(GDateTime(newValue)) { gDateTime in
11+
gtk_calendar_select_day(opaquePointer, gDateTime.pointer)
12+
}
13+
}
14+
}
15+
}

Sources/GtkBackend/GtkBackend.swift

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,7 +1501,9 @@ public final class GtkBackend: AppBackend {
15011501
}
15021502

15031503
public func createDatePicker() -> Widget {
1504-
return TimePicker()
1504+
let widget = Gtk.Calendar()
1505+
widget.date = Date()
1506+
return widget
15051507
}
15061508

15071509
public func updateDatePicker(
@@ -1512,12 +1514,18 @@ public final class GtkBackend: AppBackend {
15121514
components: DatePickerComponents,
15131515
onChange: @escaping (Date) -> Void
15141516
) {
1515-
let timePicker = datePicker as! TimePicker
1516-
timePicker.update(
1517-
calendar: environment.calendar,
1518-
date: date,
1519-
showSeconds: components.contains(.hourMinuteAndSecond)
1520-
)
1517+
if components.contains(.hourAndMinute) {
1518+
print("Warning: time picker is unimplemented on GtkBackend")
1519+
}
1520+
if environment.datePickerStyle == .wheel || environment.datePickerStyle == .compact {
1521+
print("Warning: only datePickerStyle.graphical is implemented in GtkBackend")
1522+
}
1523+
1524+
let calendarWidget = datePicker as! Gtk.Calendar
1525+
calendarWidget.date = date
1526+
calendarWidget.daySelected = { calendarWidget in
1527+
onChange(calendarWidget.date)
1528+
}
15211529
}
15221530

15231531
// MARK: Helpers
@@ -1602,6 +1610,9 @@ class CustomListBox: ListBox {
16021610
var cachedSelection: Int? = nil
16031611
}
16041612

1613+
// This kinda sorta works. Beyond the fact that it never shows the AM/PM picker, the SpinButtons
1614+
// don't behave correctly on change, and calendar.date(bySetting:value:of:) doesn't do what we need
1615+
// it to do.
16051616
final class TimePicker: Box {
16061617
private var hourCycle: Locale.HourCycle
16071618
private let hourPicker: SpinButton
@@ -1611,6 +1622,8 @@ final class TimePicker: Box {
16111622
private var secondPicker: SpinButton?
16121623
private var amPmPicker: DropDown?
16131624

1625+
var onChange: ((Date) -> Void)?
1626+
16141627
init() {
16151628
let hourCycle = Locale.current.hourCycle
16161629

@@ -1621,14 +1634,14 @@ final class TimePicker: Box {
16211634
step: 1
16221635
)
16231636

1624-
super.init(orientation: .horizontal, spacing: 0)
1637+
super.init(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0))
16251638

16261639
self.hourPicker.wrap = true
16271640
self.hourPicker.orientation = .vertical
1641+
self.hourPicker.numeric = true
16281642
self.minutePicker.wrap = true
16291643
self.minutePicker.orientation = .vertical
1630-
1631-
self.orientation = .horizontal
1644+
self.minutePicker.numeric = true
16321645

16331646
self.add(self.hourPicker)
16341647
self.add(self.hourMinuteSeparator)
@@ -1643,6 +1656,9 @@ final class TimePicker: Box {
16431656
switch hourCycle {
16441657
case .zeroToEleven, .zeroToTwentyThree: 0
16451658
case .oneToTwelve, .oneToTwentyFour: 1
1659+
#if os(macOS)
1660+
@unknown default: fatalError()
1661+
#endif
16461662
}
16471663
}
16481664

@@ -1652,6 +1668,9 @@ final class TimePicker: Box {
16521668
case .oneToTwelve: 12
16531669
case .zeroToTwentyThree: 23
16541670
case .oneToTwentyFour: 24
1671+
#if os(macOS)
1672+
@unknown default: fatalError()
1673+
#endif
16551674
}
16561675
}
16571676

@@ -1672,7 +1691,9 @@ final class TimePicker: Box {
16721691
max: Double(secondsRange.upperBound - 1),
16731692
step: 1
16741693
)
1675-
secondPicker!.value = Double(components.second!)
1694+
secondPicker!.numeric = true
1695+
secondPicker!.wrap = true
1696+
secondPicker!.text = "\(components.second!)"
16761697
insert(child: minuteSecondSeparator!, after: minutePicker)
16771698
insert(child: secondPicker!, after: minuteSecondSeparator!)
16781699
}
@@ -1688,11 +1709,19 @@ final class TimePicker: Box {
16881709
}
16891710

16901711
let minutesRange = calendar.range(of: .minute, in: .hour, for: date) ?? 0..<60
1691-
minutePicker.value = Double(components.minute!)
16921712
minutePicker.setRange(
16931713
min: Double(minutesRange.lowerBound),
16941714
max: Double(minutesRange.upperBound - 1)
16951715
)
1716+
minutePicker.text = "\(components.minute!)"
1717+
minutePicker.valueChanged = { [unowned self] minutePicker in
1718+
guard let value = Int(exactly: minutePicker.value),
1719+
let newDate = calendar.date(bySetting: .minute, value: value, of: date)
1720+
else {
1721+
return
1722+
}
1723+
self.onChange?(newDate)
1724+
}
16961725

16971726
let hoursRange = calendar.range(of: .hour, in: .day, for: date)
16981727
self.hourCycle = (calendar.locale ?? .current).hourCycle
@@ -1720,6 +1749,17 @@ final class TimePicker: Box {
17201749
self.amPmPicker = nil
17211750
}
17221751
}
1752+
1753+
hourPicker.text =
1754+
"\(TimePicker.transformToRange(components.hour!, hourCycle: self.hourCycle))"
1755+
hourPicker.valueChanged = { [unowned self] hourPicker in
1756+
guard let value = Int(exactly: hourPicker.value),
1757+
let newDate = calendar.date(bySetting: .hour, value: value, of: date)
1758+
else {
1759+
return
1760+
}
1761+
self.onChange?(newDate)
1762+
}
17231763
}
17241764

17251765
private static func transformToRange(_ value: Int, hourCycle: Locale.HourCycle) -> Int {
@@ -1728,9 +1768,9 @@ final class TimePicker: Box {
17281768
case .oneToTwelve: (value + 11) % 12 + 1
17291769
case .zeroToTwentyThree: value % 24
17301770
case .oneToTwentyFour: (value + 23) % 24 + 1
1771+
#if os(macOS)
1772+
@unknown default: fatalError()
1773+
#endif
17311774
}
17321775
}
17331776
}
1734-
1735-
final class DatePickerWidget: Box {
1736-
}

Sources/SwiftCrossUI/Views/DatePicker.swift

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ public struct DatePicker<Label: View> {
5252
/// - Parameters:
5353
/// - selection: The currently-selected date.
5454
/// - range: The range of dates to display. The backend takes this as a hint but it is not
55-
/// necessarily enforced; in particular, a backend may be able to limit the date but not the
56-
/// time, or only the year and not the month and day. As such this parameter should be
57-
/// treated as an aid to validation rather than a replacement for it.
55+
/// necessarily enforced. As such this parameter should be treated as an aid to validation
56+
/// rather than a replacement for it.
5857
/// - displayedComponents: What parts of the date/time to display in the input.
5958
/// - label: The view to be shown next to the date input.
6059
public nonisolated init(
@@ -74,9 +73,8 @@ public struct DatePicker<Label: View> {
7473
/// - label: The text to be shown next to the date input.
7574
/// - selection: The currently-selected date.
7675
/// - range: The range of dates to display. The backend takes this as a hint but it is not
77-
/// necessarily enforced; in particular, a backend may be able to limit the date but not the
78-
/// time, or only the year and not the month and day. As such this parameter should be
79-
/// treated as an aid to validation rather than a replacement for it.
76+
/// necessarily enforced. As such this parameter should be treated as an aid to validation
77+
/// rather than a replacement for it.
8078
/// - displayedComponents: What parts of the date/time to display in the input.
8179
public nonisolated init(
8280
_ label: String,

0 commit comments

Comments
 (0)