Skip to content

Commit

Permalink
Add ContainerRelativeShape (#416)
Browse files Browse the repository at this point in the history
  • Loading branch information
carson-katri authored Jul 7, 2021
1 parent 738455b commit 79a9a66
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 26 deletions.
108 changes: 108 additions & 0 deletions Sources/TokamakCore/Shapes/ContainerRelativeShape.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2020-2021 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.
//
// Created by Carson Katri on 7/6/21.
//

import Foundation

public struct ContainerRelativeShape: Shape, EnvironmentReader {
var containerShape: (CGRect, GeometryProxy) -> Path? = { _, _ in nil }

public func path(in rect: CGRect) -> Path {
containerShape(rect, GeometryProxy(size: rect.size)) ?? Rectangle().path(in: rect)
}

public init() {}

public mutating func setContent(from values: EnvironmentValues) {
containerShape = values._containerShape
}
}

extension ContainerRelativeShape: InsettableShape {
@inlinable
public func inset(by amount: CGFloat) -> some InsettableShape {
_Inset(amount: amount)
}

@usableFromInline
@frozen internal struct _Inset: InsettableShape, DynamicProperty {
@usableFromInline
internal var amount: CGFloat
@inlinable
internal init(amount: CGFloat) {
self.amount = amount
}

@usableFromInline
internal func path(in rect: CGRect) -> Path {
// FIXME: Inset the container shape.
Rectangle().path(in: rect)
}

@inlinable
internal func inset(by amount: CGFloat) -> ContainerRelativeShape._Inset {
var copy = self
copy.amount += amount
return copy
}
}
}

private extension EnvironmentValues {
enum ContainerShapeKey: EnvironmentKey {
static let defaultValue: (CGRect, GeometryProxy) -> Path? = { _, _ in nil }
}

var _containerShape: (CGRect, GeometryProxy) -> Path? {
get {
self[ContainerShapeKey.self]
}
set {
self[ContainerShapeKey.self] = newValue
}
}
}

@frozen public struct _ContainerShapeModifier<Shape>: ViewModifier where Shape: InsettableShape {
public var shape: Shape
@inlinable
public init(shape: Shape) { self.shape = shape }

public func body(content: Content) -> some View {
_ContainerShapeView(content: content, shape: shape)
}

public struct _ContainerShapeView: View {
public let content: Content
public let shape: Shape

public var body: some View {
content
.environment(\._containerShape) { rect, proxy in
shape
.inset(by: proxy.size.width) // TODO: Calculate the offset using content's geometry
.path(in: rect)
}
}
}
}

public extension View {
@inlinable
func containerShape<T>(_ shape: T) -> some View where T: InsettableShape {
modifier(_ContainerShapeModifier(shape: shape))
}
}
33 changes: 33 additions & 0 deletions Sources/TokamakCore/Shapes/Ellipse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,39 @@ public struct Circle: Shape {
public init() {}
}

extension Circle: InsettableShape {
public func inset(by amount: CGFloat) -> _Inset {
_Inset(amount: amount)
}

public struct _Inset: InsettableShape {
public var amount: CGFloat

init(amount: CGFloat) {
self.amount = amount
}

public func path(in rect: CGRect) -> Path {
.init(
storage: .ellipse(CGRect(
origin: rect.origin,
size: CGSize(
width: rect.size.width - (amount / 2),
height: rect.size.height - (amount / 2)
)
)),
sizing: .flexible
)
}

public func inset(by amount: CGFloat) -> Circle._Inset {
var copy = self
copy.amount += amount
return copy
}
}
}

public struct Capsule: Shape {
public var style: RoundedCornerStyle

Expand Down
52 changes: 26 additions & 26 deletions Sources/TokamakCore/Shapes/Rectangle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,6 @@ public struct Rectangle: Shape {
public init() {}
}

public struct RoundedRectangle: Shape {
public var cornerSize: CGSize
public var style: RoundedCornerStyle

public init(cornerSize: CGSize, style: RoundedCornerStyle = .circular) {
self.cornerSize = cornerSize
self.style = style
}

public init(cornerRadius: CGFloat, style: RoundedCornerStyle = .circular) {
let cornerSize = CGSize(width: cornerRadius, height: cornerRadius)
self.init(cornerSize: cornerSize, style: style)
}

public func path(in rect: CGRect) -> Path {
.init(
storage: .roundedRect(.init(
rect: rect,
cornerSize: cornerSize,
style: style
)),
sizing: .flexible
)
}
}

extension Rectangle: InsettableShape {
public func inset(by amount: CGFloat) -> _Inset {
_Inset(amount: amount)
Expand Down Expand Up @@ -80,3 +54,29 @@ extension Rectangle: InsettableShape {
}
}
}

public struct RoundedRectangle: Shape {
public var cornerSize: CGSize
public var style: RoundedCornerStyle

public init(cornerSize: CGSize, style: RoundedCornerStyle = .circular) {
self.cornerSize = cornerSize
self.style = style
}

public init(cornerRadius: CGFloat, style: RoundedCornerStyle = .circular) {
let cornerSize = CGSize(width: cornerRadius, height: cornerRadius)
self.init(cornerSize: cornerSize, style: style)
}

public func path(in rect: CGRect) -> Path {
.init(
storage: .roundedRect(.init(
rect: rect,
cornerSize: cornerSize,
style: style
)),
sizing: .flexible
)
}
}
3 changes: 3 additions & 0 deletions Sources/TokamakDOM/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ public typealias ButtonStyle = TokamakCore.ButtonStyle
public typealias ButtonStyleConfiguration = TokamakCore.ButtonStyleConfiguration
public typealias DefaultButtonStyle = TokamakCore.DefaultButtonStyle

public typealias TextFieldStyle = TokamakCore.TextFieldStyle

public typealias FillStyle = TokamakCore.FillStyle
public typealias ShapeStyle = TokamakCore.ShapeStyle
public typealias StrokeStyle = TokamakCore.StrokeStyle
Expand All @@ -79,6 +81,7 @@ public typealias Ellipse = TokamakCore.Ellipse
public typealias Path = TokamakCore.Path
public typealias Rectangle = TokamakCore.Rectangle
public typealias RoundedRectangle = TokamakCore.RoundedRectangle
public typealias ContainerRelativeShape = TokamakCore.ContainerRelativeShape

// MARK: Primitive values

Expand Down
13 changes: 13 additions & 0 deletions Sources/TokamakDemo/PathDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ struct PathDemo: View {
.frame(width: 50, height: 25)
}
.foregroundColor(Color.blue)
if #available(macOS 12.0, iOS 15, *) {
#if compiler(>=5.5) || os(WASI) // Xcode 13 required for `containerShape`.
ZStack {
ContainerRelativeShape()
.fill(Color.blue)
.frame(width: 100, height: 100, alignment: .center)
ContainerRelativeShape()
.fill(Color.green)
.frame(width: 50, height: 50)
}
.containerShape(Circle())
#endif
}
}
}
}
2 changes: 2 additions & 0 deletions Sources/TokamakStaticHTML/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public typealias Path = TokamakCore.Path
public typealias Rectangle = TokamakCore.Rectangle
public typealias RoundedRectangle = TokamakCore.RoundedRectangle

public typealias ContainerRelativeShape = TokamakCore.ContainerRelativeShape

// MARK: Primitive values

public typealias Color = TokamakCore.Color
Expand Down
17 changes: 17 additions & 0 deletions Tests/TokamakStaticHTMLTests/RenderingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,23 @@ final class RenderingTests: XCTestCase {
timeout: defaultSnapshotTimeout
)
}

func testContainerRelativeShape() {
#if compiler(>=5.5) || os(WASI)
assertSnapshot(
matching: ZStack {
ContainerRelativeShape()
.fill(Color.blue)
.frame(width: 100, height: 100, alignment: .center)
ContainerRelativeShape()
.fill(Color.green)
.frame(width: 50, height: 50)
}.containerShape(Circle()),
as: .image(size: .init(width: 150, height: 150)),
timeout: 10
)
#endif
}
}

#endif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 79a9a66

Please sign in to comment.