From f891edd928939fb12f16c4cc740084e072a6a247 Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Sat, 11 Jul 2020 09:10:25 -0400 Subject: [PATCH] Extend Path to match SwiftUI (#172) * Initial Path element and subpath support * Fix ambiguous .init * Add more path commands * Fix line length * Fix macOS build * Add missing file to xcodeproj --- Sources/TokamakCore/CGStubs.swift | 29 +++- Sources/TokamakCore/Shapes/Path.swift | 141 +++++++++++++++--- Sources/TokamakCore/Shapes/Rectangle.swift | 6 +- Sources/TokamakCore/Tokens/Angle.swift | 28 +++- Sources/TokamakDOM/Shapes/Path.swift | 118 +++++++++++++-- Sources/TokamakDOM/Shapes/Shape.swift | 2 + Sources/TokamakDOM/Shapes/_ShapeView.swift | 20 ++- Sources/TokamakDemo/PathDemo.swift | 57 +++++++ Sources/TokamakDemo/SVGCircle.swift | 26 ---- Sources/TokamakDemo/TokamakDemo.swift | 5 +- .../project.pbxproj | 6 + 11 files changed, 362 insertions(+), 76 deletions(-) create mode 100644 Sources/TokamakDemo/PathDemo.swift delete mode 100644 Sources/TokamakDemo/SVGCircle.swift diff --git a/Sources/TokamakCore/CGStubs.swift b/Sources/TokamakCore/CGStubs.swift index 8175ccfd..28c84ad2 100644 --- a/Sources/TokamakCore/CGStubs.swift +++ b/Sources/TokamakCore/CGStubs.swift @@ -34,6 +34,18 @@ public struct CGPoint: Equatable { public static var zero: Self { .init(x: 0, y: 0) } + + func rotate(_ angle: Angle, around origin: Self) -> Self { + let cosAngle = cos(angle.radians) + let sinAngle = sin(angle.radians) + return .init(x: cosAngle * (x - origin.x) - sinAngle * (y - origin.y) + origin.x, + y: sinAngle * (x - origin.x) + cosAngle * (y - origin.y) + origin.y) + } + + func offset(by offset: Self) -> Self { + .init(x: x + offset.x, + y: y + offset.y) + } } public struct CGSize: Equatable { @@ -54,13 +66,13 @@ public struct CGRect: Equatable { public let origin: CGPoint public let size: CGSize - public init(_ origin: CGPoint, _ size: CGSize) { + public init(origin: CGPoint, size: CGSize) { self.origin = origin self.size = size } public static var zero: Self { - .init(.zero, .zero) + .init(origin: .zero, size: .zero) } } @@ -113,7 +125,8 @@ public struct CGAffineTransform: Equatable { /// Returns an affine transformation matrix constructed from a rotation value you provide. /// - Parameters: /// - angle: The angle, in radians, by which this matrix rotates the coordinate system axes. - /// A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation. + /// A positive value specifies clockwise rotation and anegative value specifies + /// counterclockwise rotation. public init(rotationAngle angle: CGFloat) { self.init(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0) } @@ -146,7 +159,8 @@ public struct CGAffineTransform: Equatable { y: (b * point.x) + (d * point.y) + ty) } - /// Returns an affine transformation matrix constructed by combining two existing affine transforms. + /// Returns an affine transformation matrix constructed by combining two existing affine + /// transforms. /// - Parameters: /// - t2: The affine transform to concatenate to this affine transform. /// - Returns: A new affine transformation matrix. That is, `t’ = t1*t2`. @@ -178,12 +192,15 @@ public struct CGAffineTransform: Equatable { /// Returns an affine transformation matrix constructed by rotating an existing affine transform. /// - Parameters: - /// - angle: The angle, in radians, by which to rotate the affine transform. A positive value specifies clockwise rotation and a negative value specifies counterclockwise rotation. + /// - angle: The angle, in radians, by which to rotate the affine transform. + /// A positive value specifies clockwise rotation and a negative value specifies + /// counterclockwise rotation. public func rotated(by angle: CGFloat) -> Self { Self(a: cos(angle), b: sin(angle), c: -sin(angle), d: cos(angle), tx: 0, ty: 0) } - /// Returns an affine transformation matrix constructed by translating an existing affine transform. + /// Returns an affine transformation matrix constructed by translating an existing + /// affine transform. /// - Parameters: /// - tx: The value by which to move x values with the affine transform. /// - ty: The value by which to move y values with the affine transform. diff --git a/Sources/TokamakCore/Shapes/Path.swift b/Sources/TokamakCore/Shapes/Path.swift index 311cf314..54d48aa6 100644 --- a/Sources/TokamakCore/Shapes/Path.swift +++ b/Sources/TokamakCore/Shapes/Path.swift @@ -15,14 +15,31 @@ // Created by Carson Katri on 06/28/2020. // +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#endif + /// The outline of a 2D shape. public struct Path: Equatable, LosslessStringConvertible { public var description: String { - """ - \(storage) - \(elements) - \(transform) - """ + var pathString = [String]() + for element in elements { + switch element { + case let .move(to: pos): + pathString.append("\(pos.x) \(pos.y) m") + case let .line(to: pos): + pathString.append("\(pos.x) \(pos.y) l") + case let .curve(to: pos, control1: c1, control2: c2): + pathString.append("\(c1.x) \(c1.y) \(c2.x) \(c2.y) \(pos.x) \(pos.y) c") + case let .quadCurve(to: pos, control: c): + pathString.append("\(c.x) \(c.y) \(pos.x) \(pos.y) q") + case .closeSubpath: + pathString.append("h") + } + } + return pathString.joined(separator: " ") } public enum Storage: Equatable { @@ -48,8 +65,8 @@ public struct Path: Equatable, LosslessStringConvertible { public var transform: CGAffineTransform = .identity public struct _SubPath: Equatable { - let path: Path - let transform: CGAffineTransform + public let path: Path + public let transform: CGAffineTransform } public var subpaths: [_SubPath] = [] @@ -78,7 +95,8 @@ public struct Path: Equatable, LosslessStringConvertible { cornerRadius: CGFloat, style: RoundedCornerStyle = .circular) { storage = .roundedRect(FixedRoundedRect(rect: rect, - cornerSize: CGSize(width: cornerRadius, height: cornerRadius), + cornerSize: CGSize(width: cornerRadius, + height: cornerRadius), style: style)) } @@ -215,21 +233,56 @@ extension Path { } public mutating func addRect(_ rect: CGRect, transform: CGAffineTransform = .identity) { - subpaths.append(.init(path: .init(rect), transform: transform)) + move(to: rect.origin) + addLine(to: CGPoint(x: rect.size.width, y: 0) + .offset(by: rect.origin)) + addLine(to: CGPoint(x: rect.size.width, y: rect.size.height) + .offset(by: rect.origin)) + addLine(to: CGPoint(x: 0, y: rect.size.height) + .offset(by: rect.origin)) + closeSubpath() } public mutating func addRoundedRect(in rect: CGRect, cornerSize: CGSize, style: RoundedCornerStyle = .circular, transform: CGAffineTransform = .identity) { - subpaths.append(.init(path: .init(roundedRect: rect, - cornerSize: cornerSize, - style: style), - transform: transform)) + move(to: CGPoint(x: rect.size.width, y: rect.size.height / 2) + .offset(by: rect.origin)) + addLine(to: CGPoint(x: rect.size.width, y: rect.size.height - cornerSize.height) + .offset(by: rect.origin)) + addQuadCurve(to: CGPoint(x: rect.size.width - cornerSize.width, y: rect.size.height) + .offset(by: rect.origin), + control: CGPoint(x: rect.size.width, y: rect.size.height) + .offset(by: rect.origin)) + addLine(to: CGPoint(x: cornerSize.width, y: rect.size.height) + .offset(by: rect.origin)) + addQuadCurve(to: CGPoint(x: 0, y: rect.size.height - cornerSize.height) + .offset(by: rect.origin), + control: CGPoint(x: 0, y: rect.size.height) + .offset(by: rect.origin)) + addLine(to: CGPoint(x: 0, y: cornerSize.height) + .offset(by: rect.origin)) + addQuadCurve(to: CGPoint(x: cornerSize.width, y: 0) + .offset(by: rect.origin), + control: CGPoint.zero + .offset(by: rect.origin)) + addLine(to: CGPoint(x: rect.size.width - cornerSize.width, y: 0) + .offset(by: rect.origin)) + addQuadCurve(to: CGPoint(x: rect.size.width, y: cornerSize.height) + .offset(by: rect.origin), + control: CGPoint(x: rect.size.width, y: 0) + .offset(by: rect.origin)) + closeSubpath() } public mutating func addEllipse(in rect: CGRect, transform: CGAffineTransform = .identity) { - subpaths.append(.init(path: .init(ellipseIn: rect), transform: transform)) + subpaths.append(.init(path: .init(ellipseIn: .init(origin: rect.origin + .offset(by: .init(x: rect.size.width / 2, + y: rect.size.height / 2)), + size: .init(width: rect.size.width / 2, + height: rect.size.height / 2))), + transform: transform)) } public mutating func addRects(_ rects: [CGRect], transform: CGAffineTransform = .identity) { @@ -245,31 +298,79 @@ extension Path { startAngle: Angle, delta: Angle, transform: CGAffineTransform = .identity) { - // I don't know how to do this without sin/cos + addArc(center: center, + radius: radius, + startAngle: startAngle, + endAngle: startAngle + delta, + clockwise: false) } + // There's a great article on bezier curves here: + // https://pomax.github.io/bezierinfo + // FIXME: Handle negative delta public mutating func addArc(center: CGPoint, radius: CGFloat, startAngle: Angle, endAngle: Angle, clockwise: Bool, transform: CGAffineTransform = .identity) { - // I don't know how to do this without sin/cos + if clockwise { + addArc(center: center, + radius: radius, + startAngle: endAngle, + endAngle: endAngle + (.radians(.pi * 2) - endAngle) + startAngle, + clockwise: false) + } else { + let angle = abs(startAngle.radians - endAngle.radians) + if angle > .pi / 2 { + // Split the angle into 90º chunks + let chunk1 = Angle.radians(startAngle.radians + (.pi / 2)) + addArc(center: center, + radius: radius, + startAngle: startAngle, + endAngle: chunk1, + clockwise: clockwise) + addArc(center: center, + radius: radius, + startAngle: chunk1, + endAngle: endAngle, + clockwise: clockwise) + } else { + let startPoint = CGPoint(x: radius + center.x, + y: center.y) + let endPoint = CGPoint(x: (radius * cos(angle)) + center.x, + y: (radius * sin(angle)) + center.y) + let l = (4 / 3) * tan(angle / 4) + let c1 = CGPoint(x: radius + center.x, y: (l * radius) + center.y) + let c2 = CGPoint(x: ((cos(angle) + l * sin(angle)) * radius) + center.x, + y: ((sin(angle) - l * cos(angle)) * radius) + center.y) + + move(to: startPoint.rotate(startAngle, around: center)) + addCurve(to: endPoint.rotate(startAngle, around: center), + control1: c1.rotate(startAngle, around: center), + control2: c2.rotate(startAngle, around: center)) + } + } } + // FIXME: How does this arc method work? public mutating func addArc(tangent1End p1: CGPoint, tangent2End p2: CGPoint, radius: CGFloat, - transform: CGAffineTransform = .identity) { - // I don't know how to do this without sin/cos - } + transform: CGAffineTransform = .identity) {} public mutating func addPath(_ path: Path, transform: CGAffineTransform = .identity) { subpaths.append(.init(path: path, transform: transform)) } public var currentPoint: CGPoint? { - nil + switch elements.last { + case let .move(to: point): return point + case let .line(to: point): return point + case let .curve(to: point, control1: _, control2: _): return point + case let .quadCurve(to: point, control: _): return point + default: return nil + } } public func applying(_ transform: CGAffineTransform) -> Path { diff --git a/Sources/TokamakCore/Shapes/Rectangle.swift b/Sources/TokamakCore/Shapes/Rectangle.swift index 74b4610c..aa7c1b74 100644 --- a/Sources/TokamakCore/Shapes/Rectangle.swift +++ b/Sources/TokamakCore/Shapes/Rectangle.swift @@ -52,9 +52,9 @@ extension Rectangle: InsettableShape { } public func path(in rect: CGRect) -> Path { - .init(CGRect(rect.origin, - CGSize(width: rect.size.width - (amount / 2), - height: rect.size.height - (amount / 2)))) + .init(CGRect(origin: rect.origin, + size: CGSize(width: rect.size.width - (amount / 2), + height: rect.size.height - (amount / 2)))) } public func inset(by amount: CGFloat) -> Rectangle._Inset { diff --git a/Sources/TokamakCore/Tokens/Angle.swift b/Sources/TokamakCore/Tokens/Angle.swift index 4149b757..cec8e370 100644 --- a/Sources/TokamakCore/Tokens/Angle.swift +++ b/Sources/TokamakCore/Tokens/Angle.swift @@ -15,7 +15,7 @@ // Created by Max Desiatov on 06/28/2020. // -public struct Angle { +public struct Angle: AdditiveArithmetic { public var radians: Double public var degrees: Double { get { radians * (180.0 / .pi) } @@ -41,4 +41,30 @@ public struct Angle { public static func degrees(_ degrees: Double) -> Angle { Angle(degrees: degrees) } + + public static let zero: Angle = .radians(0) + + public static func + (lhs: Self, rhs: Self) -> Self { + .radians(lhs.radians + rhs.radians) + } + + public static func += (lhs: inout Self, rhs: Self) { + // swiftlint:disable:next shorthand_operator + lhs = lhs + rhs + } + + public static func - (lhs: Self, rhs: Self) -> Self { + .radians(lhs.radians - rhs.radians) + } + + public static func -= (lhs: inout Self, rhs: Self) { + // swiftlint:disable:next shorthand_operator + lhs = lhs - rhs + } +} + +extension Angle: Hashable, Comparable { + public static func < (lhs: Self, rhs: Self) -> Bool { + lhs.radians < rhs.radians + } } diff --git a/Sources/TokamakDOM/Shapes/Path.swift b/Sources/TokamakDOM/Shapes/Path.swift index 752fb7cb..55c37037 100644 --- a/Sources/TokamakDOM/Shapes/Path.swift +++ b/Sources/TokamakDOM/Shapes/Path.swift @@ -20,7 +20,7 @@ import TokamakCore public typealias Path = TokamakCore.Path extension Path: ViewDeferredToRenderer { - // TODO: Support transformations, subpaths, and read through elements to create a path + // TODO: Support transformations func svgFrom(storage: Storage, strokeStyle: StrokeStyle = .init(lineWidth: 0, lineCap: .butt, @@ -30,35 +30,86 @@ extension Path: ViewDeferredToRenderer { dashPhase: 0)) -> AnyView { let stroke = [ "stroke-width": "\(strokeStyle.lineWidth)", - "stroke": "black", // TODO: Use the environment variable "foregroundColor" ] let uniqueKeys = { (first: String, _: String) in first } switch storage { case .empty: return AnyView(EmptyView()) case let .rect(rect): - return AnyView(AnyView(HTML("rect", ["width": "\(max(0, rect.size.width))", - "height": "\(max(0, rect.size.height))"] - .merging(stroke, uniquingKeysWith: uniqueKeys)))) - case .ellipse: - return AnyView(HTML("ellipse", ["cx": "50%", "cy": "50%", "rx": "50%", "ry": "50%"] + return AnyView(AnyView(HTML("rect", [ + "width": "\(max(0, rect.size.width))", + "height": "\(max(0, rect.size.height))", + "x": "\(rect.origin.x - (rect.size.width / 2))", + "y": "\(rect.origin.y - (rect.size.height / 2))", + ].merging(stroke, uniquingKeysWith: uniqueKeys)))) + case let .ellipse(rect): + return AnyView(HTML("ellipse", ["cx": "\(rect.origin.x)", + "cy": "\(rect.origin.y)", + "rx": "\(rect.size.width)", + "ry": "\(rect.size.height)"] .merging(stroke, uniquingKeysWith: uniqueKeys))) case let .roundedRect(roundedRect): return AnyView(HTML("rect", [ "width": "\(roundedRect.rect.size.width)", "height": "\(roundedRect.rect.size.height)", "rx": "\(roundedRect.cornerSize.width)", - "ry": "\(roundedRect.style == .continuous ? roundedRect.cornerSize.width : roundedRect.cornerSize.height)", - ] - .merging(stroke, uniquingKeysWith: uniqueKeys))) + "ry": """ + \(roundedRect.style == .continuous ? + roundedRect.cornerSize.width : + roundedRect.cornerSize.height) + """, + "x": "\(roundedRect.rect.origin.x)", + "y": "\(roundedRect.rect.origin.y)", + ].merging(stroke, uniquingKeysWith: uniqueKeys))) case let .stroked(stroked): - return stroked.path.svgFrom(storage: stroked.path.storage, strokeStyle: stroked.style) + return AnyView(stroked.path.svgBody(strokeStyle: stroked.style)) case let .trimmed(trimmed): - return trimmed.path.svgFrom(storage: trimmed.path.storage, strokeStyle: strokeStyle) // TODO: Trim the path + return trimmed.path.svgFrom(storage: trimmed.path.storage, + strokeStyle: strokeStyle) // TODO: Trim the path } } - var size: CGSize { + func svgFrom(elements: [Element], + strokeStyle: StrokeStyle = .init(lineWidth: 0, + lineCap: .butt, + lineJoin: .miter, + miterLimit: 0, + dash: [], + dashPhase: 0)) -> AnyView { + var d = [String]() + for element in elements { + switch element { + case let .move(to: pos): + d.append("M\(pos.x),\(pos.y)") + case let .line(to: pos): + d.append("L\(pos.x),\(pos.y)") + case let .curve(to: pos, control1: c1, control2: c2): + d.append("C\(c1.x),\(c1.y),\(c2.x),\(c2.y),\(pos.x),\(pos.y)") + case let .quadCurve(to: pos, control: c1): + d.append("Q\(c1.x),\(c1.y),\(pos.x),\(pos.y)") + case .closeSubpath: + d.append("Z") + } + } + return AnyView(HTML("path", [ + "style": "stroke-width: \(strokeStyle.lineWidth);", + "d": d.joined(separator: "\n"), + ])) + } + + func svgFrom(subpaths: [_SubPath], + strokeStyle: StrokeStyle = .init(lineWidth: 0, + lineCap: .butt, + lineJoin: .miter, + miterLimit: 0, + dash: [], + dashPhase: 0)) -> AnyView { + AnyView(ForEach(Array(subpaths.enumerated()), id: \.offset) { _, path in + path.path.svgBody(strokeStyle: strokeStyle) + }) + } + + var storageSize: CGSize { switch storage { case .empty: return .zero @@ -73,12 +124,51 @@ extension Path: ViewDeferredToRenderer { } } + var elementsSize: CGSize { + // Curves may clip without an explicit size + let positions = elements.compactMap { elem -> CGPoint? in + switch elem { + case let .move(to: pos): return pos + case let .line(to: pos): return pos + case let .curve(to: pos, control1: _, control2: _): return pos + case let .quadCurve(to: pos, control: _): return pos + case .closeSubpath: return nil + } + } + let xPos = positions.map(\.x).sorted(by: <) + let minX = xPos.first ?? 0 + let maxX = xPos.last ?? 0 + let yPos = positions.map(\.y).sorted(by: <) + let minY = yPos.first ?? 0 + let maxY = yPos.last ?? 0 + + return CGSize(width: abs(maxX - min(0, minX)), height: abs(maxY - min(0, minY))) + } + + var size: CGSize { + .init(width: max(storageSize.width, elementsSize.width), + height: max(storageSize.height, elementsSize.height)) + } + + @ViewBuilder + func svgBody(strokeStyle: StrokeStyle = .init(lineWidth: 0, + lineCap: .butt, + lineJoin: .miter, + miterLimit: 0, + dash: [], + dashPhase: 0)) -> some View { + svgFrom(storage: storage, strokeStyle: strokeStyle) + svgFrom(elements: elements, strokeStyle: strokeStyle) + svgFrom(subpaths: subpaths, strokeStyle: strokeStyle) + } + public var deferredBody: AnyView { AnyView(HTML("svg", ["style": """ width: \(max(0, size.width)); height: \(max(0, size.height)); + overflow: visible; """]) { - svgFrom(storage: storage) + svgBody() }) } } diff --git a/Sources/TokamakDOM/Shapes/Shape.swift b/Sources/TokamakDOM/Shapes/Shape.swift index 8b26a03b..5d4a5c17 100644 --- a/Sources/TokamakDOM/Shapes/Shape.swift +++ b/Sources/TokamakDOM/Shapes/Shape.swift @@ -17,6 +17,8 @@ import TokamakCore +public typealias Shape = TokamakCore.Shape + // Border modifier extension _OverlayModifier: DOMViewModifier where Overlay == _ShapeView<_StrokedShape, Color> { diff --git a/Sources/TokamakDOM/Shapes/_ShapeView.swift b/Sources/TokamakDOM/Shapes/_ShapeView.swift index 5a8b8713..6ea3d11c 100644 --- a/Sources/TokamakDOM/Shapes/_ShapeView.swift +++ b/Sources/TokamakDOM/Shapes/_ShapeView.swift @@ -17,11 +17,27 @@ import TokamakCore +protocol ShapeAttributes { + func attributes(_ style: ShapeStyle) -> [String: String] +} + +extension _StrokedShape: ShapeAttributes { + func attributes(_ style: ShapeStyle) -> [String: String] { + if let color = style as? Color { + return ["style": "stroke: \(color); fill: none;"] + } else { + return ["style": "stroke: black; fill: none;"] + } + } +} + extension _ShapeView: ViewDeferredToRenderer { public var deferredBody: AnyView { let path = shape.path(in: .zero).deferredBody - if let fillColor = style as? Color { - return AnyView(HTML("div", ["style": "fill: \(fillColor.description)"]) { path }) + if let shapeAttributes = shape as? ShapeAttributes { + return AnyView(HTML("div", shapeAttributes.attributes(style)) { path }) + } else if let color = style as? Color { + return AnyView(HTML("div", ["style": "fill: \(color);"]) { path }) } else { return path } diff --git a/Sources/TokamakDemo/PathDemo.swift b/Sources/TokamakDemo/PathDemo.swift new file mode 100644 index 00000000..3ab10e73 --- /dev/null +++ b/Sources/TokamakDemo/PathDemo.swift @@ -0,0 +1,57 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(SwiftUI) +import SwiftUI +#else +import TokamakDOM +#endif + +struct Star: Shape { + func path(in rect: CGRect) -> Path { + Path { path in + path.move(to: .init(x: 40, y: 0)) + path.addLine(to: .init(x: 20, y: 76)) + path.addLine(to: .init(x: 80, y: 30.4)) + path.addLine(to: .init(x: 0, y: 30.4)) + path.addLine(to: .init(x: 64, y: 76)) + path.addLine(to: .init(x: 40, y: 0)) + print(path) + } + } +} + +struct PathDemo: View { + var body: some View { + VStack { + Star() + .fill(Color(red: 1, green: 0.75, blue: 0.1, opacity: 1)) + Path { path in + path.addRect(.init(origin: .zero, size: .init(width: 20, height: 20))) + path.addEllipse(in: .init(origin: .init(x: 25, y: 0), + size: .init(width: 20, height: 20))) + path.addRoundedRect(in: .init(origin: .init(x: 50, y: 0), + size: .init(width: 20, height: 20)), + cornerSize: .init(width: 4, height: 4)) + path.addArc(center: .init(x: 85, y: 10), + radius: 10, + startAngle: .degrees(90), + endAngle: .degrees(180), + clockwise: true) + } + .stroke(Color(red: 1, green: 0.75, blue: 0.1, opacity: 1), lineWidth: 4) + .padding(.vertical) + } + } +} diff --git a/Sources/TokamakDemo/SVGCircle.swift b/Sources/TokamakDemo/SVGCircle.swift deleted file mode 100644 index d15962c3..00000000 --- a/Sources/TokamakDemo/SVGCircle.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020 Tokamak contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import TokamakDOM - -struct SVGCircle: View { - var body: some View { - HTML("svg", ["width": "100%", "height": "100%"]) { - HTML("circle", [ - "cx": "50%", "cy": "50%", "r": "40%", - "stroke": "green", "stroke-width": "4", "fill": "yellow", - ]) - } - } -} diff --git a/Sources/TokamakDemo/TokamakDemo.swift b/Sources/TokamakDemo/TokamakDemo.swift index 5e56f0af..a2b74824 100644 --- a/Sources/TokamakDemo/TokamakDemo.swift +++ b/Sources/TokamakDemo/TokamakDemo.swift @@ -43,10 +43,7 @@ struct TokamakDemoView: View { } ForEachDemo() TextDemo() - #if canImport(TokamakDOM) - SVGCircle() - .frame(width: 25, height: 25) - #endif + PathDemo() TextFieldDemo() SpacerDemo() EnvironmentDemo() diff --git a/TokamakDemo Native/TokamakDemo Native.xcodeproj/project.pbxproj b/TokamakDemo Native/TokamakDemo Native.xcodeproj/project.pbxproj index 0f9f63a5..390ae056 100644 --- a/TokamakDemo Native/TokamakDemo Native.xcodeproj/project.pbxproj +++ b/TokamakDemo Native/TokamakDemo Native.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; }; 85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */; }; 85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85ED189424AD41B90085DFA0 /* NSAppDelegate.swift */; }; + B51F215024B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; }; + B51F215124B920B400CF2583 /* PathDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F214F24B920B400CF2583 /* PathDemo.swift */; }; D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; }; D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; }; D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; }; @@ -47,6 +49,7 @@ 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentDemo.swift; sourceTree = ""; }; 85ED18BD24AD46340085DFA0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 85ED18BF24AD464B0085DFA0 /* iOS Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOS Info.plist"; sourceTree = ""; }; + B51F214F24B920B400CF2583 /* PathDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathDemo.swift; sourceTree = ""; }; D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = ""; }; D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -104,6 +107,7 @@ 85ED189E24AD425E0085DFA0 /* Counter.swift */, 85ED189F24AD425E0085DFA0 /* TextFieldDemo.swift */, 85ED18A024AD425E0085DFA0 /* EnvironmentDemo.swift */, + B51F214F24B920B400CF2583 /* PathDemo.swift */, ); name = TokamakDemo; path = ../Sources/TokamakDemo; @@ -210,6 +214,7 @@ files = ( 85ED186A24AD38F20085DFA0 /* UIAppDelegate.swift in Sources */, D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */, + B51F215024B920B400CF2583 /* PathDemo.swift in Sources */, 85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */, 85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */, D1B4229024B3B9BB00682F74 /* ListDemo.swift in Sources */, @@ -227,6 +232,7 @@ files = ( 85ED18AA24AD425E0085DFA0 /* TokamakDemo.swift in Sources */, D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */, + B51F215124B920B400CF2583 /* PathDemo.swift in Sources */, 85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */, 85ED18B024AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */, D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */,