Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 */
258 changes: 258 additions & 0 deletions Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
//
// 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]);
currentX = numbers[2];
currentY = numbers[3];
lastControlX = numbers[4];
lastControlY = numbers[5];
numCount = 0;
break;
case 'q':
if (numCount != 4) return NO;
CGPathAddQuadCurveToPoint(path, NULL, numbers[0], numbers[1],
numbers[2], numbers[3]);
currentX = numbers[0];
currentY = numbers[1];
lastControlX = numbers[2];
lastControlY = 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]);
currentX = reflectedX;
currentY = reflectedY;
lastControlX = numbers[0];
lastControlY = numbers[1];
}
numCount = 0;
break;
case 'v':
if (numCount != 4) return NO;
CGPathAddCurveToPoint(path, NULL, lastControlX, lastControlY,
numbers[0], numbers[1], numbers[2], numbers[3]);
lastControlX = numbers[2];
lastControlY = 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
24 changes: 0 additions & 24 deletions Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/__CGPathParseString.h

This file was deleted.

Loading
Loading