-
Notifications
You must be signed in to change notification settings - Fork 58
Implement CGPath related C API #676
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
14a650e
Implement _CGPathCopyDescription API
Kyle-Ye 92bf4e7
Add _CGPathParseString API
Kyle-Ye 7e68498
Add _CGPathCreateRoundedRect
Kyle-Ye 8cce154
Audit _CGPathParseString implementation
Kyle-Ye 5c9ff28
Fix _CGPathParseString
Kyle-Ye File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
269
Sources/OpenSwiftUI_SPI/Overlay/CoreGraphics/CGPath+OpenSwiftUI.m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
Kyle-Ye marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
| CGPathApply(path, &info, ©_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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.