Add ProgressView (#425)

This adds `ProgressView` using the `<progress>` tag on the web.

* Add ProgressView implementation

* Fix native demo

* Enable Foundation.Progress in non-WASI environments

* Fix wasm build

* Update progress coc

* Improve snapshot copy error handling

* Use RenderingTests as directory name

* Fix snapshots CI script

* Make test failures fail the CI job

* Snapshot script debugging

* Copy failed snapshots in a different way

* Call `exit 1` when tests fail

* Use correct directory in the upload step

* Update test image

* Update .github/workflows/ci.yml

Co-authored-by: ezraberch <49635435+ezraberch@users.noreply.github.com>
Co-authored-by: Max Desiatov <max@desiatov.com>
This commit is contained in:
Carson Katri 2021-07-17 11:43:51 -04:00 committed by GitHub
parent ab5e564ada
commit 12a6256ec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 412 additions and 3 deletions

View File

@ -37,7 +37,7 @@ jobs:
# avoid building unrelated products for testing by specifying the test product explicitly # avoid building unrelated products for testing by specifying the test product explicitly
swift build --product TokamakPackageTests swift build --product TokamakPackageTests
`xcrun --find xctest` .build/debug/TokamakPackageTests.xctest || `xcrun --find xctest` .build/debug/TokamakPackageTests.xctest ||
find /var/folders -iname SnapshotTests -exec cp -r {} . \; (cp -r `find /var/folders -iname RenderingTests` . ; exit 1)
rm -rf Sources/TokamakGTKCHelpers/*.c rm -rf Sources/TokamakGTKCHelpers/*.c
@ -61,7 +61,7 @@ jobs:
if: ${{ failure() }} if: ${{ failure() }}
with: with:
name: Failed snapshots name: Failed snapshots
path: SnapshotTests path: RenderingTests
gtk_macos_build: gtk_macos_build:
runs-on: macos-latest runs-on: macos-latest

View File

@ -11,6 +11,8 @@
207C05712610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; }; 207C05712610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; };
262DA7B32695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; }; 262DA7B32695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; };
262DA7B42695D99500CABEAE /* 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 */; };
26A3BFB0269BD18A0004DA16 /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */; }; 26A3BFB0269BD18A0004DA16 /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */; };
26A3BFB1269BD18A0004DA16 /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */; }; 26A3BFB1269BD18A0004DA16 /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */; };
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; }; 3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */; };
@ -102,6 +104,7 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; }; 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.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>"; };
26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; }; 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; };
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.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>"; }; 4550BD5125B642B80088F4EA /* ShadowDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowDemo.swift; sourceTree = "<group>"; };
@ -214,6 +217,7 @@
B51F214F24B920B400CF2583 /* PathDemo.swift */, B51F214F24B920B400CF2583 /* PathDemo.swift */,
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */, D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */, B5F2BE022571443D00FB3653 /* PreferenceKeyDemo.swift */,
26AC04B52698D33A0057784E /* ProgressViewDemo.swift */,
B5DBA22A24D509B4003D3347 /* RedactDemo.swift */, B5DBA22A24D509B4003D3347 /* RedactDemo.swift */,
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */, 3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */,
8500293E24D2FF3E001A2E84 /* SliderDemo.swift */, 8500293E24D2FF3E001A2E84 /* SliderDemo.swift */,
@ -394,6 +398,7 @@
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */,
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */, 854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */,
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */, 85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */,
26AC04B62698D33A0057784E /* ProgressViewDemo.swift in Sources */,
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */, 85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -429,6 +434,7 @@
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */, D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */,
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */, 854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */,
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */, 85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
26AC04B72698D33A0057784E /* ProgressViewDemo.swift in Sources */,
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */, 85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@ -0,0 +1,212 @@
// 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.
//
// Created by Carson Katri on 7/9/21.
//
import Foundation
public struct ProgressView<Label, CurrentValueLabel>: View
where Label: View, CurrentValueLabel: View
{
let storage: Storage
enum Storage {
case custom(_CustomProgressView<Label, CurrentValueLabel>)
case foundation(_FoundationProgressView)
}
public var body: some View {
switch storage {
case let .custom(custom): custom
case let .foundation(foundation): foundation
}
}
}
public struct _CustomProgressView<Label, CurrentValueLabel>: View
where Label: View, CurrentValueLabel: View
{
var fractionCompleted: Double?
var label: Label?
var currentValueLabel: CurrentValueLabel?
@Environment(\.progressViewStyle) var style
init(
fractionCompleted: Double?,
label: Label?,
currentValueLabel: CurrentValueLabel?
) {
self.fractionCompleted = fractionCompleted
self.label = label
self.currentValueLabel = currentValueLabel
}
public var body: some View {
style.makeBody(
configuration: .init(
fractionCompleted: fractionCompleted,
label: label.map { .init(body: AnyView($0)) },
currentValueLabel: currentValueLabel.map { .init(body: AnyView($0)) }
)
)
}
}
#if os(WASI)
public struct _FoundationProgressView: View {
public var body: Never {
fatalError("`Foundation.Progress` is not available.")
}
}
#else
public struct _FoundationProgressView: View {
let progress: Progress
@State private var state: ProgressState?
struct ProgressState {
var progress: Double
var isIndeterminate: Bool
var description: String
}
init(_ progress: Progress) {
self.progress = progress
}
public var body: some View {
ProgressView(
value: progress.isIndeterminate ? nil : progress.fractionCompleted
) {
Text("\(Int(progress.fractionCompleted * 100))% completed")
} currentValueLabel: {
Text("\(progress.completedUnitCount)/\(progress.totalUnitCount)")
}
}
}
#endif
/// Override in renderers to provide a default body for determinate progress views.
public struct _FractionalProgressView: _PrimitiveView {
public let fractionCompleted: Double
init(_ fractionCompleted: Double) {
self.fractionCompleted = fractionCompleted
}
}
/// Override in renderers to provide a default body for indeterminate progress views.
public struct _IndeterminateProgressView: _PrimitiveView {}
public extension ProgressView where CurrentValueLabel == EmptyView {
init() where Label == EmptyView {
self.init(storage: .custom(
.init(fractionCompleted: nil, label: nil, currentValueLabel: nil)
))
}
init(@ViewBuilder label: () -> Label) {
self.init(storage: .custom(
.init(fractionCompleted: nil, label: label(), currentValueLabel: nil)
))
}
init<S>(_ title: S) where Label == Text, S: StringProtocol {
self.init {
Text(title)
}
}
}
public extension ProgressView {
init<V>(
value: V?,
total: V = 1.0
) where Label == EmptyView, CurrentValueLabel == EmptyView, V: BinaryFloatingPoint {
self.init(storage: .custom(
.init(
fractionCompleted: value.map { Double($0 / total) },
label: nil,
currentValueLabel: nil
)
))
}
init<V>(
value: V?,
total: V = 1.0,
@ViewBuilder label: () -> Label
) where CurrentValueLabel == EmptyView, V: BinaryFloatingPoint {
self.init(storage: .custom(
.init(
fractionCompleted: value.map { Double($0 / total) },
label: label(),
currentValueLabel: nil
)
))
}
init<V>(
value: V?,
total: V = 1.0,
@ViewBuilder label: () -> Label,
@ViewBuilder currentValueLabel: () -> CurrentValueLabel
) where V: BinaryFloatingPoint {
self.init(storage: .custom(
.init(
fractionCompleted: value.map { Double($0 / total) },
label: label(),
currentValueLabel: currentValueLabel()
)
))
}
init<S, V>(
_ title: S,
value: V?,
total: V = 1.0
) where Label == Text, CurrentValueLabel == EmptyView, S: StringProtocol, V: BinaryFloatingPoint {
self.init(
value: value,
total: total
) {
Text(title)
}
}
}
#if !os(WASI)
public extension ProgressView {
init(_ progress: Progress) where Label == EmptyView, CurrentValueLabel == EmptyView {
self.init(storage: .foundation(.init(progress)))
}
}
#endif
public extension ProgressView {
init(_ configuration: ProgressViewStyleConfiguration)
where Label == ProgressViewStyleConfiguration.Label,
CurrentValueLabel == ProgressViewStyleConfiguration.CurrentValueLabel
{
self.init(value: configuration.fractionCompleted) {
ProgressViewStyleConfiguration.Label(
body: AnyView(configuration.label)
)
} currentValueLabel: {
ProgressViewStyleConfiguration.CurrentValueLabel(
body: AnyView(configuration.currentValueLabel)
)
}
}
}

View File

@ -0,0 +1,99 @@
// 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.
//
// Created by Carson Katri on 7/9/21.
//
import Foundation
public protocol ProgressViewStyle {
associatedtype Body: View
typealias Configuration = ProgressViewStyleConfiguration
@ViewBuilder
func makeBody(configuration: Self.Configuration) -> Self.Body
}
public struct ProgressViewStyleConfiguration {
public struct Label: View {
public let body: AnyView
}
public struct CurrentValueLabel: View {
public let body: AnyView
}
public let fractionCompleted: Double?
public var label: ProgressViewStyleConfiguration.Label?
public var currentValueLabel: ProgressViewStyleConfiguration.CurrentValueLabel?
}
public struct DefaultProgressViewStyle: ProgressViewStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack { Spacer() }
configuration.label
.foregroundStyle(PrimaryContentStyle())
if let fractionCompleted = configuration.fractionCompleted {
_FractionalProgressView(fractionCompleted)
} else {
_IndeterminateProgressView()
}
configuration.currentValueLabel
.font(.caption)
.foregroundStyle(PrimaryContentStyle())
.opacity(0.5)
}
}
}
public struct _AnyProgressViewStyle: ProgressViewStyle {
public typealias Body = AnyView
private let bodyClosure: (ProgressViewStyleConfiguration) -> AnyView
public let type: Any.Type
public init<S: ProgressViewStyle>(_ style: S) {
type = S.self
bodyClosure = { configuration in
AnyView(style.makeBody(configuration: configuration))
}
}
public func makeBody(configuration: ProgressViewStyleConfiguration) -> AnyView {
bodyClosure(configuration)
}
}
extension EnvironmentValues {
private enum ProgressViewStyleKey: EnvironmentKey {
static let defaultValue = _AnyProgressViewStyle(DefaultProgressViewStyle())
}
var progressViewStyle: _AnyProgressViewStyle {
get {
self[ProgressViewStyleKey.self]
}
set {
self[ProgressViewStyleKey.self] = newValue
}
}
}
public extension View {
func progressViewStyle<S>(_ style: S) -> some View where S: ProgressViewStyle {
environment(\.progressViewStyle, .init(style))
}
}

View File

@ -130,6 +130,7 @@ public typealias NavigationLink = TokamakCore.NavigationLink
public typealias NavigationView = TokamakCore.NavigationView public typealias NavigationView = TokamakCore.NavigationView
public typealias OutlineGroup = TokamakCore.OutlineGroup public typealias OutlineGroup = TokamakCore.OutlineGroup
public typealias Picker = TokamakCore.Picker public typealias Picker = TokamakCore.Picker
public typealias ProgressView = TokamakCore.ProgressView
public typealias ScrollView = TokamakCore.ScrollView public typealias ScrollView = TokamakCore.ScrollView
public typealias Section = TokamakCore.Section public typealias Section = TokamakCore.Section
public typealias SecureField = TokamakCore.SecureField public typealias SecureField = TokamakCore.SecureField

View File

@ -0,0 +1,37 @@
// 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.
//
// Created by Carson Katri on 7/9/21.
//
import TokamakShim
struct ProgressViewDemo: View {
@State private var progress = 0.5
var body: some View {
VStack {
HStack { Spacer() }
ProgressView("Indeterminate")
ProgressView(value: progress) {
Text("Determinate")
} currentValueLabel: {
Text("\(progress)")
}
ProgressView("Increased Total", value: progress, total: 2)
Button("Make Progress") { progress += 0.1 }
}
}
}

View File

@ -138,6 +138,7 @@ struct TokamakDemoView: View {
Section(header: Text("Misc")) { Section(header: Text("Misc")) {
NavItem("Animation", destination: AnimationDemo()) NavItem("Animation", destination: AnimationDemo())
NavItem("Path", destination: PathDemo()) NavItem("Path", destination: PathDemo())
NavItem("ProgressView", destination: ProgressViewDemo())
NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8))) NavItem("Environment", destination: EnvironmentDemo().font(.system(size: 8)))
if #available(macOS 11.0, iOS 14.0, *) { if #available(macOS 11.0, iOS 14.0, *) {
NavItem("Preferences", destination: PreferenceKeyDemo()) NavItem("Preferences", destination: PreferenceKeyDemo())

View File

@ -75,6 +75,7 @@ public typealias HStack = TokamakCore.HStack
public typealias LazyHGrid = TokamakCore.LazyHGrid public typealias LazyHGrid = TokamakCore.LazyHGrid
public typealias LazyVGrid = TokamakCore.LazyVGrid public typealias LazyVGrid = TokamakCore.LazyVGrid
public typealias List = TokamakCore.List public typealias List = TokamakCore.List
public typealias ProgressView = TokamakCore.ProgressView
public typealias ScrollView = TokamakCore.ScrollView public typealias ScrollView = TokamakCore.ScrollView
public typealias Section = TokamakCore.Section public typealias Section = TokamakCore.Section
public typealias Spacer = TokamakCore.Spacer public typealias Spacer = TokamakCore.Spacer

View File

@ -0,0 +1,37 @@
// 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.
//
// Created by Carson Katri on 7/9/21.
//
import TokamakCore
extension _FractionalProgressView: _HTMLPrimitive {
public var renderedBody: AnyView {
AnyView(
HTML("progress", [
"value": "\(fractionCompleted)",
"style": "width: 100%;",
])
)
}
}
extension _IndeterminateProgressView: _HTMLPrimitive {
public var renderedBody: AnyView {
AnyView(
HTML("progress", ["style": "width: 100%;"])
)
}
}

View File

@ -255,6 +255,21 @@ final class RenderingTests: XCTestCase {
) )
} }
func testProgressView() {
assertSnapshot(
matching: VStack(spacing: 0) {
ProgressView(value: 0.5) {
Text("Loading")
} currentValueLabel: {
Text("0.5")
}
ProgressView(Progress(totalUnitCount: 3))
},
as: .image(size: .init(width: 200, height: 200)),
timeout: defaultSnapshotTimeout
)
}
func testAspectRatio() { func testAspectRatio() {
assertSnapshot( assertSnapshot(
matching: Ellipse() matching: Ellipse()

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -53,7 +53,7 @@ Table columns:
| | | | | | | |
| --- | ------------------------------------------------------------------------------ | :-: | | --- | ------------------------------------------------------------------------------ | :-: |
| | [ProgressView](https://developer.apple.com/documentation/swiftui/progressview) | | | 🚧 | [ProgressView](https://developer.apple.com/documentation/swiftui/progressview) | |
| | [Gauge](https://developer.apple.com/documentation/swiftui/gauge) | | | | [Gauge](https://developer.apple.com/documentation/swiftui/gauge) | |
| | [Label](https://developer.apple.com/documentation/swiftui/label) | | | | [Label](https://developer.apple.com/documentation/swiftui/label) | |
| ✅ | [Link](https://developer.apple.com/documentation/swiftui/link) | | | ✅ | [Link](https://developer.apple.com/documentation/swiftui/link) | |