Skip to content

Commit f375400

Browse files
committed
Reimplement tables under new layout system and allow cells to contain arbitrary views
These changes took a lot of thinking but I'm pretty happy with how they turned out
1 parent f3d438b commit f375400

File tree

17 files changed

+1468
-144
lines changed

17 files changed

+1468
-144
lines changed

Examples/Sources/SpreadsheetExample/SpreadsheetApp.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Foundation
12
import SelectedBackend
23
import SwiftCrossUI
34

@@ -25,6 +26,9 @@ class SpreadsheetState: Observable {
2526
email: "bob@example.com", occupation: "adversary"
2627
),
2728
]
29+
30+
@Observed
31+
var greeting: String?
2832
}
2933

3034
@main
@@ -37,14 +41,28 @@ struct SpreadsheetApp: App {
3741
var body: some Scene {
3842
WindowGroup("Spreadsheet") {
3943
#hotReloadable {
40-
Table(state.data) {
41-
TableColumn("Name", value: \Person.name)
42-
TableColumn("Age", value: \Person.age)
43-
TableColumn("Phone", value: \Person.phone)
44-
TableColumn("Email", value: \Person.email)
45-
TableColumn("Occupation", value: \Person.occupation)
44+
VStack(spacing: 0) {
45+
VStack {
46+
if let greeting = state.greeting {
47+
Text(greeting)
48+
} else {
49+
Text("Pending greeting...")
50+
}
51+
}
52+
.padding(10)
53+
Table(state.data) {
54+
TableColumn("Name", value: \Person.name)
55+
TableColumn("Age", value: \Person.age.description)
56+
TableColumn("Phone", value: \Person.phone)
57+
TableColumn("Email", value: \Person.email)
58+
TableColumn("Occupation", value: \Person.occupation)
59+
TableColumn("Action") { (person: Person) in
60+
Button("Greet") {
61+
state.greeting = "Hello, \(person.name)!"
62+
}
63+
}
64+
}
4665
}
47-
.padding(10)
4866
}
4967
}
5068
}

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,100 @@ public struct AppKitBackend: AppBackend {
537537

538538
imageView.image = NSImage(cgImage: cgImage, size: NSSize(width: width, height: height))
539539
}
540+
541+
public func createTable() -> Widget {
542+
let scrollView = NSScrollView()
543+
let table = NSCustomTableView()
544+
table.delegate = table.customDelegate
545+
table.dataSource = table.customDelegate
546+
table.usesAlternatingRowBackgroundColors = true
547+
table.rowHeight = 24
548+
table.columnAutoresizingStyle = .lastColumnOnlyAutoresizingStyle
549+
table.allowsColumnSelection = false
550+
scrollView.documentView = table
551+
return .view(scrollView)
552+
}
553+
554+
public func setRowCount(ofTable table: Widget, to rowCount: Int) {
555+
let table = (table.view as! NSScrollView).documentView as! NSCustomTableView
556+
table.customDelegate.rowCount = rowCount
557+
}
558+
559+
public func setColumnLabels(ofTable table: Widget, to labels: [String]) {
560+
let table = (table.view as! NSScrollView).documentView as! NSCustomTableView
561+
var columnIndices: [ObjectIdentifier: Int] = [:]
562+
let columns = labels.enumerated().map { (i, label) in
563+
let column = NSTableColumn(
564+
identifier: NSUserInterfaceItemIdentifier("Column \(i)")
565+
)
566+
column.headerCell = NSTableHeaderCell(textCell: label)
567+
// column.width = 200
568+
columnIndices[ObjectIdentifier(column)] = i
569+
return column
570+
}
571+
table.customDelegate.columnIndices = columnIndices
572+
for column in table.tableColumns {
573+
table.removeTableColumn(column)
574+
}
575+
table.customDelegate.columnCount = labels.count
576+
for column in columns {
577+
table.addTableColumn(column)
578+
}
579+
}
580+
581+
public func setCells(
582+
ofTable table: Widget,
583+
to cells: [Widget],
584+
withRowHeights rowHeights: [Int]
585+
) {
586+
let table = (table.view as! NSScrollView).documentView as! NSCustomTableView
587+
table.customDelegate.widgets = cells
588+
table.customDelegate.rowHeights = rowHeights
589+
table.reloadData()
590+
}
591+
}
592+
593+
class NSCustomTableView: NSTableView {
594+
var customDelegate = NSCustomTableViewDelegate()
595+
}
596+
597+
class NSCustomTableViewDelegate: NSObject, NSTableViewDelegate, NSTableViewDataSource {
598+
var widgets: [AppKitBackend.Widget] = []
599+
var rowHeights: [Int] = []
600+
var columnIndices: [ObjectIdentifier: Int] = [:]
601+
var rowCount = 0
602+
var columnCount = 0
603+
604+
func numberOfRows(in tableView: NSTableView) -> Int {
605+
return rowCount
606+
}
607+
608+
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
609+
return CGFloat(rowHeights[row])
610+
}
611+
612+
func tableView(
613+
_ tableView: NSTableView,
614+
viewFor tableColumn: NSTableColumn?,
615+
row: Int
616+
) -> NSView? {
617+
guard let tableColumn else {
618+
print("warning: No column provided")
619+
return nil
620+
}
621+
guard let columnIndex = columnIndices[ObjectIdentifier(tableColumn)] else {
622+
print("warning: NSTableView asked for value of non-existent column")
623+
return nil
624+
}
625+
return widgets[row * columnCount + columnIndex].view
626+
}
627+
628+
func tableView(
629+
_ tableView: NSTableView,
630+
selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet
631+
) -> IndexSet {
632+
[]
633+
}
540634
}
541635

542636
extension Color {

Sources/Gtk/Generated/Switch.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import CGtk
55
/// ![An example GtkSwitch](switch.png)
66
///
77
/// The user can control which state should be active by clicking the
8-
/// empty area, or by dragging the handle.
8+
/// empty area, or by dragging the slider.
99
///
10-
/// `GtkSwitch` can also handle situations where the underlying state
11-
/// changes with a delay. In this case, the slider position indicates
12-
/// the user's recent change (as indicated by the [property@Gtk.Switch:active]
13-
/// property), and the color indicates whether the underlying state (represented
14-
/// by the [property@Gtk.Switch:state] property) has been updated yet.
10+
/// `GtkSwitch` can also express situations where the underlying state changes
11+
/// with a delay. In this case, the slider position indicates the user's recent
12+
/// change (represented by the [property@Gtk.Switch:active] property), while the
13+
/// trough color indicates the present underlying state (represented by the
14+
/// [property@Gtk.Switch:state] property).
1515
///
1616
/// ![GtkSwitch with delayed state change](switch-state.png)
1717
///
@@ -115,6 +115,10 @@ public class Switch: Widget, Actionable {
115115

116116
/// The backend state that is controlled by the switch.
117117
///
118+
/// Applications should usually set the [property@Gtk.Switch:active] property,
119+
/// except when indicating a change to the backend state which occurs
120+
/// separately from the user's interaction.
121+
///
118122
/// See [signal@Gtk.Switch::state-set] for details.
119123
@GObjectProperty(named: "state") public var state: Bool
120124

@@ -129,18 +133,14 @@ public class Switch: Widget, Actionable {
129133
/// Emitted to change the underlying state.
130134
///
131135
/// The ::state-set signal is emitted when the user changes the switch
132-
/// position. The default handler keeps the state in sync with the
133-
/// [property@Gtk.Switch:active] property.
136+
/// position. The default handler calls [method@Gtk.Switch.set_state] with the
137+
/// value of @state.
134138
///
135139
/// To implement delayed state change, applications can connect to this
136140
/// signal, initiate the change of the underlying state, and call
137141
/// [method@Gtk.Switch.set_state] when the underlying state change is
138142
/// complete. The signal handler should return %TRUE to prevent the
139143
/// default handler from running.
140-
///
141-
/// Visually, the underlying state is represented by the trough color of
142-
/// the switch, while the [property@Gtk.Switch:active] property is
143-
/// represented by the position of the switch.
144144
public var stateSet: ((Switch) -> Void)?
145145

146146
public var notifyActive: ((Switch) -> Void)?

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -194,16 +194,19 @@ public protocol AppBackend {
194194
/// to influence the size of the image on-screen).
195195
func updateImageView(_ imageView: Widget, rgbaData: [UInt8], width: Int, height: Int)
196196

197-
/// Creates a table with an initial number of rows and columns.
198-
func createTable(rows: Int, columns: Int) -> Widget
197+
/// Creates an empty table.
198+
func createTable() -> Widget
199199
/// Sets the number of rows of a table. Existing rows outside of the new bounds should
200200
/// be deleted.
201201
func setRowCount(ofTable table: Widget, to rows: Int)
202-
/// Sets the number of columns of a table. Existing columns outside of the new bounds
203-
/// should be deleted.
204-
func setColumnCount(ofTable table: Widget, to columns: Int)
205-
/// Sets the contents of the table cell at the given position in a table.
206-
func setCell(at position: CellPosition, inTable table: Widget, to widget: Widget)
202+
/// Sets the labels of a table's columns. Also sets the number of columns of the table to the
203+
/// number of labels provided.
204+
func setColumnLabels(ofTable table: Widget, to labels: [String])
205+
/// Sets the contents of the table as a flat array of cells in order of and grouped by row. Also
206+
/// sets the height of each row's content.
207+
///
208+
/// A nested array would have significantly more overhead, especially for large arrays.
209+
func setCells(ofTable table: Widget, to cells: [Widget], withRowHeights rowHeights: [Int])
207210

208211
// MARK: Controls
209212

@@ -329,16 +332,20 @@ extension AppBackend {
329332
todo()
330333
}
331334

332-
public func createTable(rows: Int, columns: Int) -> Widget {
335+
public func createTable() -> Widget {
333336
todo()
334337
}
335338
public func setRowCount(ofTable table: Widget, to rows: Int) {
336339
todo()
337340
}
338-
public func setColumnCount(ofTable table: Widget, to columns: Int) {
341+
public func setColumnLabels(ofTable table: Widget, to labels: [String]) {
339342
todo()
340343
}
341-
public func setCell(at position: CellPosition, inTable table: Widget, to widget: Widget) {
344+
public func setCells(
345+
ofTable table: Widget,
346+
to cells: [Widget],
347+
withRowHeights rowHeights: [Int]
348+
) {
342349
todo()
343350
}
344351

Sources/SwiftCrossUI/Builders/TableBuilder.swift

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)