Skip to content

Commit fb41c3a

Browse files
committed
AppKitBackend
1 parent f65b366 commit fb41c3a

File tree

4 files changed

+193
-11
lines changed

4 files changed

+193
-11
lines changed

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public final class AppKitBackend: AppBackend {
1414
public typealias Widget = NSView
1515
public typealias Menu = NSMenu
1616
public typealias Alert = NSAlert
17+
public typealias Path = NSBezierPath
1718

1819
public let defaultTableRowContentHeight = 20
1920
public let defaultTableCellVerticalPadding = 4
@@ -1143,6 +1144,171 @@ public final class AppKitBackend: AppBackend {
11431144
tapGestureTarget.longPressHandler = action
11441145
}
11451146
}
1147+
1148+
final class NSBezierPathView: NSView {
1149+
var path: NSBezierPath!
1150+
var fillColor: NSColor = .clear
1151+
var strokeColor: NSColor = .clear
1152+
1153+
override func draw(_ dirtyRect: NSRect) {
1154+
fillColor.set()
1155+
path.fill()
1156+
strokeColor.set()
1157+
path.stroke()
1158+
}
1159+
}
1160+
1161+
public func createPathWidget() -> NSView {
1162+
NSBezierPathView()
1163+
}
1164+
1165+
public func createPath() -> Path {
1166+
NSBezierPath()
1167+
}
1168+
1169+
func applyStrokeStyle(_ strokeStyle: StrokeStyle, to path: NSBezierPath) {
1170+
path.lineWidth = CGFloat(strokeStyle.width)
1171+
1172+
path.lineCapStyle =
1173+
switch strokeStyle.cap {
1174+
case .butt:
1175+
.butt
1176+
case .round:
1177+
.round
1178+
case .square:
1179+
.square
1180+
}
1181+
1182+
switch strokeStyle.join {
1183+
case .miter(let limit):
1184+
path.lineJoinStyle = .miter
1185+
path.miterLimit = CGFloat(limit)
1186+
case .round:
1187+
path.lineJoinStyle = .round
1188+
case .bevel:
1189+
path.lineJoinStyle = .bevel
1190+
}
1191+
}
1192+
1193+
public func updatePath(_ path: Path, _ source: SwiftCrossUI.Path, pointsChanged: Bool) {
1194+
applyStrokeStyle(source.strokeStyle, to: path)
1195+
1196+
if pointsChanged {
1197+
path.removeAllPoints()
1198+
1199+
for action in source.actions {
1200+
switch action {
1201+
case .moveTo(let point):
1202+
path.move(to: NSPoint(x: point.x, y: point.y))
1203+
case .lineTo(let point):
1204+
if path.isEmpty {
1205+
path.move(to: .zero)
1206+
}
1207+
path.line(to: NSPoint(x: point.x, y: point.y))
1208+
case .quadCurve(let control, let end):
1209+
if path.isEmpty {
1210+
path.move(to: .zero)
1211+
}
1212+
1213+
if #available(macOS 14, *) {
1214+
// Use the native quadratic curve function
1215+
path.curve(
1216+
to: NSPoint(x: end.x, y: end.y),
1217+
controlPoint: NSPoint(x: control.x, y: control.y)
1218+
)
1219+
} else {
1220+
let start = path.currentPoint
1221+
// Build a cubic curve that follows the same path as the quadratic
1222+
path.curve(
1223+
to: NSPoint(x: end.x, y: end.y),
1224+
controlPoint1: NSPoint(
1225+
x: (start.x + 2.0 * control.x) / 3.0,
1226+
y: (start.y + 2.0 * control.y) / 3.0
1227+
),
1228+
controlPoint2: NSPoint(
1229+
x: (2.0 * control.x + end.x) / 3.0,
1230+
y: (2.0 * control.y + end.y) / 3.0
1231+
)
1232+
)
1233+
}
1234+
case .cubicCurve(let control1, let control2, let end):
1235+
if path.isEmpty {
1236+
path.move(to: .zero)
1237+
}
1238+
1239+
path.curve(
1240+
to: NSPoint(x: end.x, y: end.y),
1241+
controlPoint1: NSPoint(x: control1.x, y: control1.y),
1242+
controlPoint2: NSPoint(x: control2.x, y: control2.y)
1243+
)
1244+
case .rectangle(let rect):
1245+
path.appendRect(
1246+
NSRect(
1247+
origin: NSPoint(x: rect.x, y: rect.y),
1248+
size: NSSize(
1249+
width: CGFloat(rect.width),
1250+
height: CGFloat(rect.height)
1251+
)
1252+
)
1253+
)
1254+
case .circle(let center, let radius):
1255+
path.appendOval(
1256+
in: NSRect(
1257+
origin: NSPoint(x: center.x - radius, y: center.y - radius),
1258+
size: NSSize(
1259+
width: CGFloat(radius) * 2.0,
1260+
height: CGFloat(radius) * 2.0
1261+
)
1262+
)
1263+
)
1264+
case .arc(
1265+
let center,
1266+
let radius,
1267+
let startAngle,
1268+
let endAngle,
1269+
let clockwise
1270+
):
1271+
path.appendArc(
1272+
withCenter: NSPoint(x: center.x, y: center.y),
1273+
radius: CGFloat(radius),
1274+
startAngle: CGFloat(startAngle),
1275+
endAngle: CGFloat(endAngle),
1276+
clockwise: clockwise
1277+
)
1278+
case .transform(let transform):
1279+
path.transform(
1280+
using: Foundation.AffineTransform(
1281+
m11: CGFloat(transform.linearTransform.x),
1282+
m12: CGFloat(transform.linearTransform.z),
1283+
m21: CGFloat(transform.linearTransform.y),
1284+
m22: CGFloat(transform.linearTransform.w),
1285+
tX: CGFloat(transform.translation.x),
1286+
tY: CGFloat(transform.translation.y)
1287+
)
1288+
)
1289+
}
1290+
}
1291+
}
1292+
}
1293+
1294+
public func renderPath(
1295+
_ path: Path,
1296+
container: Widget,
1297+
strokeColor: Color,
1298+
fillColor: Color,
1299+
overrideStrokeStyle: StrokeStyle?
1300+
) {
1301+
if let overrideStrokeStyle {
1302+
applyStrokeStyle(overrideStrokeStyle, to: path)
1303+
}
1304+
1305+
let widget = container as! NSBezierPathView
1306+
widget.path = path
1307+
widget.strokeColor = strokeColor.nsColor
1308+
widget.fillColor = fillColor.nsColor
1309+
1310+
widget.setNeedsDisplay(widget.bounds)
1311+
}
11461312
}
11471313

11481314
final class NSCustomTapGestureTarget: NSView {

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,8 @@ public protocol AppBackend {
530530
)
531531

532532
// MARK: Paths
533+
/// Create a widget that can contain a path.
534+
func createPathWidget() -> Widget
533535
/// Create a path. It will not be shown until ``renderPath(_:container:)`` is called.
534536
func createPath() -> Path
535537
/// Update a path. The updates do not need to be visible before ``renderPath(_:container:)``
@@ -542,8 +544,8 @@ public protocol AppBackend {
542544
/// Draw a path to the screen.
543545
/// - Parameters:
544546
/// - path: The path to be rendered.
545-
/// - container: The container widget that the path will render in. It has no other
546-
/// children.
547+
/// - container: The container widget that the path will render in. Created with
548+
/// ``createPathWidget()``.
547549
/// - strokeColor: The color to draw the path's stroke.
548550
/// - fillColor: The color to shade the path's fill.
549551
/// - overrideStrokeStyle: If present, a value to override the path's stroke style.
@@ -872,13 +874,16 @@ extension AppBackend {
872874
}
873875

874876
// MARK: Paths
875-
func createPath() -> Path {
877+
public func createPathWidget() -> Widget {
876878
todo()
877879
}
878-
func updatePath(_ path: Path, _ source: SwiftCrossUI.Path, pointsChanged: Bool) {
880+
public func createPath() -> Path {
879881
todo()
880882
}
881-
func renderPath(
883+
public func updatePath(_ path: Path, _ source: SwiftCrossUI.Path, pointsChanged: Bool) {
884+
todo()
885+
}
886+
public func renderPath(
882887
_ path: Path,
883888
container: Widget,
884889
strokeColor: Color,

Sources/SwiftCrossUI/Views/Shapes/Shape.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ extension Shape {
4242
public func asWidget<Backend: AppBackend>(
4343
_ children: any ViewGraphNodeChildren, backend: Backend
4444
) -> Backend.Widget {
45-
let container = backend.createContainer()
45+
let container = backend.createPathWidget()
4646
let storage = children as! ShapeStorage
4747
storage.backendPath = backend.createPath()
4848
storage.oldPath = nil

Sources/UIKitBackend/UIKitBackend+Path.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import SwiftCrossUI
22
import UIKit
33

4+
final class PathWidget: BaseViewWidget {
5+
let shapeLayer = CAShapeLayer()
6+
7+
override init() {
8+
super.init()
9+
10+
layer.addSublayer(shapeLayer)
11+
}
12+
}
13+
414
extension UIKitBackend {
515
public typealias Path = UIBezierPath
616

17+
public func createPathWidget() -> any WidgetProtocol {
18+
BaseViewWidget()
19+
}
20+
721
public func createPath() -> UIBezierPath {
822
UIBezierPath()
923
}
@@ -100,7 +114,8 @@ extension UIKitBackend {
100114
applyStrokeStyle(overrideStrokeStyle, to: path)
101115
}
102116

103-
let shapeLayer = container.view.layer.sublayers?[0] as? CAShapeLayer ?? CAShapeLayer()
117+
let widget = container as! PathWidget
118+
let shapeLayer = widget.shapeLayer
104119

105120
shapeLayer.path = path.cgPath
106121
shapeLayer.lineWidth = path.lineWidth
@@ -129,10 +144,6 @@ extension UIKitBackend {
129144

130145
shapeLayer.strokeColor = strokeColor.cgColor
131146
shapeLayer.fillColor = fillColor.cgColor
132-
133-
if shapeLayer.superlayer !== container.view.layer {
134-
container.view.layer.addSublayer(shapeLayer)
135-
}
136147
}
137148
}
138149

0 commit comments

Comments
 (0)