Skip to content

Commit a687258

Browse files
authored
Merge pull request #29 from PureSwift/feature/graphics
2 parents c420831 + 078451e commit a687258

7 files changed

Lines changed: 348 additions & 4 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// Gradient.swift
3+
// AndroidSwiftUICore
4+
//
5+
6+
/// A location in a view, normalized to its size (0…1 on each axis).
7+
public struct UnitPoint: Equatable, Sendable {
8+
public var x: Double
9+
public var y: Double
10+
public init(x: Double, y: Double) { self.x = x; self.y = y }
11+
12+
public static let leading = UnitPoint(x: 0, y: 0.5)
13+
public static let trailing = UnitPoint(x: 1, y: 0.5)
14+
public static let top = UnitPoint(x: 0.5, y: 0)
15+
public static let bottom = UnitPoint(x: 0.5, y: 1)
16+
public static let topLeading = UnitPoint(x: 0, y: 0)
17+
public static let topTrailing = UnitPoint(x: 1, y: 0)
18+
public static let bottomLeading = UnitPoint(x: 0, y: 1)
19+
public static let bottomTrailing = UnitPoint(x: 1, y: 1)
20+
public static let center = UnitPoint(x: 0.5, y: 0.5)
21+
}
22+
23+
/// An ordered list of colors for a gradient.
24+
public struct Gradient: Equatable, Sendable {
25+
public var colors: [Color]
26+
public init(colors: [Color]) { self.colors = colors }
27+
}
28+
29+
/// A linear gradient fill, usable as a view.
30+
public struct LinearGradient: View {
31+
32+
internal let colors: [Color]
33+
internal let startPoint: UnitPoint
34+
internal let endPoint: UnitPoint
35+
36+
public init(colors: [Color], startPoint: UnitPoint, endPoint: UnitPoint) {
37+
self.colors = colors
38+
self.startPoint = startPoint
39+
self.endPoint = endPoint
40+
}
41+
42+
public init(gradient: Gradient, startPoint: UnitPoint, endPoint: UnitPoint) {
43+
self.init(colors: gradient.colors, startPoint: startPoint, endPoint: endPoint)
44+
}
45+
46+
public typealias Body = Never
47+
}
48+
49+
extension LinearGradient: PrimitiveView {
50+
public func _render(in context: ResolveContext) -> RenderNode {
51+
RenderNode(type: "LinearGradient", id: context.path, props: [
52+
"colors": .array(colors.map { $0.propValue }),
53+
"startX": .double(startPoint.x), "startY": .double(startPoint.y),
54+
"endX": .double(endPoint.x), "endY": .double(endPoint.y),
55+
])
56+
}
57+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// Shapes.swift
3+
// AndroidSwiftUICore
4+
//
5+
// Shape views fill their frame with a fill color. A shape without an explicit
6+
// `.frame` renders at zero size (the layout engine's fill-the-parent behavior
7+
// is not modeled); the catalog always frames them.
8+
//
9+
10+
private func shapeNode(_ kind: String, fill: Color?, cornerRadius: Double? = nil, context: ResolveContext) -> RenderNode {
11+
var props: [String: PropValue] = ["shape": .string(kind)]
12+
if let fill { props["fill"] = fill.propValue }
13+
if let cornerRadius { props["cornerRadius"] = .double(cornerRadius) }
14+
return RenderNode(type: "Shape", id: context.path, props: props)
15+
}
16+
17+
public struct Rectangle: View {
18+
internal var fillColor: Color?
19+
public init() {}
20+
public func fill(_ color: Color) -> Rectangle { var copy = self; copy.fillColor = color; return copy }
21+
public typealias Body = Never
22+
}
23+
24+
extension Rectangle: PrimitiveView {
25+
public func _render(in context: ResolveContext) -> RenderNode {
26+
shapeNode("rectangle", fill: fillColor, context: context)
27+
}
28+
}
29+
30+
public struct Circle: View {
31+
internal var fillColor: Color?
32+
public init() {}
33+
public func fill(_ color: Color) -> Circle { var copy = self; copy.fillColor = color; return copy }
34+
public typealias Body = Never
35+
}
36+
37+
extension Circle: PrimitiveView {
38+
public func _render(in context: ResolveContext) -> RenderNode {
39+
shapeNode("circle", fill: fillColor, context: context)
40+
}
41+
}
42+
43+
public struct Capsule: View {
44+
internal var fillColor: Color?
45+
public init() {}
46+
public func fill(_ color: Color) -> Capsule { var copy = self; copy.fillColor = color; return copy }
47+
public typealias Body = Never
48+
}
49+
50+
extension Capsule: PrimitiveView {
51+
public func _render(in context: ResolveContext) -> RenderNode {
52+
shapeNode("capsule", fill: fillColor, context: context)
53+
}
54+
}
55+
56+
public struct RoundedRectangle: View {
57+
internal let cornerRadius: Double
58+
internal var fillColor: Color?
59+
public init(cornerRadius: Double) { self.cornerRadius = cornerRadius }
60+
public func fill(_ color: Color) -> RoundedRectangle { var copy = self; copy.fillColor = color; return copy }
61+
public typealias Body = Never
62+
}
63+
64+
extension RoundedRectangle: PrimitiveView {
65+
public func _render(in context: ResolveContext) -> RenderNode {
66+
shapeNode("roundedRectangle", fill: fillColor, cornerRadius: cornerRadius, context: context)
67+
}
68+
}

AndroidSwiftUICore/Sources/AndroidSwiftUICore/Primitives/Views.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,22 +43,32 @@ extension Color: PrimitiveView {
4343
}
4444
}
4545

46-
/// An image referenced by name. Rendering is placeholder-level until an asset
47-
/// pipeline exists.
46+
/// An image. `Image(systemName:)` maps a curated set of SF Symbol names to
47+
/// Material icons in the interpreter; a named asset stays placeholder-level
48+
/// until an asset pipeline exists.
4849
public struct Image: View {
4950

5051
internal let name: String
52+
internal let systemName: String?
5153

5254
public init(_ name: String) {
5355
self.name = name
56+
self.systemName = nil
57+
}
58+
59+
public init(systemName: String) {
60+
self.name = systemName
61+
self.systemName = systemName
5462
}
5563

5664
public typealias Body = Never
5765
}
5866

5967
extension Image: PrimitiveView {
6068
public func _render(in context: ResolveContext) -> RenderNode {
61-
RenderNode(type: "Image", id: context.path, props: ["name": .string(name)])
69+
var props: [String: PropValue] = ["name": .string(name)]
70+
if let systemName { props["systemName"] = .string(systemName) }
71+
return RenderNode(type: "Image", id: context.path, props: props)
6272
}
6373
}
6474

AndroidSwiftUICore/Tests/AndroidSwiftUICoreTests/EvaluatorTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,40 @@ struct ModifierTests {
181181
#expect(flag?.args["value"] == .bool(true))
182182
}
183183
}
184+
185+
// MARK: - Graphics
186+
187+
@Suite("Graphics")
188+
struct GraphicsTests {
189+
190+
@Test("Shapes emit a Shape node with their kind and fill")
191+
func shapes() {
192+
let rect = ViewHost(Rectangle().fill(.blue)).evaluate()
193+
#expect(rect.type == "Shape")
194+
#expect(rect.props["shape"] == .string("rectangle"))
195+
#expect(rect.props["fill"] == Color.blue.propValue)
196+
197+
let rounded = ViewHost(RoundedRectangle(cornerRadius: 12)).evaluate()
198+
#expect(rounded.props["shape"] == .string("roundedRectangle"))
199+
#expect(rounded.props["cornerRadius"] == .double(12))
200+
201+
#expect(ViewHost(Circle()).evaluate().props["shape"] == .string("circle"))
202+
#expect(ViewHost(Capsule()).evaluate().props["shape"] == .string("capsule"))
203+
}
204+
205+
@Test("LinearGradient emits its colors and endpoints")
206+
func gradient() {
207+
let node = ViewHost(LinearGradient(colors: [.blue, .purple], startPoint: .leading, endPoint: .trailing)).evaluate()
208+
#expect(node.type == "LinearGradient")
209+
#expect(node.props["colors"] == .array([Color.blue.propValue, Color.purple.propValue]))
210+
#expect(node.props["startX"] == .double(0))
211+
#expect(node.props["endX"] == .double(1))
212+
}
213+
214+
@Test("Image(systemName:) carries the symbol name")
215+
func systemImage() {
216+
let node = ViewHost(Image(systemName: "star.fill")).evaluate()
217+
#expect(node.type == "Image")
218+
#expect(node.props["systemName"] == .string("star.fill"))
219+
}
220+
}

Demo/App.swiftpm/Sources/Catalog.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ struct CatalogEntry: Identifiable {
3434
CatalogEntry(id: "stack", title: "Stacks", screen: AnyCatalogScreen(StackPlayground())),
3535
CatalogEntry(id: "spacer", title: "Spacer & Divider", screen: AnyCatalogScreen(SpacerDividerPlayground())),
3636
CatalogEntry(id: "color", title: "Color", screen: AnyCatalogScreen(ColorPlayground())),
37+
CatalogEntry(id: "graphics", title: "Graphics", screen: AnyCatalogScreen(GraphicsPlayground())),
3738
CatalogEntry(id: "scroll", title: "ScrollView", screen: AnyCatalogScreen(ScrollViewPlayground())),
3839
CatalogEntry(id: "list", title: "List", screen: AnyCatalogScreen(ListPlayground())),
3940
CatalogEntry(id: "grid", title: "Grid", screen: AnyCatalogScreen(GridPlayground())),
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#if canImport(AndroidSwiftUI)
2+
import AndroidSwiftUI
3+
#else
4+
import SwiftUI
5+
#endif
6+
7+
struct GraphicsPlayground: View {
8+
var body: some View {
9+
ScrollView {
10+
VStack(alignment: .leading, spacing: 0) {
11+
Example("Shapes") {
12+
HStack(spacing: 12) {
13+
Rectangle().fill(.blue).frame(width: 60, height: 60)
14+
Circle().fill(.green).frame(width: 60, height: 60)
15+
Capsule().fill(.orange).frame(width: 90, height: 40)
16+
RoundedRectangle(cornerRadius: 12).fill(.purple).frame(width: 60, height: 60)
17+
}
18+
}
19+
Example("Linear gradient (horizontal)") {
20+
LinearGradient(colors: [.blue, .purple], startPoint: .leading, endPoint: .trailing)
21+
.frame(height: 60)
22+
.cornerRadius(8)
23+
}
24+
Example("Linear gradient (vertical)") {
25+
LinearGradient(colors: [.orange, .red, .pink], startPoint: .top, endPoint: .bottom)
26+
.frame(height: 80)
27+
.cornerRadius(8)
28+
}
29+
Example("SF Symbols") {
30+
HStack(spacing: 16) {
31+
Image(systemName: "star.fill").foregroundColor(.yellow)
32+
Image(systemName: "heart.fill").foregroundColor(.red)
33+
Image(systemName: "trash").foregroundColor(.gray)
34+
Image(systemName: "gear")
35+
Image(systemName: "bell.fill").foregroundColor(.blue)
36+
}
37+
}
38+
Example("Symbols sized") {
39+
HStack(spacing: 16) {
40+
Image(systemName: "star.fill").frame(width: 40, height: 40).foregroundColor(.orange)
41+
Image(systemName: "cart.fill").frame(width: 40, height: 40).foregroundColor(.green)
42+
Image(systemName: "person.fill").frame(width: 40, height: 40).foregroundColor(.blue)
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)