Skip to content

Commit ba9b12f

Browse files
committed
Create experimental Qt5 backend (only has enough implemented to run CounterExample)
1 parent d1212a5 commit ba9b12f

File tree

5 files changed

+287
-63
lines changed

5 files changed

+287
-63
lines changed

Examples/Counter/CounterApp.swift

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1+
import QtBackend
12
import SwiftCrossUI
23

3-
#if canImport(GtkBackend)
4-
import GtkBackend
5-
typealias SelectedBackend = GtkBackend
6-
#else
7-
#error("No valid backends found")
8-
#endif
4+
// #if canImport(GtkBackend)
5+
// import GtkBackend
6+
// typealias SelectedBackend = GtkBackend
7+
// #else
8+
// #error("No valid backends found")
9+
// #endif
10+
11+
typealias SelectedBackend = QtBackend
912

1013
class CounterState: Observable {
1114
@Observed

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ var dependencies: [Package.Dependency] = [
1212
url: "https://github.com/apple/swift-syntax.git",
1313
from: "508.0.0"
1414
),
15+
.package(
16+
url: "https://github.com/Longhanks/qlift",
17+
revision: "ddab1f1ecc113ad4f8e05d2999c2734cdf706210"
18+
),
1519
]
1620

1721
#if swift(>=5.6) && !os(Windows)
@@ -103,6 +107,17 @@ if let version = getGtk4MinorVersion() {
103107
exampleDependencies.append("AppKitBackend")
104108
#endif
105109

110+
if checkQtInstalled() {
111+
conditionalTargets.append(
112+
.target(
113+
name: "QtBackend",
114+
dependencies: ["SwiftCrossUI", .product(name: "Qlift", package: "qlift")]
115+
)
116+
)
117+
backendTargets.append("QtBackend")
118+
exampleDependencies.append("QtBackend")
119+
}
120+
106121
let package = Package(
107122
name: "swift-cross-ui",
108123
platforms: [.macOS(.v10_15)],
@@ -180,6 +195,25 @@ let package = Package(
180195
] + conditionalTargets
181196
)
182197

198+
func checkQtInstalled() -> Bool {
199+
#if os(Windows)
200+
// TODO: Test Qt backend on Windows
201+
return false
202+
#else
203+
let process = Process()
204+
process.executableURL = URL(fileURLWithPath: "/bin/bash")
205+
process.arguments = ["-c", "qmake --version"]
206+
process.standardOutput = Pipe()
207+
do {
208+
try process.run()
209+
process.waitUntilExit()
210+
return true
211+
} catch {
212+
return false
213+
}
214+
#endif
215+
}
216+
183217
func getGtk4MinorVersion() -> Int? {
184218
#if os(Windows)
185219
guard let pkgConfigPath = ProcessInfo.processInfo.environment["PKG_CONFIG_PATH"],

Sources/QtBackend/QtBackend.swift

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import Foundation
2+
import Qlift
3+
import SwiftCrossUI
4+
5+
// TODO: Remove default padding from QBoxLayout related widgets.
6+
// TODO: Fix window size code, currently seems to get pretty ignored.
7+
8+
public struct QtBackend: AppBackend {
9+
public typealias Widget = QWidget
10+
11+
private class InternalState {
12+
var buttonClickActions: [ObjectIdentifier: () -> Void] = [:]
13+
var paddingContainerChildren: [ObjectIdentifier: Widget] = [:]
14+
}
15+
16+
private var internalState: InternalState
17+
18+
public init(appIdentifier: String) {
19+
internalState = InternalState()
20+
}
21+
22+
public func run<AppRoot: App>(
23+
_ app: AppRoot,
24+
_ setViewGraph: @escaping (ViewGraph<AppRoot>) -> Void
25+
) where AppRoot.Backend == Self {
26+
let application = QApplication()
27+
28+
let viewGraph = ViewGraph(for: app, backend: self)
29+
setViewGraph(viewGraph)
30+
31+
// TODO: app.windowProperties
32+
let mainWindow = MainWindow()
33+
mainWindow.setProperties(app.windowProperties)
34+
mainWindow.setRoot(viewGraph.rootNode.widget)
35+
mainWindow.show()
36+
37+
_ = application.exec()
38+
}
39+
40+
public func runInMainThread(action: @escaping () -> Void) {
41+
DispatchQueue.main.async {
42+
action()
43+
}
44+
}
45+
46+
public func show(_ widget: Widget) {
47+
widget.show()
48+
}
49+
50+
public func createVStack(spacing: Int) -> Widget {
51+
let layout = QVBoxLayout()
52+
layout.spacing = Int32(spacing)
53+
54+
let widget = QWidget()
55+
widget.layout = layout
56+
return widget
57+
}
58+
59+
public func addChild(_ child: Widget, toVStack container: Widget) {
60+
(container.layout as! QVBoxLayout).add(widget: child)
61+
}
62+
63+
public func setSpacing(ofVStack widget: Widget, to spacing: Int) {
64+
(widget.layout as! QVBoxLayout).spacing = Int32(spacing)
65+
}
66+
67+
public func createHStack(spacing: Int) -> Widget {
68+
let layout = QHBoxLayout()
69+
layout.spacing = Int32(spacing)
70+
71+
let widget = QWidget()
72+
widget.layout = layout
73+
return widget
74+
}
75+
76+
public func addChild(_ child: Widget, toHStack container: Widget) {
77+
(container.layout as! QHBoxLayout).add(widget: child)
78+
}
79+
80+
public func setSpacing(ofHStack widget: Widget, to spacing: Int) {
81+
(widget.layout as! QHBoxLayout).spacing = Int32(spacing)
82+
}
83+
84+
public func createPaddingContainer(for child: Widget) -> Widget {
85+
let container = createVStack(spacing: 0)
86+
addChild(child, toVStack: container)
87+
internalState.paddingContainerChildren[ObjectIdentifier(container)] = child
88+
return container
89+
}
90+
91+
public func getChild(ofPaddingContainer container: Widget) -> Widget {
92+
return internalState.paddingContainerChildren[ObjectIdentifier(container)]!
93+
}
94+
95+
public func setPadding(
96+
ofPaddingContainer container: Widget,
97+
top: Int,
98+
bottom: Int,
99+
leading: Int,
100+
trailing: Int
101+
) {
102+
(container.layout! as! QVBoxLayout).contentsMargins = QMargins(
103+
left: Int32(leading),
104+
top: Int32(top),
105+
right: Int32(trailing),
106+
bottom: Int32(bottom)
107+
)
108+
}
109+
110+
public func createTextView(content: String, shouldWrap: Bool) -> Widget {
111+
let label = QLabel(text: content)
112+
return label
113+
}
114+
115+
public func setContent(ofTextView textView: Widget, to content: String) {
116+
(textView as! QLabel).text = content
117+
}
118+
119+
public func setWrap(ofTextView textView: Widget, to shouldWrap: Bool) {
120+
// TODO: Implement text wrap setting
121+
}
122+
123+
public func createButton(label: String, action: @escaping () -> Void) -> Widget {
124+
let button = QPushButton(text: label)
125+
126+
// Internal state is required to avoid multiple subsequent calls to setAction adding
127+
// new handlers instead of replacing the existing handler
128+
internalState.buttonClickActions[ObjectIdentifier(button)] = action
129+
button.connectClicked(receiver: nil) { [weak internalState] in
130+
guard let internalState = internalState else {
131+
return
132+
}
133+
internalState.buttonClickActions[ObjectIdentifier(button)]?()
134+
}
135+
136+
return button
137+
}
138+
139+
public func setLabel(ofButton button: Widget, to label: String) {
140+
(button as! QPushButton).text = label
141+
}
142+
143+
public func setAction(ofButton button: Widget, to action: @escaping () -> Void) {
144+
internalState.buttonClickActions[ObjectIdentifier(button)] = action
145+
}
146+
}
147+
148+
class MainWindow: QMainWindow {
149+
override init(parent: QWidget? = nil, flags: Qt.WindowFlags = .Widget) {
150+
super.init(parent: parent, flags: flags)
151+
}
152+
153+
func setProperties(_ properties: WindowProperties) {
154+
windowTitle = properties.title
155+
156+
let size = properties.defaultSize
157+
geometry = QRect(
158+
x: 0,
159+
y: 0,
160+
width: Int32(size?.width ?? 0),
161+
height: Int32(size?.height ?? 0)
162+
)
163+
164+
let policy: QSizePolicy.Policy = .Maximum
165+
sizePolicy = QSizePolicy(horizontal: policy, vertical: policy)
166+
}
167+
168+
func setRoot(_ widget: QWidget) {
169+
centralWidget = widget
170+
}
171+
}

0 commit comments

Comments
 (0)