Skip to content

Commit a02da75

Browse files
committed
Create ...Representable protocol for each b-end (fix ui/appkit ones)
1 parent 94a7674 commit a02da75

File tree

13 files changed

+962
-43
lines changed

13 files changed

+962
-43
lines changed

.github/workflows/build-test-and-docs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ jobs:
4242
swift build --target GtkBackend && \
4343
swift build --target Gtk3Backend && \
4444
swift build --target GtkExample && \
45+
# Work around SwiftPM incremental build issue
46+
swift package clean && \
4547
swift build --target CounterExample && \
4648
swift build --target ControlsExample && \
4749
swift build --target RandomNumberGeneratorExample && \
@@ -288,6 +290,8 @@ jobs:
288290
working-directory: ./Examples
289291
run: |
290292
swift build --target GtkExample && \
293+
# Work around SwiftPM incremental build issue
294+
swift package clean && \
291295
swift build --target CounterExample && \
292296
swift build --target ControlsExample && \
293297
swift build --target RandomNumberGeneratorExample && \

Examples/Sources/AdvancedCustomizationExample/AdvancedCustomizationApp.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ struct CounterApp: App {
1919
@State var name = ""
2020

2121
var body: some Scene {
22-
WindowGroup("CounterExample: \(count)") {
22+
WindowGroup("Inspect modifier and custom native views") {
2323
#hotReloadable {
2424
ScrollView {
25+
CustomNativeButton(label: "Custom native button")
26+
2527
HStack(spacing: 20) {
2628
Button("-") {
2729
count -= 1
@@ -32,10 +34,7 @@ struct CounterApp: App {
3234
#if canImport(AppKitBackend)
3335
text.isSelectable = true
3436
#elseif canImport(UIKitBackend)
35-
#if !targetEnvironment(macCatalyst)
36-
text.isHighlighted = true
37-
text.highlightTextColor = .yellow
38-
#endif
37+
text.isUserInteractionEnabled = true
3938
#elseif canImport(WinUIBackend)
4039
text.isTextSelectionEnabled = true
4140
#elseif canImport(GtkBackend)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
struct CustomNativeButton {
2+
typealias Coordinator = Void
3+
4+
var label: String
5+
}
6+
7+
#if canImport(GtkBackend)
8+
import GtkBackend
9+
import Gtk
10+
11+
extension CustomNativeButton: GtkWidgetRepresentable {
12+
func makeGtkWidget(context: GtkWidgetRepresentableContext<Coordinator>) -> Gtk.Button {
13+
Gtk.Button()
14+
}
15+
16+
func updateGtkWidget(
17+
_ button: Gtk.Button,
18+
context: GtkWidgetRepresentableContext<Coordinator>
19+
) {
20+
button.label = label
21+
button.css.clear()
22+
button.css.set(properties: [.backgroundColor(.init(1, 0, 1, 1))])
23+
}
24+
}
25+
#endif
26+
27+
#if canImport(Gtk3Backend)
28+
import Gtk3Backend
29+
import Gtk3
30+
31+
extension CustomNativeButton: Gtk3WidgetRepresentable {
32+
func makeGtk3Widget(context: Gtk3WidgetRepresentableContext<Coordinator>) -> Gtk3.Button {
33+
Gtk3.Button()
34+
}
35+
36+
func updateGtk3Widget(
37+
_ button: Gtk3.Button,
38+
context: Gtk3WidgetRepresentableContext<Coordinator>
39+
) {
40+
button.label = label
41+
button.css.clear()
42+
button.css.set(properties: [.backgroundColor(.init(1, 0, 1, 1))])
43+
}
44+
}
45+
#endif
46+
47+
#if canImport(AppKitBackend)
48+
import AppKitBackend
49+
import AppKit
50+
51+
extension CustomNativeButton: NSViewRepresentable {
52+
func makeNSView(context: NSViewRepresentableContext<Coordinator>) -> NSButton {
53+
NSButton()
54+
}
55+
56+
func updateNSView(
57+
_ button: NSButton,
58+
context: NSViewRepresentableContext<Coordinator>
59+
) {
60+
button.title = label
61+
button.bezelColor = .magenta
62+
}
63+
}
64+
#endif
65+
66+
#if canImport(UIKitBackend)
67+
import UIKitBackend
68+
import UIKit
69+
70+
extension CustomNativeButton: UIViewRepresentable {
71+
func makeUIView(context: UIViewRepresentableContext<Coordinator>) -> UIButton {
72+
UIButton()
73+
}
74+
75+
func updateUIView(
76+
_ button: UIButton,
77+
context: UIViewRepresentableContext<Coordinator>
78+
) {
79+
button.setTitle(label, for: .normal)
80+
if #available(iOS 15.0, *) {
81+
button.configuration = .bordered()
82+
}
83+
}
84+
}
85+
#endif
86+
87+
#if canImport(WinUIBackend)
88+
import WinUIBackend
89+
import WinUI
90+
import UWP
91+
92+
extension CustomNativeButton: WinUIElementRepresentable {
93+
func makeWinUIElement(
94+
context: WinUIElementRepresentableContext<Coordinator>
95+
) -> WinUI.Button {
96+
WinUI.Button()
97+
}
98+
99+
func updateWinUIElement(
100+
_ button: WinUI.Button,
101+
context: WinUIElementRepresentableContext<Coordinator>
102+
) {
103+
let block = TextBlock()
104+
block.text = label
105+
button.content = block
106+
let brush = WinUI.SolidColorBrush()
107+
brush.color = UWP.Color(a: 255, r: 255, g: 0, b: 255)
108+
button.background = brush
109+
}
110+
}
111+
#endif

Sources/AppKitBackend/NSViewRepresentable.swift

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public protocol NSViewRepresentable: View where Content == Never {
6565
/// This method is called after all AppKit lifecycle methods, such as
6666
/// `nsView.didMoveToSuperview()`. The default implementation does nothing.
6767
/// - Parameters:
68-
/// - nsVIew: The view being dismantled.
68+
/// - nsView: The view being dismantled.
6969
/// - coordinator: The coordinator.
7070
static func dismantleNSView(_ nsView: NSViewType, coordinator: Coordinator)
7171
}
@@ -76,32 +76,43 @@ extension NSViewRepresentable {
7676
}
7777

7878
public func determineViewSize(
79-
for proposal: SIMD2<Int>, nsView: NSViewType,
79+
for proposal: SIMD2<Int>,
80+
nsView: NSViewType,
8081
context _: NSViewRepresentableContext<Coordinator>
8182
) -> ViewSize {
8283
let intrinsicSize = nsView.intrinsicContentSize
8384
let sizeThatFits = nsView.fittingSize
8485

8586
let roundedSizeThatFits = SIMD2(
8687
Int(sizeThatFits.width.rounded(.up)),
87-
Int(sizeThatFits.height.rounded(.up)))
88+
Int(sizeThatFits.height.rounded(.up))
89+
)
8890
let roundedIntrinsicSize = SIMD2(
8991
Int(intrinsicSize.width.rounded(.awayFromZero)),
90-
Int(intrinsicSize.height.rounded(.awayFromZero)))
92+
Int(intrinsicSize.height.rounded(.awayFromZero))
93+
)
9194

9295
return ViewSize(
9396
size: SIMD2(
94-
intrinsicSize.width < 0.0 ? proposal.x : roundedSizeThatFits.x,
95-
intrinsicSize.height < 0.0 ? proposal.y : roundedSizeThatFits.y
97+
intrinsicSize.width < 0.0
98+
? proposal.x
99+
: max(min(proposal.x, roundedSizeThatFits.x), roundedIntrinsicSize.x),
100+
intrinsicSize.height < 0.0
101+
? proposal.y
102+
: max(min(proposal.y, roundedSizeThatFits.y), roundedIntrinsicSize.y)
96103
),
97104
// The 10 here is a somewhat arbitrary constant value so that it's always the same.
98105
// See also `Color` and `Picker`, which use the same constant.
99106
idealSize: SIMD2(
100107
intrinsicSize.width < 0.0 ? 10 : roundedIntrinsicSize.x,
101108
intrinsicSize.height < 0.0 ? 10 : roundedIntrinsicSize.y
102109
),
110+
// We don't have a nice way of measuring these, so just set them to the
111+
// view's minimum sizes along each dimension to at least be correct.
112+
idealWidthForProposedHeight: max(0, roundedSizeThatFits.x),
113+
idealHeightForProposedWidth: max(0, roundedSizeThatFits.y),
103114
minimumWidth: max(0, roundedIntrinsicSize.x),
104-
minimumHeight: max(0, roundedIntrinsicSize.x),
115+
minimumHeight: max(0, roundedIntrinsicSize.y),
105116
maximumWidth: nil,
106117
maximumHeight: nil
107118
)
@@ -154,6 +165,9 @@ extension View where Self: NSViewRepresentable {
154165
let representingWidget = widget as! RepresentingWidget<Self>
155166
representingWidget.update(with: environment)
156167

168+
// We need to do this for `fittingSize` to work correctly (it takes all
169+
// constraints into account).
170+
backend.setSize(of: representingWidget, to: proposedSize)
157171
let size = representingWidget.representable.determineViewSize(
158172
for: proposedSize,
159173
nsView: representingWidget.subview,
@@ -209,14 +223,17 @@ final class RepresentingWidget<Representable: NSViewRepresentable>: NSView {
209223
}()
210224

211225
func update(with environment: EnvironmentValues) {
212-
if context == nil {
213-
context = .init(
226+
if var context {
227+
context.environment = environment
228+
representable.updateNSView(subview, context: context)
229+
self.context = context
230+
} else {
231+
let context = NSViewRepresentableContext(
214232
coordinator: representable.makeCoordinator(),
215233
environment: environment
216234
)
217-
} else {
218-
context!.environment = environment
219-
representable.updateNSView(subview, context: context!)
235+
self.context = context
236+
representable.updateNSView(subview, context: context)
220237
}
221238
}
222239

Sources/Gtk/Widgets/Fixed.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ open class Fixed: Widget {
4141
public var children: [Widget] = []
4242

4343
/// Creates a new `GtkFixed`.
44-
public convenience init() {
45-
self.init(gtk_fixed_new())
44+
public init() {
45+
super.init(gtk_fixed_new())
4646
}
4747

4848
public func put(_ child: Widget, x: Double, y: Double) {

Sources/Gtk/Widgets/Widget.swift

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ open class Widget: GObject {
6262
}
6363

6464
public func setSizeRequest(width: Int, height: Int) {
65-
gtk_widget_set_size_request(widgetPointer, Int32(width), Int32(height))
65+
gtk_widget_set_size_request(widgetPointer, gint(width), gint(height))
6666
}
6767

6868
public func getSizeRequest() -> Size {
69-
var width: Int32 = 0
70-
var height: Int32 = 0
69+
var width: gint = 0
70+
var height: gint = 0
7171
gtk_widget_get_size_request(widgetPointer, &width, &height)
7272
return Size(width: Int(width), height: Int(height))
7373
}
@@ -82,6 +82,38 @@ open class Widget: GObject {
8282
)
8383
}
8484

85+
public struct MeasureResult {
86+
public var minimum: Int
87+
public var natural: Int
88+
public var minimumBaseline: Int
89+
public var naturalBaseline: Int
90+
}
91+
92+
public func measure(
93+
orientation: Orientation,
94+
forPerpendicularSize perpendicularSize: Int
95+
) -> MeasureResult {
96+
var minimum: gint = 0
97+
var natural: gint = 0
98+
var minimumBaseline: gint = 0
99+
var naturalBaseline: gint = 0
100+
gtk_widget_measure(
101+
widgetPointer,
102+
orientation.toGtk(),
103+
gint(perpendicularSize),
104+
&minimum,
105+
&natural,
106+
&minimumBaseline,
107+
&naturalBaseline
108+
)
109+
return MeasureResult(
110+
minimum: Int(minimum),
111+
natural: Int(natural),
112+
minimumBaseline: Int(minimumBaseline),
113+
naturalBaseline: Int(naturalBaseline)
114+
)
115+
}
116+
85117
public func insertActionGroup(_ name: String, _ actionGroup: any GActionGroup) {
86118
gtk_widget_insert_action_group(
87119
widgetPointer,

Sources/Gtk3/Widgets/Fixed.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ import CGtk3
3737
/// If you know none of these things are an issue for your application,
3838
/// and prefer the simplicity of `GtkFixed`, by all means use the
3939
/// widget. But you should be aware of the tradeoffs.
40-
public class Fixed: Widget {
40+
open class Fixed: Widget {
4141
public var children: [Widget] = []
4242

4343
/// Creates a new `GtkFixed`.
44-
public convenience init() {
45-
self.init(gtk_fixed_new())
44+
public init() {
45+
super.init(gtk_fixed_new())
4646
}
4747

4848
public func put(_ child: Widget, x: Int, y: Int) {

Sources/Gtk3/Widgets/Widget.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,39 @@ open class Widget: GObject {
131131
)
132132
}
133133

134+
public struct MeasureResult {
135+
public var minimum: Int
136+
public var natural: Int
137+
}
138+
139+
public func measure(
140+
orientation: Orientation,
141+
forPerpendicularSize perpendicularSize: Int
142+
) -> MeasureResult {
143+
var minimum: gint = 0
144+
var natural: gint = 0
145+
switch orientation {
146+
case .horizontal:
147+
gtk_widget_get_preferred_width_for_height(
148+
widgetPointer,
149+
gint(perpendicularSize),
150+
&minimum,
151+
&natural
152+
)
153+
case .vertical:
154+
gtk_widget_get_preferred_height_for_width(
155+
widgetPointer,
156+
gint(perpendicularSize),
157+
&minimum,
158+
&natural
159+
)
160+
}
161+
return MeasureResult(
162+
minimum: Int(minimum),
163+
natural: Int(natural)
164+
)
165+
}
166+
134167
public func insertActionGroup(_ name: String, _ actionGroup: any GActionGroup) {
135168
gtk_widget_insert_action_group(
136169
widgetPointer,

0 commit comments

Comments
 (0)