Skip to content

Commit 8a28f1c

Browse files
committed
Started implementing experimental AppKit backend
1 parent b9c4b36 commit 8a28f1c

File tree

3 files changed

+449
-47
lines changed

3 files changed

+449
-47
lines changed

Package.swift

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,19 @@ var conditionalProducts: [Product] = []
4141
var conditionalTargets: [Target] = []
4242
var exampleDependencies: [Target.Dependency] = ["SwiftCrossUI"]
4343
var fileViewerExampleDependencies: [Target.Dependency] = ["SwiftCrossUI"]
44+
var backendTargets: [String] = []
4445

4546
// If Gtk is detected, add Gtk-related products and targets
4647
if let version = getGtk4MinorVersion() {
4748
var gtkSwiftSettings: [SwiftSetting] = []
4849
var gtkExampleDependencies: [Target.Dependency] = ["Gtk"]
4950
exampleDependencies.append("GtkBackend")
51+
backendTargets.append("GtkBackend")
5052

5153
// Conditionally enable features that rely on Gtk 4.10
5254
if version >= 10 {
5355
conditionalTargets.append(
54-
.target(
55-
name: "FileDialog",
56-
dependencies: ["CGtk", "Gtk"]
57-
)
56+
.target(name: "FileDialog", dependencies: ["CGtk", "Gtk"])
5857
)
5958

6059
gtkExampleDependencies.append("FileDialog")
@@ -64,29 +63,15 @@ if let version = getGtk4MinorVersion() {
6463

6564
conditionalProducts.append(
6665
contentsOf: [
67-
.library(
68-
name: "GtkBackend",
69-
targets: ["GtkBackend"]
70-
),
71-
.library(
72-
name: "Gtk",
73-
targets: ["Gtk"]
74-
),
75-
76-
.executable(
77-
name: "GtkExample",
78-
targets: ["GtkExample"]
79-
),
66+
.library(name: "GtkBackend", targets: ["GtkBackend"]),
67+
.library(name: "Gtk", targets: ["Gtk"]),
68+
.executable(name: "GtkExample", targets: ["GtkExample"]),
8069
]
8170
)
8271

8372
conditionalTargets.append(
8473
contentsOf: [
85-
.target(
86-
name: "GtkBackend",
87-
dependencies: ["SwiftCrossUI", "Gtk", "CGtk"],
88-
path: "Sources/GtkBackend"
89-
),
74+
.target(name: "GtkBackend", dependencies: ["SwiftCrossUI", "Gtk", "CGtk"]),
9075
.systemLibrary(
9176
name: "CGtk",
9277
pkgConfig: "gtk4",
@@ -95,11 +80,7 @@ if let version = getGtk4MinorVersion() {
9580
.apt(["libgtk-4-dev clang"]),
9681
]
9782
),
98-
.target(
99-
name: "Gtk",
100-
dependencies: ["CGtk"],
101-
swiftSettings: gtkSwiftSettings
102-
),
83+
.target(name: "Gtk", dependencies: ["CGtk"], swiftSettings: gtkSwiftSettings),
10384
.executableTarget(
10485
name: "GtkExample",
10586
dependencies: gtkExampleDependencies,
@@ -116,18 +97,18 @@ if let version = getGtk4MinorVersion() {
11697
)
11798
}
11899

100+
#if canImport(AppKit)
101+
conditionalTargets.append(.target(name: "AppKitBackend", dependencies: ["SwiftCrossUI"]))
102+
backendTargets.append("AppKitBackend")
103+
exampleDependencies.append("AppKitBackend")
104+
#endif
105+
119106
let package = Package(
120107
name: "swift-cross-ui",
121108
platforms: [.macOS(.v10_15)],
122109
products: [
123-
.library(
124-
name: "SwiftCrossUI",
125-
targets: ["SwiftCrossUI"]
126-
),
127-
.executable(
128-
name: "CounterExample",
129-
targets: ["CounterExample"]
130-
),
110+
.library(name: "SwiftCrossUI", targets: ["SwiftCrossUI"] + backendTargets),
111+
.executable(name: "CounterExample", targets: ["CounterExample"]),
131112
.executable(
132113
name: "RandomNumberGeneratorExample",
133114
targets: ["RandomNumberGeneratorExample"]
@@ -140,18 +121,9 @@ let package = Package(
140121
name: "GreetingGeneratorExample",
141122
targets: ["GreetingGeneratorExample"]
142123
),
143-
.executable(
144-
name: "FileViewerExample",
145-
targets: ["FileViewerExample"]
146-
),
147-
.executable(
148-
name: "NavigationExample",
149-
targets: ["NavigationExample"]
150-
),
151-
.executable(
152-
name: "SplitExample",
153-
targets: ["SplitExample"]
154-
),
124+
.executable(name: "FileViewerExample", targets: ["FileViewerExample"]),
125+
.executable(name: "NavigationExample", targets: ["NavigationExample"]),
126+
.executable(name: "SplitExample", targets: ["SplitExample"]),
155127
] + conditionalProducts,
156128
dependencies: dependencies,
157129
targets: [
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import AppKit
2+
import SwiftCrossUI
3+
4+
public struct AppKitBackend: AppBackend {
5+
public typealias Widget = NSView
6+
7+
public init(appIdentifier: String) {}
8+
9+
public func run<AppRoot: App>(
10+
_ app: AppRoot,
11+
_ setViewGraph: @escaping (ViewGraph<AppRoot>) -> Void
12+
) where AppRoot.Backend == Self {
13+
let nsApp = NSApplication.shared
14+
nsApp.setActivationPolicy(.regular)
15+
16+
var styleMask: NSWindow.StyleMask = [.titled, .closable]
17+
if app.windowProperties.resizable {
18+
styleMask.insert(.resizable)
19+
}
20+
21+
let window = NSWindow(
22+
contentRect: NSMakeRect(
23+
0,
24+
0,
25+
CGFloat(app.windowProperties.defaultSize?.width ?? 0),
26+
CGFloat(app.windowProperties.defaultSize?.height ?? 0)
27+
),
28+
styleMask: styleMask,
29+
backing: .buffered,
30+
defer: true
31+
)
32+
window.title = app.windowProperties.title
33+
window.makeKeyAndOrderFront(nil)
34+
35+
let viewGraph = ViewGraph(for: app, backend: self)
36+
setViewGraph(viewGraph)
37+
38+
window.contentView = viewGraph.rootNode.widget
39+
40+
nsApp.activate(ignoringOtherApps: true)
41+
42+
nsApp.run()
43+
}
44+
45+
public func runInMainThread(action: @escaping () -> Void) {
46+
DispatchQueue.main.async {
47+
action()
48+
}
49+
}
50+
51+
public func show(_ widget: Widget) {}
52+
53+
public func createTextView(content: String, shouldWrap: Bool) -> Widget {
54+
if shouldWrap {
55+
return NSTextField(wrappingLabelWithString: content)
56+
} else {
57+
return NSTextField(labelWithString: content)
58+
}
59+
}
60+
61+
public func setContent(ofTextView textView: Widget, to content: String) {
62+
(textView as! NSTextField).stringValue = content
63+
}
64+
65+
public func setWrap(ofTextView textView: Widget, to shouldWrap: Bool) {}
66+
67+
public func createVStack(spacing: Int) -> Widget {
68+
let view = NSStackView()
69+
view.orientation = .vertical
70+
view.spacing = CGFloat(spacing)
71+
return view
72+
}
73+
74+
public func addChild(_ child: Widget, toVStack container: Widget) {
75+
let container = container as! NSStackView
76+
container.addView(child, in: .bottom)
77+
}
78+
79+
public func setSpacing(ofVStack widget: Widget, to spacing: Int) {
80+
(widget as! NSStackView).spacing = CGFloat(spacing)
81+
}
82+
83+
public func createHStack(spacing: Int) -> Widget {
84+
let view = NSStackView()
85+
view.orientation = .horizontal
86+
view.spacing = CGFloat(spacing)
87+
return view
88+
}
89+
90+
public func addChild(_ child: Widget, toHStack container: Widget) {
91+
(container as! NSStackView).addView(child, in: .bottom)
92+
}
93+
94+
public func setSpacing(ofHStack widget: Widget, to spacing: Int) {
95+
(widget as! NSStackView).spacing = CGFloat(spacing)
96+
}
97+
98+
public func createButton(label: String, action: @escaping () -> Void) -> Widget {
99+
let button = NSButton(title: label, target: nil, action: nil)
100+
button.onAction = { _ in
101+
action()
102+
}
103+
return button
104+
}
105+
106+
public func setLabel(ofButton button: Widget, to label: String) {
107+
(button as! NSButton).title = label
108+
}
109+
110+
public func setAction(ofButton button: Widget, to action: @escaping () -> Void) {
111+
(button as! NSButton).onAction = { _ in
112+
action()
113+
}
114+
}
115+
116+
public func createPaddingContainer(for child: Widget) -> Widget {
117+
return NSStackView(views: [child])
118+
}
119+
120+
public func getChild(ofPaddingContainer container: Widget) -> Widget {
121+
return (container as! NSStackView).views[0]
122+
}
123+
124+
public func setPadding(
125+
ofPaddingContainer container: Widget,
126+
top: Int,
127+
bottom: Int,
128+
leading: Int,
129+
trailing: Int
130+
) {
131+
let view = container as! NSStackView
132+
view.edgeInsets.top = CGFloat(top)
133+
view.edgeInsets.bottom = CGFloat(bottom)
134+
view.edgeInsets.left = CGFloat(leading)
135+
view.edgeInsets.right = CGFloat(trailing)
136+
}
137+
}
138+
139+
// Source: https://gist.github.com/sindresorhus/3580ce9426fff8fafb1677341fca4815
140+
enum AssociationPolicy {
141+
case assign
142+
case retainNonatomic
143+
case copyNonatomic
144+
case retain
145+
case copy
146+
147+
var rawValue: objc_AssociationPolicy {
148+
switch self {
149+
case .assign:
150+
return .OBJC_ASSOCIATION_ASSIGN
151+
case .retainNonatomic:
152+
return .OBJC_ASSOCIATION_RETAIN_NONATOMIC
153+
case .copyNonatomic:
154+
return .OBJC_ASSOCIATION_COPY_NONATOMIC
155+
case .retain:
156+
return .OBJC_ASSOCIATION_RETAIN
157+
case .copy:
158+
return .OBJC_ASSOCIATION_COPY
159+
}
160+
}
161+
}
162+
163+
// Source: https://gist.github.com/sindresorhus/3580ce9426fff8fafb1677341fca4815
164+
final class ObjectAssociation<T: Any> {
165+
private let policy: AssociationPolicy
166+
167+
init(policy: AssociationPolicy = .retainNonatomic) {
168+
self.policy = policy
169+
}
170+
171+
subscript(index: AnyObject) -> T? {
172+
get {
173+
// Force-cast is fine here as we want it to fail loudly if we don't use the correct type.
174+
// swiftlint:disable:next force_cast
175+
objc_getAssociatedObject(index, Unmanaged.passUnretained(self).toOpaque()) as! T?
176+
}
177+
set {
178+
objc_setAssociatedObject(
179+
index, Unmanaged.passUnretained(self).toOpaque(), newValue, policy.rawValue)
180+
}
181+
}
182+
}
183+
184+
// Source: https://gist.github.com/sindresorhus/3580ce9426fff8fafb1677341fca4815
185+
extension NSControl {
186+
typealias ActionClosure = ((NSControl) -> Void)
187+
188+
private struct AssociatedKeys {
189+
static let onActionClosure = ObjectAssociation<ActionClosure>()
190+
}
191+
192+
@objc
193+
private func callClosure(_ sender: NSControl) {
194+
onAction?(sender)
195+
}
196+
197+
/**
198+
Closure version of `.action`.
199+
```
200+
let button = NSButton(title: "Unicorn", target: nil, action: nil)
201+
button.onAction = { sender in
202+
print("Button action: \(sender)")
203+
}
204+
```
205+
*/
206+
var onAction: ActionClosure? {
207+
get {
208+
return AssociatedKeys.onActionClosure[self]
209+
}
210+
set {
211+
AssociatedKeys.onActionClosure[self] = newValue
212+
action = #selector(callClosure)
213+
target = self
214+
}
215+
}
216+
}

0 commit comments

Comments
 (0)