From 3accb8a3c6b12bc176d08192fd8a2646f73df97c Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 21 Sep 2025 21:45:49 +0800 Subject: [PATCH 1/7] Add ToggleState --- .../View/Toggle/ToggleState.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Sources/OpenSwiftUICore/View/Toggle/ToggleState.swift diff --git a/Sources/OpenSwiftUICore/View/Toggle/ToggleState.swift b/Sources/OpenSwiftUICore/View/Toggle/ToggleState.swift new file mode 100644 index 000000000..7853adaba --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Toggle/ToggleState.swift @@ -0,0 +1,49 @@ +// +// ToggleState.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package enum ToggleState: UInt { + case on + case off + case mixed + + package init(_ isOn: Bool) { + self = isOn ? .on : .off + } + + package mutating func toggle() { + self = self == .on ? .off : .on + } + + package static func stateFor( + item: T, + in collection: C + ) -> ToggleState where T: Equatable, C: Collection, C.Element == Binding { + if collection.allSatisfy({ item == $0.wrappedValue }) { + return .on + } else if collection.allSatisfy({ item != $0.wrappedValue }) { + return .off + } else { + return .mixed + } + } +} + +extension ToggleState: Codable {} + +extension ToggleState: CaseIterable {} + +extension ToggleState: StronglyHashable {} + +extension ToggleState: CustomDebugStringConvertible { + package var debugDescription: String { + switch self { + case .on: "on" + case .off: "off" + case .mixed: "mixed" + } + } +} From 2ff9bf9ad9015e4c9ff9eedefc1315d4d482ceeb Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 21 Sep 2025 23:57:43 +0800 Subject: [PATCH 2/7] Add TintColor API --- .../Graphic/Color/AccentColor.swift | 10 + Sources/OpenSwiftUICore/Shape/Tint.swift | 177 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Graphic/Color/AccentColor.swift create mode 100644 Sources/OpenSwiftUICore/Shape/Tint.swift diff --git a/Sources/OpenSwiftUICore/Graphic/Color/AccentColor.swift b/Sources/OpenSwiftUICore/Graphic/Color/AccentColor.swift new file mode 100644 index 000000000..4b61c447f --- /dev/null +++ b/Sources/OpenSwiftUICore/Graphic/Color/AccentColor.swift @@ -0,0 +1,10 @@ +// +// AccentColor.swift +// OpenSwiftUICore +// +// ID: AA5C9AAB6528C7C6B599DF55246DE53A (SwiftUICore) + +extension Color { + // FIXME + static var accent: Color { .clear } +} diff --git a/Sources/OpenSwiftUICore/Shape/Tint.swift b/Sources/OpenSwiftUICore/Shape/Tint.swift new file mode 100644 index 000000000..abf018e77 --- /dev/null +++ b/Sources/OpenSwiftUICore/Shape/Tint.swift @@ -0,0 +1,177 @@ +// +// TintShape.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: EB037BD7690CB8A700384AACA7B075E4 (SwiftUICore) + +// MARK: - View + tint ShapeStyle + +@available(OpenSwiftUI_v4_0, *) +extension View { + /// Sets the tint within this view. + /// + /// Use this method to override the default accent color for this view with + /// a given styling. Unlike an app's accent color, which can be + /// overridden by user preference, tint is always respected and should + /// be used as a way to provide additional meaning to the control. + /// + /// Controls which are unable to style themselves using the given type of + /// `ShapeStyle` will try to approximate the styling as best as they can + /// (i.e. controls which can not style themselves using a gradient will + /// attempt to use the color of the gradient's first stop). + /// + /// This example shows a linear gauge tinted with a + /// gradient from ``ShapeStyle/blue`` to ``ShapeStyle/red``. + /// + /// struct ControlTint: View { + /// var body: some View { + /// Gauge(value: 75, in: 0...100) { + /// Text("Temperature") + /// } + /// .gaugeStyle(.linearCapacity) + /// .tint(Gradient(colors: [.blue, .orange, .red])) + /// } + /// } + /// + /// Some controls adapt to the tint color differently based on their style, + /// the current platform, and the surrounding context. For example, in + /// macOS, a button with the ``PrimitiveButtonStyle/bordered`` style doesn't + /// tint its background, but one with the + /// ``PrimitiveButtonStyle/borderedProminent`` style does. In macOS, neither + /// of these button styles tint their label, but they do in other platforms. + /// + /// - Parameter tint: The tint to apply. + @inlinable + nonisolated public func tint(_ tint: S?) -> some View where S: ShapeStyle { + environment(\.tint, tint.map(AnyShapeStyle.init)) + } +} + +// MARK: - TintPlacement + +@_spi(Private) +@available(OpenSwiftUI_v6_0, *) +public struct TintPlacement: Hashable { + @available(macOS, unavailable) + @available(tvOS, unavailable) + public static var switchThumb: TintPlacement { + .init(guts: .switchThumb) + } + + enum Guts { + case switchThumb + } + + let guts: Guts +} + +@_spi(Private) +@available(*, unavailable) +extension TintPlacement: Sendable {} + +// MARK: - View + tintPlacement + +@_spi(Private) +@available(OpenSwiftUI_v6_0, *) +extension View { + @inlinable + nonisolated public func tint(_ tint: S?, for placement: TintPlacement) -> some View where S: ShapeStyle { + transformEnvironment(\.placementTint) { value in + if let tint { + value[placement] = .init(tint) + } + } + } +} + +// MARK: - View + tint Color + +@available(OpenSwiftUI_v3_0, *) +extension View { + /// Sets the tint color within this view. + /// + /// Use this method to override the default accent color for this view. + /// Unlike an app's accent color, which can be overridden by user + /// preference, the tint color is always respected and should be used as a + /// way to provide additional meaning to the control. + /// + /// This example shows Answer and Decline buttons with ``ShapeStyle/green`` + /// and ``ShapeStyle/red`` tint colors, respectively. + /// + /// struct ControlTint: View { + /// var body: some View { + /// HStack { + /// Button { + /// // Answer the call + /// } label: { + /// Label("Answer", systemImage: "phone") + /// } + /// .tint(.green) + /// Button { + /// // Decline the call + /// } label: { + /// Label("Decline", systemImage: "phone.down") + /// } + /// .tint(.red) + /// } + /// .buttonStyle(.borderedProminent) + /// .padding() + /// } + /// } + /// + /// Some controls adapt to the tint color differently based on their style, + /// the current platform, and the surrounding context. For example, in + /// macOS, a button with the ``PrimitiveButtonStyle/bordered`` style doesn't + /// tint its background, but one with the + /// ``PrimitiveButtonStyle/borderedProminent`` style does. In macOS, neither + /// of these button styles tint their label, but they do in other platforms. + /// + /// - Parameter tint: The tint ``Color`` to apply. + @inlinable + @_disfavoredOverload + nonisolated public func tint(_ tint: Color?) -> some View { + environment(\.tintColor, tint) + } +} + +// MARK: - EnvironmentValues + tint + +private struct TintKey: EnvironmentKey { + package static var defaultValue: AnyShapeStyle? { nil } +} + +private struct PlacementTintKey: EnvironmentKey { + package static var defaultValue: [TintPlacement: AnyShapeStyle] { [:] } +} + +extension EnvironmentValues { + @usableFromInline + package var tint: AnyShapeStyle? { + get { self[TintKey.self] } + set { self[TintKey.self] = newValue } + } + + @_spi(Private) + @usableFromInline + package var placementTint: [TintPlacement: AnyShapeStyle] { + get { self[PlacementTintKey.self] } + set { self[PlacementTintKey.self] = newValue } + } +} + +@available(OpenSwiftUI_v3_0, *) +extension EnvironmentValues { + @usableFromInline + package var tintColor: Color? { + get { tint?.fallbackColor(in: self) } + set { tint = newValue.map { AnyShapeStyle($0) } } + } + + package var resolvedTintColor: Color.Resolved { + (tintColor ?? .accent).resolve(in: self) + } +} + +// MARK: - TintShapeStyle [TODO] From 5bcd636d9b73b65bc04e278a7fd88db0256269ee Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 21 Sep 2025 23:58:05 +0800 Subject: [PATCH 3/7] Add ToggleStyle --- .../Platform/PlatformViewCoordinator.swift | 16 +- Sources/OpenSwiftUI/View/Toggle/Switch.swift | 83 ---- .../View/Toggle/SwitchToggleStyle.swift | 218 +++++++++ .../OpenSwiftUI/View/Toggle/ToggleStyle.swift | 463 ++++++++++++++++++ 4 files changed, 694 insertions(+), 86 deletions(-) delete mode 100644 Sources/OpenSwiftUI/View/Toggle/Switch.swift create mode 100644 Sources/OpenSwiftUI/View/Toggle/SwitchToggleStyle.swift create mode 100644 Sources/OpenSwiftUI/View/Toggle/ToggleStyle.swift diff --git a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewCoordinator.swift b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewCoordinator.swift index 251fdaaf2..1034dfad4 100644 --- a/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewCoordinator.swift +++ b/Sources/OpenSwiftUI/Integration/Representable/Platform/PlatformViewCoordinator.swift @@ -13,6 +13,16 @@ import OpenSwiftUICore #if canImport(Darwin) @objc #endif -class PlatformViewCoordinator: NSObject {} - -// TODO: weakDispatchUpdate +class PlatformViewCoordinator: NSObject { + var weakDispatchUpdate: (() -> Void) -> Void { + { [weak self] update in + guard let self else { + update() + return + } + Update.dispatchImmediately { // FIXME: reason: nil + update() + } + } + } +} diff --git a/Sources/OpenSwiftUI/View/Toggle/Switch.swift b/Sources/OpenSwiftUI/View/Toggle/Switch.swift deleted file mode 100644 index 8477e928b..000000000 --- a/Sources/OpenSwiftUI/View/Toggle/Switch.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Switch.swift -// OpenSwiftUI -// -// Audited for iOS 15.5 -// Status: Blocked by Color -// ID: 1246D37251EA3A918B392E2B95F8B7EF - -#if os(iOS) || os(visionOS) -import UIKit - -private struct Switch: UIViewRepresentable { - typealias UIViewType = UISwitch - typealias Coordinator = PlatformSwitchCoordinator - - @Binding var isOn: Bool - var tint: Color? - - func makeUIView(context: Context) -> UISwitch { - let view = UISwitch() - view.addTarget( - context.coordinator, - action: #selector(PlatformSwitchCoordinator.isOnChanged), - for: .valueChanged - ) - return view - } - - func updateUIView(_ uiView: UISwitch, context: Context) { - let isOn = isOn - let animated: Bool - if let _ = context.transaction.animation, !context.transaction.disablesAnimations { - animated = true - } else { - animated = false - } - uiView.setOn(isOn, animated: animated) - uiView.preferredStyle = .sliding - - let color: UIColor? - if let _ = tint { - // TODO: Resolve the color from the environment - color = nil - } else { - color = nil - } - let onTintColor = uiView.onTintColor - if let color { - if onTintColor == nil || color != onTintColor { - uiView.onTintColor = color - } - } else { - if onTintColor != nil { - uiView.onTintColor = nil - } - } - context.coordinator._isOn = _isOn - } - - func makeCoordinator() -> Coordinator { - PlatformSwitchCoordinator(isOn: _isOn) - } - -} - -private class PlatformSwitchCoordinator: PlatformViewCoordinator { - var _isOn: Binding - - init(isOn: Binding) { - _isOn = isOn - super.init() - } - - @objc - func isOnChanged(_ sender: UISwitch) { - Update.dispatchImmediately { - _isOn.wrappedValue = sender.isOn - } - sender.setOn(_isOn.wrappedValue, animated: true) - } -} - -#endif diff --git a/Sources/OpenSwiftUI/View/Toggle/SwitchToggleStyle.swift b/Sources/OpenSwiftUI/View/Toggle/SwitchToggleStyle.swift new file mode 100644 index 000000000..d5a500bb7 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Toggle/SwitchToggleStyle.swift @@ -0,0 +1,218 @@ +// +// SwitchToggleStyle.swift +// OpenSwiftUI +// +// Audited for iOS 6.5.4 +// Status: WIP +// ID: 1246D37251EA3A918B392E2B95F8B7EF (SwiftUI) + +import OpenSwiftUICore + +// MARK: - SwitchToggleStyle [WIP] + +extension ToggleStyle where Self == SwitchToggleStyle { + @_alwaysEmitIntoClient + @MainActor + @preconcurrency + public static var `switch`: SwitchToggleStyle { + .init() + } +} + +/// A toggle style that displays a leading label and a trailing switch. +/// +/// Use the ``ToggleStyle/switch`` static variable to create this style: +/// +/// Toggle("Enhance Sound", isOn: $isEnhanced) +/// .toggleStyle(.switch) +/// +@available(OpenSwiftUI_v1_0, *) +public struct SwitchToggleStyle: ToggleStyle { + @Environment(\.controlSize) + private var controlSize: ControlSize + +// @Environment(\.tintColor) +// private var controlTint: Color? + +// @Environment(\.placementTint) +// private var placementTint: [TintPlacement: AnyShapeStyle] + + @Environment(\.effectiveFont) + private var font: Font + + let tint: Color? + + /// Creates a switch toggle style. + /// + /// Don't call this initializer directly. Instead, use the + /// ``ToggleStyle/switch`` static variable to create this style: + /// + /// Toggle("Enhance Sound", isOn: $isEnhanced) + /// .toggleStyle(.switch) + /// + public init() { + tint = nil + } + + /// Creates a switch style with a tint color. + @available(OpenSwiftUI_v2_0, *) + @available(*, deprecated, message: "Use ``View/tint(_)`` instead.") + @available(tvOS, unavailable) + public init(tint: Color) { + self.tint = tint + } + + public func makeBody(configuration: Configuration) -> some View { + #if canImport(Darwin) + Switch(isOn: configuration.$isOn, tint: tint) + .fixedSize() + // .contentShape(Capsule()) + // .accessibilityLabel + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif + } +} + +@available(*, unavailable) +extension SwitchToggleStyle: Sendable {} + +#if os(iOS) || os(visionOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif + +// MARK: - Switch [WIP] + +#if os(iOS) || os(visionOS) +private struct Switch: UIViewRepresentable { + typealias UIViewType = UISwitch + + typealias Coordinator = PlatformSwitchCoordinator + + @Binding var isOn: Bool + + var tint: Color? + + func makeUIView(context: Context) -> UISwitch { + let view = UISwitch() + view.addTarget( + context.coordinator, + action: #selector(PlatformSwitchCoordinator.isOnChanged), + for: .valueChanged + ) + return view + } + + func updateUIView(_ uiView: UISwitch, context: Context) { + let isOn = isOn + let animated: Bool + if let _ = context.transaction.animation, !context.transaction.disablesAnimations { + animated = true + } else { + animated = false + } + uiView.setOn(isOn, animated: animated) + uiView.preferredStyle = .sliding + + let color: UIColor? + if let _ = tint { + // TODO: Resolve the color from the environment + color = nil + } else { + color = nil + } + let onTintColor = uiView.onTintColor + if let color { + if onTintColor == nil || color != onTintColor { + uiView.onTintColor = color + } + } else { + if onTintColor != nil { + uiView.onTintColor = nil + } + } + context.coordinator._isOn = $isOn + } + func makeCoordinator() -> Coordinator { + PlatformSwitchCoordinator(isOn: $isOn) + } +} +#elseif os(macOS) +private struct Switch: NSViewRepresentable { + typealias NSViewType = NSSwitch + + typealias Coordinator = PlatformSwitchCoordinator + + @Binding var isOn: Bool + + var tint: Color? + + func makeNSView(context: Context) -> NSSwitch { + let view = NSSwitch() + return view + } + + func updateNSView(_ nsView: NSSwitch, context: Context) { + + } + + func makeCoordinator() -> Coordinator { + PlatformSwitchCoordinator(isOn: $isOn) + } +} +#endif + + +// MARK: - PlatformSwitchCoordinator + +#if os(iOS) || os(visionOS) +private class PlatformSwitchCoordinator: PlatformViewCoordinator { + var _isOn: Binding + + var isOn: Bool { + get { _isOn.wrappedValue } + set { _isOn.wrappedValue = newValue } + } + + init(isOn: Binding) { + _isOn = isOn + super.init() + } + + @objc + func isOnChanged(_ sender: UISwitch) { + weakDispatchUpdate { + isOn = sender.isOn + } + sender.setOn(isOn, animated: !_isOn.transaction.disablesAnimations) + } +} +#else +private class PlatformSwitchCoordinator: PlatformViewCoordinator { + var _isOn: Binding + + var isOn: Bool { + get { _isOn.wrappedValue } + set { _isOn.wrappedValue = newValue } + } + + init(isOn: Binding) { + _isOn = isOn + super.init() + } + + @objc + func isOnChanged(_ sender: NSSwitch) { + weakDispatchUpdate { + isOn = sender.state == .on + } + if _isOn.transaction.disablesAnimations { + sender.state = isOn ? .on : .off + } else { + sender.animator().state = isOn ? .on : .off + } + } +} +#endif diff --git a/Sources/OpenSwiftUI/View/Toggle/ToggleStyle.swift b/Sources/OpenSwiftUI/View/Toggle/ToggleStyle.swift new file mode 100644 index 000000000..237ba0387 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Toggle/ToggleStyle.swift @@ -0,0 +1,463 @@ +// +// ToggleStyle.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Blocked by ArchivedView +// ID: FB08626C1326F7E32DC674FF8C676196 (SwiftUI) + +@_spi(ForOpenSwiftUIOnly) +public import OpenSwiftUICore + +// MARK: - ToggleStyle + +/// The appearance and behavior of a toggle. +/// +/// To configure the style for a single ``Toggle`` or for all toggle instances +/// in a view hierarchy, use the ``View/toggleStyle(_:)`` modifier. You can +/// specify one of the built-in toggle styles, like ``ToggleStyle/switch`` or +/// ``ToggleStyle/button``: +/// +/// Toggle(isOn: $isFlagged) { +/// Label("Flag", systemImage: "flag.fill") +/// } +/// .toggleStyle(.button) +/// +/// Alternatively, you can create and apply a custom style. +/// +/// ### Custom styles +/// +/// To create a custom style, declare a type that conforms to the `ToggleStyle` +/// protocol and implement the required ``ToggleStyle/makeBody(configuration:)`` +/// method. For example, you can define a checklist toggle style: +/// +/// struct ChecklistToggleStyle: ToggleStyle { +/// func makeBody(configuration: Configuration) -> some View { +/// // Return a view that has checklist appearance and behavior. +/// } +/// } +/// +/// Inside the method, use the `configuration` parameter, which is an instance +/// of the ``ToggleStyleConfiguration`` structure, to get the label and +/// a binding to the toggle state. To see examples of how to use these items +/// to construct a view that has the appearance and behavior of a toggle, see +/// ``ToggleStyle/makeBody(configuration:)``. +/// +/// To provide easy access to the new style, declare a corresponding static +/// variable in an extension to `ToggleStyle`: +/// +/// extension ToggleStyle where Self == ChecklistToggleStyle { +/// static var checklist: ChecklistToggleStyle { .init() } +/// } +/// +/// You can then use your custom style: +/// +/// Toggle(activity.name, isOn: $activity.isComplete) +/// .toggleStyle(.checklist) +/// +/// A type conforming to this protocol inherits `@preconcurrency @MainActor` +/// isolation from the protocol if the conformance is included in the type's +/// base declaration: +/// +/// struct MyCustomType: Transition { +/// // `@preconcurrency @MainActor` isolation by default +/// } +/// +/// Isolation to the main actor is the default, but it's not required. Declare +/// the conformance in an extension to opt out of main actor isolation: +/// +/// extension MyCustomType: Transition { +/// // `nonisolated` by default +/// } +/// +@available(OpenSwiftUI_v1_0, *) +@MainActor +@preconcurrency +public protocol ToggleStyle { + + /// A view that represents the appearance and interaction of a toggle. + /// + /// OpenSwiftUI infers this type automatically based on the ``View`` + /// instance that you return from your implementation of the + /// ``makeBody(configuration:)`` method. + associatedtype Body: View + + /// Creates a view that represents the body of a toggle. + /// + /// Implement this method when you define a custom toggle style that + /// conforms to the ``ToggleStyle`` protocol. Use the `configuration` + /// input --- a ``ToggleStyleConfiguration`` instance --- to access the + /// toggle's label and state. Return a view that has the appearance and + /// behavior of a toggle. For example you can create a toggle that displays + /// a label and a circle that's either empty or filled with a checkmark: + /// + /// struct ChecklistToggleStyle: ToggleStyle { + /// func makeBody(configuration: Configuration) -> some View { + /// Button { + /// configuration.isOn.toggle() + /// } label: { + /// HStack { + /// Image(systemName: configuration.isOn + /// ? "checkmark.circle.fill" + /// : "circle") + /// configuration.label + /// } + /// } + /// .tint(.primary) + /// .buttonStyle(.borderless) + /// } + /// } + /// + /// The `ChecklistToggleStyle` toggle style provides a way to both observe + /// and modify the toggle state: the circle fills for the on state, and + /// users can tap or click the toggle to change the state. By using a + /// customized ``Button`` to compose the toggle's body, OpenSwiftUI + /// automatically provides the behaviors that users expect from a + /// control that has button-like characteristics. + /// + /// You can present a collection of toggles that use this style in a stack: + /// + /// ![A screenshot of three items stacked vertically. All have a circle + /// followed by a label. The first has the label Walk the dog, and the + /// circle is filled. The second has the label Buy groceries, and the + /// circle is filled. The third has the label Call Mom, and the cirlce is + /// empty.](ToggleStyle-makeBody-1-iOS) + /// + /// When updating a view hierarchy, the system calls your implementation + /// of the `makeBody(configuration:)` method for each ``Toggle`` instance + /// that uses the associated style. + /// + /// ### Modify the current style + /// + /// Rather than create an entirely new style, you can alternatively + /// modify a toggle's current style. Use the ``Toggle/init(_:)`` + /// initializer inside the `makeBody(configuration:)` method to create + /// and modify a toggle based on a `configuration` value. For example, + /// you can create a style that adds padding and a red border to the + /// current style: + /// + /// struct RedBorderToggleStyle: ToggleStyle { + /// func makeBody(configuration: Configuration) -> some View { + /// Toggle(configuration) + /// .padding() + /// .border(.red) + /// } + /// } + /// + /// If you create a `redBorder` static variable from this style, + /// you can apply the style to toggles that already use another style, like + /// the built-in ``ToggleStyle/switch`` and ``ToggleStyle/button`` styles: + /// + /// Toggle("Switch", isOn: $isSwitchOn) + /// .toggleStyle(.redBorder) + /// .toggleStyle(.switch) + /// + /// Toggle("Button", isOn: $isButtonOn) + /// .toggleStyle(.redBorder) + /// .toggleStyle(.button) + /// + /// Both toggles appear with the usual styling, each with a red border: + /// + /// ![A screenshot of a switch toggle with a red border, and a button + /// toggle with a red border.](ToggleStyle-makeBody-2-iOS) + /// + /// Apply the custom style closer to the toggle than the + /// modified style because OpenSwiftUI evaluates style view modifiers in order + /// from outermost to innermost. If you apply the styles in the other + /// order, the red border style doesn't have an effect, because the + /// built-in styles override it completely. + /// + /// - Parameter configuration: The properties of the toggle, including a + /// label and a binding to the toggle's state. + /// - Returns: A view that has behavior and appearance that enables it + /// to function as a ``Toggle``. + @ViewBuilder + func makeBody(configuration: Configuration) -> Self.Body + + /// The properties of a toggle instance. + /// + /// You receive a `configuration` parameter of this type --- which is an + /// alias for the ``ToggleStyleConfiguration`` type --- when you implement + /// the required ``makeBody(configuration:)`` method in a custom toggle + /// style implementation. + typealias Configuration = ToggleStyleConfiguration +} + +// MARK: - ToggleStyleConfiguration [WIP] + +/// The properties of a toggle instance. +/// +/// When you define a custom toggle style by creating a type that conforms to +/// the ``ToggleStyle`` protocol, you implement the +/// ``ToggleStyle/makeBody(configuration:)`` method. That method takes a +/// `ToggleStyleConfiguration` input that has the information you need +/// to define the behavior and appearance of a ``Toggle``. +/// +/// The configuration structure's ``label-swift.property`` reflects the +/// toggle's content, which might be the value that you supply to the +/// `label` parameter of the ``Toggle/init(isOn:label:)`` initializer. +/// Alternatively, it could be another view that OpenSwiftUI builds from an +/// initializer that takes a string input, like ``Toggle/init(_:isOn:)``. +/// In either case, incorporate the label into the toggle's view to help +/// the user understand what the toggle does. For example, the built-in +/// ``ToggleStyle/switch`` style horizontally stacks the label with the +/// control element. +/// +/// The structure's ``isOn`` property provides a ``Binding`` to the state +/// of the toggle. Adjust the appearance of the toggle based on this value. +/// For example, the built-in ``ToggleStyle/button`` style fills the button's +/// background when the property is `true`, but leaves the background empty +/// when the property is `false`. Change the value when the user performs +/// an action that's meant to change the toggle, like the button does when +/// tapped or clicked by the user. +public struct ToggleStyleConfiguration { + /// A type-erased label of a toggle. + /// + /// OpenSwiftUI provides a value of this type --- which is a ``View`` type --- + /// as the ``label-swift.property`` to your custom toggle style + /// implementation. Use the label to help define the appearance of the + /// toggle. + public struct Label: ViewAlias { + nonisolated init() {} + } + + /// A view that describes the effect of switching the toggle between states. + /// + /// Use this value in your implementation of the + /// ``ToggleStyle/makeBody(configuration:)`` method when defining a custom + /// ``ToggleStyle``. Access it through the that method's `configuration` + /// parameter. + /// + /// Because the label is a ``View``, you can incorporate it into the + /// view hierarchy that you return from your style definition. For example, + /// you can combine the label with a circle image in an ``HStack``: + /// + /// HStack { + /// Image(systemName: configuration.isOn + /// ? "checkmark.circle.fill" + /// : "circle") + /// configuration.label + /// } + /// + public let label: ToggleStyleConfiguration.Label + + /// A binding to a state property that indicates whether the toggle is on. + /// + /// Because this value is a ``Binding``, you can both read and write it + /// in your implementation of the ``ToggleStyle/makeBody(configuration:)`` + /// method when defining a custom ``ToggleStyle``. Access it through + /// that method's `configuration` parameter. + /// + /// Read this value to set the appearance of the toggle. For example, you + /// can choose between empty and filled circles based on the `isOn` value: + /// + /// Image(systemName: configuration.isOn + /// ? "checkmark.circle.fill" + /// : "circle") + /// + /// Write this value when the user takes an action that's meant to change + /// the state of the toggle. For example, you can toggle it inside the + /// `action` closure of a ``Button`` instance: + /// + /// Button { + /// configuration.isOn.toggle() + /// } label: { + /// // Draw the toggle. + /// } + /// + @Binding + public var isOn: Bool + + @Binding + private var toggleState: ToggleState + + /// Whether the ``Toggle`` is currently in a mixed state. + /// + /// Use this property to determine whether the toggle style should render + /// a mixed state presentation. A mixed state corresponds to an underlying + /// collection with a mix of true and false Bindings. + /// To toggle the state, use the ``Bool.toggle()`` method on the ``isOn`` + /// binding. + /// + /// In the following example, a custom style uses the `isMixed` property + /// to render the correct toggle state using symbols: + /// + /// struct SymbolToggleStyle: ToggleStyle { + /// func makeBody(configuration: Configuration) -> some View { + /// Button { + /// configuration.isOn.toggle() + /// } label: { + /// Image( + /// systemName: configuration.isMixed + /// ? "minus.circle.fill" : configuration.isOn + /// ? "checkmark.circle.fill" : "circle.fill") + /// configuration.label + /// } + /// } + /// } + @available(OpenSwiftUI_v4_0, *) + public var isMixed: Bool + + enum Effect { + // case appIntent(AppIntentAction) + case binding + } + + var effect: Effect + + init( + label: ToggleStyleConfiguration.Label, + isOn: Binding, + toggleState: Binding, + isMixed: Bool, + effect: Effect + ) { + self.label = label + self._isOn = isOn + self._toggleState = toggleState + self.isMixed = isMixed + self.effect = effect + } +} + +@available(*, unavailable) +extension ToggleStyleConfiguration: Sendable {} + +@available(*, unavailable) +extension ToggleStyleConfiguration.Label: Sendable {} + +// MARK: - ToggleStateBool + +struct ToggleStateBool: Projection { + typealias Base = ToggleState + + typealias Projected = Bool + + func get(base: ToggleState) -> Bool { + base == .on + } + + func set(base: inout ToggleState, newValue: Bool) { + base = newValue ? .on : .off + } +} + +// MARK: - ResolvedToggleStyle + +struct ResolvedToggleStyle: StyleableView { + var configuration: ToggleStyleConfiguration + + static var defaultStyleModifier = ToggleStyleModifier(style: .switch) +} + +// MARK: - ToggleStyleModifier + +struct ToggleStyleModifier