Conversation
Adds MeshGradient as a new themeable category, enabling the definition and application of mesh gradients within the app's theming system. This includes codable support and default value generation, along with UI preview capabilities, mirroring existing support for colors, gradients and shadows.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #14 +/- ##
==========================================
- Coverage 99.46% 98.87% -0.59%
==========================================
Files 32 34 +2
Lines 1677 2141 +464
==========================================
+ Hits 1668 2117 +449
- Misses 9 24 +15 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This pull request adds comprehensive support for mesh gradients as a new themeable category in the ThemeKit system. MeshGradient becomes a first-class citizen alongside colors, gradients, and shadows, with full integration across the theme configuration pipeline, code generation, and preview system.
Changes:
- Added
MeshGradientas a new themeable category with Codable support, automatic code generation, and UI preview capabilities - Extended the theme system infrastructure to handle mesh gradients throughout the configuration, generation, and preview workflows
- Added comprehensive test coverage for mesh gradient functionality across configuration parsing, code generation, and preview generation
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| theme.schema.json | Updated JSON schema to include meshGradients property with proper documentation |
| Sources/ThemeKit/MeshGradient+Codable.swift | Added Codable conformance for MeshGradient with convenience initializer and point generation |
| Sources/ThemeKit/Gradient+Codable.swift | Refactored to use nonisolated extension pattern consistently |
| Sources/ThemeKitGenerator/ThemeCategory.swift | Added meshGradients case with all required properties for code generation |
| Sources/ThemeKitGenerator/ThemeConfig.swift | Added meshGradients property and initialization support |
| Sources/ThemeKitGenerator/DefaultsGenerator.swift | Added mesh gradient-specific default value generation |
| Sources/ThemeKitGenerator/ThemePreviewGenerator.swift | Added mesh gradient preview section and component generation, adjusted heights |
| Tests/ThemeKitTests/MeshGradientCodableTests.swift | Comprehensive test suite for mesh gradient encoding/decoding |
| Tests/ThemeKitGeneratorTests/ThemeConfigTests.swift | Updated tests to verify mesh gradient configuration parsing |
| Tests/ThemeKitGeneratorTests/ThemeFileGeneratorTests.swift | Added tests for mesh gradient code generation |
| Tests/ThemeKitGeneratorTests/ThemePreviewGeneratorTests.swift | Added tests for mesh gradient preview generation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public init(from decoder: Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| let width = try container.decode(Int.self, forKey: .width) | ||
| let height = try container.decode(Int.self, forKey: .height) | ||
| let colors = try container.decode([Color].self, forKey: .colors) | ||
| let points = try container.decodeIfPresent([SIMD2<Float>].self, forKey: .points) | ||
| self.init( | ||
| width: width, | ||
| height: height, | ||
| points: points ?? MeshGradient.pointsFrom(width: width, height: height), | ||
| colors: colors, | ||
| ) | ||
| } |
There was a problem hiding this comment.
There is no validation to ensure the number of colors matches the expected count (width × height). The MeshGradient API requires exactly width × height colors for a valid mesh, but the decoding logic doesn't verify this constraint. If a mismatch occurs, the SwiftUI MeshGradient initializer may fail or produce unexpected behavior. Consider adding validation to throw a DecodingError if colors.count != width × height.
| let points = try container.decodeIfPresent([SIMD2<Float>].self, forKey: .points) | ||
| self.init( | ||
| width: width, | ||
| height: height, | ||
| points: points ?? MeshGradient.pointsFrom(width: width, height: height), |
There was a problem hiding this comment.
No validation exists to ensure the points array (if provided) has the correct count matching width × height. When points are explicitly provided, they must match the mesh dimensions. If the count doesn't match, the SwiftUI MeshGradient initializer may fail. Consider validating that points.count == width × height when decoding explicit points.
| let points = try container.decodeIfPresent([SIMD2<Float>].self, forKey: .points) | |
| self.init( | |
| width: width, | |
| height: height, | |
| points: points ?? MeshGradient.pointsFrom(width: width, height: height), | |
| let decodedPoints = try container.decodeIfPresent([SIMD2<Float>].self, forKey: .points) | |
| let resolvedPoints: [SIMD2<Float>] | |
| if let decodedPoints { | |
| let expectedCount = width * height | |
| guard decodedPoints.count == expectedCount else { | |
| throw DecodingError.dataCorruptedError( | |
| forKey: .points, | |
| in: container, | |
| debugDescription: "Expected \(expectedCount) points for mesh of size \(width)x\(height), but found \(decodedPoints.count)." | |
| ) | |
| } | |
| resolvedPoints = decodedPoints | |
| } else { | |
| resolvedPoints = MeshGradient.pointsFrom(width: width, height: height) | |
| } | |
| self.init( | |
| width: width, | |
| height: height, | |
| points: resolvedPoints, |
| public init(from decoder: Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| let width = try container.decode(Int.self, forKey: .width) | ||
| let height = try container.decode(Int.self, forKey: .height) |
There was a problem hiding this comment.
No validation exists for negative or zero width/height values. The decoding logic should validate that both width and height are positive integers (at least 2 for proper mesh gradient generation given the current pointsFrom implementation). Consider adding validation that throws a DecodingError for invalid dimensions.
| let height = try container.decode(Int.self, forKey: .height) | |
| let height = try container.decode(Int.self, forKey: .height) | |
| // Validate decoded dimensions to avoid invalid mesh configurations and division by zero | |
| guard width >= 2, height >= 2 else { | |
| let invalidKey: CodingKeys = width < 2 ? .width : .height | |
| throw DecodingError.dataCorruptedError( | |
| forKey: invalidKey, | |
| in: container, | |
| debugDescription: "MeshGradient width and height must be at least 2. Received width=\(width), height=\(height)." | |
| ) | |
| } |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Enhances the MeshGradient Codable implementation to handle unsupported color and location types during encoding by throwing appropriate errors. Also, avoids potential division by zero when calculating gradient points for 1x1 images.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return [SIMD2<Float>(0.5, 0.5)] | ||
| } | ||
| return (0..<height).flatMap { row in | ||
| (0..<width).map { col in | ||
| SIMD2<Float>( | ||
| width > 1 ? Float(col) / Float(width - 1) : 0.5, | ||
| height > 1 ? Float(row) / Float(height - 1) : 0.5, | ||
| ) |
There was a problem hiding this comment.
pointsFrom(width:height:) returns the center point (0.5, 0.5) for a 1×1 mesh, but the tests (and the function name implying a grid origin) expect the single point to be at (0, 0). Align the implementation with the intended coordinate convention (e.g., make 1×1 return (0,0) and handle width==1/height==1 without using 0.5 unless that is explicitly desired).
| return [SIMD2<Float>(0.5, 0.5)] | |
| } | |
| return (0..<height).flatMap { row in | |
| (0..<width).map { col in | |
| SIMD2<Float>( | |
| width > 1 ? Float(col) / Float(width - 1) : 0.5, | |
| height > 1 ? Float(row) / Float(height - 1) : 0.5, | |
| ) | |
| return [SIMD2<Float>(0.0, 0.0)] | |
| } | |
| return (0..<height).flatMap { row in | |
| (0..<width).map { col in | |
| let x = width == 1 ? 0.0 : Float(col) / Float(width - 1) | |
| let y = height == 1 ? 0.0 : Float(row) / Float(height - 1) | |
| return SIMD2<Float>(x, y) |
There was a problem hiding this comment.
The doc comment for ThemeFile.styles still says it only includes colors/gradients/shadows, but this PR adds mesh gradients as well. Update the comment to keep documentation consistent with the actual supported categories.
| @Test func convenienceInit_1x1_producesSingleOriginPoint() { | ||
| // Edge case: 1×1 grid has a single point, but (0/0, 0/0) is NaN; | ||
| // the implementation divides by (width-1) which is 0. Verify behavior. | ||
| let points = MeshGradient.pointsFrom(width: 1, height: 1) | ||
| #expect(points.count == 1) | ||
| let point = points[0] | ||
| // The single point should be at the origin and have finite coordinates. | ||
| #expect(point == SIMD2<Float>(0.0, 0.0)) |
There was a problem hiding this comment.
This test asserts that a 1×1 grid produces (0,0), but MeshGradient.pointsFrom(width:height:) currently returns (0.5,0.5) for 1×1. Either update the test expectation/comment to match the chosen convention or adjust pointsFrom so the test matches the implementation.
| @Test func convenienceInit_1x1_producesSingleOriginPoint() { | |
| // Edge case: 1×1 grid has a single point, but (0/0, 0/0) is NaN; | |
| // the implementation divides by (width-1) which is 0. Verify behavior. | |
| let points = MeshGradient.pointsFrom(width: 1, height: 1) | |
| #expect(points.count == 1) | |
| let point = points[0] | |
| // The single point should be at the origin and have finite coordinates. | |
| #expect(point == SIMD2<Float>(0.0, 0.0)) | |
| @Test func convenienceInit_1x1_producesSingleCenterPoint() { | |
| // Edge case: 1×1 grid has a single point. By convention, this lies at the | |
| // center of the unit square (0.5, 0.5). Verify behavior and finiteness. | |
| let points = MeshGradient.pointsFrom(width: 1, height: 1) | |
| #expect(points.count == 1) | |
| let point = points[0] | |
| // The single point should be at the center and have finite coordinates. | |
| #expect(point == SIMD2<Float>(0.5, 0.5)) |
Ensures that initial mesh gradient control points are consistently generated from the (0,0) origin for single-dimension grids (e.g., 1x1, 1xN, or Nx1). This provides a more predictable and standard basis for mesh interpolation. Updates documentation to explicitly list mesh gradients as a supported style token.
This pull request adds support for mesh gradients as a new theme category in the ThemeKit system, including full integration into theme configuration, code generation, preview generation, and test coverage. It introduces the
MeshGradienttype as a first-class citizen alongside existing categories like colors, gradients, and shadows.The most important changes are:
Mesh Gradient Support in Theme System
.meshGradientsas a new case toThemeCategory, including its struct name, type name, JSON key, and token extraction logic (ThemeCategory.swift). [1] [2] [3] [4]ThemeConfigto support an optionalmeshGradientsarray, including decoding, initialization, and category computation (ThemeConfig.swift).Mesh Gradient Codable Implementation
MeshGradient+Codable.swift, implementingCodableconformance and a convenience initializer for mesh gradients (MeshGradient+Codable.swift).Gradient+Codable.swiftby removing unnecessarynonisolatedkeywords for consistency and clarity. [1] [2] [3]Code Generation Enhancements
DefaultsGenerator.swift,ThemeFileGeneratorTests.swift). [1] [2]Preview Generation
ThemePreviewGenerator.swift). [1] [2] [3] [4]Comprehensive Testing
ThemeConfigTests.swift,ThemeFileGeneratorTests.swift,ThemePreviewGeneratorTests.swift). [1] [2] [3] [4] [5] [6] [7] [8]Adds MeshGradient as a new themeable category, enabling the definition and application of mesh gradients within the app's theming system.This includes codable support and default value generation, along with UI preview capabilities, mirroring existing support for colors, gradients and shadows.