Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,14 @@ extension Target {
var swiftSettings = swiftSettings ?? []
swiftSettings.append(.define("OPENRENDERBOX_RENDERBOX"))
self.swiftSettings = swiftSettings

var cSettings = cSettings ?? []
cSettings.append(.define("OPENRENDERBOX_RENDERBOX"))
self.cSettings = cSettings

var cxxSettings = cxxSettings ?? []
cxxSettings.append(.define("OPENRENDERBOX_RENDERBOX"))
self.cxxSettings = cxxSettings
}

func addCoreUISettings() {
Expand Down
7 changes: 1 addition & 6 deletions Sources/OpenSwiftUICore/Shape/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ package import OpenRenderBoxShims
import OpenSwiftUI_SPI
public import OpenCoreGraphicsShims

#if canImport(CoreGraphics)
@_silgen_name("__CGPathParseString")
private func __CGPathParseString(_ path: CGMutablePath, _ utf8CString: UnsafePointer<CChar>) -> Bool
#endif

// MARK: - Path

/// The outline of a 2D shape.
Expand Down Expand Up @@ -280,7 +275,7 @@ public struct Path: Equatable, LosslessStringConvertible, @unchecked Sendable {
guard let str = nsString.utf8String else {
return nil
}
guard __CGPathParseString(mutablePath, str) else {
guard _CGPathParseString(mutablePath, str) else {
return nil
}
storage = .path(PathBox(mutablePath))
Expand Down
73 changes: 73 additions & 0 deletions Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// CGPath+OpenSwiftUI.h
// OpenSwiftUI_SPI

#ifndef CGPath_OpenSwiftUI_h
#define CGPath_OpenSwiftUI_h

#include "OpenSwiftUIBase.h"

#if OPENSWIFTUI_TARGET_OS_DARWIN

#include <Foundation/Foundation.h>
#include <CoreGraphics/CoreGraphics.h>

OPENSWIFTUI_ASSUME_NONNULL_BEGIN

/// Parses a path string and appends the path elements to a mutable path.
///
/// The string format uses space-separated numbers followed by command characters:
///
/// | Command | Parameters | Description |
/// |---------|------------|-------------|
/// | `m` | x y | Move to point |
/// | `l` | x y | Line to point |
/// | `c` | cp1x cp1y cp2x cp2y x y | Cubic Bézier curve |
/// | `q` | cpx cpy x y | Quadratic Bézier curve |
/// | `t` | x y | Smooth quadratic curve (reflects previous control point) |
/// | `v` | cp2x cp2y x y | Smooth cubic curve (uses last point as cp1) |
/// | `y` | cp1x cp1y x y | Shorthand cubic (cp2 equals endpoint) |
/// | `h` | (none) | Close subpath |
/// | `re` | x y width height | Rectangle |
///
/// Whitespace characters (space, tab, newline, carriage return) are skipped.
/// Numbers can be integers, decimals, or special values like `Inf`.
///
/// - Parameters:
/// - path: The mutable path to append elements to.
/// - utf8CString: The path string to parse.
/// - Returns: `YES` if parsing succeeded, `NO` if the string is malformed.
BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString);

/// Creates a string description of a path with optional coordinate rounding.
///
/// - Parameters:
/// - path: The path to describe.
/// - step: The rounding step for coordinates. When non-zero, coordinates
/// are rounded to the nearest multiple of this value. Pass 0 for no rounding.
/// - Returns: A string representation of the path using SVG-like commands
/// (m for move, l for line, h for close).
NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step);

/// Creates a rounded rectangle path with the specified corner radii.
///
/// The corner radii are automatically clamped to fit within the rectangle:
/// - Negative values are treated as 0
/// - Values exceeding half the width or height are reduced accordingly
///
/// - Parameters:
/// - rect: The rectangle to create the path from.
/// - cornerWidth: The horizontal radius of the rounded corners.
/// - cornerHeight: The vertical radius of the rounded corners.
/// - useRB: If `YES`, uses RenderBox for path creation (when available).
/// If `NO`, uses CoreGraphics directly.
/// - Returns: A new path representing the rounded rectangle. Returns a plain
/// rectangle path if either corner dimension is 0 or if the rect is empty.
CF_RETURNS_RETAINED
CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB);

OPENSWIFTUI_ASSUME_NONNULL_END

#endif

#endif /* CGPath_OpenSwiftUI_h */
269 changes: 269 additions & 0 deletions Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
//
// CGPath+OpenSwiftUI.m
// OpenSwiftUI_SPI

#import "CGPath+OpenSwiftUI.h"

#if OPENSWIFTUI_TARGET_OS_DARWIN

#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
#import <xlocale.h>

#if OPENRENDERBOX_RENDERBOX
@import RenderBox;
#else
@import OpenRenderBox;
#endif

BOOL _CGPathParseString(CGMutablePathRef path, const char *utf8CString) {
double numbers[6];
int numCount = 0;
CGFloat currentX = 0.0, currentY = 0.0;
CGFloat lastControlX = 0.0, lastControlY = 0.0;
const char *ptr = utf8CString;
do {
while (*ptr <= 0x1f) {
switch (*ptr) {
case 0: return true;
case 9: case 10: case 12: case 13: ptr++; break;
default: return false;
}
}
unsigned char c = (unsigned char)*ptr;
BOOL isNumberStart = NO;
switch (c) {
case ' ': break;
case '+': case '-': case '.': case '0' ... '9':
case 'E': case 'P': case 'X': case 'e': case 'p': case 'x':
isNumberStart = YES;
break;
case 'I':
if (ptr[1] == 'n' && ptr[2] == 'f') { isNumberStart = YES; }
break;
case 'm':
if (numCount != 2) return NO;
CGPathMoveToPoint(path, NULL,
numbers[0], numbers[1]);
currentX = lastControlX = numbers[0];
currentY = lastControlY = numbers[1];
numCount = 0;
break;
case 'l':
if (numCount != 2) return NO;
CGPathAddLineToPoint(path, NULL,
numbers[0], numbers[1]);
currentX = lastControlX = numbers[0];
currentY = lastControlY = numbers[1];
numCount = 0;
break;
case 'c':
if (numCount != 6) return NO;
CGPathAddCurveToPoint(path, NULL,
numbers[0], numbers[1],
numbers[2], numbers[3],
numbers[4], numbers[5]);
lastControlX = numbers[2];
lastControlY = numbers[3];
currentX = numbers[4];
currentY = numbers[5];
numCount = 0;
break;
case 'q':
if (numCount != 4) return NO;
CGPathAddQuadCurveToPoint(path, NULL,
numbers[0], numbers[1],
numbers[2], numbers[3]);
lastControlX = numbers[0];
lastControlY = numbers[1];
currentX = numbers[2];
currentY = numbers[3];
numCount = 0;
break;
case 't':
if (numCount != 2) return NO;
CGFloat reflectedX = currentX * 2.0 - lastControlX;
CGFloat reflectedY = currentY * 2.0 - lastControlY;
CGPathAddQuadCurveToPoint(path, NULL,
reflectedX, reflectedY,
numbers[0], numbers[1]);
lastControlX = reflectedX;
lastControlY = reflectedY;
currentX = numbers[0];
currentY = numbers[1];
numCount = 0;
break;
case 'v':
if (numCount != 4) return NO;
CGPathAddCurveToPoint(path, NULL,
currentX, currentY,
numbers[0], numbers[1],
numbers[2], numbers[3]);
lastControlX = numbers[0];
lastControlY = numbers[1];
currentX = numbers[2];
currentY = numbers[3];
numCount = 0;
break;
case 'y':
if (numCount != 4) return NO;
CGPathAddCurveToPoint(path, NULL,
numbers[0], numbers[1],
numbers[2], numbers[3],
numbers[2], numbers[3]);
lastControlX = numbers[2];
lastControlY = numbers[3];
numCount = 0;
break;
case 'h':
if (numCount != 0) return NO;
CGPathCloseSubpath(path);
lastControlX = 0.0;
lastControlY = 0.0;
numCount = 0;
break;
case 'r':
if (ptr[1] != 'e') return NO;
if (numCount != 4) return NO;
CGPathAddRect(path, NULL,
CGRectMake(numbers[0], numbers[1],
numbers[2], numbers[3]));
ptr++;
numCount = 0;
break;
default:
return false;
}
if (isNumberStart) {
if (numCount == 6) return false;
char *endPtr;
numbers[numCount++] = strtod_l(ptr, &endPtr, NULL);
ptr = endPtr;
} else {
ptr++;
}
} while (1);
}

typedef struct PathInfo {
CFMutableStringRef description;
CGFloat step;
CGFloat inverseStep;
} PathInfo;

#define APPEND_COORD(coord) do { \
CGFloat value = (coord); \
if (path_info->step != 0.0) { \
value = path_info->step * round(value * path_info->inverseStep); \
} \
char buffer[64]; \
snprintf_l(buffer, 64, NULL, "%g ", value); \
CFStringAppendCString(path_info->description, buffer, kCFStringEncodingUTF8); \
} while (0)

#define APPEND_POINTS(count) do { \
for (int i = 0; i < (count); i++) { \
APPEND_COORD(element->points[i].x); \
APPEND_COORD(element->points[i].y); \
} \
} while (0)

void copy_path_iter(void * __nullable info, const CGPathElement * element) {
PathInfo *path_info = (PathInfo *)info;
if (path_info->description != NULL) {
CFStringAppend(path_info->description, CFSTR(" "));
}
UniChar ch;
switch (element->type) {
case kCGPathElementMoveToPoint:
APPEND_POINTS(1);
ch = 'm';
break;
case kCGPathElementAddLineToPoint:
APPEND_POINTS(1);
ch = 'l';
break;
case kCGPathElementAddQuadCurveToPoint:
APPEND_POINTS(2);
ch = 'q';
break;
case kCGPathElementAddCurveToPoint:
APPEND_POINTS(3);
ch = 'c';
break;
case kCGPathElementCloseSubpath:
ch = 'h';
break;
default:
return;
}
CFStringAppendCharacters(path_info->description, &ch, 1);
}

#undef APPEND_COORD
#undef APPEND_POINTS

NSString * _CGPathCopyDescription(CGPathRef path, CGFloat step) {
PathInfo info = {
CFStringCreateMutable(kCFAllocatorDefault, 0),
step,
1.0 / step
};
CGPathApply(path, &info, &copy_path_iter);
return (__bridge_transfer NSString *)(info.description);
}

CGPathRef _CGPathCreateRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, BOOL useRB) {
// Clamp corner dimensions to be non-negative
if (cornerWidth < 0.0) {
cornerWidth = 0.0;
}
if (cornerHeight < 0.0) {
cornerHeight = 0.0;
}

// If either corner dimension is 0, or rect is empty, return a plain rectangle
if (cornerWidth == 0.0 || cornerHeight == 0.0 || CGRectIsEmpty(rect)) {
return CGPathCreateWithRect(rect, NULL);
}

if (useRB) {
#if OPENRENDERBOX_RENDERBOX
// RBPath rbPath = RBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES);
// CGPathRef cgPath = RBPathCopyCGPath(rbPath);
// RBPathRelease(rbPath);
// return cgPath;
#else
// ORBPath rbPath = ORBPathMakeRoundedRect(NULL, rect, cornerWidth, cornerHeight, YES);
// CGPathRef cgPath = ORBPathCopyCGPath(rbPath);
// ORBPathRelease(rbPath);
// return cgPath;
#endif
}

// Use CoreGraphics path creation
CGFloat width = CGRectGetWidth(rect);
CGFloat height = CGRectGetHeight(rect);

// Clamp cornerWidth to at most half the width
if (cornerWidth * 2.0 > width) {
cornerWidth = nextafter(width * 0.5, 0.0);
}

// Clamp cornerHeight to at most half the height
if (cornerHeight * 2.0 > height) {
cornerHeight = nextafter(height * 0.5, 0.0);
}

// Final validation
if (cornerWidth < 0.0 || cornerWidth * 2.0 > width) {
return CGPathCreateWithRect(rect, NULL);
}
if (cornerHeight < 0.0 || cornerHeight * 2.0 > height) {
return CGPathCreateWithRect(rect, NULL);
}

return CGPathCreateWithRoundedRect(rect, cornerWidth, cornerHeight, NULL);
}

#endif
Loading
Loading