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 basic ButtonStyle implementation #214

Merged
merged 10 commits into from
Aug 1, 2020
6 changes: 6 additions & 0 deletions NativeDemo/TokamakDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
D1B4229324B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */; };
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
D1E5FDB224C1D59400E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
Expand Down Expand Up @@ -98,6 +100,7 @@
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppStorageDemo.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>"; };
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>"; };
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -159,6 +162,7 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup;
children = (
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */,
D1B4228E24B3B9BB00682F74 /* ListDemo.swift */,
Expand Down Expand Up @@ -330,6 +334,7 @@
B5C76E4A24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
D1C726F324CB63C6003B576D /* ButtonStyleDemo.swift in Sources */,
854A1A9124B3E3630027BC32 /* ToggleDemo.swift in Sources */,
85ED18A524AD425E0085DFA0 /* TextDemo.swift in Sources */,
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */,
Expand All @@ -353,6 +358,7 @@
B5C76E4B24C73ED5003EABB2 /* AppStorageDemo.swift in Sources */,
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */,
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
D1C726F424CB63C6003B576D /* ButtonStyleDemo.swift in Sources */,
854A1A9324B3F28F0027BC32 /* ToggleDemo.swift in Sources */,
85ED18AE24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */,
Expand Down
15 changes: 15 additions & 0 deletions Sources/TokamakCore/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ public struct EnvironmentValues: CustomStringConvertible {
}
}

struct IsEnabledKey: EnvironmentKey {
static let defaultValue = true
}

extension EnvironmentValues {
public var isEnabled: Bool {
get {
self[IsEnabledKey.self]
}
set {
self[IsEnabledKey.self] = newValue
}
}
}

struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
let environmentValues: EnvironmentValues

Expand Down
95 changes: 95 additions & 0 deletions Sources/TokamakCore/Styles/ButtonStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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 Gene Z. Ragan on 07/22/2020.
Comment on lines +14 to +15
Copy link
Member

Choose a reason for hiding this comment

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

What’s the format for the header? Are we keeping these lines or removing them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I personally don't mind their presence (or absence) at all. They are added by Xcode, while files with no timestamps like that were created in other editors I guess (mine certainly were). I think adding these lines to files that don't currently have them is too much hassle, so if you want consistency it would be probably easier to just remove them everywhere.


public struct ButtonStyleConfiguration {
public struct Label: View {
let content: AnyView
public var body: Never {
neverBody("ButtonStyleConfiguration.Label")
}
}

public let label: Label
public let isPressed: Bool
}

/// This is a helper class that works around absence of "package private" access control in Swift
public struct _ButtonStyleConfigurationProxy {
public struct Label {
public typealias Subject = ButtonStyleConfiguration.Label
public let subject: Subject

public init(_ subject: Subject) { self.subject = subject }

public var content: AnyView { subject.content }
}

public typealias Subject = ButtonStyleConfiguration
public let subject: Subject

public init(label: AnyView, isPressed: Bool) {
subject = .init(label: .init(content: label), isPressed: isPressed)
}

public var label: ButtonStyleConfiguration.Label { subject.label }
}

public protocol ButtonStyle {
associatedtype Body: View

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

typealias Configuration = ButtonStyleConfiguration
}

public struct _AnyButtonStyle: ButtonStyle {
public typealias Body = AnyView

private let bodyClosure: (ButtonStyleConfiguration) -> AnyView

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

public func makeBody(configuration: ButtonStyleConfiguration) -> AnyView {
bodyClosure(configuration)
}
}

public enum _ButtonStyleKey: EnvironmentKey {
public static var defaultValue: _AnyButtonStyle {
fatalError("\(self) must have a renderer-provided default value")
}
}

extension EnvironmentValues {
var buttonStyle: _AnyButtonStyle {
get {
self[_ButtonStyleKey.self]
}
set {
self[_ButtonStyleKey.self] = newValue
}
}
}

extension View {
public func buttonStyle<S>(_ style: S) -> some View where S: ButtonStyle {
environment(\.buttonStyle, _AnyButtonStyle(style))
}
}
6 changes: 3 additions & 3 deletions Sources/TokamakCore/Styles/ToggleStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public struct _AnyToggleStyle: ToggleStyle {
}
}

public enum ToggleStyleKey: EnvironmentKey {
public enum _ToggleStyleKey: EnvironmentKey {
public static var defaultValue: _AnyToggleStyle {
fatalError("\(self) must have a renderer-provided default value")
}
Expand All @@ -61,10 +61,10 @@ public enum ToggleStyleKey: EnvironmentKey {
extension EnvironmentValues {
var toggleStyle: _AnyToggleStyle {
get {
self[ToggleStyleKey.self]
self[_ToggleStyleKey.self]
}
set {
self[ToggleStyleKey.self] = newValue
self[_ToggleStyleKey.self] = newValue
}
}
}
Expand Down
37 changes: 19 additions & 18 deletions Sources/TokamakCore/Views/Buttons/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,30 @@
/// Button("\(counter)", action: { counter += 1 })
/// }
public struct Button<Label>: View where Label: View {
let label: Label

let action: () -> ()
let button: _Button<Label>

public init(action: @escaping () -> (), @ViewBuilder label: () -> Label) {
self.label = label()
button = _Button(action: action, label: label())
}

public var body: some View {
button
}
}

public struct _Button<Label>: View where Label: View {
public let label: Label
public let action: () -> ()
@State public var isPressed = false
@Environment(\.buttonStyle) public var buttonStyle: _AnyButtonStyle

public init(action: @escaping () -> (), label: Label) {
self.label = label
self.action = action
}

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

Expand All @@ -60,18 +73,6 @@ extension Button where Label == Text {

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

/// This is a helper class that works around absence of "package private" access control in Swift
public struct _ButtonProxy<Label> where Label: View {
let subject: Button<Label>

public init(_ subject: Button<Label>) { self.subject = subject }
public var action: () -> () { subject.action }
}

extension _ButtonProxy where Label == Text {
public var label: _TextProxy { _TextProxy(subject.label) }
}
10 changes: 6 additions & 4 deletions Sources/TokamakDOM/DOMRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public final class DOMRenderer: Renderer {

public init<V: View>(_ view: V,
_ ref: JSObjectRef,
_ rootEnvironment: EnvironmentValues? = nil) {
_: EnvironmentValues? = nil) {
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
rootRef = ref
rootRef.style = """
display: flex;
Expand All @@ -102,7 +102,8 @@ public final class DOMRenderer: Renderer {

// Establish default settings
var environment = EnvironmentValues()
environment[ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[_ButtonStyleKey] = _AnyButtonStyle(DefaultButtonStyle())
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[keyPath: \._defaultAppStorage] = LocalStorage.standard
_DefaultSceneStorageProvider.default = SessionStorage.standard

Expand All @@ -122,7 +123,7 @@ public final class DOMRenderer: Renderer {

init<A: App>(_ app: A,
_ ref: JSObjectRef,
_ rootEnvironment: EnvironmentValues? = nil) {
_: EnvironmentValues? = nil) {
rootRef = ref
rootRef.style = """
display: flex;
Expand All @@ -139,7 +140,8 @@ public final class DOMRenderer: Renderer {

// Establish default settings
var environment = EnvironmentValues()
environment[ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[_ButtonStyleKey] = _AnyButtonStyle(DefaultButtonStyle())
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[keyPath: \._defaultAppStorage] = LocalStorage.standard
_DefaultSceneStorageProvider.default = SessionStorage.standard

Expand Down
24 changes: 24 additions & 0 deletions Sources/TokamakDOM/Styles/ButtonStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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 Gene Z. Ragan on 07/22/2020.
//

import TokamakCore

public struct DefaultButtonStyle: ButtonStyle {
public func makeBody(configuration: ButtonStyleConfiguration) -> some View {
configuration.label
}
}
21 changes: 18 additions & 3 deletions Sources/TokamakDOM/Views/Buttons/Button.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,25 @@

import TokamakCore

extension Button: ViewDeferredToRenderer where Label == Text {
extension _Button: ViewDeferredToRenderer where Label == Text {
public var deferredBody: AnyView {
AnyView(HTML("button", listeners: ["click": { _ in _ButtonProxy(self).action() }]) {
_ButtonProxy(self).label.subject
AnyView(HTML("button", listeners: [
j-f1 marked this conversation as resolved.
Show resolved Hide resolved
"click": { _ in action() },
"onmousedown": { _ in isPressed = true },
"onmouseup": { _ in isPressed = false },
j-f1 marked this conversation as resolved.
Show resolved Hide resolved
]) {
buttonStyle.makeBody(
configuration: _ButtonStyleConfigurationProxy(
label: AnyView(label),
isPressed: isPressed
).subject
)
})
}
}

extension ButtonStyleConfiguration.Label: ViewDeferredToRenderer {
public var deferredBody: AnyView {
_ButtonStyleConfigurationProxy.Label(self).content
}
}
48 changes: 48 additions & 0 deletions Sources/TokamakDemo/ButtonStyleDemo.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.

#if canImport(SwiftUI)
import SwiftUI
#else
import TokamakCore
import TokamakDOM
#endif

struct PressedButtonStyle: ButtonStyle {
let pressedColor: Color

func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(configuration.isPressed ? pressedColor : .blue)
.padding(15)
}
}

public struct ButtonStyleDemo: View {
public var body: some View {
VStack {
Button("Default Style") {
print("tapped")
return
}
Button("Pressed Button Style") {
print("tapped")
return
}
.buttonStyle(
PressedButtonStyle(pressedColor: Color.red)
)
}
}
}
1 change: 1 addition & 0 deletions Sources/TokamakDemo/TokamakDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ var links: [NavItem] {
.zIndex(1)
Text("I'm on top")
}.padding(20)),
NavItem("ButtonStyle", destination: ButtonStyleDemo()),
NavItem("ForEach", destination: ForEachDemo()),
NavItem("Text", destination: TextDemo()),
NavItem("Toggle", destination: ToggleDemo()),
Expand Down