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 + } + } +} 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] } +} 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) 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 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