From c63074015dbee8353672094b563b3eca973af633 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 6 Oct 2026 22:26:37 +0800 Subject: [PATCH 01/33] feat(button): update button style API and configuration --- .../View/Control/Button/ButtonStyle.swift | 162 ++++++++++++++++++ .../Button/ButtonStyle/ButtonStyle.swift | 15 -- .../ButtonStyleConfiguration.swift | 30 ---- .../ButtonStyle/ButtonStyleConvertible.swift | 12 -- 4 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonStyle.swift delete mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyle.swift delete mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConfiguration.swift delete mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConvertible.swift diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle.swift new file mode 100644 index 000000000..5efa9209e --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle.swift @@ -0,0 +1,162 @@ +// +// ButtonStyle.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: AEEDD090E917AC57C12008D974DC6805 (SwiftUI) + +import OpenAttributeGraphShims +public import OpenSwiftUICore + +// MARK: - ButtonStyle + +/// A type that applies standard interaction behavior and a custom appearance to +/// all buttons within a view hierarchy. +/// +/// To configure the current button style for a view hierarchy, use the +/// ``View/buttonStyle(_:)`` modifier. Specify a style that conforms to +/// `ButtonStyle` when creating a button that uses the standard button +/// interaction behavior defined for each platform. To create a button with +/// custom interaction behavior, use ``PrimitiveButtonStyle`` instead. +@available(OpenSwiftUI_v1_0, *) +@preconcurrency +@MainActor +public protocol ButtonStyle { + /// A view that represents the body of a button. + associatedtype Body: View + + /// Creates a view that represents the body of a button. + /// + /// The system calls this method for each ``Button`` instance in a view + /// hierarchy where this style is the current button style. + /// + /// - Parameter configuration: The properties of the button. + @ViewBuilder + func makeBody(configuration: Configuration) -> Self.Body + + /// The properties of a button. + typealias Configuration = ButtonStyleConfiguration +} + +/// The properties of a button. +@available(OpenSwiftUI_v1_0, *) +public struct ButtonStyleConfiguration { + /// A type-erased label of a button. + public struct Label: ViewAlias { + public typealias Body = Never + + nonisolated package init() { + _openSwiftUIEmptyStub() + } + } + + /// An optional semantic role that describes the button's purpose. + /// + /// A value of `nil` means that the Button doesn't have an assigned role. If + /// the button does have a role, use it to make adjustments to the button's + /// appearance. The following example shows a custom style that uses + /// bold text when the role is ``ButtonRole/cancel``, + /// ``ShapeStyle/red`` text when the role is ``ButtonRole/destructive``, + /// and adds no special styling otherwise: + /// + /// struct MyButtonStyle: ButtonStyle { + /// func makeBody(configuration: Configuration) -> some View { + /// configuration.label + /// .font( + /// configuration.role == .cancel ? .title2.bold() : .title2) + /// .foregroundColor( + /// configuration.role == .destructive ? Color.red : nil) + /// } + /// } + /// + /// You can create one of each button using this style to see the effect: + /// + /// VStack(spacing: 20) { + /// Button("Cancel", role: .cancel) {} + /// Button("Delete", role: .destructive) {} + /// Button("Continue") {} + /// } + /// .buttonStyle(MyButtonStyle()) + /// + /// ![A screenshot of three buttons stacked vertically. The first says + /// Cancel in black, bold letters. The second says Delete in red, regular + /// weight letters. The third says Continue in black, regular weight + /// letters.](ButtonStyleConfiguration-role-1) + @available(OpenSwiftUI_v3_0, *) + public let role: ButtonRole? + + /// A view that describes the effect of pressing the button. + public let label: Label + + /// A Boolean that indicates whether the user is currently pressing the + /// button. + public let isPressed: Bool + + init(isPressed: Bool, role: ButtonRole?) { + self.label = Label() + self.isPressed = isPressed + self.role = role + } +} + +@available(*, unavailable) +extension ButtonStyleConfiguration: Sendable {} + +@available(*, unavailable) +extension ButtonStyleConfiguration.Label: Sendable {} + +// MARK: - View + ButtonStyle + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Sets the style for buttons within this view to a button style with a + /// custom appearance and standard interaction behavior. + /// + /// Use this modifier to set a specific style for all button instances + /// within a view: + /// + /// HStack { + /// Button("Sign In", action: signIn) + /// Button("Register", action: register) + /// } + /// .buttonStyle(.bordered) + /// + /// You can also use this modifier to set the style for controls that + /// acquire a button style through composition, like the + /// ``Menu`` and + /// ``Toggle`` views in the following example: + /// + /// VStack { + /// Menu("Terms and Conditions") { + /// Button("Open in Preview", action: openInPreview) + /// Button("Save as PDF", action: saveAsPDF) + /// } + /// Toggle("Remember Password", isOn: $isToggleOn) + /// Toggle("Flag", isOn: $flagged) + /// Button("Sign In", action: signIn) + /// } + /// .menuStyle(.button) + /// .toggleStyle(.button) + /// .buttonStyle(.bordered) + /// + /// The + /// ``View/menuStyle(_:)`` + /// modifier causes the Terms and Conditions menu to render as a button. + /// Similarly, the + /// ``View/toggleStyle(_:)`` modifier causes the two toggles to + /// render as buttons. The button style modifier then causes not only + /// the explicit Sign In ``Button``, but also the menu and toggles with + /// button styling, to render with the bordered button style. + nonisolated public func buttonStyle(_ style: S) -> some View where S: ButtonStyle { + modifier(ButtonStyleContainerModifier(style: style)) + } +} + +@_spi(UIFrameworks) +@available(OpenSwiftUI_v4_0, *) +public protocol ButtonStyleConvertible { + associatedtype ButtonStyleRepresentation: ButtonStyle + + var buttonStyleRepresentation: ButtonStyleRepresentation { get } +} diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyle.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyle.swift deleted file mode 100644 index e3dd576a3..000000000 --- a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyle.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ButtonStyle.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -public protocol ButtonStyle { - associatedtype Body: View - - @ViewBuilder - func makeBody(configuration: Configuration) -> Self.Body - - typealias Configuration = ButtonStyleConfiguration -} diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConfiguration.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConfiguration.swift deleted file mode 100644 index cb9b2ae66..000000000 --- a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConfiguration.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// ButtonStyleConfiguration.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -public struct ButtonStyleConfiguration { - public struct Label: ViewAlias { - public typealias Body = Never - - package init() {} - } - - // 0x0 - 2 Byte - public let role: ButtonRole? - - // 0 Bit - public let label: Label - - // 0x2 - 1 Bit - public let isPressed: Bool - - @inline(__always) - init(isPressed: Bool, role: ButtonRole?) { - self.label = Label() - self.isPressed = isPressed - self.role = role - } -} diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConvertible.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConvertible.swift deleted file mode 100644 index 08335a9b0..000000000 --- a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonStyleConvertible.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ButtonStyleConvertible.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -internal protocol ButtonStyleConvertible { - associatedtype ButtonStyleRepresentation: ButtonStyle - - var buttonStyleRepresentation: ButtonStyleRepresentation { get } -} From b4948dcc66e099372423cfc33c2d166c92e75827 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 6 Oct 2026 22:45:22 +0800 Subject: [PATCH 02/33] feat(button): update button role API --- .../View/Control/Button/ButtonRole.swift | 54 +++++++++++++++++++ .../Button/ButtonStyle/ButtonRole.swift | 17 ------ 2 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonRole.swift delete mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonRole.swift diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonRole.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonRole.swift new file mode 100644 index 000000000..f22f06fec --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/Button/ButtonRole.swift @@ -0,0 +1,54 @@ +// +// ButtonRole.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +/// A value that describes the purpose of a button. +/// +/// A button role provides a description of a button's purpose. For example, +/// the ``ButtonRole/destructive`` role indicates that a button performs +/// a destructive action, like delete user data: +/// +/// Button("Delete", role: .destructive) { delete() } +/// +@available(OpenSwiftUI_v3_0, *) +public struct ButtonRole: Equatable, Sendable { + /// A role that indicates a destructive button. + /// + /// Use this role for a button that deletes user data, or performs an + /// irreversible operation. A destructive button signals by its appearance + /// that the user should carefully consider whether to tap or click the + /// button. For example, OpenSwiftUI presents a destructive button that you add + /// with the ``View/swipeActions(edge:allowsFullSwipe:content:)`` + /// modifier using a red background: + /// + /// List { + /// ForEach(items) { item in + /// Text(item.title) + /// .swipeActions { + /// Button(role: .destructive) { delete() } label: { + /// Label("Delete", systemImage: "trash") + /// } + /// } + /// } + /// } + /// .navigationTitle("Shopping List") + /// + /// ![A screenshot of a list of three items, where the second item is + /// shifted to the left, and the row displays a red button with a trash + /// icon on the right side.](ButtonRole-destructive-1) + public static let destructive: ButtonRole = .init(rawValue: 1 << 0) + + /// A role that indicates a button that cancels an operation. + /// + /// Use this role for a button that cancels the current operation. + public static let cancel: ButtonRole = .init(rawValue: 1 << 2) + + public static func == (a: ButtonRole, b: ButtonRole) -> Bool { + a.rawValue == b.rawValue + } + + private var rawValue: UInt8 +} diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonRole.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonRole.swift deleted file mode 100644 index 415980a21..000000000 --- a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/ButtonRole.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ButtonRole.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -public struct ButtonRole: Equatable, Sendable { - public static let destructive: ButtonRole = .init(rawValue: 1) - public static let cancel: ButtonRole = .init(rawValue: 4) - - public static func == (a: ButtonRole, b: ButtonRole) -> Bool { - a.rawValue == b.rawValue - } - - var rawValue: UInt8 -} From bd5384b664d7e00f131fbe2951dd82a62f542973 Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 6 Oct 2026 23:03:27 +0800 Subject: [PATCH 03/33] feat(button): update primitive button style API --- .../ButtonStyle/PrimitiveButtonStyle.swift | 15 - .../PrimitiveButtonStyleConfiguration.swift | 35 --- .../Control/Button/PrimitiveButtonStyle.swift | 283 ++++++++++++++++++ .../View/Navigation/NavigationLink.swift | 14 + 4 files changed, 297 insertions(+), 50 deletions(-) delete mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyle.swift delete mode 100644 Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyleConfiguration.swift create mode 100644 Sources/OpenSwiftUI/View/Control/Button/PrimitiveButtonStyle.swift create mode 100644 Sources/OpenSwiftUI/View/Navigation/NavigationLink.swift diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyle.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyle.swift deleted file mode 100644 index 774866ead..000000000 --- a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyle.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// PrimitiveButtonStyle.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -public protocol PrimitiveButtonStyle { - associatedtype Body: View - - @ViewBuilder - func makeBody(configuration: Configuration) -> Self.Body - - typealias Configuration = PrimitiveButtonStyleConfiguration -} diff --git a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyleConfiguration.swift b/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyleConfiguration.swift deleted file mode 100644 index 2c1c7c38f..000000000 --- a/Sources/OpenSwiftUI/View/Control/Button/ButtonStyle/PrimitiveButtonStyleConfiguration.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// PrimitiveButtonStyleConfiguration.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -public struct PrimitiveButtonStyleConfiguration { - public struct Label: ViewAlias { - public typealias Body = Never - - package init() {} - } - - // 0x0 - 2 Byte - public let role: ButtonRole? - - // 0 Bit - public let label: Label - - public func trigger() { - action() - } - - // 0x8 - @inline(__always) - let action: () -> Void - - @inline(__always) - init(role: ButtonRole?, action: @escaping () -> Void) { - self.label = Label() - self.action = action - self.role = role - } -} diff --git a/Sources/OpenSwiftUI/View/Control/Button/PrimitiveButtonStyle.swift b/Sources/OpenSwiftUI/View/Control/Button/PrimitiveButtonStyle.swift new file mode 100644 index 000000000..b9936ada3 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/Button/PrimitiveButtonStyle.swift @@ -0,0 +1,283 @@ +// +// PrimitiveButtonStyle.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete (Blocked by Navigation and Menu) +// ID: 722D0A4BA446408AC7D14833D23ECE10 (SwiftUI) + +public import OpenSwiftUICore + +// MARK: - PrimitiveButtonStyle + +/// A type that applies custom interaction behavior and a custom appearance to +/// all buttons within a view hierarchy. +/// +/// To configure the current button style for a view hierarchy, use the +/// ``View/buttonStyle(_:)`` modifier. Specify a style that conforms to +/// `PrimitiveButtonStyle` to create a button with custom interaction +/// behavior. To create a button with the standard button interaction behavior +/// defined for each platform, use ``ButtonStyle`` instead. +/// +/// 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, *) +@preconcurrency +@MainActor +public protocol PrimitiveButtonStyle { + /// A view that represents the body of a button. + associatedtype Body: View + + /// Creates a view that represents the body of a button. + /// + /// The system calls this method for each ``Button`` instance in a view + /// hierarchy where this style is the current button style. + /// + /// - Parameter configuration: The properties of the button. + @ViewBuilder + func makeBody(configuration: Configuration) -> Self.Body + + /// The properties of a button. + typealias Configuration = PrimitiveButtonStyleConfiguration +} + +/// The properties of a button. +@available(OpenSwiftUI_v1_0, *) +public struct PrimitiveButtonStyleConfiguration { + /// A type-erased label of a button. + public struct Label: ViewAlias { + public typealias Body = Never + + nonisolated package init() { + _openSwiftUIEmptyStub() + } + } + + /// An optional semantic role describing the button's purpose. + /// + /// A value of `nil` means that the Button has no assigned role. If the + /// button does have a role, use it to make adjustments to the button's + /// appearance. The following example shows a custom style that uses + /// bold text when the role is ``ButtonRole/cancel``, + /// ``ShapeStyle/red`` text when the role is ``ButtonRole/destructive``, + /// and adds no special styling otherwise: + /// + /// struct MyButtonStyle: PrimitiveButtonStyle { + /// func makeBody(configuration: Configuration) -> some View { + /// configuration.label + /// .onTapGesture { + /// configuration.trigger() + /// } + /// .font( + /// configuration.role == .cancel ? .title2.bold() : .title2) + /// .foregroundColor( + /// configuration.role == .destructive ? Color.red : nil) + /// } + /// } + /// + /// You can create one of each button using this style to see the effect: + /// + /// VStack(spacing: 20) { + /// Button("Cancel", role: .cancel) {} + /// Button("Delete", role: .destructive) {} + /// Button("Continue") {} + /// } + /// .buttonStyle(MyButtonStyle()) + /// + /// ![A screenshot of three buttons stacked vertically. The first says + /// Cancel in black, bold letters. The second says Delete in red, regular + /// weight letters. The third says Continue in black, regular weight + /// letters.](PrimitiveButtonStyleConfiguration-role-1) + @available(OpenSwiftUI_v3_0, *) + public let role: ButtonRole? + + /// A view that describes the effect of calling the button's action. + public let label: Label + + /// Performs the button's action. + public func trigger() { + action() + } + + let action: ButtonAction + + init(role: ButtonRole?, action: ButtonAction) { + self.label = Label() + self.action = action + self.role = role + } +} + +@available(*, unavailable) +extension PrimitiveButtonStyleConfiguration: Sendable {} + +@available(*, unavailable) +extension PrimitiveButtonStyleConfiguration.Label: Sendable {} + +// MARK: - View + PrimitiveButtonStyle + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Sets the style for buttons within this view to a button style with a + /// custom appearance and custom interaction behavior. + /// + /// Use this modifier to set a specific style for button instances + /// within a view: + /// + /// HStack { + /// Button("Sign In", action: signIn) + /// Button("Register", action: register) + /// } + /// .buttonStyle(.bordered) + /// + nonisolated public func buttonStyle(_ style: S) -> some View where S: PrimitiveButtonStyle { + modifier(PrimitiveButtonStyleContainerModifier(style: style)) + } +} + +// MARK: - ResolvedButtonStyle [Blocked by Navigation] + +struct ResolvedButtonStyle: StyleableView { + var configuration: PrimitiveButtonStyleConfiguration + + static var defaultStyleModifier: ButtonStyleModifier { + ButtonStyleModifier(style: DefaultButtonStyle()) + } + + var body: some View { + Button(configuration) + // TODO: Apply ShowsNavigationIndicatorDisclosureIndicatorModifier. + .transformPlatformItemList(LayoutPlatformItemListFlags.self) { [role = configuration.role] list in + for index in list.items.indices { + list.items[index].buttonRole = role + } + } + // TODO: Apply AccessibilityButtonModifier, including the app-intent action. + .accessibilityShowsLargeContentViewer(.placeholder) { + configuration.label + } + .modifier( + KeyboardShortcutBindingBehavior( + action: configuration.trigger, + label: configuration.label + ) + .requiring(NavigationButtonInput.Inverted.self) + ) + // TODO: Apply DefinesSearchCompletionModifier in .textInputSuggestions. + .input(TextSelectionForbidden.self) + // TODO: Apply ButtonStyleDebugOverlayModifier when VisualizeViewsEnabled. + } +} + +// MARK: - ButtonStylePredicate + +struct ButtonStylePredicate