Skip to content

Commit 23f82d4

Browse files
committed
Create ...Representable protocol for each b-end (fix ui/appkit ones)
1 parent 2797197 commit 23f82d4

File tree

16 files changed

+951
-39
lines changed

16 files changed

+951
-39
lines changed

Examples/CustomButton.d

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

Examples/CustomButton.o

29.7 KB
Binary file not shown.

Examples/CustomButton.swiftdeps

76.1 KB
Binary file not shown.

Examples/CustomButton.swiftdeps~

79.9 KB
Binary file not shown.

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+
CustomButton(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 CustomButton {
2+
typealias Coordinator = Void
3+
4+
var label: String
5+
}
6+
7+
#if canImport(GtkBackend)
8+
import GtkBackend
9+
import Gtk
10+
11+
extension CustomButton: 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 CustomButton: 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 CustomButton: 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 CustomButton: 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 CustomButton: 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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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: Int32 = 0
97+
var natural: Int32 = 0
98+
var minimumBaseline: Int32 = 0
99+
var naturalBaseline: Int32 = 0
100+
gtk_widget_measure(
101+
widgetPointer,
102+
orientation.toGtk(),
103+
Int32(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) {

0 commit comments

Comments
 (0)