Skip to content

Commit

Permalink
Implement AnyShape (#497)
Browse files Browse the repository at this point in the history
* Implement AnyShape.

* Remove unnecessary body implementation.
  • Loading branch information
filip-sakel authored Jun 24, 2022
1 parent 0b182d9 commit c4717d5
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 0 deletions.
165 changes: 165 additions & 0 deletions Sources/TokamakCore/Animation/_AnyAnimatableData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Copyright 2022 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.

/// A type-eraser for `VectorArithmetic`.
public struct _AnyAnimatableData: VectorArithmetic {
private var box: _AnyAnimatableDataBox?

private init(_ box: _AnyAnimatableDataBox?) {
self.box = box
}
}

/// A box for vector arithmetic types.
///
/// Conforming types are only expected to handle value types (enums and structs).
/// Classes aren't really mutable so that scaling, but even then subclassing is impossible,
/// at least in my attempts. Also `VectorArithmetic` does not have a self-conforming
/// existential. Thus the problem of two types being equal but not sharing a common
/// supertype is avoided. Consider a type `Super` that has subtypes `A : Super` and
/// `B : Super`; casting both `A.self as? B.Type` and `B.self as? A.Type` fail.
/// This is important for static operators, since non-type-erased operators get this right.
/// Thankfully, only no-inheritance types are supported.
private protocol _AnyAnimatableDataBox {
var value: Any { get }

func equals(_ other: Any) -> Bool

func add(_ other: Any) -> _AnyAnimatableDataBox
func subtract(_ other: Any) -> _AnyAnimatableDataBox

mutating func scale(by scalar: Double)
var magnitudeSquared: Double { get }
}

private struct _ConcreteAnyAnimatableDataBox<
Base: VectorArithmetic
>: _AnyAnimatableDataBox {
var base: Base

var value: Any {
base
}

// MARK: Equatable

func equals(_ other: Any) -> Bool {
guard let other = other as? Base else {
return false
}

return base == other
}

// MARK: AdditiveArithmetic

func add(_ other: Any) -> _AnyAnimatableDataBox {
guard let other = other as? Base else {
// TODO: Look into whether this should crash.
// SwiftUI didn't crash on the first beta.
return self
}

return Self(base: base + other)
}

func subtract(_ other: Any) -> _AnyAnimatableDataBox {
guard let other = other as? Base else {
// TODO: Look into whether this should crash.
// SwiftUI didn't crash on the first beta.
return self
}

return Self(base: base - other)
}

// MARK: VectorArithmetic

mutating func scale(by scalar: Double) {
base.scale(by: scalar)
}

var magnitudeSquared: Double {
base.magnitudeSquared
}
}

public extension _AnyAnimatableData {
// MARK: Equatable

static func == (lhs: Self, rhs: Self) -> Bool {
switch (rhs.box, lhs.box) {
case let (rhsBox?, lhsBox?):
return rhsBox.equals(lhsBox.value)

case (.some, nil), (nil, .some):
return false

case (nil, nil):
return true
}
}

// MARK: AdditiveArithmetic

static func + (lhs: Self, rhs: Self) -> Self {
switch (rhs.box, lhs.box) {
case let (rhsBox?, lhsBox?):
return Self(rhsBox.add(lhsBox.value))

case (let box?, nil), (nil, let box?):
return Self(box)

case (nil, nil):
return lhs
}
}

static func - (lhs: Self, rhs: Self) -> Self {
switch (rhs.box, lhs.box) {
case let (rhsBox?, lhsBox?):
return Self(rhsBox.subtract(lhsBox.value))

case (let box?, nil), (nil, let box?):
return Self(box)

case (nil, nil):
return lhs
}
}

static var zero: _AnyAnimatableData {
_AnyAnimatableData(nil)
}

// MARK: VectorArithmetic

mutating func scale(by rhs: Double) {
box?.scale(by: rhs)
}

var magnitudeSquared: Double {
box?.magnitudeSquared ?? 0
}
}

public extension _AnyAnimatableData {
init<Data: VectorArithmetic>(_ data: Data) {
box = _ConcreteAnyAnimatableDataBox(base: data)
}

var value: Any {
box?.value ?? ()
}
}
49 changes: 49 additions & 0 deletions Sources/TokamakCore/Layout/ProposedViewSize.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright 2022 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 Foundation

public struct ProposedViewSize: Equatable, Sendable {
public var width: CGFloat?
public var height: CGFloat?

@inlinable
public init(width: CGFloat?, height: CGFloat?) {
self.width = width
self.height = height
}
}

public extension ProposedViewSize {
@inlinable
init(_ size: CGSize) {
self.init(width: size.width, height: size.height)
}

static let unspecified = ProposedViewSize(width: nil, height: nil)
static let zero = ProposedViewSize(width: 0, height: 0)
static let infinity = ProposedViewSize(width: .infinity, height: .infinity)
}

public extension ProposedViewSize {
@inlinable
func replacingUnspecifiedDimensions(
by size: CGSize = CGSize(width: 10, height: 10)
) -> CGSize {
CGSize(
width: width ?? size.width,
height: height ?? size.height
)
}
}
76 changes: 76 additions & 0 deletions Sources/TokamakCore/Shapes/AnyShape.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2022 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 Foundation

internal protocol AnyShapeBox {
var animatableDataBox: _AnyAnimatableData { get set }

func path(in rect: CGRect) -> Path

func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
}

private struct ConcreteAnyShapeBox<Base: Shape>: AnyShapeBox {
var base: Base

var animatableDataBox: _AnyAnimatableData {
get {
_AnyAnimatableData(base.animatableData)
}
set {
guard let newData = newValue.value as? Base.AnimatableData else {
// TODO: Should this crash?
return
}

base.animatableData = newData
}
}

func path(in rect: CGRect) -> Path {
base.path(in: rect)
}

func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
base.sizeThatFits(proposal)
}
}

public struct AnyShape: Shape {
internal var box: AnyShapeBox

private init(_ box: AnyShapeBox) {
self.box = box
}
}

public extension AnyShape {
init<S: Shape>(_ shape: S) {
box = ConcreteAnyShapeBox(base: shape)
}

func path(in rect: CGRect) -> Path {
box.path(in: rect)
}

func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
box.sizeThatFits(proposal)
}

var animatableData: _AnyAnimatableData {
get { box.animatableDataBox }
set { box.animatableDataBox = newValue }
}
}
15 changes: 15 additions & 0 deletions Sources/TokamakCore/Shapes/Shape.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ public protocol Shape: Animatable, View {
func path(in rect: CGRect) -> Path

static var role: ShapeRole { get }

func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize
}

public extension Shape {
func sizeThatFits(_ proposal: ProposedViewSize) -> CGSize {
// TODO: Check if SwiftUI changes this behavior.

// SwiftUI seems to not compute the path at all and just return
// the following.
CGSize(
width: proposal.width ?? 10,
height: proposal.height ?? 10
)
}
}

public enum ShapeRole: Hashable {
Expand Down

0 comments on commit c4717d5

Please sign in to comment.