From 0f9c4bea22af280bcde1c7e64e26df0ae9021d0f Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 02:20:03 +0800 Subject: [PATCH 01/15] feat: add Section view APIs --- .../OpenSwiftUI/View/Section/Section.swift | 292 ++++++++++++++++++ 1 file changed, 292 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/Section/Section.swift diff --git a/Sources/OpenSwiftUI/View/Section/Section.swift b/Sources/OpenSwiftUI/View/Section/Section.swift new file mode 100644 index 000000000..853b4eb40 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Section/Section.swift @@ -0,0 +1,292 @@ +// +// Section.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 323999C1FFD4730F4D14ABD69CA73C8C (SwiftUI) + +public import OpenSwiftUICore + +// MARK: - Section + +/// A container view that you can use to add hierarchy within certain views. +/// +/// Use `Section` instances in views like ``List``, ``Picker``, and +/// ``Form`` to organize content into separate sections. Each section has +/// custom content that you provide on a per-instance basis. You can also +/// provide headers and footers for each section. +/// +/// ### Collapsible sections +/// +/// Create sections that expand and collapse by using an initializer that +/// accepts an `isExpanded` binding. A collapsible section in a ``List`` that +/// uses the ``ListStyle/sidebar`` style shows a disclosure indicator next to +/// the section's header. Tapping on the disclosure indicator toggles the +/// appearance of the section's content. +/// +/// > Note: Not all contexts provide a default control to trigger collapse or +/// expansion. +@available(OpenSwiftUI_v1_0, *) +public struct Section { + var header: Parent + var content: Content + var footer: Footer + var isExpanded: Binding? + + init(header: Parent, content: Content, footer: Footer) { + self.header = header + self.content = content + self.footer = footer + isExpanded = nil + } +} + +@available(*, unavailable) +extension Section: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension Section: View, PrimitiveView, PubliclyPrimitiveView where Parent: View, Content: View, Footer: View { + public typealias Body = Never + + var internalBody: some View { + ResolvedSectionStyle(configuration: .init(isExpanded: isExpanded)) + .viewAlias(SectionStyleConfiguration.Header.self) { + header.accessibilityConfiguration( + AccessibilityTraitsModifier(traits: .init(trait: .isHeader)) + ) + } + .viewAlias(SectionStyleConfiguration.Footer.self) { + footer + } + .viewAlias(SectionStyleConfiguration.RawContent.self) { + content._trait(SectionActionsTraitKey.self, nil) + } + } + + @available(OpenSwiftUI_v2_0, *) + nonisolated public static func _viewListCount(inputs: _ViewListCountInputs) -> Int? { + defaultViewListCount(inputs: inputs) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Section where Parent: View, Content: View, Footer: View { + /// Creates a section with a header, footer, and the provided section + /// content. + /// + /// - Parameters: + /// - content: The section's content. + /// - header: A view to use as the section's header. + /// - footer: A view to use as the section's footer. + @_alwaysEmitIntoClient + public init( + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Parent, + @ViewBuilder footer: () -> Footer + ) { + self.init(header: header(), footer: footer(), content: content) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Section where Parent == EmptyView, Content: View, Footer: View { + /// Creates a section with a footer and the provided section content. + /// - Parameters: + /// - content: The section's content. + /// - footer: A view to use as the section's footer. + @_alwaysEmitIntoClient + public init(@ViewBuilder content: () -> Content, @ViewBuilder footer: () -> Footer) { + self.init(footer: footer(), content: content) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Section where Parent: View, Content: View, Footer == EmptyView { + /// Creates a section with a header and the provided section content. + /// - Parameters: + /// - content: The section's content. + /// - header: A view to use as the section's header. + @_alwaysEmitIntoClient + public init(@ViewBuilder content: () -> Content, @ViewBuilder header: () -> Parent) { + self.init(header: header(), content: content) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Section where Parent == EmptyView, Content: View, Footer == EmptyView { + /// Creates a section with the provided section content. + /// - Parameters: + /// - content: The section's content. + public init(@ViewBuilder content: () -> Content) { + self.init(header: EmptyView(), content: content(), footer: EmptyView()) + } +} + +@available(OpenSwiftUI_v3_0, *) +extension Section where Parent == Text, Content: View, Footer == EmptyView { + /// Creates a section with the provided section content. + /// - Parameters: + /// - titleKey: The key for the section's localized title, which describes + /// the contents of the section. + /// - content: The section's content. + public init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> Content) { + self.init(header: Text(titleKey), content: content(), footer: EmptyView()) + } + + /// Creates a section with the provided section content. + /// - Parameters: + /// - title: A string that describes the contents of the section. + /// - content: The section's content. + @_disfavoredOverload + public init(_ title: S, @ViewBuilder content: () -> Content) where S: StringProtocol { + self.init(header: Text(title), content: content(), footer: EmptyView()) + } +} + +@available(OpenSwiftUI_v5_0, *) +extension Section where Parent == Text, Content: View, Footer == EmptyView { + /// Creates a section with the provided section content. + /// - Parameters: + /// - titleKey: The key for the section's localized title, which describes + /// the contents of the section. + /// - isExpanded: A binding to a Boolean value that determines the section's + /// expansion state (expanded or collapsed). + /// - content: The section's content. + public init( + _ titleKey: LocalizedStringKey, + isExpanded: Binding, + @ViewBuilder content: () -> Content + ) { + self.init(isExpanded: isExpanded, content: content) { + Text(titleKey) + } + } + + /// Creates a section with the provided section content. + /// - Parameters: + /// - title: A string that describes the contents of the section. + /// - isExpanded: A binding to a Boolean value that determines the section's + /// expansion state (expanded or collapsed). + /// - content: The section's content. + @_disfavoredOverload + public init( + _ title: S, + isExpanded: Binding, + @ViewBuilder content: () -> Content + ) where S: StringProtocol { + self.init(isExpanded: isExpanded, content: content) { + Text(title) + } + } +} + +@_spi(Private) +@available(OpenSwiftUI_iOS_v4_0, *) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +extension Section where Parent: View, Content: View, Footer: View { + /// Sets whether a `Section` can be collapsed by the user. + /// + /// Currently this modifier only applies to sections in `.sidebar`-styled + /// `List` views. + public func collapsible(_ collapsible: Bool) -> some View { + environment(\.canCollapseSections, collapsible) + } +} + +// MARK: - IsCollapsibleTraitKey + +@available(OpenSwiftUI_v1_0, *) +@usableFromInline +struct IsCollapsibleTraitKey: _ViewTraitKey { + @inlinable + static var defaultValue: Bool { true } +} + +@available(*, unavailable) +extension IsCollapsibleTraitKey: Sendable {} + +@available(OpenSwiftUI_v5_0, *) +extension Section where Parent: View, Content: View, Footer == EmptyView { + /// Creates a section with a header, the provided section content, and a binding + /// representing the section's expansion state. + /// + /// - Parameters: + /// - isExpanded: A binding to a Boolean value that determines the section's + /// expansion state (expanded or collapsed). + /// - content: The section's content. + /// - header: A view to use as the section's header. + public init( + isExpanded: Binding, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Parent + ) { + self.content = content() + self.header = header() + footer = EmptyView() + self.isExpanded = isExpanded + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Section where Parent: View, Content: View, Footer: View { + /// Creates a section with a header, footer, and the provided section content. + /// - Parameters: + /// - header: A view to use as the section's header. + /// - footer: A view to use as the section's footer. + /// - content: The section's content. + @available(iOS, deprecated: 100000.0, renamed: "Section(content:header:footer:)") + @available(macOS, deprecated: 100000.0, renamed: "Section(content:header:footer:)") + @available(tvOS, deprecated: 100000.0, renamed: "Section(content:header:footer:)") + @available(watchOS, deprecated: 100000.0, renamed: "Section(content:header:footer:)") + @available(visionOS, deprecated: 100000.0, renamed: "Section(content:header:footer:)") + public init(header: Parent, footer: Footer, @ViewBuilder content: () -> Content) { + self.init(header: header, content: content(), footer: footer) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Section where Parent == EmptyView, Content: View, Footer: View { + /// Creates a section with a footer and the provided section content. + /// - Parameters: + /// - footer: A view to use as the section's footer. + /// - content: The section's content. + @available(iOS, deprecated: 100000.0, renamed: "Section(content:footer:)") + @available(macOS, deprecated: 100000.0, renamed: "Section(content:footer:)") + @available(tvOS, deprecated: 100000.0, renamed: "Section(content:footer:)") + @available(watchOS, deprecated: 100000.0, renamed: "Section(content:footer:)") + @available(visionOS, deprecated: 100000.0, renamed: "Section(content:footer:)") + public init(footer: Footer, @ViewBuilder content: () -> Content) { + self.init(header: EmptyView(), content: content(), footer: footer) + } +} + +@available(OpenSwiftUI_v1_0, *) +extension Section where Parent: View, Content: View, Footer == EmptyView { + /// Creates a section with a header and the provided section content. + /// - Parameters: + /// - header: A view to use as the section's header. + /// - content: The section's content. + @available(iOS, deprecated: 100000.0, renamed: "Section(content:header:)") + @available(macOS, deprecated: 100000.0, renamed: "Section(content:header:)") + @available(tvOS, deprecated: 100000.0, renamed: "Section(content:header:)") + @available(watchOS, deprecated: 100000.0, renamed: "Section(content:header:)") + @available(visionOS, deprecated: 100000.0, renamed: "Section(content:header:)") + public init(header: Parent, @ViewBuilder content: () -> Content) { + self.init(header: header, content: content(), footer: EmptyView()) + } +} + +// MARK: - EnvironmentValues + canCollapseSections + +extension EnvironmentValues { + var canCollapseSections: Bool { + get { self[CollapsibleSectionsEnvironmentKey.self] } + set { self[CollapsibleSectionsEnvironmentKey.self] = newValue } + } +} + +private struct CollapsibleSectionsEnvironmentKey: EnvironmentKey { + static var defaultValue: Bool { true } +} From df668d66a89bcfda97a8ca0ff6b529403da867d1 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 02:45:40 +0800 Subject: [PATCH 02/15] feat: add Section style support --- .../View/Section/SectionStyle.swift | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/Section/SectionStyle.swift diff --git a/Sources/OpenSwiftUI/View/Section/SectionStyle.swift b/Sources/OpenSwiftUI/View/Section/SectionStyle.swift new file mode 100644 index 000000000..7a5b49b53 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Section/SectionStyle.swift @@ -0,0 +1,76 @@ +// +// SectionStyle.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +import OpenSwiftUICore + +// MARK: - SectionStyle + +protocol SectionStyle { + associatedtype Body: View + + @ViewBuilder + func makeBody(configuration: Configuration) -> Body + + typealias Configuration = SectionStyleConfiguration +} + +extension View { + nonisolated func sectionStyle(_ style: S) -> some View where S: SectionStyle { + modifier(SectionStyleModifier(style: style)) + } + + nonisolated func sectionStyle(_ style: S, in context: C) -> some View where S: SectionStyle, C: StyleContext { + modifier(SectionStyleModifier(style: style).requiring(C.self)) + } +} + +// MARK: - SectionStyleConfiguration + +struct SectionStyleConfiguration { + struct Header: ViewAlias {} + struct Footer: ViewAlias {} + struct Actions: ViewAlias {} + struct RawContent: ViewAlias {} + + let header: Header = .init() + let footer: Footer = .init() + var actions: Actions = .init() + let rawContent: RawContent = .init() + var isExpanded: Binding? + + @ViewBuilder + var content: some View { + if isExpanded?.wrappedValue ?? true { + rawContent + } + } +} + +// MARK: - ResolvedSectionStyle + +struct ResolvedSectionStyle: StyleableView { + var configuration: SectionStyleConfiguration + + static var defaultStyleModifier: SectionStyleModifier { + .init(style: DefaultSectionStyle()) + } +} + +// MARK: - SectionStyleModifier + +struct SectionStyleModifier: StyleModifier where S: SectionStyle { + var style: S + + init(style: S) { + self.style = style + } + + func styleBody(configuration: SectionStyleConfiguration) -> some View { + style.makeBody(configuration: configuration) + ._trait(IsExpandedTraitKey.self, configuration.isExpanded) + } +} From 65e18ed9b2601ff4b7097482b314e17b4746dfd4 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 02:50:18 +0800 Subject: [PATCH 03/15] feat: add PubliclyPrimitiveView bridge --- .../View/PubliclyPrimitiveView.swift | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/PubliclyPrimitiveView.swift diff --git a/Sources/OpenSwiftUI/View/PubliclyPrimitiveView.swift b/Sources/OpenSwiftUI/View/PubliclyPrimitiveView.swift new file mode 100644 index 000000000..2a1c1c098 --- /dev/null +++ b/Sources/OpenSwiftUI/View/PubliclyPrimitiveView.swift @@ -0,0 +1,69 @@ +// +// PubliclyPrimitiveView.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 47701A835A1B253AFF3923CF965CEFD6 (SwiftUI) + +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore + +// MARK: - PubliclyPrimitiveView + +protocol PubliclyPrimitiveView: PrimitiveView { + associatedtype InternalBody: View + + @MainActor + @preconcurrency + var internalBody: InternalBody { get } +} + +extension PubliclyPrimitiveView { + nonisolated public static func _makeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + defaultMakeView(view: view, inputs: inputs) + } + + nonisolated static func defaultMakeView( + view: _GraphValue, + inputs: _ViewInputs + ) -> _ViewOutputs { + InternalBody._makeView(view: .init(MakeBody(view: view.value)), inputs: inputs) + } + + nonisolated public static func _makeViewList( + view: _GraphValue, + inputs: _ViewListInputs + ) -> _ViewListOutputs { + defaultMakeViewList(view: view, inputs: inputs) + } + + nonisolated static func defaultMakeViewList( + view: _GraphValue, + inputs: _ViewListInputs + ) -> _ViewListOutputs { + InternalBody._makeViewList(view: .init(MakeBody(view: view.value)), inputs: inputs) + } + + nonisolated public static func _viewListCount(inputs: _ViewListCountInputs) -> Int? { + defaultViewListCount(inputs: inputs) + } + + nonisolated static func defaultViewListCount(inputs: _ViewListCountInputs) -> Int? { + InternalBody._viewListCount(inputs: inputs) + } +} + +// MARK: - MakeBody + +private struct MakeBody: Rule where V: PubliclyPrimitiveView { + @Attribute var view: V + + var value: V.InternalBody { + view.internalBody + } +} From 9427277e3f8027a38d645a6f0a46816040d96ef0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 11:42:36 +0800 Subject: [PATCH 04/15] feat: add section expansion trait --- .../View/IsExpandedTraitKey.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Sources/OpenSwiftUICore/View/IsExpandedTraitKey.swift diff --git a/Sources/OpenSwiftUICore/View/IsExpandedTraitKey.swift b/Sources/OpenSwiftUICore/View/IsExpandedTraitKey.swift new file mode 100644 index 000000000..cb7bcad97 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/IsExpandedTraitKey.swift @@ -0,0 +1,16 @@ +// +// IsExpandedTraitKey.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +@available(OpenSwiftUI_v4_0, *) +@usableFromInline +package struct IsExpandedTraitKey: _ViewTraitKey { + @inlinable + package static var defaultValue: Binding? { nil } +} + +@available(*, unavailable) +extension IsExpandedTraitKey: Sendable {} From 206d290fdf26d93d43f99f76fe018ffa25b71d13 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 17:22:53 +0800 Subject: [PATCH 05/15] feat: add section actions and editing modifiers --- Sources/OpenSwiftUI/View/List/ListStyle.swift | 2 - Sources/OpenSwiftUI/View/List/OnDelete.swift | 97 +++++++++++++ Sources/OpenSwiftUI/View/List/OnMove.swift | 135 ++++++++++++++++++ .../View/Section/SectionActions.swift | 79 ++++++++++ 4 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 Sources/OpenSwiftUI/View/List/OnDelete.swift create mode 100644 Sources/OpenSwiftUI/View/List/OnMove.swift create mode 100644 Sources/OpenSwiftUI/View/Section/SectionActions.swift diff --git a/Sources/OpenSwiftUI/View/List/ListStyle.swift b/Sources/OpenSwiftUI/View/List/ListStyle.swift index 42eacdf0c..6a4044bdb 100644 --- a/Sources/OpenSwiftUI/View/List/ListStyle.swift +++ b/Sources/OpenSwiftUI/View/List/ListStyle.swift @@ -9,8 +9,6 @@ import OpenSwiftUICore struct AnyListStyleContext: StyleContext {} -struct SidebarSectionActionStyleContext: StyleContext {} - // TODO: ListStyleContext conformance and matching rules. struct PlainListStyleContext: StyleContext {} diff --git a/Sources/OpenSwiftUI/View/List/OnDelete.swift b/Sources/OpenSwiftUI/View/List/OnDelete.swift new file mode 100644 index 000000000..3f1ffc8f7 --- /dev/null +++ b/Sources/OpenSwiftUI/View/List/OnDelete.swift @@ -0,0 +1,97 @@ +// +// OnDelete.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +public import Foundation +public import OpenSwiftUICore + +@available(OpenSwiftUI_v1_0, *) +@usableFromInline +struct OnDeleteTraitKey: _ViewTraitKey { + @inlinable + static var defaultValue: ((IndexSet) -> Void)? { nil } + + @usableFromInline + typealias Value = ((IndexSet) -> Void)? +} + +@available(*, unavailable) +extension OnDeleteTraitKey: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension DynamicViewContent { + /// Sets the deletion action for the dynamic view. You must delete the + /// corresponding item within `action`, as it will be called after the + /// row has already been removed from the ``List``. + /// + /// - Parameter action: The action that you want OpenSwiftUI to perform when + /// elements in the view are deleted. OpenSwiftUI passes a set of indices to the + /// closure that's relative to the dynamic view's underlying collection of + /// data. + /// + /// - Returns: A view that calls `action` when elements are deleted from the + /// original view. + @inlinable + nonisolated public func onDelete(perform action: ((IndexSet) -> Void)?) -> some DynamicViewContent { + modifier(_TraitWritingModifier(value: action)) + } +} + +@available(OpenSwiftUI_v1_0, *) +@usableFromInline +struct IsDeleteDisabledTraitKey: _ViewTraitKey { + @inlinable + static var defaultValue: Bool { false } + + @usableFromInline + typealias Value = Bool +} + +@available(*, unavailable) +extension IsDeleteDisabledTraitKey: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Adds a condition for whether the view's view hierarchy is deletable. + @inlinable + nonisolated public func deleteDisabled(_ isDisabled: Bool) -> some View { + _trait(IsDeleteDisabledTraitKey.self, isDisabled) + } +} + +struct OnDeleteDefaultTraitKey: _ViewTraitKey { + static var defaultValue: ((IndexSet) -> Void)? { nil } +} + +struct DeleteInteraction { + var contentID: Int + var indexSet: IndexSet + var onDelete: ((IndexSet) -> Void)? + + func delete() { + guard let onDelete else { + preconditionFailure("Attempting to delete row (indices: \(indexSet) with no associated delete action.") + } + Update.enqueueAction(reason: nil) { + onDelete(indexSet) + } + } +} + +extension ViewTraitCollection { + var deleteInteraction: DeleteInteraction? { + guard let offset = self[DynamicViewContentOffsetTraitKey.self], + let contentID = self[DynamicViewContentIDTraitKey.self], + let onDelete = self[OnDeleteTraitKey.self] ?? self[OnDeleteDefaultTraitKey.self] else { + return nil + } + return DeleteInteraction( + contentID: contentID, + indexSet: IndexSet(integer: offset), + onDelete: self[IsDeleteDisabledTraitKey.self] ? nil : onDelete + ) + } +} diff --git a/Sources/OpenSwiftUI/View/List/OnMove.swift b/Sources/OpenSwiftUI/View/List/OnMove.swift new file mode 100644 index 000000000..f86f7fdc1 --- /dev/null +++ b/Sources/OpenSwiftUI/View/List/OnMove.swift @@ -0,0 +1,135 @@ +// +// OnMove.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +public import Foundation +public import OpenSwiftUICore + +@available(OpenSwiftUI_v1_0, *) +@usableFromInline +struct OnMoveTraitKey: _ViewTraitKey { + @inlinable + static var defaultValue: ((IndexSet, Int) -> Void)? { nil } + + @usableFromInline + typealias Value = ((IndexSet, Int) -> Void)? +} + +@available(*, unavailable) +extension OnMoveTraitKey: Sendable {} + +@available(OpenSwiftUI_v3_0, *) +@available(macOS, unavailable) +@available(watchOS, unavailable) +@available(tvOS, unavailable) +@usableFromInline +struct OnMoveClipBackgroundTraitKey: _ViewTraitKey { + @inlinable + static var defaultValue: Bool { false } + + @usableFromInline + typealias Value = Bool +} + +@available(*, unavailable) +extension OnMoveClipBackgroundTraitKey: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension DynamicViewContent { + /// Sets the move action for the dynamic view. + /// + /// - Parameters: + /// - action: A closure that OpenSwiftUI invokes when elements in the dynamic + /// view are moved. The closure takes two arguments that represent the + /// offset relative to the dynamic view's underlying collection of data. + /// Pass `nil` to disable the ability to move items. + /// + /// - Returns: A view that calls `action` when elements are moved within the + /// original view. + @inlinable + nonisolated public func onMove(perform action: ((IndexSet, Int) -> Void)?) -> some DynamicViewContent { + modifier(_TraitWritingModifier(value: action)) + } +} + +@_spi(Private) +@available(OpenSwiftUI_v3_0, *) +@available(macOS, unavailable) +@available(watchOS, unavailable) +@available(tvOS, unavailable) +extension DynamicViewContent { + /// Sets the move action and whether to clip its background. + /// + /// - Parameters: + /// - clipBackground: Whether to clip the background during a move. + /// - action: The action to perform when elements move, or `nil` to disable + /// moving. + /// - Returns: A view with the move action and background clipping setting. + @inlinable + nonisolated public func onMove( + clipBackground: Bool, + perform action: ((IndexSet, Int) -> Void)? + ) -> some DynamicViewContent { + modifier(_TraitWritingModifier(value: action)) + .modifier(_TraitWritingModifier(value: clipBackground)) + } +} + +@available(OpenSwiftUI_v1_0, *) +@usableFromInline +struct IsMoveDisabledTraitKey: _ViewTraitKey { + @inlinable + static var defaultValue: Bool { false } + + @usableFromInline + typealias Value = Bool +} + +@available(*, unavailable) +extension IsMoveDisabledTraitKey: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Adds a condition for whether the view's view hierarchy is movable. + @inlinable + nonisolated public func moveDisabled(_ isDisabled: Bool) -> some View { + _trait(IsMoveDisabledTraitKey.self, isDisabled) + } +} + +struct OnMoveDefaultTraitKey: _ViewTraitKey { + static var defaultValue: ((IndexSet, Int) -> Void)? { nil } +} + +struct MoveInteraction: CustomStringConvertible { + var onMove: ((IndexSet, Int) -> Void)? + var clipBackground: Bool + var sourceOffsets: IndexSet + var contentID: Int + + func move(to destination: Int) { + guard let onMove else { + preconditionFailure("Attempting to move (\(sourceOffsets) → \(destination)) with no associated move action.") + } + let destination = sourceOffsets.integerLessThan(destination) == nil ? destination : destination &+ 1 + Update.enqueueAction(reason: nil) { + onMove(sourceOffsets, destination) + } + } + + var description: String { + "MoveInteraction(source: \(sourceOffsets), contentID: \(contentID))" + } +} + +extension ViewTraitCollection { + var onMove: ((IndexSet, Int) -> Void)? { + guard !self[IsMoveDisabledTraitKey.self] else { + return nil + } + return self[OnMoveTraitKey.self] ?? self[OnMoveDefaultTraitKey.self] + } +} diff --git a/Sources/OpenSwiftUI/View/Section/SectionActions.swift b/Sources/OpenSwiftUI/View/Section/SectionActions.swift new file mode 100644 index 000000000..e114df27d --- /dev/null +++ b/Sources/OpenSwiftUI/View/Section/SectionActions.swift @@ -0,0 +1,79 @@ +// +// SectionActions.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +public import OpenSwiftUICore + +@available(OpenSwiftUI_v6_0, *) +@available(tvOS, unavailable) +@available(watchOS, unavailable) +extension View { + /// Adds custom actions to a section. + /// + /// On iOS, the actions are displayed as items after the content + /// of the section. On macOS, the actions are displayed + /// when a user hovers over the section. + /// + /// The following example adds an 'Add' button to + /// the 'Categories' section. + /// + /// List { + /// Label("Home", systemImage: "house") + /// Label("Alerts", systemImage: "bell") + /// + /// Section("Categories") { + /// Label("Climate", systemImage: "fan") + /// Label("Lights", systemImage: "lightbulb") + /// } + /// .sectionActions { + /// Button("Add Category", systemImage: "plus") { } + /// } + /// } + /// + nonisolated public func sectionActions(@ViewBuilder content: () -> Content) -> some View where Content: View { + let actions = content() + return viewAlias(SectionStyleConfiguration.Actions.self) { + actions + } + ._trait(SectionActionsTraitKey.self, AnyView(actions)) + } +} + +struct SectionActionsTraitKey: _ViewTraitKey { + static var defaultValue: AnyView? { nil } +} + +struct SidebarSectionActionButtonStyle: PrimitiveButtonStyle { + func makeBody(configuration: Configuration) -> some View { + Button(configuration) + #if os(macOS) + .buttonStyle(SidebarSectionActionButtonStyle_Mac()) + #else + .buttonStyle(PlatformItemListButtonStyle()) + #endif + } +} + +struct SidebarSectionActionStyleContext: StyleContext {} + +struct InlineActionsSectionStyle: SectionStyle { + func makeBody(configuration: Configuration) -> some View { + Section { + configuration.content + .sectionStyle(DefaultSectionStyle()) + configuration.actions + .moveDisabled(true) + .deleteDisabled(true) + } header: { + configuration.header + .sectionStyle(DefaultSectionStyle()) + } footer: { + configuration.footer + .sectionStyle(DefaultSectionStyle()) + } + .sectionStyle(PlainSectionStyle()) + } +} From c3da378ff1f62c098ac98ead619f9a1c486321c4 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 18:01:45 +0800 Subject: [PATCH 06/15] feat: add macOS sidebar section action button style --- .../SidebarSectionActionButtonStyle_Mac.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/Control/Button/SidebarSectionActionButtonStyle_Mac.swift diff --git a/Sources/OpenSwiftUI/View/Control/Button/SidebarSectionActionButtonStyle_Mac.swift b/Sources/OpenSwiftUI/View/Control/Button/SidebarSectionActionButtonStyle_Mac.swift new file mode 100644 index 000000000..9393a4e5d --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/Button/SidebarSectionActionButtonStyle_Mac.swift @@ -0,0 +1,27 @@ +// +// SidebarSectionActionButtonStyle_Mac.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#if os(macOS) +import OpenSwiftUICore + +struct SidebarSectionActionButtonStyle_Mac: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .frame(minWidth: 12, minHeight: 12) + .contentShape(Rectangle()) + .labelStyle(.iconOnly) + .defaultForegroundStyle( + configuration.isPressed + ? HierarchicalShapeStyle.secondary + : HierarchicalShapeStyle.tertiary + ) + .help { + configuration.label + } + } +} +#endif From ddf3f187fdb57917edfd9e65de298a516132409d Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 18:02:33 +0800 Subject: [PATCH 07/15] feat: apply sidebar section action button style --- .../OpenSwiftUI/View/Control/Button/DefaultButtonStyle.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUI/View/Control/Button/DefaultButtonStyle.swift b/Sources/OpenSwiftUI/View/Control/Button/DefaultButtonStyle.swift index 5d9113d84..95a34b074 100644 --- a/Sources/OpenSwiftUI/View/Control/Button/DefaultButtonStyle.swift +++ b/Sources/OpenSwiftUI/View/Control/Button/DefaultButtonStyle.swift @@ -68,7 +68,10 @@ public struct DefaultButtonStyle: PrimitiveButtonStyle { // TODO: macOS ToolbarButtonStyle in ToolbarStyleContext. // TODO: macOS SheetToolbarButtonStyle in SheetToolbarStyleContext. // TODO: macOS ListAccessoryBarButtonStyle in ListAccessoryBarStyleContext. - // TODO: SidebarSectionActionButtonStyle in SidebarSectionActionStyleContext. + .modifier( + PrimitiveButtonStyleContainerModifier(style: SidebarSectionActionButtonStyle()) + .requiring(SidebarSectionActionStyleContext.self) + ) .modifier( PrimitiveButtonStyleContainerModifier(style: PlatformItemListButtonStyle()) .requiring(AccessibilityQuickActionStyleContext.self) From 2503fbb99455cac17ba7708e25f5bd6fe7e4a973 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 19:26:48 +0800 Subject: [PATCH 08/15] feat: complete platform item strategies --- .../PlatformItems/PlatformItemsStrategy.swift | 95 ++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsStrategy.swift b/Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsStrategy.swift index b721a1e54..f198167c9 100644 --- a/Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsStrategy.swift +++ b/Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsStrategy.swift @@ -3,7 +3,7 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: WIP +// Status: Complete import OpenAttributeGraphShims import OpenSwiftUICore @@ -21,3 +21,96 @@ protocol PlatformItemsStrategy { static func makeInputs(_ inputs: inout _ViewInputs) static func makeContent(from outputs: _ViewOutputs) -> OptionalAttribute } + +// MARK: - MultiPlatformItemsStrategy + +protocol MultiPlatformItemsStrategy: PlatformItemsStrategy where Content == PlatformItems {} + +extension MultiPlatformItemsStrategy { + static var defaultValue: PlatformItems { .init(features: itemsFeatures) } + + static var itemsFeatures: PlatformItems.Features { .multiple } + + static func hasChanges(from oldContent: PlatformItems, to newContent: PlatformItems) -> Bool { + oldContent.seed.matches(newContent.seed) && oldContent.id == newContent.id + } + + static func makeInputs(_ inputs: inout _ViewInputs) { + inputs.preferences.requiresPlatformItems = true + inputs.requestedTextRepresentation = PlatformItemsTextRepresentable.self + inputs.requestedImageRepresentation = PlatformItemsImageRepresentable.self + inputs.platformItemsFeatures.formUnion(itemsFeatures) + inputs.platformItemFeatures.formUnion(itemFeatures) + inputs.requestsPlatformItems = true + } + + static func makeContent(from outputs: _ViewOutputs) -> OptionalAttribute { + .init(outputs.preferences.platformItems) + } +} + +// MARK: - UnaryPlatformItemsStrategy + +protocol UnaryPlatformItemsStrategy: PlatformItemsStrategy where Content == PlatformItem {} + +extension UnaryPlatformItemsStrategy { + static var defaultValue: PlatformItem { .init(id: .init(ids: [.invalid])) } + + static var itemsFeatures: PlatformItems.Features { [] } + + static func hasChanges(from oldContent: PlatformItem, to newContent: PlatformItem) -> Bool { + !oldContent.seed.matches(newContent.seed) || oldContent.id != newContent.id + } + + static func makeInputs(_ inputs: inout _ViewInputs) { + inputs.preferences.requiresPlatformItems = true + inputs.requestedTextRepresentation = PlatformItemsTextRepresentable.self + inputs.requestedImageRepresentation = PlatformItemsImageRepresentable.self + inputs.platformItemsFeatures.formUnion(itemsFeatures) + inputs.platformItemFeatures.formUnion(itemFeatures) + inputs.requestsPlatformItems = true + if itemFeatures.contains(.accessibility) { + inputs.preferences.add(AccessibilityAttachment.Key.self) + } + } + + static func makeContent(from outputs: _ViewOutputs) -> OptionalAttribute { + .init(outputs.platformItem) + } +} + +// MARK: - MultiButtonPlatformItemsStrategy + +struct MultiButtonPlatformItemsStrategy: MultiPlatformItemsStrategy { + static var itemFeatures: PlatformItem.Features { + [.text, .image, .selection, .secondaryText] + } +} + +// MARK: - LabelPlatformItemsStrategy + +struct LabelPlatformItemsStrategy: UnaryPlatformItemsStrategy { + static var itemFeatures: PlatformItem.Features { + [.text, .image, .secondaryText, .iconText, .accessibility] + } +} + +// MARK: - TextPlatformItemsStrategy + +struct TextPlatformItemsStrategy: UnaryPlatformItemsStrategy { + static var itemFeatures: PlatformItem.Features { .text } +} + +// MARK: - ButtonPlatformItemsStrategy + +struct ButtonPlatformItemsStrategy: UnaryPlatformItemsStrategy { + static var itemFeatures: PlatformItem.Features { + [.text, .image, .selection, .secondaryText] + } +} + +// MARK: - StyledTextPlatformItemsStrategy + +struct StyledTextPlatformItemsStrategy: UnaryPlatformItemsStrategy { + static var itemFeatures: PlatformItem.Features { [.text, .styledText] } +} From 3b764514b9a4360b815188ded8dc6b7117852437 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 20:33:30 +0800 Subject: [PATCH 09/15] feat: add platform item reader and trait writer --- .../PlatformItems/PlatformItemsReader.swift | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsReader.swift diff --git a/Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsReader.swift b/Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsReader.swift new file mode 100644 index 000000000..974b1c2fb --- /dev/null +++ b/Sources/OpenSwiftUI/Integration/PlatformItems/PlatformItemsReader.swift @@ -0,0 +1,136 @@ +// +// PlatformItemsReader.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 2D2DE8D65225CC97305A8AAD72170302 (SwiftUI) + +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore + +// MARK: - PlatformItemsReader + +struct PlatformItemsReader: PrimitiveView, UnaryView + where Strategy: PlatformItemsStrategy, Content: View, Source: View { + var content: (Strategy.Content) -> Content + var source: ZStack + + init( + for strategy: Strategy, + @ViewBuilder content: @escaping (Strategy.Content) -> Content, + @ViewBuilder source: () -> Source + ) { + self.content = content + self.source = ZStack(content: source) + } + + static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + let source = view[offset: { .of(&$0.source) }] + let hostKeys = inputs.intern(PreferenceKeys(), id: .defaultValue) + var sourceInputs = inputs.withoutGeometryDependencies + sourceInputs.preferences = PreferencesInputs(hostKeys: hostKeys) + Strategy.makeInputs(&sourceInputs) + let outputs = ZStack.makeDebuggableView( + view: source, + inputs: sourceInputs + ) + let child = Child( + content: Strategy.makeContent(from: outputs), + viewContent: view.value[offset: { .of(&$0.content) }], + oldContent: Strategy.defaultValue + ) + return Content.makeDebuggableView(view: _GraphValue(child), inputs: inputs) + } + + private struct Child: StatefulRule { + @OptionalAttribute var content: Strategy.Content? + @Attribute var viewContent: (Strategy.Content) -> Content + var oldContent: Strategy.Content + + typealias Value = Content + + init( + content: OptionalAttribute, + viewContent: Attribute<(Strategy.Content) -> Content>, + oldContent: Strategy.Content + ) { + self._content = content + self._viewContent = viewContent + self.oldContent = oldContent + } + + mutating func updateValue() { + let (viewContent, viewChanged) = $viewContent.changedValue() + let (content, contentChanged) = $content?.changedValue() ?? (Strategy.defaultValue, false) + defer { oldContent = content } + let changed = Strategy.hasChanges(from: oldContent, to: content) + guard !hasValue || (contentChanged && changed) || viewChanged else { + return + } + value = withObservation { + viewContent(content) + } + } + } +} + +// MARK: - PlatformItemTraitWriter + +struct PlatformItemTraitWriter: PrimitiveViewModifier + where Strategy: UnaryPlatformItemsStrategy, Source: View, + Trait: _ViewTraitKey, Trait.Value == PlatformItem { + var source: Source + + nonisolated static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + body(_Graph(), inputs) + } + + nonisolated static func _makeViewList( + modifier: _GraphValue, + inputs: _ViewListInputs, + body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs + ) -> _ViewListOutputs { + var childInputs = inputs + var sourceInputs = inputs.withoutGeometryDependencies + Strategy.makeInputs(&sourceInputs) + let source = modifier[offset: { .of(&$0.source) }] + let outputs = Source._makeView( + view: source, + inputs: sourceInputs + ) + let addTrait = AddTrait( + content: .init(outputs.platformItem), + traits: .init(inputs.traits), + oldContent: Strategy.defaultValue + ) + childInputs.traits = Attribute(addTrait) + childInputs.addTraitKey(Trait.self) + return body(_Graph(), childInputs) + } + + private struct AddTrait: StatefulRule, AsyncAttribute { + @OptionalAttribute var content: PlatformItem? + @OptionalAttribute var traits: ViewTraitCollection? + var oldContent: PlatformItem + + typealias Value = ViewTraitCollection + + mutating func updateValue() { + let (content, contentChanged) = $content?.changedValue() ?? (Strategy.defaultValue, false) + let (incomingTraits, traitsChanged) = $traits?.changedValue() ?? (ViewTraitCollection(), false) + defer { oldContent = content } + let changed = Strategy.hasChanges(from: oldContent, to: content) + guard !hasValue || (contentChanged && changed) || traitsChanged else { + return + } + var traits = incomingTraits + traits[Trait.self] = content + value = traits + } + } +} From 63049d40405c36e6b1288cc988890efedacc7983 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 1 Nov 2026 21:06:28 +0800 Subject: [PATCH 10/15] feat: add help text and tooltip support --- Sources/OpenSwiftUI/View/Help/HelpStyle.swift | 133 +++++++++++++++++ Sources/OpenSwiftUI/View/Help/HelpTag.swift | 86 +++++++++++ .../View/Help/TooltipModifier.swift | 139 ++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/Help/HelpStyle.swift create mode 100644 Sources/OpenSwiftUI/View/Help/HelpTag.swift create mode 100644 Sources/OpenSwiftUI/View/Help/TooltipModifier.swift diff --git a/Sources/OpenSwiftUI/View/Help/HelpStyle.swift b/Sources/OpenSwiftUI/View/Help/HelpStyle.swift new file mode 100644 index 000000000..e539bf0a2 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Help/HelpStyle.swift @@ -0,0 +1,133 @@ +// +// HelpStyle.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 2E9CED02B61F68F209568461F8033B51 (SwiftUI) + +public import OpenSwiftUICore + +// MARK: - HelpStyle + +@_spi(UIFrameworks) +@available(OpenSwiftUI_v4_0, *) +@preconcurrency +@MainActor +public protocol HelpStyle { + associatedtype Body: View + + @ViewBuilder + func makeBody(configuration: Configuration) -> Body + + typealias Configuration = HelpStyleConfiguration +} + +// MARK: - HelpStyleConfiguration + +@_spi(UIFrameworks) +@available(OpenSwiftUI_v4_0, *) +public struct HelpStyleConfiguration { + public struct Content: ViewAlias { + public typealias Body = Never + + nonisolated package init() { + _openSwiftUIEmptyStub() + } + } + + public let content: Content = .init() + public let text: Text +} + +@_spi(UIFrameworks) +@available(*, unavailable) +extension HelpStyleConfiguration.Content: Sendable {} + +@_spi(UIFrameworks) +@available(*, unavailable) +extension HelpStyleConfiguration: Sendable {} + +// MARK: - HelpView + +struct HelpView: View where Content: View { + var content: Content + var text: Text + + init(content: Content, text: Text) { + self.content = content + self.text = text + } + + var body: some View { + ResolvedHelpStyle(configuration: .init(text: text)) + .viewAlias(HelpStyleConfiguration.Content.self) { + content + } + } +} + +// MARK: - ResolvedHelpStyle + +private struct ResolvedHelpStyle: StyleableView { + var configuration: HelpStyleConfiguration + + static var defaultStyleModifier: HelpStyleModifier { + .init(style: DefaultHelpStyle()) + } + + var body: some View { + HelpView(content: configuration.content, text: configuration.text) + .accessibility(AccessibilityProperties.HintsKey.self, [configuration.text]) + } +} + +// MARK: - EmptyHelpStyle + +private struct EmptyHelpStyle: HelpStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.content + } +} + +// MARK: - TooltipHelpStyle + +private struct TooltipHelpStyle: HelpStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.content + .modifier(TooltipModifier(text: configuration.text)) + } +} + +// MARK: - DefaultHelpStyle + +struct DefaultHelpStyle: HelpStyle, AnyDefaultStyle { + func makeBody(configuration: Configuration) -> some View { + HelpView(content: configuration.content, text: configuration.text) + .helpStyle(TooltipHelpStyle()) + .helpStyle(EmptyHelpStyle()) + } +} + +// MARK: - View + helpStyle + +extension View { + @inline(__always) + func helpStyle(_ style: S) -> some View where S: HelpStyle { + modifier(HelpStyleModifier(style: style)) + } +} + +// MARK: - HelpStyleModifier + +struct HelpStyleModifier: StyleModifier where S: HelpStyle { + var style: S + + init(style: S) { + self.style = style + } + + func styleBody(configuration: HelpStyleConfiguration) -> some View { + style.makeBody(configuration: configuration) + } +} diff --git a/Sources/OpenSwiftUI/View/Help/HelpTag.swift b/Sources/OpenSwiftUI/View/Help/HelpTag.swift new file mode 100644 index 000000000..e221b3a34 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Help/HelpTag.swift @@ -0,0 +1,86 @@ +// +// HelpTag.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 7529EF7F24AB68C916419BA46B1AEA8F (SwiftUI) + +public import OpenSwiftUICore + +@available(OpenSwiftUI_v2_0, *) +extension View { + /// Adds help text to a view using a localized string that you provide. + /// + /// Adding help to a view configures the view's accessibility hint and + /// its help tag (also called a _tooltip_) in macOS or visionOS. + /// For more information on using help tags, see + /// [Offering help](https://developer.apple.com/design/human-interface-guidelines/offering-help) + /// in the Human Interface Guidelines. + /// + /// Button(action: composeMessage) { + /// Image(systemName: "square.and.pencil") + /// } + /// .help("Compose a new message") + /// + /// - Parameter textKey: The key for the localized text to use as help. + nonisolated public func help(_ textKey: LocalizedStringKey) -> some View { + HelpView(content: self, text: Text(textKey)) + } + + /// Adds help text to a view using a text view that you provide. + /// + /// Adding help to a view configures the view's accessibility hint and + /// its help tag (also called a _tooltip_) in macOS or visionOS. + /// For more information on using help tags, see + /// [Offering help](https://developer.apple.com/design/human-interface-guidelines/offering-help) + /// in the Human Interface Guidelines. + /// + /// Slider("Opacity", value: $selectedShape.opacity) + /// .help(Text("Adjust the opacity of the selected \(selectedShape.name)")) + /// + /// - Parameter text: The ``Text`` view to use as help. + nonisolated public func help(_ text: Text) -> some View { + HelpView(content: self, text: text) + } + + /// Adds help text to a view using a string that you provide. + /// + /// Adding help to a view configures the view's accessibility hint and + /// its help tag (also called a _tooltip_) in macOS or visionOS. + /// For more information on using help tags, see + /// [Offering help](https://developer.apple.com/design/human-interface-guidelines/offering-help) + /// in the Human Interface Guidelines. + /// + /// Image(systemName: "pin.circle") + /// .foregroundColor(pointOfInterest.tintColor) + /// .help(pointOfInterest.name) + /// + /// - Parameter text: The text to use as help. + @_disfavoredOverload + nonisolated public func help(_ text: S) -> some View where S: StringProtocol { + HelpView(content: self, text: Text(text)) + } +} + +#if os(macOS) +extension View { + @inline(__always) + func help