Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Canvas and TimelineView to DOM renderer #449

Merged
merged 11 commits into from Sep 28, 2021
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Expand Up @@ -33,7 +33,7 @@ jobs:
shell: bash
run: |
set -ex
sudo xcode-select --switch /Applications/Xcode_12.5.app/Contents/Developer/
sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer/
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
# avoid building unrelated products for testing by specifying the test product explicitly
swift build --product TokamakPackageTests
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
Expand All @@ -43,10 +43,11 @@ jobs:

xcodebuild -version

# make sure Tokamak can be built on macOS so that Xcode autocomplete works
xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
xcpretty --color
# Make sure Tokamak can be built on macOS so that Xcode autocomplete works.
# Disable macOS builds until Monterey is available on GHA.
# xcodebuild -scheme TokamakDemo -destination 'generic/platform=macOS' \
# CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | \
# xcpretty --color

cd "NativeDemo"
xcodebuild -scheme iOS -destination 'generic/platform=iOS' \
Expand Down
14 changes: 10 additions & 4 deletions NativeDemo/TokamakDemo.xcodeproj/project.pbxproj
Expand Up @@ -13,10 +13,12 @@
26136824269E8EB5006F372E /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26136822269E8EB5006F372E /* TransitionDemo.swift */; };
262DA7B32695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; };
262DA7B42695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; };
26AC04B62698D33A0057784E /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AC04B52698D33A0057784E /* ProgressViewDemo.swift */; };
26AC04B72698D33A0057784E /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AC04B52698D33A0057784E /* ProgressViewDemo.swift */; };
2681096D26F7715400078F4E /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2681096C26F7715400078F4E /* CanvasDemo.swift */; };
2681096E26F7715400078F4E /* CanvasDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2681096C26F7715400078F4E /* CanvasDemo.swift */; };
26A3BFB0269BD18A0004DA16 /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */; };
26A3BFB1269BD18A0004DA16 /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */; };
26AC04B62698D33A0057784E /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AC04B52698D33A0057784E /* ProgressViewDemo.swift */; };
26AC04B72698D33A0057784E /* ProgressViewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26AC04B52698D33A0057784E /* ProgressViewDemo.swift */; };
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4550BD5125B642B80088F4EA /* ShadowDemo.swift */; };
Expand Down Expand Up @@ -107,8 +109,9 @@
207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
26136822269E8EB5006F372E /* TransitionDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionDemo.swift; sourceTree = "<group>"; };
262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.swift; sourceTree = "<group>"; };
26AC04B52698D33A0057784E /* ProgressViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewDemo.swift; sourceTree = "<group>"; };
2681096C26F7715400078F4E /* CanvasDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CanvasDemo.swift; sourceTree = "<group>"; };
26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; };
26AC04B52698D33A0057784E /* ProgressViewDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressViewDemo.swift; sourceTree = "<group>"; };
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
4550BD5125B642B80088F4EA /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderDemo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -139,7 +142,7 @@
D1316F1F2500352200224A67 /* StackDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StackDemo.swift; sourceTree = "<group>"; };
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListDemo.swift; sourceTree = "<group>"; };
D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutlineGroupDemo.swift; sourceTree = "<group>"; };
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; };
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = ButtonStyleDemo.swift; sourceTree = "<group>"; tabWidth = 2; };
D1D6B62224D817350041E1D9 /* GeometryReaderDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeometryReaderDemo.swift; sourceTree = "<group>"; };
D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libTokamakShim.a; sourceTree = BUILT_PRODUCTS_DIR; };
D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokamakShim.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -209,6 +212,7 @@
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */,
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
2681096C26F7715400078F4E /* CanvasDemo.swift */,
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
85ED189E24AD425E0085DFA0 /* Counter.swift */,
207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */,
Expand Down Expand Up @@ -394,6 +398,7 @@
D1316F202500352200224A67 /* StackDemo.swift in Sources */,
8500293F24D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
4550BD5225B642B80088F4EA /* ShadowDemo.swift in Sources */,
2681096D26F7715400078F4E /* CanvasDemo.swift in Sources */,
85ED18A924AD425E0085DFA0 /* TokamakDemo.swift in Sources */,
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */,
Expand Down Expand Up @@ -431,6 +436,7 @@
B5F2BE042571443D00FB3653 /* PreferenceKeyDemo.swift in Sources */,
8500294024D2FF3E001A2E84 /* SliderDemo.swift in Sources */,
4550BD5325B642B80088F4EA /* ShadowDemo.swift in Sources */,
2681096E26F7715400078F4E /* CanvasDemo.swift in Sources */,
85ED18B624AD42D70085DFA0 /* NSAppDelegate.swift in Sources */,
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -113,7 +113,7 @@ app.

## Requirements for app developers

- macOS 11 and Xcode 12.5. Xcode 13 is currently not supported.
- macOS 11 and Xcode 13.
- [Swift 5.4 or later](https://swift.org/download/) and Ubuntu 18.04 if you'd like to use Linux.
Other Linux distributions are currently not supported.

Expand Down
21 changes: 21 additions & 0 deletions Sources/TokamakCore/Modifiers/LifecycleModifier.swift
Expand Up @@ -19,8 +19,29 @@ public extension View {
modifier(_AppearanceActionModifier(appear: action))
}

@_spi(TokamakCore)
func _onUpdate(perform action: (() -> ())? = nil) -> some View {
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
modifier(_LifecycleActionModifier(update: action))
}

@_spi(TokamakCore)
func _onUnmount(perform action: (() -> ())? = nil) -> some View {
modifier(_AppearanceActionModifier(disappear: action))
}
}

protocol LifecycleActionType {
var update: (() -> ())? { get }
}

struct _LifecycleActionModifier: ViewModifier {
var update: (() -> ())?

typealias Body = Never
}

extension ModifiedContent: LifecycleActionType
where Content: View, Modifier == _LifecycleActionModifier
{
var update: (() -> ())? { modifier.update }
}
4 changes: 3 additions & 1 deletion Sources/TokamakCore/Modifiers/ModifiedContent.swift
Expand Up @@ -16,8 +16,10 @@ protocol ModifierContainer {
var environmentModifier: EnvironmentModifier? { get }
}

protocol ModifiedContentProtocol {}

/// A value with a modifier applied to it.
public struct ModifiedContent<Content, Modifier> {
public struct ModifiedContent<Content, Modifier>: ModifiedContentProtocol {
@Environment(\.self) public var environment
public typealias Body = Never
public private(set) var content: Content
Expand Down
15 changes: 15 additions & 0 deletions Sources/TokamakCore/Modifiers/StyleModifiers.swift
Expand Up @@ -169,6 +169,21 @@ public extension View {
modifier(_OverlayModifier(overlay: overlay, alignment: alignment))
}

@inlinable
func overlay<V>(
alignment: Alignment = .center,
@ViewBuilder content: () -> V
) -> some View where V: View {
modifier(_OverlayModifier(overlay: content(), alignment: alignment))
}

@inlinable
func overlay<S>(
_ style: S
) -> some View where S: ShapeStyle {
overlay(Rectangle().fill(style))
}

func border<S>(_ content: S, width: CGFloat = 1) -> some View where S: ShapeStyle {
overlay(Rectangle().strokeBorder(content, lineWidth: width))
}
Expand Down
45 changes: 45 additions & 0 deletions Sources/TokamakCore/MountedViews/MountedCompositeView.swift
Expand Up @@ -33,6 +33,8 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
transaction.disablesAnimations = true
self.transaction = transaction

updateVariadicView()

let childBody = reconciler.render(compositeView: self)

if let traitModifier = view.view as? _TraitWritingModifierProtocol {
Expand Down Expand Up @@ -114,6 +116,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
var transaction = transaction
transaction.disablesAnimations = false
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
updateVariadicView()
let element = reconciler.render(compositeView: self)
reconciler.reconcile(
self,
Expand All @@ -135,5 +138,47 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
)
}
)

if let lifecycleActions = view.view as? LifecycleActionType {
lifecycleActions.update?()
}
}

private func updateVariadicView() {
if var tree = view.view as? _VariadicView_AnyTree {
let elements = ((tree.anyContent.view as? GroupView)?.recursiveChildren ?? [tree.anyContent])
.enumerated()
.map { (pair: EnumeratedSequence<[AnyView]>.Element) -> _VariadicView_Children.Element in
var viewTraits = _ViewTraitStore(values: [:])
if let traitModifier = pair.element.view as? _TraitWritingModifierProtocol {
traitModifier.modifyViewTraitStore(&viewTraits)
}
return _VariadicView_Children.Element(
view: pair.element,
id: AnyHashable(pair.offset),
// TODO: Retrieve the ID from the `IDView`. Maybe this should use traits too.
viewTraits: viewTraits,
onTraitsUpdated: { _ in }
)
}
tree.children = _VariadicView_Children(elements: elements)
view.view = tree
}
}
}

private extension GroupView {
var recursiveChildren: [AnyView] {
var allChildren = [AnyView]()
for child in children {
if !(child.view is ModifiedContentProtocol),
let group = child.view as? GroupView
{
allChildren.append(contentsOf: group.recursiveChildren)
} else {
allChildren.append(child)
}
}
return allChildren
}
}
13 changes: 12 additions & 1 deletion Sources/TokamakCore/Shapes/Ellipse.swift
Expand Up @@ -27,7 +27,18 @@ public struct Ellipse: Shape {

public struct Circle: Shape {
public func path(in rect: CGRect) -> Path {
.init(storage: .ellipse(rect), sizing: .flexible)
.init(
storage: .ellipse(
.init(
// Center the circle in the rect.
x: rect.origin.x + (rect.width > rect.height ? (rect.width - rect.height) / 2 : 0),
y: rect.origin.y + (rect.height > rect.width ? (rect.height - rect.width) / 2 : 0),
width: min(rect.width, rect.height),
height: min(rect.width, rect.height)
)
),
sizing: .flexible
)
}

public init() {}
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Shapes/Path/Path.swift
Expand Up @@ -20,7 +20,7 @@ import Foundation
/// The outline of a 2D shape.
public struct Path: Equatable, LosslessStringConvertible {
public class _PathBox: Equatable {
var elements: [Element] = []
public var elements: [Element] = []
public static func == (lhs: Path._PathBox, rhs: Path._PathBox) -> Bool {
lhs.elements == rhs.elements
}
Expand Down
51 changes: 49 additions & 2 deletions Sources/TokamakCore/Shapes/Rectangle.swift
Expand Up @@ -39,8 +39,8 @@ extension Rectangle: InsettableShape {
storage: .rect(CGRect(
origin: rect.origin,
size: CGSize(
width: rect.size.width - (amount / 2),
height: rect.size.height - (amount / 2)
width: max(0, rect.size.width - (amount / 2)),
height: max(0, rect.size.height - (amount / 2))
)
)),
sizing: .flexible
Expand Down Expand Up @@ -80,3 +80,50 @@ public struct RoundedRectangle: Shape {
)
}
}

extension RoundedRectangle: InsettableShape {
@inlinable
public func inset(by amount: CGFloat) -> some InsettableShape {
_Inset(base: self, amount: amount)
}

@usableFromInline
struct _Inset: InsettableShape {
@usableFromInline var base: RoundedRectangle
@usableFromInline var amount: CGFloat

@inlinable
init(base: RoundedRectangle, amount: CGFloat) {
self.base = base
self.amount = amount
}

@usableFromInline
func path(in rect: CGRect) -> Path {
.init(
storage: .roundedRect(.init(
rect: CGRect(
origin: rect.origin,
size: CGSize(
width: max(0, rect.size.width - (amount / 2)),
height: max(0, rect.size.height - (amount / 2))
)
),
cornerSize: CGSize(
width: max(0, base.cornerSize.width - (amount / 2)),
height: max(0, base.cornerSize.height - (amount / 2))
),
style: base.style
)),
sizing: .flexible
)
}

@usableFromInline
func inset(by amount: CGFloat) -> Self {
var copy = self
copy.amount += amount
return copy
}
}
}
47 changes: 47 additions & 0 deletions Sources/TokamakCore/ViewTraits/TagValueTraitKey.swift
@@ -0,0 +1,47 @@
// Copyright 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 9/23/21.
//

import Foundation

public extension View {
@inlinable
func tag<V>(_ tag: V) -> some View where V: Hashable {
_trait(TagValueTraitKey<V>.self, .tagged(tag))
}

@inlinable
func _untagged() -> some View {
_trait(IsAuxiliaryContentTraitKey.self, true)
}
}

@usableFromInline
struct TagValueTraitKey<V>: _ViewTraitKey where V: Hashable {
@usableFromInline
enum Value {
case untagged
case tagged(V)
}

@inlinable static var defaultValue: Value { .untagged }
}

@usableFromInline
struct IsAuxiliaryContentTraitKey: _ViewTraitKey {
@inlinable static var defaultValue: Bool { false }
@usableFromInline typealias Value = Bool
}