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 Toggle implementation #159

Merged
merged 25 commits into from
Jul 20, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions Sources/TokamakCore/Shapes/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,21 @@ public struct Path: Equatable, LosslessStringConvertible {
public init(roundedRect rect: CGRect,
cornerSize: CGSize,
style: RoundedCornerStyle = .circular) {
storage = .roundedRect(FixedRoundedRect(rect: rect,
cornerSize: cornerSize,
style: style))
storage = .roundedRect(FixedRoundedRect(
rect: rect,
cornerSize: cornerSize,
style: style
))
}

public init(roundedRect rect: CGRect,
cornerRadius: CGFloat,
style: RoundedCornerStyle = .circular) {
storage = .roundedRect(FixedRoundedRect(rect: rect,
cornerSize: CGSize(width: cornerRadius, height: cornerRadius),
style: style))
storage = .roundedRect(FixedRoundedRect(
rect: rect,
cornerSize: CGSize(width: cornerRadius, height: cornerRadius),
style: style
))
}

public init(ellipseIn rect: CGRect) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/TokamakCore/StackReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ public final class StackReconciler<R: Renderer> {
view: V,
target: R.TargetType,
renderer: R,
environment: EnvironmentValues,
scheduler: @escaping (@escaping () -> ()) -> ()
) {
self.renderer = renderer
self.scheduler = scheduler
rootTarget = target

rootView = view.makeMountedView(target, EnvironmentValues())
rootView = view.makeMountedView(target, environment)

rootView.mount(with: self)
}
Expand Down
76 changes: 76 additions & 0 deletions Sources/TokamakCore/Styles/ToggleStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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 Jed Fox on 07/04/2020.
//
// swiftlint:disable line_length
// Adapted from https://github.com/SwiftWebUI/SwiftWebUI/blob/16b84d46/Sources/SwiftWebUI/Views/Forms/Toggle.swift
// swiftlint:enable line_length
//

// NOTE: ToggleStyleConfiguration.label is supposed to be a special Never View.
// It seems like during the rendering process it’s dynamically replaced with the actual label.
// That’s complicated so instead we’re providing the label view directly.

public struct ToggleStyleConfiguration {
public let label: AnyView
@Binding public var isOn: Swift.Bool
}

public protocol ToggleStyle {
associatedtype Body: View

func makeBody(configuration: Self.Configuration) -> Self.Body

typealias Configuration = ToggleStyleConfiguration
}

public struct AnyToggleStyle: ToggleStyle {
j-f1 marked this conversation as resolved.
Show resolved Hide resolved
public typealias Body = AnyView

private var bodyBuild: (ToggleStyleConfiguration) -> AnyView
j-f1 marked this conversation as resolved.
Show resolved Hide resolved

public init<S: ToggleStyle>(_ style: S) {
bodyBuild = { configuration in
AnyView(style.makeBody(configuration: configuration))
}
}

public func makeBody(configuration: ToggleStyleConfiguration) -> AnyView {
bodyBuild(configuration)
}
}

public enum ToggleStyleKey: EnvironmentKey {
public static var defaultValue: AnyToggleStyle {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the single defaultValue provided for this key. Thus fatalError is what we get, which needs to change for it to work.

Copy link
Member Author

@j-f1 j-f1 Jul 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the default environment includes a value for the key:

var environment = EnvironmentValues()
environment[ToggleStyleKey] = AnyToggleStyle(DefaultToggleStyle())

https://github.com/swiftwasm/Tokamak/pull/159/files#diff-6f90e40d15b14ddcb32508552c68db47R71

fatalError("\(self) must have a renderer-provided default value")
}
}

extension EnvironmentValues {
var toggleStyle: AnyToggleStyle {
get {
self[ToggleStyleKey.self]
}
set {
self[ToggleStyleKey.self] = newValue
}
}
}

extension View {
public func toggleStyle<S>(_ style: S) -> some View where S: ToggleStyle {
environment(\.toggleStyle, AnyToggleStyle(style))
}
}
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Views/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public struct Button<Label>: View where Label: View {
}

public var body: Never {
neverBody("Text")
neverBody("Button")
}
}

Expand Down
6 changes: 6 additions & 0 deletions Sources/TokamakCore/Views/SecureField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ extension SecureField where Label == Text {
}
}

extension SecureField: ParentView {
public var children: [AnyView] {
(label as? GroupView)?.children ?? [AnyView(label)]
}
}

/// This is a helper class that works around absence of "package private" access control in Swift
public struct _SecureFieldProxy {
public let subject: SecureField<Text>
Expand Down
6 changes: 6 additions & 0 deletions Sources/TokamakCore/Views/TextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ extension TextField where Label == Text {
// ) where S : StringProtocol
}

extension TextField: ParentView {
public var children: [AnyView] {
(label as? GroupView)?.children ?? [AnyView(label)]
}
}

/// This is a helper class that works around absence of "package private" access control in Swift
public struct _TextFieldProxy {
public let subject: TextField<Text>
Expand Down
46 changes: 46 additions & 0 deletions Sources/TokamakCore/Views/Toggle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2018-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 Jed Fox on 07/04/2020.
//

public struct Toggle<Label>: View where Label: View {
@Binding var isOn: Bool
var label: Label
@Environment(\.toggleStyle) var toggleStyle: AnyToggleStyle
public init(isOn: Binding<Bool>, label: () -> Label) {
_isOn = isOn
self.label = label()
}

public var body: AnyView {
toggleStyle.makeBody(
configuration: ToggleStyleConfiguration(label: AnyView(label), isOn: $isOn)
)
}
}

extension Toggle where Label == Text {
public init<S>(_ title: S, isOn: Binding<Bool>) where S: StringProtocol {
self.init(isOn: isOn) {
Text(title)
}
}
}

extension Toggle: ParentView {
public var children: [AnyView] {
(label as? GroupView)?.children ?? [AnyView(label)]
}
}
9 changes: 7 additions & 2 deletions Sources/TokamakDOM/DOMRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,21 @@ public final class DOMRenderer: Renderer {

public init<V: View>(_ view: V, _ ref: JSObjectRef) {
rootRef = ref
rootRef.style = "display: flex; width: 100%; height: 100%; justify-content: center; align-items: center; overflow: hidden;"
rootRef.style = "display: flex; width: 100%; height: 100%;"
+ "justify-content: center; align-items: center; overflow: hidden;"
j-f1 marked this conversation as resolved.
Show resolved Hide resolved

let rootStyle = document.createElement!("style").object!
rootStyle.innerHTML = .string(tokamakStyles)
_ = head.appendChild!(rootStyle)

var environment = EnvironmentValues()
environment[ToggleStyleKey] = AnyToggleStyle(DefaultToggleStyle())

reconciler = StackReconciler(
view: view,
target: DOMNode(view, ref),
renderer: self
renderer: self,
environment: environment
) { closure in
let fn = JSClosure { _ in
closure()
Expand Down
10 changes: 7 additions & 3 deletions Sources/TokamakDOM/Shapes/Path.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,21 @@ extension Path: ViewDeferredToRenderer {
return AnyView(HTML("ellipse", ["cx": "50%", "cy": "50%", "rx": "50%", "ry": "50%"]
.merging(stroke, uniquingKeysWith: uniqueKeys)))
case let .roundedRect(roundedRect):
let ry = roundedRect.style == .continuous
? roundedRect.cornerSize.width
: roundedRect.cornerSize.height
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)",
"ry": "\(ry)",
]
.merging(stroke, uniquingKeysWith: uniqueKeys)))
.merging(stroke, uniquingKeysWith: uniqueKeys)))
case let .stroked(stroked):
return stroked.path.svgFrom(storage: stroked.path.storage, strokeStyle: stroked.style)
case let .trimmed(trimmed):
return trimmed.path.svgFrom(storage: trimmed.path.storage, strokeStyle: strokeStyle) // TODO: Trim the path
// TODO: Trim the path
return trimmed.path.svgFrom(storage: trimmed.path.storage, strokeStyle: strokeStyle)
}
}

Expand Down
48 changes: 48 additions & 0 deletions Sources/TokamakDOM/Styles/ToggleStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 Jed Fox on 07/04/2020.
//

import TokamakCore

public struct DefaultToggleStyle: ToggleStyle {
public func makeBody(configuration: Configuration) -> some View {
CheckboxToggleStyle().makeBody(configuration: configuration)
}
}

public struct CheckboxToggleStyle: ToggleStyle {
public func makeBody(configuration: ToggleStyleConfiguration) -> some View {
var attrs = ["type": "checkbox"]
if configuration.isOn {
attrs["checked"] = "checked"
}
return HTML("label") {
HTML("input", attrs, listeners: [
"change": { event in
let checked = event.target.object?.checked.boolean ?? false
configuration.isOn = checked
},
])
configuration.label
}
}
}

// FIXME: implement
// public struct SwitchToggleStyle: ToggleStyle {
// public func makeBody(configuration: ToggleStyleConfiguration) -> some View {
// }
// }
8 changes: 4 additions & 4 deletions Sources/TokamakDOM/Views/SecureField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ extension SecureField: ViewDeferredToRenderer where Label == Text {
], listeners: [
"keypress": { event in if event.key == "Enter" { proxy.onCommit() } },
"input": { event in
if let newValue = event.target.object?.value.string {
proxy.textBinding.wrappedValue = newValue
}
},
if let newValue = event.target.object?.value.string {
proxy.textBinding.wrappedValue = newValue
}
},
]))
}
}
8 changes: 4 additions & 4 deletions Sources/TokamakDOM/Views/TextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ extension TextField: ViewDeferredToRenderer where Label == Text {
"blur": { _ in proxy.onEditingChanged(false) },
"keypress": { event in if event.key == "Enter" { proxy.onCommit() } },
"input": { event in
if let newValue = event.target.object?.value.string {
proxy.textBinding.wrappedValue = newValue
}
},
if let newValue = event.target.object?.value.string {
proxy.textBinding.wrappedValue = newValue
}
},
]))
}
}
20 changes: 20 additions & 0 deletions Sources/TokamakDOM/Views/Toggle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 Jed Fox on 07/04/2020.
//

import TokamakCore

public typealias Toggle = TokamakCore.Toggle
j-f1 marked this conversation as resolved.
Show resolved Hide resolved
33 changes: 33 additions & 0 deletions Sources/TokamakDemo/ToggleDemo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 TokamakCore
import TokamakDOM
#endif

struct ToggleDemo: View {
@State var checked = false

var body: some View {
VStack {
Toggle("Check me!", isOn: $checked)
Toggle(isOn: Binding(get: { true }, set: { _ in })) {
Text("I’m always checked!").foregroundColor(.red).italic()
}
}
}
}
5 changes: 4 additions & 1 deletion Sources/TokamakDemo/TokamakDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ struct TokamakDemoView: View {
.padding(20)
}
ForEachDemo()
TextDemo()
Group {
TextDemo()
ToggleDemo()
}
#if canImport(TokamakDOM)
SVGCircle()
.frame(width: 25, height: 25)
Expand Down