diff --git a/Sources/TokamakCore/Styles/ButtonStyle.swift b/Sources/TokamakCore/Styles/ButtonStyle.swift index 074f1fe05..489386698 100644 --- a/Sources/TokamakCore/Styles/ButtonStyle.swift +++ b/Sources/TokamakCore/Styles/ButtonStyle.swift @@ -14,88 +14,35 @@ // // Created by Gene Z. Ragan on 07/22/2020. -public struct ButtonStyleConfiguration { - public struct Label: _PrimitiveView { - let content: AnyView - } - - public let label: Label - public let isPressed: Bool -} - -public struct DefaultButtonStyle: ButtonStyle { - public init() {} - public func makeBody(configuration: ButtonStyleConfiguration) -> some View { - configuration.label - } -} - -/// This is a helper type 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 - + @ViewBuilder 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(_ style: S) { - type = S.self - bodyClosure = { configuration in - AnyView(style.makeBody(configuration: configuration)) - } +public struct ButtonStyleConfiguration { + public struct Label: View { + public let body: AnyView } - public func makeBody(configuration: ButtonStyleConfiguration) -> AnyView { - bodyClosure(configuration) - } + public let role: ButtonRole? + public let label: ButtonStyleConfiguration.Label + public let isPressed: Bool } -public enum _ButtonStyleKey: EnvironmentKey { - public static var defaultValue: _AnyButtonStyle { - _AnyButtonStyle(DefaultButtonStyle()) - } -} +struct AnyButtonStyle: ButtonStyle { + let bodyClosure: (ButtonStyleConfiguration) -> AnyView + let type: Any.Type -extension EnvironmentValues { - var buttonStyle: _AnyButtonStyle { - get { - self[_ButtonStyleKey.self] - } - set { - self[_ButtonStyleKey.self] = newValue + init(_ style: S) { + type = S.self + bodyClosure = { + AnyView(style.makeBody(configuration: $0)) } } -} -public extension View { - func buttonStyle(_ style: S) -> some View where S: ButtonStyle { - environment(\.buttonStyle, _AnyButtonStyle(style)) + func makeBody(configuration: Configuration) -> some View { + bodyClosure(configuration) } } diff --git a/Sources/TokamakCore/Styles/ControlGroupStyle.swift b/Sources/TokamakCore/Styles/ControlGroupStyle.swift new file mode 100644 index 000000000..7b772cd43 --- /dev/null +++ b/Sources/TokamakCore/Styles/ControlGroupStyle.swift @@ -0,0 +1,104 @@ +// 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 Carson Katri on 7/12/21. +// + +import Foundation + +public protocol ControlGroupStyle { + associatedtype Body: View + @ViewBuilder + func makeBody(configuration: Self.Configuration) -> Self.Body + typealias Configuration = ControlGroupStyleConfiguration +} + +public struct ControlGroupStyleConfiguration { + public struct Content: View { + public let body: AnyView + } + + public let content: ControlGroupStyleConfiguration.Content +} + +public struct AutomaticControlGroupStyle: ControlGroupStyle { + public init() {} + + public func makeBody(configuration: Self.Configuration) -> some View { + Picker( + selection: .constant(AnyHashable?.none), + label: EmptyView() + ) { + if let parentView = configuration.content.body.view as? ParentView { + ForEach(Array(parentView.children.enumerated()), id: \.offset) { + $0.element + } + } else { + configuration.content + } + } + .pickerStyle(SegmentedPickerStyle()) + } +} + +public struct NavigationControlGroupStyle: ControlGroupStyle { + public init() {} + + public func makeBody(configuration: Self.Configuration) -> some View { + HStack { + configuration.content + } + } +} + +public struct _AnyControlGroupStyle: ControlGroupStyle { + public typealias Body = AnyView + + private let bodyClosure: (ControlGroupStyleConfiguration) -> AnyView + public let type: Any.Type + + public init(_ style: S) { + type = S.self + bodyClosure = { configuration in + AnyView(style.makeBody(configuration: configuration)) + } + } + + public func makeBody(configuration: ControlGroupStyleConfiguration) -> AnyView { + bodyClosure(configuration) + } +} + +extension EnvironmentValues { + private enum ControlGroupStyleKey: EnvironmentKey { + static let defaultValue = _AnyControlGroupStyle(AutomaticControlGroupStyle()) + } + + var controlGroupStyle: _AnyControlGroupStyle { + get { + self[ControlGroupStyleKey.self] + } + set { + self[ControlGroupStyleKey.self] = newValue + } + } +} + +public extension View { + func controlGroupStyle( + _ style: S + ) -> some View where S: ControlGroupStyle { + environment(\.controlGroupStyle, .init(style)) + } +} diff --git a/Sources/TokamakCore/Styles/PrimitiveButtonStyle.swift b/Sources/TokamakCore/Styles/PrimitiveButtonStyle.swift new file mode 100644 index 000000000..3c7bdaac7 --- /dev/null +++ b/Sources/TokamakCore/Styles/PrimitiveButtonStyle.swift @@ -0,0 +1,133 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Created by Carson Katri on 7/12/21. +// + +public protocol PrimitiveButtonStyle { + associatedtype Body: View + @ViewBuilder + func makeBody(configuration: Self.Configuration) -> Self.Body + typealias Configuration = PrimitiveButtonStyleConfiguration +} + +public struct PrimitiveButtonStyleConfiguration { + public struct Label: View { + public let body: AnyView + } + + public let role: ButtonRole? + public let label: PrimitiveButtonStyleConfiguration.Label + + let action: () -> () + public func trigger() { action() } +} + +public struct DefaultButtonStyle: PrimitiveButtonStyle { + public init() {} + + public func makeBody(configuration: Configuration) -> some View { + BorderedButtonStyle().makeBody(configuration: configuration) + } +} + +public struct PlainButtonStyle: ButtonStyle { + public init() {} + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundColor(configuration.isPressed ? .secondary : .primary) + } +} + +public struct BorderedButtonStyle: PrimitiveButtonStyle { + public init() {} + + public func makeBody(configuration: Configuration) -> some View { + _PrimitiveButtonStyleBody(style: self, configuration: configuration) { + configuration.label + } + } +} + +public struct BorderlessButtonStyle: ButtonStyle { + public init() {} + + public func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundColor(configuration.isPressed ? .primary : .secondary) + } +} + +public struct LinkButtonStyle: ButtonStyle { + public init() {} + + public func makeBody(configuration: Configuration) -> some View { + configuration.label.body + .foregroundColor( + configuration + .isPressed ? Color(red: 128 / 255, green: 192 / 255, blue: 240 / 255) : .blue + ) + } +} + +struct AnyPrimitiveButtonStyle: PrimitiveButtonStyle { + let bodyClosure: (PrimitiveButtonStyleConfiguration) -> AnyView + let type: Any.Type + + init(_ style: S) { + type = S.self + bodyClosure = { + AnyView(style.makeBody(configuration: $0)) + } + } + + func makeBody(configuration: Self.Configuration) -> AnyView { + bodyClosure(configuration) + } +} + +extension EnvironmentValues { + enum ButtonStyleKey: EnvironmentKey { + enum ButtonStyleKeyValue { + case primitiveButtonStyle(AnyPrimitiveButtonStyle) + case buttonStyle(AnyButtonStyle) + } + + public static let defaultValue: ButtonStyleKeyValue = .primitiveButtonStyle( + .init(DefaultButtonStyle()) + ) + } + + var buttonStyle: ButtonStyleKey.ButtonStyleKeyValue { + get { + self[ButtonStyleKey.self] + } + set { + self[ButtonStyleKey.self] = newValue + } + } +} + +public extension View { + func buttonStyle( + _ style: S + ) -> some View where S: PrimitiveButtonStyle { + environment(\.buttonStyle, .primitiveButtonStyle(.init(style))) + } + + func buttonStyle(_ style: S) -> some View where S: ButtonStyle { + environment(\.buttonStyle, .buttonStyle(.init(style))) + } +} diff --git a/Sources/TokamakDOM/Styles/ButtonStyle.swift b/Sources/TokamakCore/Tokens/Controls/ButtonRole.swift similarity index 63% rename from Sources/TokamakDOM/Styles/ButtonStyle.swift rename to Sources/TokamakCore/Tokens/Controls/ButtonRole.swift index 025a50655..2b197420a 100644 --- a/Sources/TokamakDOM/Styles/ButtonStyle.swift +++ b/Sources/TokamakCore/Tokens/Controls/ButtonRole.swift @@ -1,4 +1,4 @@ -// Copyright 2020 Tokamak contributors +// 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. @@ -12,13 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// Created by Gene Z. Ragan on 07/22/2020. +// Created by Carson Katri on 7/12/21. // -import TokamakCore +public struct ButtonRole: Equatable { + public static let destructive = ButtonRole(rawValue: 0) + public static let cancel = ButtonRole(rawValue: 1) -extension ButtonStyleConfiguration.Label: DOMPrimitive { - var renderedBody: AnyView { - _ButtonStyleConfigurationProxy.Label(self).content + private let rawValue: Int + private init(rawValue: Int) { + self.rawValue = rawValue } } diff --git a/Sources/TokamakCore/Tokens/Controls/ControlSize.swift b/Sources/TokamakCore/Tokens/Controls/ControlSize.swift new file mode 100644 index 000000000..c31560a03 --- /dev/null +++ b/Sources/TokamakCore/Tokens/Controls/ControlSize.swift @@ -0,0 +1,47 @@ +// 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 Carson Katri on 7/12/21. +// + +public enum ControlSize: CaseIterable, Hashable { + case mini + case small + case regular + case large +} + +extension EnvironmentValues { + private enum ControlSizeKey: EnvironmentKey { + static var defaultValue: ControlSize = .regular + } + + public var controlSize: ControlSize { + get { + self[ControlSizeKey.self] + } + set { + self[ControlSizeKey.self] = newValue + } + } +} + +public extension View { + @inlinable + func controlSize( + _ controlSize: ControlSize + ) -> some View { + environment(\.controlSize, controlSize) + } +} diff --git a/Sources/TokamakCore/Tokens/Controls/Prominence.swift b/Sources/TokamakCore/Tokens/Controls/Prominence.swift new file mode 100644 index 000000000..3889e89fb --- /dev/null +++ b/Sources/TokamakCore/Tokens/Controls/Prominence.swift @@ -0,0 +1,59 @@ +// 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 Carson Katri on 7/12/21. +// + +public enum Prominence: Hashable { + case standard + case increased +} + +extension EnvironmentValues { + private enum ControlProminenceKey: EnvironmentKey { + static var defaultValue: Prominence = .standard + } + + public var controlProminence: Prominence { + get { + self[ControlProminenceKey.self] + } + set { + self[ControlProminenceKey.self] = newValue + } + } + + private enum HeaderProminenceKey: EnvironmentKey { + static var defaultValue: Prominence = .standard + } + + public var headerProminence: Prominence { + get { + self[HeaderProminenceKey.self] + } + set { + self[HeaderProminenceKey.self] = newValue + } + } +} + +public extension View { + func controlProminence(_ prominence: Prominence) -> some View { + environment(\.controlProminence, prominence) + } + + func headerProminence(_ prominence: Prominence) -> some View { + environment(\.headerProminence, prominence) + } +} diff --git a/Sources/TokamakCore/Views/Buttons/Button.swift b/Sources/TokamakCore/Views/Buttons/Button.swift deleted file mode 100644 index d00d46219..000000000 --- a/Sources/TokamakCore/Views/Buttons/Button.swift +++ /dev/null @@ -1,76 +0,0 @@ -// 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 Max Desiatov on 02/12/2018. -// - -/// A control that performs an action when triggered. -/// -/// Available when `Label` conforms to `View`. -/// -/// A button is created using a `Label`; the `action` initializer argument (a method or closure) -/// is to be called on click. -/// -/// @State private var counter: Int = 0 -/// var body: some View { -/// Button(action: { counter += 1 }) { -/// Text("\(counter)") -/// } -/// } -/// -/// When your label is `Text`, you can create the button by directly passing a `String`: -/// -/// @State private var counter: Int = 0 -/// var body: some View { -/// Button("\(counter)", action: { counter += 1 }) -/// } -public struct Button