Add basic `ButtonStyle` implementation (#214)

This based off the `buttonstyles` branch by @Outcue.

Initially it didn't work because mounted host views didn't propagate their environment on updates. This is now fixed by adding `updateEnvironment` function on `MountedElement` base class and calling it in the initializer. Manual environment updates are no longer needed in `makeMounted...` factory functions. `makeMountedApp` is no longer needed at all and `MountedApp` initializer can be used directly then.
This commit is contained in:
Max Desiatov 2020-08-01 18:46:34 +01:00 committed by GitHub
parent 2c539d9319
commit e37d13017c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 349 additions and 154 deletions

View File

@ -41,6 +41,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 */; };
@ -101,6 +103,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>"; };
@ -162,6 +165,7 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup;
children = (
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
85ED189E24AD425E0085DFA0 /* Counter.swift */,
@ -335,6 +339,7 @@
3DCDE44424CA6AD400910F17 /* SidebarDemo.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 */,
@ -359,6 +364,7 @@
3DCDE44524CA6AD400910F17 /* SidebarDemo.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 */,

View File

@ -24,34 +24,3 @@ public protocol DynamicProperty {
extension DynamicProperty {
public mutating func update() {}
}
extension TypeInfo {
/// Extract all `DynamicProperty` from a type, recursively.
/// This is necessary as a `DynamicProperty` can be nested.
/// `EnvironmentValues` can also be injected at this point.
func dynamicProperties(_ environment: EnvironmentValues,
source: inout Any,
shouldUpdate: Bool) -> [PropertyInfo] {
var dynamicProps = [PropertyInfo]()
for prop in properties where prop.type is DynamicProperty.Type {
dynamicProps.append(prop)
// swiftlint:disable force_try
let propInfo = try! typeInfo(of: prop.type)
propInfo.injectEnvironment(from: environment, into: &source)
var extracted = try! prop.get(from: source)
dynamicProps.append(
contentsOf: propInfo.dynamicProperties(environment,
source: &extracted,
shouldUpdate: shouldUpdate)
)
// swiftlint:disable:next force_cast
var extractedDynamicProp = extracted as! DynamicProperty
if shouldUpdate {
extractedDynamicProp.update()
}
try! prop.set(value: extractedDynamicProp, on: &source)
// swiftlint:enable force_try
}
return dynamicProps
}
}

View File

@ -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

View File

@ -57,20 +57,3 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
)
}
}
extension _AnyApp {
func makeMountedApp<R>(
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
) -> MountedApp<R> where R: Renderer {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: type)
var modified = app
info.injectEnvironment(from: environmentValues, into: &modified)
var result = self
result.app = modified
return MountedApp(result, parentTarget, environmentValues)
}
}

View File

@ -17,24 +17,15 @@
import CombineShim
class MountedCompositeElement<R: Renderer>: MountedElement<R>, Hashable {
static func == (lhs: MountedCompositeElement<R>,
rhs: MountedCompositeElement<R>) -> Bool {
lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
class MountedCompositeElement<R: Renderer>: MountedElement<R> {
let parentTarget: R.TargetType
var state = [Any]()
var subscriptions = [AnyCancellable]()
init(_ app: _AnyApp, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
init<A: App>(_ app: A, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
self.parentTarget = parentTarget
super.init(app, environmentValues)
super.init(_AnyApp(app), environmentValues)
}
init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
@ -47,3 +38,14 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R>, Hashable {
super.init(view, environmentValues)
}
}
extension MountedCompositeElement: Hashable {
static func == (lhs: MountedCompositeElement<R>,
rhs: MountedCompositeElement<R>) -> Bool {
lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}

View File

@ -22,6 +22,14 @@ enum MountedElementKind {
case app(_AnyApp)
case scene(_AnyScene)
case view(AnyView)
var type: Any.Type {
switch self {
case let .app(app): return app.type
case let .scene(scene): return scene.type
case let .view(view): return view.type
}
}
}
public class MountedElement<R: Renderer> {
@ -65,14 +73,6 @@ public class MountedElement<R: Renderer> {
}
}
var elementType: Any.Type {
switch element {
case let .app(app): return app.type
case let .scene(scene): return scene.type
case let .view(view): return view.type
}
}
var typeConstructorName: String {
switch element {
case .app: fatalError("""
@ -91,16 +91,34 @@ public class MountedElement<R: Renderer> {
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) {
element = .app(app)
self.environmentValues = environmentValues
updateEnvironment()
}
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) {
element = .scene(scene)
self.environmentValues = environmentValues
updateEnvironment()
}
init(_ view: AnyView, _ environmentValues: EnvironmentValues) {
element = .view(view)
self.environmentValues = environmentValues
updateEnvironment()
}
@discardableResult func updateEnvironment() -> TypeInfo {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: element.type)
switch element {
case .app:
environmentValues = info.injectEnvironment(from: environmentValues, into: &app.app)
case .scene:
environmentValues = info.injectEnvironment(from: environmentValues, into: &scene.scene)
case .view:
environmentValues = info.injectEnvironment(from: environmentValues, into: &view.view)
}
return info
}
func mount(with reconciler: StackReconciler<R>) {
@ -117,10 +135,20 @@ public class MountedElement<R: Renderer> {
}
extension TypeInfo {
func injectEnvironment(from environmentValues: EnvironmentValues, into element: inout Any) {
fileprivate func injectEnvironment(
from environmentValues: EnvironmentValues,
into element: inout Any
) -> EnvironmentValues {
var modifiedEnv = environmentValues
// swiftlint:disable force_try
// Extract the view from the AnyView for modification, apply Environment changes:
if genericTypes.contains(where: { $0 is EnvironmentModifier.Type }),
let modifier = try! property(named: "modifier").get(from: element) as? EnvironmentModifier {
modifier.modifyEnvironment(&modifiedEnv)
}
// Inject @Environment values
// swiftlint:disable force_cast
// swiftlint:disable force_try
// `DynamicProperty`s can have `@Environment` properties contained in them,
// so we have to inject into them as well.
for dynamicProp in properties.filter({ $0.type is DynamicProperty.Type }) {
@ -128,18 +156,45 @@ extension TypeInfo {
var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty
for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
var wrapper = try! prop.get(from: propWrapper) as! EnvironmentReader
wrapper.setContent(from: environmentValues)
wrapper.setContent(from: modifiedEnv)
try! prop.set(value: wrapper, on: &propWrapper)
}
try! dynamicProp.set(value: propWrapper, on: &element)
}
for prop in properties.filter({ $0.type is EnvironmentReader.Type }) {
var wrapper = try! prop.get(from: element) as! EnvironmentReader
wrapper.setContent(from: environmentValues)
wrapper.setContent(from: modifiedEnv)
try! prop.set(value: wrapper, on: &element)
}
// swiftlint:enable force_try
// swiftlint:enable force_cast
return modifiedEnv
}
/// Extract all `DynamicProperty` from a type, recursively.
/// This is necessary as a `DynamicProperty` can be nested.
/// `EnvironmentValues` can also be injected at this point.
func dynamicProperties(_ environment: EnvironmentValues,
source: inout Any) -> [PropertyInfo] {
var dynamicProps = [PropertyInfo]()
for prop in properties where prop.type is DynamicProperty.Type {
dynamicProps.append(prop)
// swiftlint:disable force_try
let propInfo = try! typeInfo(of: prop.type)
_ = propInfo.injectEnvironment(from: environment, into: &source)
var extracted = try! prop.get(from: source)
dynamicProps.append(
contentsOf: propInfo.dynamicProperties(environment,
source: &extracted)
)
// swiftlint:disable:next force_cast
var extractedDynamicProp = extracted as! DynamicProperty
extractedDynamicProp.update()
try! prop.set(value: extractedDynamicProp, on: &source)
// swiftlint:enable force_try
}
return dynamicProps
}
}
@ -148,30 +203,12 @@ extension AnyView {
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
) -> MountedElement<R> {
// Find Environment changes
var modifiedEnv = environmentValues
// swiftlint:disable force_try
// Extract the view from the AnyView for modification
let viewInfo = try! typeInfo(of: type)
if viewInfo.genericTypes.filter({ $0 is EnvironmentModifier.Type }).count > 0 {
// Apply Environment changes:
if let modifier = try! viewInfo
.property(named: "modifier")
.get(from: view) as? EnvironmentModifier {
modifier.modifyEnvironment(&modifiedEnv)
}
}
var modifiedView = view
viewInfo.injectEnvironment(from: environmentValues, into: &modifiedView)
var anyView = self
anyView.view = modifiedView
if anyView.type == EmptyView.self {
return MountedEmptyView(anyView, modifiedEnv)
} else if anyView.bodyType == Never.self && !(anyView.type is ViewDeferredToRenderer.Type) {
return MountedHostView(anyView, parentTarget, modifiedEnv)
if type == EmptyView.self {
return MountedEmptyView(self, environmentValues)
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) {
return MountedHostView(self, parentTarget, environmentValues)
} else {
return MountedCompositeView(anyView, parentTarget, modifiedEnv)
return MountedCompositeView(self, parentTarget, environmentValues)
}
}
}

View File

@ -38,9 +38,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
}
override func mount(with reconciler: StackReconciler<R>) {
guard
let target = reconciler.renderer?.mountTarget(to: parentTarget,
with: self)
guard let target = reconciler.renderer?.mountTarget(to: parentTarget, with: self)
else { return }
self.target = target
@ -68,6 +66,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
override func update(with reconciler: StackReconciler<R>) {
guard let target = target else { return }
updateEnvironment()
target.view = view
reconciler.renderer?.update(target: target, with: self)
@ -96,10 +95,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
let newChild: MountedElement<R>
if firstChild.typeConstructorName == mountedChildren[0].view.typeConstructorName {
child.view = firstChild
// Inject Environment
// swiftlint:disable:next force_try
let viewInfo = try! typeInfo(of: child.view.type)
viewInfo.injectEnvironment(from: environmentValues, into: &child.view.view)
child.updateEnvironment()
child.update(with: reconciler)
newChild = child
} else {

View File

@ -89,28 +89,19 @@ extension _AnyScene {
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues
) -> MountedScene<R> {
// swiftlint:disable:next force_try
let info = try! typeInfo(of: type)
var modified = scene
info.injectEnvironment(from: environmentValues, into: &modified)
var title: String?
if let titledSelf = modified as? TitledScene,
if let titledSelf = scene as? TitledScene,
let text = titledSelf.title {
title = _TextProxy(text).rawText
}
let children: [MountedElement<R>]
if let deferredScene = modified as? SceneDeferredToRenderer {
if let deferredScene = scene as? SceneDeferredToRenderer {
children = [deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues)]
} else if let groupScene = modified as? GroupScene {
} else if let groupScene = scene as? GroupScene {
children = groupScene.children.map { $0.makeMountedScene(parentTarget, environmentValues) }
} else {
children = []
}
var result = self
result.scene = modified
return .init(result, title, children, parentTarget, environmentValues)
return .init(self, title, children, parentTarget, environmentValues)
}
}

View File

@ -90,7 +90,7 @@ public final class StackReconciler<R: Renderer> {
self.scheduler = scheduler
rootTarget = target
rootElement = _AnyApp(app).makeMountedApp(target, environment)
rootElement = MountedApp(app, target, environment)
rootElement.mount(with: self)
if let mountedApp = rootElement as? MountedApp<R> {
@ -147,9 +147,8 @@ public final class StackReconciler<R: Renderer> {
if state.getter == nil || state.setter == nil {
state.getter = { compositeElement.state[id] }
// Avoiding an indirect reference cycle here: this closure can be
// owned by callbacks owned by view's target, which is strongly referenced
// by the reconciler.
// Avoiding an indirect reference cycle here: this closure can be owned by callbacks
// owned by view's target, which is strongly referenced by the reconciler.
state.setter = { [weak self, weak compositeElement] newValue in
guard let element = compositeElement else { return }
self?.queueStateUpdate(for: element, id: id) { $0 = newValue }
@ -178,16 +177,15 @@ public final class StackReconciler<R: Renderer> {
func render<T>(compositeElement: MountedCompositeElement<R>,
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>) -> T {
let info = try! typeInfo(of: compositeElement.elementType)
info.injectEnvironment(from: compositeElement.environmentValues,
into: &compositeElement[keyPath: bodyKeypath])
let needsSubscriptions = compositeElement.subscriptions.isEmpty
let info = compositeElement.updateEnvironment()
var stateIdx = 0
let dynamicProps = info.dynamicProperties(compositeElement.environmentValues,
source: &compositeElement[keyPath: bodyKeypath],
shouldUpdate: true)
let dynamicProps = info.dynamicProperties(
compositeElement.environmentValues,
source: &compositeElement[keyPath: bodyKeypath]
)
let needsSubscriptions = compositeElement.subscriptions.isEmpty
for property in dynamicProps {
// Setup state/subscriptions
if property.type is ValueStorage.Type {

View File

@ -0,0 +1,97 @@
// 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.
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 let type: Any.Type
public init<S: ButtonStyle>(_ style: S) {
type = S.self
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))
}
}

View File

@ -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
public init(action: @escaping () -> (), label: Label) {
self.label = label
self.action = action
}
public var body: Never {
neverBody("Button")
neverBody("_Button")
}
}
@ -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) }
}

View File

@ -33,7 +33,8 @@ public struct Text: View {
let storage: _Storage
let modifiers: [_Modifier]
@Environment(\.font) var font: Font?
@Environment(\.font) var font
@Environment(\.foregroundColor) var foregroundColor
public enum _Storage {
case verbatim(String)
@ -99,15 +100,12 @@ public struct _TextProxy {
public var modifiers: [Text._Modifier] {
[
.font(subject.font),
.color(subject.foregroundColor),
] + subject.modifiers
}
}
public extension Text {
func foregroundColor(_ color: Color?) -> Text {
.init(storage: storage, modifiers: modifiers + [.color(color)])
}
func font(_ font: Font?) -> Text {
.init(storage: storage, modifiers: modifiers + [.font(font)])
}

View File

@ -51,6 +51,9 @@ public typealias RadioGroupPickerStyle = TokamakCore.RadioGroupPickerStyle
public typealias SegmentedPickerStyle = TokamakCore.SegmentedPickerStyle
public typealias WheelPickerStyle = TokamakCore.WheelPickerStyle
public typealias ButtonStyle = TokamakCore.ButtonStyle
public typealias ButtonStyleConfiguration = TokamakCore.ButtonStyleConfiguration
// MARK: Shapes
public typealias Shape = TokamakCore.Shape

View File

@ -23,6 +23,7 @@ extension EnvironmentValues {
static var defaultEnvironment: Self {
var environment = EnvironmentValues()
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
environment[_ButtonStyleKey] = _AnyButtonStyle(DefaultButtonStyle())
environment._defaultAppStorage = LocalStorage.standard
_DefaultSceneStorageProvider.default = SessionStorage.standard

View File

@ -53,6 +53,16 @@ let tokamakStyles = """
flex-direction: column;
margin-left: 1em;
}
._tokamak-buttonstyle-reset {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: transparent;
border: none;
margin: 0;
padding: 0;
font-size: inherit;
}
"""
let rootNodeStyles = """

View File

@ -0,0 +1,30 @@
// 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
}
}
extension ButtonStyleConfiguration.Label: ViewDeferredToRenderer {
public var deferredBody: AnyView {
_ButtonStyleConfigurationProxy.Label(self).content
}
}

View File

@ -17,10 +17,26 @@
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
let attributes: [String: String]
if buttonStyle.type == DefaultButtonStyle.self {
attributes = [:]
} else {
attributes = ["class": "_tokamak-buttonstyle-reset"]
}
return AnyView(HTML("button", attributes, listeners: [
"click": { _ in action() },
"pointerdown": { _ in isPressed = true },
"pointerup": { _ in isPressed = false },
]) {
buttonStyle.makeBody(
configuration: _ButtonStyleConfigurationProxy(
label: AnyView(label),
isPressed: isPressed
).subject
)
})
}
}

View File

@ -0,0 +1,41 @@
// 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.
import TokamakShim
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")
}
Button("Pressed Button Style") {
print("tapped")
}
.buttonStyle(
PressedButtonStyle(pressedColor: Color.red)
)
}
}
}

View File

@ -26,7 +26,7 @@ public struct ToggleDemo: View {
VStack {
Toggle("Check me!", isOn: $checked)
Toggle(isOn: Binding(get: { true }, set: { _ in })) {
Text("Im always checked!").foregroundColor(.red).italic()
Group { Text("Im always checked!").italic() }.foregroundColor(.red)
}
}
}

View File

@ -100,6 +100,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()),