Skip to content

Commit 5f393c5

Browse files
authored
Implement CGPath related C API (#676)
* Implement _CGPathCopyDescription API * Add _CGPathParseString API * Add _CGPathCreateRoundedRect * Audit _CGPathParseString implementation * Fix _CGPathParseString
1 parent 4a07ebe commit 5f393c5

File tree

7 files changed

+484
-42
lines changed

7 files changed

+484
-42
lines changed

Package.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,14 @@ extension Target {
392392
var swiftSettings = swiftSettings ?? []
393393
swiftSettings.append(.define("OPENRENDERBOX_RENDERBOX"))
394394
self.swiftSettings = swiftSettings
395+
396+
var cSettings = cSettings ?? []
397+
cSettings.append(.define("OPENRENDERBOX_RENDERBOX"))
398+
self.cSettings = cSettings
399+
400+
var cxxSettings = cxxSettings ?? []
401+
cxxSettings.append(.define("OPENRENDERBOX_RENDERBOX"))
402+
self.cxxSettings = cxxSettings
395403
}
396404

397405
func addCoreUISettings() {

Sources/OpenSwiftUICore/Shape/Path.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@ package import OpenRenderBoxShims
1212
import OpenSwiftUI_SPI
1313
public import OpenCoreGraphicsShims
1414

15-
#if canImport(CoreGraphics)
16-
@_silgen_name("__CGPathParseString")
17-
private func __CGPathParseString(_ path: CGMutablePath, _ utf8CString: UnsafePointer<CChar>) -> Bool
18-
#endif
19-
2015
// MARK: - Path
2116

2217
/// The outline of a 2D shape.
@@ -280,7 +275,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable {
280275
guard let str = nsString.utf8String else {
281276
return nil
282277
}
283-
guard __CGPathParseString(mutablePath, str) else {
278+
guard _CGPathParseString(mutablePath, str) else {
284279
return nil
285280
}
286281
storage = .path(PathBox(mutablePath))
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// CGPath+OpenSwiftUI.h
3+
// OpenSwiftUI_SPI
4+
5+
#ifndef CGPath_OpenSwiftUI_h
6+
#define CGPath_OpenSwiftUI_h
7+
8+
#include "OpenSwiftUIBase.h"
9+
10+
#if OPENSWIFTUI_TARGET_OS_DARWIN
11+
12+
#include <Foundation/Foundation.h>
13+
#include <CoreGraphics/CoreGraphics.h>
14+
15+
OPENSWIFTUI_ASSUME_NONNULL_BEGIN
16+
17+
/// Parses a path string and appends the path elements to a mutable path.
18+
///
19+
/// The string format uses space-separated numbers followed by command characters:
20+
///
21+
/// | Command | Parameters | Description |
22+
/// |---------|------------|-------------|
23+
/// | `m` | x y | Move to point |
24+
/// | `l` | x y | Line to point |
25+
/// | `c` | cp1x cp1y cp2x cp2y x y | Cubic Bézier curve |
26+
/// | `q` | cpx cpy x y | Quadratic Bézier curve |
27+
/// | `t` | x y | Smooth quadratic curve (reflects previous control point) |
28+
/// | `v` | cp2x cp2y x y | Smooth cubic curve (uses last point as cp1) |
29+
/// | `y` | cp1x cp1y x y | Shorthand cubic (cp2 equals endpoint) |
30+
/// | `h` | (none) | Close subpath |
31+
/// | `re` | x y width height | Rectangle |
32+
///
33+
/// Whitespace characters (space, tab, newline, carriage return) are skipped.
34+
/// Numbers can be integers, decimals, or special values like `Inf`.
35+
///
36+
/// - Parameters:
37+
/// - path: The mutable path to append elements to.
38+
/// - utf8CString: The path string to parse.
39+
/// - Returns: `YES` if parsing succeeded, `NO` if the string is malformed.
40+
BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString);
41+
42+
/// Creates a string description of a path with optional coordinate rounding.
43+
///
44+
/// - Parameters:
45+
/// - path: The path to describe.
46+
/// - step: The rounding step for coordinates. When non-zero, coordinates
47+
/// are rounded to the nearest multiple of this value. Pass 0 for no rounding.
48+
/// - Returns: A string representation of the path using SVG-like commands
49+
/// (m for move, l for line, h for close).
50+
NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step);
51+
52+
/// Creates a rounded rectangle path with the specified corner radii.
53+
///
54+
/// The corner radii are automatically clamped to fit within the rectangle:
55+
/// - Negative values are treated as 0
56+
/// - Values exceeding half the width or height are reduced accordingly
57+
///
58+
/// - Parameters:
59+
/// - rect: The rectangle to create the path from.
60+
/// - cornerWidth: The horizontal radius of the rounded corners.
61+
/// - cornerHeight: The vertical radius of the rounded corners.
62+
/// - useRB: If `YES`, uses RenderBox for path creation (when available).
63+
/// If `NO`, uses CoreGraphics directly.
64+
/// - Returns: A new path representing the rounded rectangle. Returns a plain
65+
/// rectangle path if either corner dimension is 0 or if the rect is empty.
66+
CF_RETURNS_RETAINED
67+
CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB);
68+
69+
OPENSWIFTUI_ASSUME_NONNULL_END
70+
71+
#endif
72+
73+
#endif /* CGPath_OpenSwiftUI_h */
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
//
2+
// CGPath+OpenSwiftUI.m
3+
// OpenSwiftUI_SPI
4+
5+
#import "CGPath+OpenSwiftUI.h"
6+
7+
#if OPENSWIFTUI_TARGET_OS_DARWIN
8+
9+
#import <Foundation/Foundation.h>
10+
#import <CoreGraphics/CoreGraphics.h>
11+
#import <xlocale.h>
12+
13+
#if OPENRENDERBOX_RENDERBOX
14+
@import RenderBox;
15+
#else
16+
@import OpenRenderBox;
17+
#endif
18+
19+
BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) {
20+
double numbers[6];
21+
int numCount = 0;
22+
CGFloat currentX = 0.0, currentY = 0.0;
23+
CGFloat lastControlX = 0.0, lastControlY = 0.0;
24+
const char *ptr = utf8CString;
25+
do {
26+
while (*ptr <= 0x1f) {
27+
switch (*ptr) {
28+
case 0: return true;
29+
case 9: case 10: case 12: case 13: ptr++; break;
30+
default: return false;
31+
}
32+
}
33+
unsigned char c = (unsigned char)*ptr;
34+
BOOL isNumberStart = NO;
35+
switch (c) {
36+
case ' ': break;
37+
case '+': case '-': case '.': case '0' ... '9':
38+
case 'E': case 'P': case 'X': case 'e': case 'p': case 'x':
39+
isNumberStart = YES;
40+
break;
41+
case 'I':
42+
if (ptr[1] == 'n' && ptr[2] == 'f') { isNumberStart = YES; }
43+
break;
44+
case 'm':
45+
if (numCount != 2) return NO;
46+
CGPathMoveToPoint(path, NULL,
47+
numbers[0], numbers[1]);
48+
currentX = lastControlX = numbers[0];
49+
currentY = lastControlY = numbers[1];
50+
numCount = 0;
51+
break;
52+
case 'l':
53+
if (numCount != 2) return NO;
54+
CGPathAddLineToPoint(path, NULL,
55+
numbers[0], numbers[1]);
56+
currentX = lastControlX = numbers[0];
57+
currentY = lastControlY = numbers[1];
58+
numCount = 0;
59+
break;
60+
case 'c':
61+
if (numCount != 6) return NO;
62+
CGPathAddCurveToPoint(path, NULL,
63+
numbers[0], numbers[1],
64+
numbers[2], numbers[3],
65+
numbers[4], numbers[5]);
66+
lastControlX = numbers[2];
67+
lastControlY = numbers[3];
68+
currentX = numbers[4];
69+
currentY = numbers[5];
70+
numCount = 0;
71+
break;
72+
case 'q':
73+
if (numCount != 4) return NO;
74+
CGPathAddQuadCurveToPoint(path, NULL,
75+
numbers[0], numbers[1],
76+
numbers[2], numbers[3]);
77+
lastControlX = numbers[0];
78+
lastControlY = numbers[1];
79+
currentX = numbers[2];
80+
currentY = numbers[3];
81+
numCount = 0;
82+
break;
83+
case 't':
84+
if (numCount != 2) return NO;
85+
CGFloat reflectedX = currentX * 2.0 - lastControlX;
86+
CGFloat reflectedY = currentY * 2.0 - lastControlY;
87+
CGPathAddQuadCurveToPoint(path, NULL,
88+
reflectedX, reflectedY,
89+
numbers[0], numbers[1]);
90+
lastControlX = reflectedX;
91+
lastControlY = reflectedY;
92+
currentX = numbers[0];
93+
currentY = numbers[1];
94+
numCount = 0;
95+
break;
96+
case 'v':
97+
if (numCount != 4) return NO;
98+
CGPathAddCurveToPoint(path, NULL,
99+
currentX, currentY,
100+
numbers[0], numbers[1],
101+
numbers[2], numbers[3]);
102+
lastControlX = numbers[0];
103+
lastControlY = numbers[1];
104+
currentX = numbers[2];
105+
currentY = numbers[3];
106+
numCount = 0;
107+
break;
108+
case 'y':
109+
if (numCount != 4) return NO;
110+
CGPathAddCurveToPoint(path, NULL,
111+
numbers[0], numbers[1],
112+
numbers[2], numbers[3],
113+
numbers[2], numbers[3]);
114+
lastControlX = numbers[2];
115+
lastControlY = numbers[3];
116+
numCount = 0;
117+
break;
118+
case 'h':
119+
if (numCount != 0) return NO;
120+
CGPathCloseSubpath(path);
121+
lastControlX = 0.0;
122+
lastControlY = 0.0;
123+
numCount = 0;
124+
break;
125+
case 'r':
126+
if (ptr[1] != 'e') return NO;
127+
if (numCount != 4) return NO;
128+
CGPathAddRect(path, NULL,
129+
CGRectMake(numbers[0], numbers[1],
130+
numbers[2], numbers[3]));
131+
ptr++;
132+
numCount = 0;
133+
break;
134+
default:
135+
return false;
136+
}
137+
if (isNumberStart) {
138+
if (numCount == 6) return false;
139+
char *endPtr;
140+
numbers[numCount++] = strtod_l(ptr, &endPtr, NULL);
141+
ptr = endPtr;
142+
} else {
143+
ptr++;
144+
}
145+
} while (1);
146+
}
147+
148+
typedef struct PathInfo {
149+
CFMutableStringRef description;
150+
CGFloat step;
151+
CGFloat inverseStep;
152+
} PathInfo;
153+
154+
#define APPEND_COORD(coord) do { \
155+
CGFloat value = (coord); \
156+
if (path_info->step != 0.0) { \
157+
value = path_info->step * round(value * path_info->inverseStep); \
158+
} \
159+
char buffer[64]; \
160+
snprintf_l(buffer, 64, NULL, "%g ", value); \
161+
CFStringAppendCString(path_info->description, buffer, kCFStringEncodingUTF8); \
162+
} while (0)
163+
164+
#define APPEND_POINTS(count) do { \
165+
for (int i = 0; i < (count); i++) { \
166+
APPEND_COORD(element->points[i].x); \
167+
APPEND_COORD(element->points[i].y); \
168+
} \
169+
} while (0)
170+
171+
void copy_path_iter(void * __nullable info, const CGPathElement * element) {
172+
PathInfo *path_info = (PathInfo *)info;
173+
if (path_info->description != NULL) {
174+
CFStringAppend(path_info->description, CFSTR(" "));
175+
}
176+
UniChar ch;
177+
switch (element->type) {
178+
case kCGPathElementMoveToPoint:
179+
APPEND_POINTS(1);
180+
ch = 'm';
181+
break;
182+
case kCGPathElementAddLineToPoint:
183+
APPEND_POINTS(1);
184+
ch = 'l';
185+
break;
186+
case kCGPathElementAddQuadCurveToPoint:
187+
APPEND_POINTS(2);
188+
ch = 'q';
189+
break;
190+
case kCGPathElementAddCurveToPoint:
191+
APPEND_POINTS(3);
192+
ch = 'c';
193+
break;
194+
case kCGPathElementCloseSubpath:
195+
ch = 'h';
196+
break;
197+
default:
198+
return;
199+
}
200+
CFStringAppendCharacters(path_info->description, &ch, 1);
201+
}
202+
203+
#undef APPEND_COORD
204+
#undef APPEND_POINTS
205+
206+
NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step) {
207+
PathInfo info = {
208+
CFStringCreateMutable(kCFAllocatorDefault, 0),
209+
step,
210+
1.0 / step
211+
};
212+
CGPathApply(path, &info, &copy_path_iter);
213+
return (__bridge_transfer NSString *)(info.description);
214+
}
215+
216+
CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB) {
217+
// Clamp corner dimensions to be non-negative
218+
if (cornerWidth < 0.0) {
219+
cornerWidth = 0.0;
220+
}
221+
if (cornerHeight < 0.0) {
222+
cornerHeight = 0.0;
223+
}
224+
225+
// If either corner dimension is 0, or rect is empty, return a plain rectangle
226+
if (cornerWidth == 0.0 || cornerHeight == 0.0 || CGRectIsEmpty(rect)) {
227+
return CGPathCreateWithRect(rect, NULL);
228+
}
229+
230+
if (useRB) {
231+
#if OPENRENDERBOX_RENDERBOX
232+
// RBPath rbPath = RBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES);
233+
// CGPathRef cgPath = RBPathCopyCGPath(rbPath);
234+
// RBPathRelease(rbPath);
235+
// return cgPath;
236+
#else
237+
// ORBPath rbPath = ORBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES);
238+
// CGPathRef cgPath = ORBPathCopyCGPath(rbPath);
239+
// ORBPathRelease(rbPath);
240+
// return cgPath;
241+
#endif
242+
}
243+
244+
// Use CoreGraphics path creation
245+
CGFloat width = CGRectGetWidth(rect);
246+
CGFloat height = CGRectGetHeight(rect);
247+
248+
// Clamp cornerWidth to at most half the width
249+
if (cornerWidth * 2.0 > width) {
250+
cornerWidth = nextafter(width * 0.5, 0.0);
251+
}
252+
253+
// Clamp cornerHeight to at most half the height
254+
if (cornerHeight * 2.0 > height) {
255+
cornerHeight = nextafter(height * 0.5, 0.0);
256+
}
257+
258+
// Final validation
259+
if (cornerWidth < 0.0 || cornerWidth * 2.0 > width) {
260+
return CGPathCreateWithRect(rect, NULL);
261+
}
262+
if (cornerHeight < 0.0 || cornerHeight * 2.0 > height) {
263+
return CGPathCreateWithRect(rect, NULL);
264+
}
265+
266+
return CGPathCreateWithRoundedRect(rect, cornerWidth, cornerHeight, NULL);
267+
}
268+
269+
#endif

0 commit comments

Comments
 (0)