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:
parent
2c539d9319
commit
e37d13017c
|
@ -41,6 +41,8 @@
|
||||||
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
D1B4229124B3B9BB00682F74 /* ListDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228E24B3B9BB00682F74 /* ListDemo.swift */; };
|
||||||
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
|
D1B4229224B3B9BB00682F74 /* OutlineGroupDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B4228F24B3B9BB00682F74 /* OutlineGroupDemo.swift */; };
|
||||||
D1B4229324B3B9BB00682F74 /* 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 */; };
|
D1E5FDAD24C1D57000E7485E /* TokamakShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E5FDAC24C1D57000E7485E /* TokamakShim.swift */; };
|
||||||
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
|
D1E5FDAF24C1D58E00E7485E /* libTokamakShim.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1E5FDA424C1D54B00E7485E /* libTokamakShim.a */; };
|
||||||
D1E5FDB224C1D59400E7485E /* 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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
D1EE7EA624C0DD2100C0D127 /* PickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickerDemo.swift; sourceTree = "<group>"; };
|
||||||
|
@ -162,6 +165,7 @@
|
||||||
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
|
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D1C726F224CB63C6003B576D /* ButtonStyleDemo.swift */,
|
||||||
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
|
B5C76E4924C73ED4003EABB2 /* AppStorageDemo.swift */,
|
||||||
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
|
B56F22DF24BC89FD001738DF /* ColorDemo.swift */,
|
||||||
85ED189E24AD425E0085DFA0 /* Counter.swift */,
|
85ED189E24AD425E0085DFA0 /* Counter.swift */,
|
||||||
|
@ -335,6 +339,7 @@
|
||||||
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
3DCDE44424CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
||||||
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
|
85ED18AD24AD425E0085DFA0 /* TextFieldDemo.swift in Sources */,
|
||||||
85ED18A724AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
|
85ED18A724AD425E0085DFA0 /* ForEachDemo.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 */,
|
||||||
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */,
|
85ED18AB24AD425E0085DFA0 /* Counter.swift in Sources */,
|
||||||
|
@ -359,6 +364,7 @@
|
||||||
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
3DCDE44524CA6AD400910F17 /* SidebarDemo.swift in Sources */,
|
||||||
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */,
|
85ED18AC24AD425E0085DFA0 /* Counter.swift in Sources */,
|
||||||
85ED18A824AD425E0085DFA0 /* ForEachDemo.swift in Sources */,
|
85ED18A824AD425E0085DFA0 /* ForEachDemo.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 */,
|
||||||
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */,
|
85ED18A624AD425E0085DFA0 /* TextDemo.swift in Sources */,
|
||||||
|
|
|
@ -24,34 +24,3 @@ public protocol DynamicProperty {
|
||||||
extension DynamicProperty {
|
extension DynamicProperty {
|
||||||
public mutating func update() {}
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
struct _EnvironmentValuesWritingModifier: ViewModifier, EnvironmentModifier {
|
||||||
let environmentValues: EnvironmentValues
|
let environmentValues: EnvironmentValues
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,24 +17,15 @@
|
||||||
|
|
||||||
import CombineShim
|
import CombineShim
|
||||||
|
|
||||||
class MountedCompositeElement<R: Renderer>: MountedElement<R>, Hashable {
|
class MountedCompositeElement<R: Renderer>: MountedElement<R> {
|
||||||
static func == (lhs: MountedCompositeElement<R>,
|
|
||||||
rhs: MountedCompositeElement<R>) -> Bool {
|
|
||||||
lhs === rhs
|
|
||||||
}
|
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(ObjectIdentifier(self))
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentTarget: R.TargetType
|
let parentTarget: R.TargetType
|
||||||
|
|
||||||
var state = [Any]()
|
var state = [Any]()
|
||||||
var subscriptions = [AnyCancellable]()
|
var subscriptions = [AnyCancellable]()
|
||||||
|
|
||||||
init(_ app: _AnyApp, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
|
init<A: App>(_ app: A, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
|
||||||
self.parentTarget = parentTarget
|
self.parentTarget = parentTarget
|
||||||
super.init(app, environmentValues)
|
super.init(_AnyApp(app), environmentValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
|
init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) {
|
||||||
|
@ -47,3 +38,14 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R>, Hashable {
|
||||||
super.init(view, environmentValues)
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,14 @@ enum MountedElementKind {
|
||||||
case app(_AnyApp)
|
case app(_AnyApp)
|
||||||
case scene(_AnyScene)
|
case scene(_AnyScene)
|
||||||
case view(AnyView)
|
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> {
|
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 {
|
var typeConstructorName: String {
|
||||||
switch element {
|
switch element {
|
||||||
case .app: fatalError("""
|
case .app: fatalError("""
|
||||||
|
@ -91,16 +91,34 @@ public class MountedElement<R: Renderer> {
|
||||||
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) {
|
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) {
|
||||||
element = .app(app)
|
element = .app(app)
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
|
updateEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) {
|
init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) {
|
||||||
element = .scene(scene)
|
element = .scene(scene)
|
||||||
self.environmentValues = environmentValues
|
self.environmentValues = environmentValues
|
||||||
|
updateEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ view: AnyView, _ environmentValues: EnvironmentValues) {
|
init(_ view: AnyView, _ environmentValues: EnvironmentValues) {
|
||||||
element = .view(view)
|
element = .view(view)
|
||||||
self.environmentValues = environmentValues
|
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>) {
|
func mount(with reconciler: StackReconciler<R>) {
|
||||||
|
@ -117,10 +135,20 @@ public class MountedElement<R: Renderer> {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TypeInfo {
|
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
|
// Inject @Environment values
|
||||||
// swiftlint:disable force_cast
|
// swiftlint:disable force_cast
|
||||||
// swiftlint:disable force_try
|
|
||||||
// `DynamicProperty`s can have `@Environment` properties contained in them,
|
// `DynamicProperty`s can have `@Environment` properties contained in them,
|
||||||
// so we have to inject into them as well.
|
// so we have to inject into them as well.
|
||||||
for dynamicProp in properties.filter({ $0.type is DynamicProperty.Type }) {
|
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
|
var propWrapper = try! dynamicProp.get(from: element) as! DynamicProperty
|
||||||
for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
|
for prop in propInfo.properties.filter({ $0.type is EnvironmentReader.Type }) {
|
||||||
var wrapper = try! prop.get(from: propWrapper) as! EnvironmentReader
|
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! prop.set(value: wrapper, on: &propWrapper)
|
||||||
}
|
}
|
||||||
try! dynamicProp.set(value: propWrapper, on: &element)
|
try! dynamicProp.set(value: propWrapper, on: &element)
|
||||||
}
|
}
|
||||||
for prop in properties.filter({ $0.type is EnvironmentReader.Type }) {
|
for prop in properties.filter({ $0.type is EnvironmentReader.Type }) {
|
||||||
var wrapper = try! prop.get(from: element) as! EnvironmentReader
|
var wrapper = try! prop.get(from: element) as! EnvironmentReader
|
||||||
wrapper.setContent(from: environmentValues)
|
wrapper.setContent(from: modifiedEnv)
|
||||||
try! prop.set(value: wrapper, on: &element)
|
try! prop.set(value: wrapper, on: &element)
|
||||||
}
|
}
|
||||||
// swiftlint:enable force_try
|
// swiftlint:enable force_try
|
||||||
// swiftlint:enable force_cast
|
// 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,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues
|
_ environmentValues: EnvironmentValues
|
||||||
) -> MountedElement<R> {
|
) -> MountedElement<R> {
|
||||||
// Find Environment changes
|
if type == EmptyView.self {
|
||||||
var modifiedEnv = environmentValues
|
return MountedEmptyView(self, environmentValues)
|
||||||
// swiftlint:disable force_try
|
} else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) {
|
||||||
// Extract the view from the AnyView for modification
|
return MountedHostView(self, parentTarget, environmentValues)
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
return MountedCompositeView(anyView, parentTarget, modifiedEnv)
|
return MountedCompositeView(self, parentTarget, environmentValues)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,9 +38,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
override func mount(with reconciler: StackReconciler<R>) {
|
override func mount(with reconciler: StackReconciler<R>) {
|
||||||
guard
|
guard let target = reconciler.renderer?.mountTarget(to: parentTarget, with: self)
|
||||||
let target = reconciler.renderer?.mountTarget(to: parentTarget,
|
|
||||||
with: self)
|
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
self.target = target
|
self.target = target
|
||||||
|
@ -68,6 +66,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
override func update(with reconciler: StackReconciler<R>) {
|
override func update(with reconciler: StackReconciler<R>) {
|
||||||
guard let target = target else { return }
|
guard let target = target else { return }
|
||||||
|
|
||||||
|
updateEnvironment()
|
||||||
target.view = view
|
target.view = view
|
||||||
reconciler.renderer?.update(target: target, with: self)
|
reconciler.renderer?.update(target: target, with: self)
|
||||||
|
|
||||||
|
@ -96,10 +95,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
|
||||||
let newChild: MountedElement<R>
|
let newChild: MountedElement<R>
|
||||||
if firstChild.typeConstructorName == mountedChildren[0].view.typeConstructorName {
|
if firstChild.typeConstructorName == mountedChildren[0].view.typeConstructorName {
|
||||||
child.view = firstChild
|
child.view = firstChild
|
||||||
// Inject Environment
|
child.updateEnvironment()
|
||||||
// swiftlint:disable:next force_try
|
|
||||||
let viewInfo = try! typeInfo(of: child.view.type)
|
|
||||||
viewInfo.injectEnvironment(from: environmentValues, into: &child.view.view)
|
|
||||||
child.update(with: reconciler)
|
child.update(with: reconciler)
|
||||||
newChild = child
|
newChild = child
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -89,28 +89,19 @@ extension _AnyScene {
|
||||||
_ parentTarget: R.TargetType,
|
_ parentTarget: R.TargetType,
|
||||||
_ environmentValues: EnvironmentValues
|
_ environmentValues: EnvironmentValues
|
||||||
) -> MountedScene<R> {
|
) -> 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?
|
var title: String?
|
||||||
if let titledSelf = modified as? TitledScene,
|
if let titledSelf = scene as? TitledScene,
|
||||||
let text = titledSelf.title {
|
let text = titledSelf.title {
|
||||||
title = _TextProxy(text).rawText
|
title = _TextProxy(text).rawText
|
||||||
}
|
}
|
||||||
let children: [MountedElement<R>]
|
let children: [MountedElement<R>]
|
||||||
if let deferredScene = modified as? SceneDeferredToRenderer {
|
if let deferredScene = scene as? SceneDeferredToRenderer {
|
||||||
children = [deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues)]
|
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) }
|
children = groupScene.children.map { $0.makeMountedScene(parentTarget, environmentValues) }
|
||||||
} else {
|
} else {
|
||||||
children = []
|
children = []
|
||||||
}
|
}
|
||||||
|
return .init(self, title, children, parentTarget, environmentValues)
|
||||||
var result = self
|
|
||||||
result.scene = modified
|
|
||||||
return .init(result, title, children, parentTarget, environmentValues)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ public final class StackReconciler<R: Renderer> {
|
||||||
self.scheduler = scheduler
|
self.scheduler = scheduler
|
||||||
rootTarget = target
|
rootTarget = target
|
||||||
|
|
||||||
rootElement = _AnyApp(app).makeMountedApp(target, environment)
|
rootElement = MountedApp(app, target, environment)
|
||||||
|
|
||||||
rootElement.mount(with: self)
|
rootElement.mount(with: self)
|
||||||
if let mountedApp = rootElement as? MountedApp<R> {
|
if let mountedApp = rootElement as? MountedApp<R> {
|
||||||
|
@ -147,9 +147,8 @@ public final class StackReconciler<R: Renderer> {
|
||||||
if state.getter == nil || state.setter == nil {
|
if state.getter == nil || state.setter == nil {
|
||||||
state.getter = { compositeElement.state[id] }
|
state.getter = { compositeElement.state[id] }
|
||||||
|
|
||||||
// Avoiding an indirect reference cycle here: this closure can be
|
// Avoiding an indirect reference cycle here: this closure can be owned by callbacks
|
||||||
// owned by callbacks owned by view's target, which is strongly referenced
|
// owned by view's target, which is strongly referenced by the reconciler.
|
||||||
// by the reconciler.
|
|
||||||
state.setter = { [weak self, weak compositeElement] newValue in
|
state.setter = { [weak self, weak compositeElement] newValue in
|
||||||
guard let element = compositeElement else { return }
|
guard let element = compositeElement else { return }
|
||||||
self?.queueStateUpdate(for: element, id: id) { $0 = newValue }
|
self?.queueStateUpdate(for: element, id: id) { $0 = newValue }
|
||||||
|
@ -178,16 +177,15 @@ public final class StackReconciler<R: Renderer> {
|
||||||
func render<T>(compositeElement: MountedCompositeElement<R>,
|
func render<T>(compositeElement: MountedCompositeElement<R>,
|
||||||
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
|
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
|
||||||
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>) -> T {
|
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>) -> T {
|
||||||
let info = try! typeInfo(of: compositeElement.elementType)
|
let info = compositeElement.updateEnvironment()
|
||||||
info.injectEnvironment(from: compositeElement.environmentValues,
|
|
||||||
into: &compositeElement[keyPath: bodyKeypath])
|
|
||||||
|
|
||||||
let needsSubscriptions = compositeElement.subscriptions.isEmpty
|
|
||||||
|
|
||||||
var stateIdx = 0
|
var stateIdx = 0
|
||||||
let dynamicProps = info.dynamicProperties(compositeElement.environmentValues,
|
let dynamicProps = info.dynamicProperties(
|
||||||
source: &compositeElement[keyPath: bodyKeypath],
|
compositeElement.environmentValues,
|
||||||
shouldUpdate: true)
|
source: &compositeElement[keyPath: bodyKeypath]
|
||||||
|
)
|
||||||
|
|
||||||
|
let needsSubscriptions = compositeElement.subscriptions.isEmpty
|
||||||
for property in dynamicProps {
|
for property in dynamicProps {
|
||||||
// Setup state/subscriptions
|
// Setup state/subscriptions
|
||||||
if property.type is ValueStorage.Type {
|
if property.type is ValueStorage.Type {
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,17 +36,30 @@
|
||||||
/// Button("\(counter)", action: { counter += 1 })
|
/// Button("\(counter)", action: { counter += 1 })
|
||||||
/// }
|
/// }
|
||||||
public struct Button<Label>: View where Label: View {
|
public struct Button<Label>: View where Label: View {
|
||||||
let label: Label
|
let button: _Button<Label>
|
||||||
|
|
||||||
let action: () -> ()
|
|
||||||
|
|
||||||
public init(action: @escaping () -> (), @ViewBuilder label: () -> 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
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: Never {
|
public var body: Never {
|
||||||
neverBody("Button")
|
neverBody("_Button")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,18 +73,6 @@ extension Button where Label == Text {
|
||||||
|
|
||||||
extension Button: ParentView {
|
extension Button: ParentView {
|
||||||
public var children: [AnyView] {
|
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) }
|
|
||||||
}
|
|
||||||
|
|
|
@ -33,7 +33,8 @@ public struct Text: View {
|
||||||
let storage: _Storage
|
let storage: _Storage
|
||||||
let modifiers: [_Modifier]
|
let modifiers: [_Modifier]
|
||||||
|
|
||||||
@Environment(\.font) var font: Font?
|
@Environment(\.font) var font
|
||||||
|
@Environment(\.foregroundColor) var foregroundColor
|
||||||
|
|
||||||
public enum _Storage {
|
public enum _Storage {
|
||||||
case verbatim(String)
|
case verbatim(String)
|
||||||
|
@ -99,15 +100,12 @@ public struct _TextProxy {
|
||||||
public var modifiers: [Text._Modifier] {
|
public var modifiers: [Text._Modifier] {
|
||||||
[
|
[
|
||||||
.font(subject.font),
|
.font(subject.font),
|
||||||
|
.color(subject.foregroundColor),
|
||||||
] + subject.modifiers
|
] + subject.modifiers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Text {
|
public extension Text {
|
||||||
func foregroundColor(_ color: Color?) -> Text {
|
|
||||||
.init(storage: storage, modifiers: modifiers + [.color(color)])
|
|
||||||
}
|
|
||||||
|
|
||||||
func font(_ font: Font?) -> Text {
|
func font(_ font: Font?) -> Text {
|
||||||
.init(storage: storage, modifiers: modifiers + [.font(font)])
|
.init(storage: storage, modifiers: modifiers + [.font(font)])
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,9 @@ public typealias RadioGroupPickerStyle = TokamakCore.RadioGroupPickerStyle
|
||||||
public typealias SegmentedPickerStyle = TokamakCore.SegmentedPickerStyle
|
public typealias SegmentedPickerStyle = TokamakCore.SegmentedPickerStyle
|
||||||
public typealias WheelPickerStyle = TokamakCore.WheelPickerStyle
|
public typealias WheelPickerStyle = TokamakCore.WheelPickerStyle
|
||||||
|
|
||||||
|
public typealias ButtonStyle = TokamakCore.ButtonStyle
|
||||||
|
public typealias ButtonStyleConfiguration = TokamakCore.ButtonStyleConfiguration
|
||||||
|
|
||||||
// MARK: Shapes
|
// MARK: Shapes
|
||||||
|
|
||||||
public typealias Shape = TokamakCore.Shape
|
public typealias Shape = TokamakCore.Shape
|
||||||
|
|
|
@ -23,6 +23,7 @@ extension EnvironmentValues {
|
||||||
static var defaultEnvironment: Self {
|
static var defaultEnvironment: Self {
|
||||||
var environment = EnvironmentValues()
|
var environment = EnvironmentValues()
|
||||||
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
|
environment[_ToggleStyleKey] = _AnyToggleStyle(DefaultToggleStyle())
|
||||||
|
environment[_ButtonStyleKey] = _AnyButtonStyle(DefaultButtonStyle())
|
||||||
environment._defaultAppStorage = LocalStorage.standard
|
environment._defaultAppStorage = LocalStorage.standard
|
||||||
_DefaultSceneStorageProvider.default = SessionStorage.standard
|
_DefaultSceneStorageProvider.default = SessionStorage.standard
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,16 @@ let tokamakStyles = """
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-left: 1em;
|
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 = """
|
let rootNodeStyles = """
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,10 +17,26 @@
|
||||||
|
|
||||||
import TokamakCore
|
import TokamakCore
|
||||||
|
|
||||||
extension Button: ViewDeferredToRenderer where Label == Text {
|
extension _Button: ViewDeferredToRenderer where Label == Text {
|
||||||
public var deferredBody: AnyView {
|
public var deferredBody: AnyView {
|
||||||
AnyView(HTML("button", listeners: ["click": { _ in _ButtonProxy(self).action() }]) {
|
let attributes: [String: String]
|
||||||
_ButtonProxy(self).label.subject
|
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
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ public struct ToggleDemo: View {
|
||||||
VStack {
|
VStack {
|
||||||
Toggle("Check me!", isOn: $checked)
|
Toggle("Check me!", isOn: $checked)
|
||||||
Toggle(isOn: Binding(get: { true }, set: { _ in })) {
|
Toggle(isOn: Binding(get: { true }, set: { _ in })) {
|
||||||
Text("I’m always checked!").foregroundColor(.red).italic()
|
Group { Text("I’m always checked!").italic() }.foregroundColor(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,7 @@ var links: [NavItem] {
|
||||||
.zIndex(1)
|
.zIndex(1)
|
||||||
Text("I'm on top")
|
Text("I'm on top")
|
||||||
}.padding(20)),
|
}.padding(20)),
|
||||||
|
NavItem("ButtonStyle", destination: ButtonStyleDemo()),
|
||||||
NavItem("ForEach", destination: ForEachDemo()),
|
NavItem("ForEach", destination: ForEachDemo()),
|
||||||
NavItem("Text", destination: TextDemo()),
|
NavItem("Text", destination: TextDemo()),
|
||||||
NavItem("Toggle", destination: ToggleDemo()),
|
NavItem("Toggle", destination: ToggleDemo()),
|
||||||
|
|
Loading…
Reference in New Issue