diff --git a/Sources/COpenSwiftUI/Overlay/Accessibility/OpenSwiftUIAccessibilityPrivate.h b/Sources/COpenSwiftUI/Overlay/Accessibility/OpenSwiftUIAccessibilityPrivate.h new file mode 100644 index 000000000..51b8cce53 --- /dev/null +++ b/Sources/COpenSwiftUI/Overlay/Accessibility/OpenSwiftUIAccessibilityPrivate.h @@ -0,0 +1,47 @@ +// +// OpenSwiftUIAccessibilityPrivate.h +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef OpenSwiftUIAccessibilityPrivate_h +#define OpenSwiftUIAccessibilityPrivate_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import +#if OPENSWIFTUI_TARGET_OS_OSX +#import +#endif + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +#if OPENSWIFTUI_TARGET_OS_OSX +typedef OPENSWIFTUI_ENUM(NSInteger, AXAttributeLockSource) { + AXAttributeLockSourceNone = 0, +}; +#endif + +@interface NSObject (OpenSwiftUIAccessibilityPrivate) ++ (BOOL)_isFromOpenSwiftUI; +- (void)_performSelector:(SEL)selector withObject:(nullable id)object; + +@property (nonatomic, nullable, copy) BOOL (^accessibilityOpenSwiftUIDefaultActionStoredBlock)(void); +@property (nonatomic, nullable, strong) id accessibilityOpenSwiftUIStoredLinkRotor; +@property (nonatomic, nullable, strong) id accessibilityNodeForPlatformElement; + +#if OPENSWIFTUI_TARGET_OS_OSX +@property (nonatomic) BOOL _accessibilityAllowsAutomationElements; +- (AXAttributeLockSource)accessibilityLockSourceForAttribute:(NSAccessibilityAttributeName)attribute OPENSWIFTUI_SWIFT_NAME(accessibilityLockSource(for:)); +- (void)setAccessibilityLockSource:(AXAttributeLockSource)source forAttribute:(NSAccessibilityAttributeName)attribute OPENSWIFTUI_SWIFT_NAME(setAccessibilityLockSource(_:for:)); +#endif +@end + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ + +#endif /* OpenSwiftUIAccessibilityPrivate_h */ diff --git a/Sources/COpenSwiftUI/Overlay/Accessibility/OpenSwiftUIAccessibilityPrivate.m b/Sources/COpenSwiftUI/Overlay/Accessibility/OpenSwiftUIAccessibilityPrivate.m new file mode 100644 index 000000000..6481ed651 --- /dev/null +++ b/Sources/COpenSwiftUI/Overlay/Accessibility/OpenSwiftUIAccessibilityPrivate.m @@ -0,0 +1,58 @@ +// +// OpenSwiftUIAccessibilityPrivate.m +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#import "OpenSwiftUIAccessibilityPrivate.h" +#import "../Foundation/OpenSwiftUI+NSObject.h" + +#if OPENSWIFTUI_TARGET_OS_IOS || OPENSWIFTUI_TARGET_OS_VISION || OPENSWIFTUI_TARGET_OS_OSX + +#import + +@implementation NSObject (OpenSwiftUIAccessibilityPrivate) +- (BOOL (^)(void))accessibilityOpenSwiftUIDefaultActionStoredBlock { + return objc_getAssociatedObject(self, @selector(accessibilityOpenSwiftUIDefaultActionStoredBlock)); +} + +- (void)setAccessibilityOpenSwiftUIDefaultActionStoredBlock:(BOOL (^)(void))block { + objc_setAssociatedObject(self, @selector(accessibilityOpenSwiftUIDefaultActionStoredBlock), block, OBJC_ASSOCIATION_COPY); +} + +- (id)accessibilityOpenSwiftUIStoredLinkRotor { + return objc_getAssociatedObject(self, @selector(accessibilityOpenSwiftUIStoredLinkRotor)); +} + +- (void)setAccessibilityOpenSwiftUIStoredLinkRotor:(id)rotor { + objc_setAssociatedObject(self, @selector(accessibilityOpenSwiftUIStoredLinkRotor), rotor, OBJC_ASSOCIATION_RETAIN); +} + +#if OPENSWIFTUI_TARGET_OS_OSX +- (AXAttributeLockSource)accessibilityLockSourceForAttribute:(NSAccessibilityAttributeName)attribute { + NSDictionary *sources = objc_getAssociatedObject(self, @selector(accessibilityLockSourceForAttribute:)); + return [sources[attribute] integerValue]; +} + +- (void)setAccessibilityLockSource:(AXAttributeLockSource)source forAttribute:(NSAccessibilityAttributeName)attribute { + NSMutableDictionary *sources = objc_getAssociatedObject(self, @selector(accessibilityLockSourceForAttribute:)); + if (sources == nil) { + sources = [[NSMutableDictionary alloc] init]; + objc_setAssociatedObject(self, @selector(accessibilityLockSourceForAttribute:), sources, OBJC_ASSOCIATION_RETAIN); + } + sources[attribute] = @(source); +} + +- (BOOL)_accessibilityAllowsAutomationElements { + NSNumber *value = objc_getAssociatedObject(self, @selector(_accessibilityAllowsAutomationElements)); + return value.boolValue; +} + +- (void)set_accessibilityAllowsAutomationElements:(BOOL)value { + objc_setAssociatedObject(self, @selector(_accessibilityAllowsAutomationElements), @(value), OBJC_ASSOCIATION_RETAIN); +} +#endif +@end + +#endif /* OPENSWIFTUI_TARGET_OS_IOS || OPENSWIFTUI_TARGET_OS_VISION || OPENSWIFTUI_TARGET_OS_OSX */ diff --git a/Sources/COpenSwiftUI/Overlay/Foundation/OpenSwiftUI+NSObject.h b/Sources/COpenSwiftUI/Overlay/Foundation/OpenSwiftUI+NSObject.h new file mode 100644 index 000000000..57cd15de0 --- /dev/null +++ b/Sources/COpenSwiftUI/Overlay/Foundation/OpenSwiftUI+NSObject.h @@ -0,0 +1,28 @@ +// +// OpenSwiftUI+NSObject.h +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef OpenSwiftUI_NSObject_h +#define OpenSwiftUI_NSObject_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +@interface NSObject (OpenSwiftUI) ++ (BOOL)_isFromOpenSwiftUI; +- (void)_performSelector:(SEL)selector withObject:(nullable id)object; +@end + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ + +#endif /* OpenSwiftUI_NSObject_h */ diff --git a/Sources/COpenSwiftUI/Overlay/Foundation/OpenSwiftUI+NSObject.m b/Sources/COpenSwiftUI/Overlay/Foundation/OpenSwiftUI+NSObject.m new file mode 100644 index 000000000..61f4a2323 --- /dev/null +++ b/Sources/COpenSwiftUI/Overlay/Foundation/OpenSwiftUI+NSObject.m @@ -0,0 +1,25 @@ +// +// OpenSwiftUI+NSObject.m +// COpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#import "OpenSwiftUI+NSObject.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +@implementation NSObject (OpenSwiftUI) ++ (BOOL)_isFromOpenSwiftUI { + return NO; +} + +- (void)_performSelector:(SEL)selector withObject:(id)object { + if (object != nil) { + void (*implementation)(id, SEL) = (void (*)(id, SEL))[object methodForSelector:selector]; + implementation(object, selector); + } +} +@end + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ diff --git a/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h b/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h index 37ef98f54..d1962909e 100644 --- a/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h +++ b/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.h @@ -15,6 +15,22 @@ OPENSWIFTUI_ASSUME_NONNULL_BEGIN +OPENSWIFTUI_EXTERN_C_BEGIN + +OPENSWIFTUI_EXPORT +Class _Nullable AXNSTableViewCellMockElementClass(void); + +OPENSWIFTUI_EXPORT +void NSAccessibilityBeginInternalAccessors(void); + +OPENSWIFTUI_EXPORT +void NSAccessibilityEndInternalAccessors(void); + +OPENSWIFTUI_EXPORT +id _Nullable NSAccessibilityEntryPointValueForAttribute(id element, NSAccessibilityAttributeName attribute); + +OPENSWIFTUI_EXTERN_C_END + @interface NSApplication (OpenSwiftUI_SPI) - (BOOL)_shouldLoadMainNibNamed:(nullable NSString *)name; diff --git a/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.m b/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.m index d90f7d2a6..790292908 100644 --- a/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.m +++ b/Sources/COpenSwiftUI/Shims/AppKit/AppKit_Private.m @@ -11,6 +11,15 @@ #import +Class _Nullable AXNSTableViewCellMockElementClass(void) { + static Class tableViewCellMockElementClass; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + tableViewCellMockElementClass = NSClassFromString(@"NSTableViewCellMockElement"); + }); + return tableViewCellMockElementClass; +} + @implementation NSApplication (OpenSwiftUI_SPI) - (void)markAppLaunchComplete_openswiftui_safe_wrapper { diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityAdditions.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityAdditions.swift new file mode 100644 index 000000000..2fb7038a5 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityAdditions.swift @@ -0,0 +1,48 @@ +// +// AccessibilityAdditions.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: F0090706A080B6CC8EC7D33D6ADC922D (SwiftUI) + +import Foundation +import OpenSwiftUICore +#if canImport(ObjectiveC) +import ObjectiveC +#endif +import COpenSwiftUI + +// MARK: - NSObject + AccessibilityNode + +#if canImport(ObjectiveC) +@objc(OpenSwiftUIAccessibilityPrivate) +#endif +extension NSObject { + var accessibilityNodeForPlatformElement: AccessibilityNode? { + get { + #if canImport(ObjectiveC) + objc_getAssociatedObject( + self, + &accessibilityNodeForPlatformElementKey + ) as? AccessibilityNode + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif + } + set { + #if canImport(ObjectiveC) + objc_setAssociatedObject( + self, + &accessibilityNodeForPlatformElementKey, + newValue, + .OBJC_ASSOCIATION_ASSIGN + ) + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif + } + } +} + +private var accessibilityNodeForPlatformElementKey: UInt8 = 0 diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityFocus.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityFocus.swift new file mode 100644 index 000000000..72b4aab2f --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityFocus.swift @@ -0,0 +1,122 @@ +// +// AccessibilityFocus.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: D40E4D2B9502265C452AA4AA6D326657 (SwiftUI) + +import Foundation +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore +#if os(macOS) +import AppKit +import COpenSwiftUI +#endif + +// MARK: - AccessibilityFocus + +struct AccessibilityFocus { + var byTechnology: [AccessibilityTechnology: Target] = [:] + + struct Target { + weak var platformAccessibilityElement: PlatformAccessibilityElement? + + var ancestorAccessibilityNode: AccessibilityNode? { + if let node = platformAccessibilityElement as? AccessibilityNode { + return node + } + if let node = platformAccessibilityElement?.accessibilityNodeForPlatformElement { + return node + } + guard let platformAccessibilityElement else { + return nil + } + if let node = platformAccessibilityElement.accessibilityNodeForPlatformElement { + return node + } + var node: AccessibilityNode? + platformAccessibilityElement.traverseAncestors { element in + if let ancestor = element as? AccessibilityNode { + node = ancestor + return false + } + if let ancestor = element.accessibilityNodeForPlatformElement { + node = ancestor + return false + } + return true + } + return node + } + + func match(focusStoreNode: AccessibilityNode) -> Match? { + if let node = (platformAccessibilityElement as? AccessibilityNode) + ?? platformAccessibilityElement?.accessibilityNodeForPlatformElement, + node == focusStoreNode { + return .directlyFocused + } + if let element = focusStoreNode.platformElement, + let target = platformAccessibilityElement, + element === target { + return .directlyFocused + } + if let ancestor = ancestorAccessibilityNode, + ancestor == focusStoreNode { + return .platformChildFocused + } + if let ancestor = ancestorAccessibilityNode { + var isContainerChildFocused = false + ancestor.traverseAncestors { element in + if element === (focusStoreNode.platformElement ?? focusStoreNode) { + isContainerChildFocused = true + return false + } + return true + } + if isContainerChildFocused { + return .containerChildFocused + } + } + #if os(macOS) + if let element = platformAccessibilityElement, + let tableCellClass = AXNSTableViewCellMockElementClass(), + element.isKind(of: tableCellClass), + let children = element.valueForAttribute(.children, asType: [PlatformAccessibilityElement].self), + children.count == 1, + children[0] === (focusStoreNode.platformElement ?? focusStoreNode) { + return .implicitlyFocused + } + #endif + return nil + } + } + + enum Match: CaseIterable, Hashable { + case directlyFocused + case implicitlyFocused + case platformChildFocused + case containerChildFocused + } +} + +// MARK: - AccessibilityFocusInputKey + +private struct AccessibilityFocusInputKey: ViewInput { + static var defaultValue: OptionalAttribute { .init() } +} + +extension _ViewInputs { + var accessibilityFocus: Attribute? { + get { base.accessibilityFocus } + set { base.accessibilityFocus = newValue } + } +} + +extension _GraphInputs { + var accessibilityFocus: Attribute? { + get { self[AccessibilityFocusInputKey.self].attribute } + set { self[AccessibilityFocusInputKey.self] = .init(newValue) } + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityNode.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityNode.swift new file mode 100644 index 000000000..167e240e2 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityNode.swift @@ -0,0 +1,32 @@ +// +// AccessibilityNode.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: 2F6327E72581B7F866C81F7546545BE8 (SwiftUI) + +import Foundation +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore + +#if canImport(UIKit) +import UIKit +typealias AccessibilityNodeBase = UIResponder +#else +typealias AccessibilityNodeBase = NSObject +#endif + +// FIXME +class AccessibilityNode: AccessibilityNodeBase { + var platformElement: PlatformAccessibilityElement? { + // TODO: Resolve the platform element from accessibility attachments. + _openSwiftUIUnimplementedFailure() + } +} + +//private struct AccessibilityAttachmentStorage { +// var attachment: AccessibilityAttachment +// var geometry: AccessibilityGeometryStorage +// let token: AccessibilityAttachmentToken +//} diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeList.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeList.swift index 9e3686333..904aeadd7 100644 --- a/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeList.swift +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeList.swift @@ -1,26 +1,22 @@ +// +// AccessibilityNodeList.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore + +// MARK: - AccessibilityNodeList -// FIXME -struct AccessibilityNodeList { +struct AccessibilityNodeList: Equatable { var nodes: [AccessibilityNode] var version: DisplayList.Version } -class AccessibilityNode { - // TODO -} - -// TODO -struct AccessibilityNodeProxy { - static func makeProxyForIdentifiedView( - with list: AccessibilityNodeList?, - environment: EnvironmentValues - ) -> AccessibilityNodeProxy? { - nil - } -} - -// MARK: - AccessibilityNodesKey [6.4.41] +// MARK: - AccessibilityNodesKey struct AccessibilityNodesKey: PreferenceKey { static let defaultValue = AccessibilityNodeList(nodes: [], version: .init()) diff --git a/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeProxy.swift b/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeProxy.swift new file mode 100644 index 000000000..4dd38bd55 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/AccessibilityNodeProxy.swift @@ -0,0 +1,18 @@ +// +// AccessibilityNodeProxy.swift +// OpenSwiftUI + +@_spi(Ultraviolet) +@available(OpenSwiftUI_v3_0, *) +public struct AccessibilityNodeProxy: Equatable, Hashable, Identifiable, Codable { + public var id: Int + + // TODO + + static func makeProxyForIdentifiedView( + with list: AccessibilityNodeList?, + environment: EnvironmentValues + ) -> AccessibilityNodeProxy? { + nil + } +} diff --git a/Sources/OpenSwiftUI/Accessibility/PlatformAccessibilityElement.swift b/Sources/OpenSwiftUI/Accessibility/PlatformAccessibilityElement.swift new file mode 100644 index 000000000..958d80cb0 --- /dev/null +++ b/Sources/OpenSwiftUI/Accessibility/PlatformAccessibilityElement.swift @@ -0,0 +1,66 @@ +// +// PlatformAccessibilityElement.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP + +import Foundation +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore +#if os(macOS) +import AppKit +import COpenSwiftUI +#elseif canImport(UIKit) +import UIKit +#endif + +// MARK: - PlatformAccessibilityElementProtocol + +extension PlatformAccessibilityElementProtocol where Self: NSObject { + func traverseAncestors(_ body: (PlatformAccessibilityElement) -> Bool) { + #if os(macOS) || canImport(UIKit) + var element: PlatformAccessibilityElement? = self + while let current = element { + guard body(current) else { + return + } + #if os(macOS) + if let cell = current as? NSCell, let controlView = cell.controlView { + element = controlView + } else if let parent = current.valueForAttribute(.parent, asType: NSObject.self), + !(parent is NSWindow), !(parent is NSView) { + element = parent + } else if let view = current as? NSView, let superview = view.superview { + element = superview + } else { + element = current.valueForAttribute(.parent, asType: NSObject.self) + } + #else + if current.responds(to: #selector(getter: UIAccessibilityElement.accessibilityContainer)), + let container = (current as AnyObject).accessibilityContainer as? PlatformAccessibilityElement { + element = container + } else if let view = current as? UIView { + element = view.superview + } else { + element = nil + } + #endif + } + #else + _openSwiftUIUnimplementedFailure() + #endif + } + + #if os(macOS) + func valueForAttribute(_ attribute: NSAccessibility.Attribute, asType: Value.Type) -> Value? { + NSAccessibilityBeginInternalAccessors() + defer { NSAccessibilityEndInternalAccessors() } + guard let value = NSAccessibilityEntryPointValueForAttribute(self, attribute), + !(value is NSNull) else { + return nil + } + return value as? Value + } + #endif +} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift deleted file mode 100644 index 4d775ef2c..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityBoundedNumber.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// AccessibilityBoundedNumber.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -import Foundation - -struct AccessibilityBoundedNumber { - var number: AccessibilityNumber - var lowerBound: AccessibilityNumber? - var upperBound: AccessibilityNumber? - var stride: AccessibilityNumber? - - init?(for value: S, in range: ClosedRange?, by strideValue: S.Stride?) { - let clampedValue = range.map { value.clamped(to: $0) } - let newValue = clampedValue ?? value - guard let numericValue = newValue as? AccessibilityNumeric, - let numberValue = numericValue.asNumber() else { - return nil - } - number = numberValue - if let range { - lowerBound = range.minimumValue?.asNumber() - upperBound = range.maximumValue?.asNumber() - } - if let strideValue, - let numericStride = strideValue as? AccessibilityNumeric { - stride = numericStride.asNumber() - } - } -} - -extension AccessibilityBoundedNumber: AccessibilityValue { - // This kind of description logic is very strange - // But that's how Apple's implementation even on iOS 17 :) - // eg. - // For 1.5 and [1.0, 2.0], the accessiblity output would be 150% - // For 1.5 and [1.3, 2.3], the accessiblity output would be 1.5 - var localizedDescription: String? { - let range: Double = if let lowerBound, let upperBound { - upperBound.base.doubleValue - lowerBound.base.doubleValue - } else { - .zero - } - if abs(range - 100) >= .ulpOfOne { - let style: NumberFormatter.Style = (abs(range - 1.0) < .ulpOfOne) ? .percent : .decimal - return NumberFormatter.localizedString(from: number.base, number: style) - } else { - return NumberFormatter.localizedString(from: NSNumber(value: number.base.doubleValue / 100), number: .percent) - } - } - - var displayDescription: String? { - localizedDescription - } - - var value: NSNumber { number.value } - var minValue: NSNumber? { lowerBound?.value } - var maxValue: NSNumber? { upperBound?.value } - static var type: AnyAccessibilityValueType { .boundedNumber } -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift deleted file mode 100644 index 74cf8ecae..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumber.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// AccessibilityNumber.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -import Foundation - -struct AccessibilityNumber { - var base: NSNumber -} - -extension AccessibilityNumber: AccessibilityValue { - var localizedDescription: String? { - NumberFormatter.localizedString(from: value, number: .decimal) - } - var displayDescription: String? { localizedDescription } - var value: NSNumber { base } - var minValue: NSNumber? { nil } - var maxValue: NSNumber? { nil } - static var type: AnyAccessibilityValueType { .number } -} - -extension AccessibilityNumber: ExpressibleByFloatLiteral { - init(floatLiteral value: Double) { - base = NSNumber(floatLiteral: value) - } -} - -extension AccessibilityNumber: ExpressibleByIntegerLiteral { - init(integerLiteral value: Int) { - base = NSNumber(integerLiteral: value) - } -} - -extension AccessibilityNumber: Codable { - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let data = try container.decode(Data.self) - self.base = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: data)! - } - - func encode(to encoder: Encoder) throws { - let data = try NSKeyedArchiver.archivedData(withRootObject: base, requiringSecureCoding: true) - var container = encoder.singleValueContainer() - try container.encode(data) - } -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumeric.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumeric.swift deleted file mode 100644 index 06e6b65da..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityNumeric.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// AccessibilityNumber.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -import Foundation - -protocol AccessibilityNumeric { - var isValidMinValue: Bool { get } - var isValidMaxValue: Bool { get } - func asNumber() -> AccessibilityNumber? -} - -extension AccessibilityNumeric where Self: FixedWidthInteger { - var isValidMinValue: Bool { - // TODO: Add Unit Test and check usage - if Self.bitWidth == 8 || !Self.isSigned { - true - } else { - self != .min - } - } - - var isValidMaxValue: Bool { self != .max } -} - -extension Int: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension Int8: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension Int16: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension Int32: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension Int64: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension UInt: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension UInt8: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension UInt16: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension UInt32: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension UInt64: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension AccessibilityNumeric where Self: BinaryFloatingPoint { - var isValidMinValue: Bool { - isFinite && self > -Self.greatestFiniteMagnitude - } - - var isValidMaxValue: Bool { - isFinite && self < Self.greatestFiniteMagnitude - } -} - -extension Float: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} - -extension Double: AccessibilityNumeric { - func asNumber() -> AccessibilityNumber? { AccessibilityNumber(base: .init(value: self)) } -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityPlatformSafe.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityPlatformSafe.swift deleted file mode 100644 index 59b426eb6..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityPlatformSafe.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// AccessibilityPlatformSafe.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -import Foundation - -protocol AccessibilityPlatformSafe {} - -extension String: AccessibilityPlatformSafe {} -extension Double: AccessibilityPlatformSafe {} -extension Int: AccessibilityPlatformSafe {} -extension UInt: AccessibilityPlatformSafe {} -extension UInt8: AccessibilityPlatformSafe {} -extension Bool: AccessibilityPlatformSafe {} -extension NSNumber: AccessibilityPlatformSafe {} -extension Optional: AccessibilityPlatformSafe where Wrapped: AccessibilityPlatformSafe {} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilitySliderValue.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilitySliderValue.swift deleted file mode 100644 index 16c065626..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilitySliderValue.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// AccessibilitySliderValue.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -struct AccessibilitySliderValue: AccessibilityValueByProxy { - var base: AccessibilityBoundedNumber - static var type: AnyAccessibilityValueType { .slider } -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift deleted file mode 100644 index 08344e05c..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValue.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// AccessibilityValue.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -import Foundation - -protocol AccessibilityValue: Equatable { - associatedtype PlatformValue: AccessibilityPlatformSafe - var localizedDescription: String? { get } - var displayDescription: String? { get } - var value: PlatformValue { get } - var minValue: PlatformValue? { get } - var maxValue: PlatformValue? { get } - var step: PlatformValue? { get } - static var type: AnyAccessibilityValueType { get } -} - -extension AccessibilityValue { - var minValue: PlatformValue? { nil } - var maxValue: PlatformValue? { nil } - var step: PlatformValue? { nil } -} - -extension AccessibilityValue where PlatformValue: CustomStringConvertible { - var localizedDescription: String? { value.description } - var displayDescription: String? { value.description } -} - -extension AccessibilityValue where Self == Self.PlatformValue { - var value: PlatformValue { self } -} - -extension Int: AccessibilityValue { - typealias PlatformValue = Int - static var type: AnyAccessibilityValueType { .int } -} - -extension Double: AccessibilityValue { - typealias PlatformValue = Double - static var type: AnyAccessibilityValueType { .number } -} - -extension Bool: AccessibilityValue { - typealias PlatformValue = Bool - static var type: AnyAccessibilityValueType { .bool } -} - -extension String: AccessibilityValue { - typealias PlatformValue = String - static var type: AnyAccessibilityValueType { .string } -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValueByProxy.swift b/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValueByProxy.swift deleted file mode 100644 index 3c41f76f4..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AccessibilityValueByProxy.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// AccessibilityValueByProxy.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -protocol AccessibilityValueByProxy: AccessibilityValue { - associatedtype Base: AccessibilityValue - var base: Base { get } -} - -extension AccessibilityValueByProxy { - var localizedDescription: String? { base.localizedDescription } - var displayDescription: String? { base.displayDescription } - var value: Base.PlatformValue { base.value } - var minValue: Base.PlatformValue? { base.minValue } - var maxValue: Base.PlatformValue? { base.maxValue } - var step: Base.PlatformValue? { base.step } -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValue.swift b/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValue.swift deleted file mode 100644 index 9b3b2caa6..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValue.swift +++ /dev/null @@ -1,153 +0,0 @@ -// -// AnyAccessibilityValue.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: WIP -// ID: 333660CD735494DA92CEC2878E6C8CC5 - -// MARK: - AbstractAnyAccessibilityValue - -private protocol AbstractAnyAccessibilityValue: Codable { - var localizedDescription: String? { get } - var displayDescription: String? { get } - var value: Any { get } - var minValue: Any? { get } - var maxValue: Any? { get } - var step: Any? { get } - var type: AnyAccessibilityValueType { get } - func `as`(_ type: Value.Type) -> Value? - func isEqual(to value: AbstractAnyAccessibilityValue) -> Bool -} - -// MARK: - AnyAccessibilityValue - -struct AnyAccessibilityValue { - private var base: AbstractAnyAccessibilityValue - - init(_ base: some Codable & AccessibilityValue) { - self.base = ConcreteBase(base: base) - } -} - -extension AnyAccessibilityValue: Equatable { - static func == (lhs: AnyAccessibilityValue, rhs: AnyAccessibilityValue) -> Bool { - lhs.base.isEqual(to: rhs.base) - } -} - -extension AnyAccessibilityValue: Codable { - private enum Keys: CodingKey { - case type - case value - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: Keys.self) - let type = try container.decode(AnyAccessibilityValueType.self, forKey: .type) - switch type { - case .int: base = try container.decode(ConcreteBase.self, forKey: .value) - case .double: base = try container.decode(ConcreteBase.self, forKey: .value) - case .bool: base = try container.decode(ConcreteBase.self, forKey: .value) - case .string: base = try container.decode(ConcreteBase.self, forKey: .value) - case .slider: base = try container.decode(ConcreteBase.self, forKey: .value) - case .boundedNumber: base = try container.decode(ConcreteBase.self, forKey: .value) - case .number: base = try container.decode(ConcreteBase.self, forKey: .value) - } - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: Keys.self) - try container.encode(base.type, forKey: .type) - try base.encode(to: container.superEncoder(forKey: .value)) - } -} - -extension AnyAccessibilityValue: AbstractAnyAccessibilityValue { - var localizedDescription: String? { base.localizedDescription } - var displayDescription: String? { base.displayDescription } - var value: Any { base.value } - var minValue: Any? { base.minValue } - var maxValue: Any? { base.maxValue } - var step: Any? { base.step } - var type: AnyAccessibilityValueType { base.type } - func `as`(_ type: Value.Type) -> Value? where Value: AccessibilityValue { - base.as(type) - } - - fileprivate func isEqual(to value: AbstractAnyAccessibilityValue) -> Bool { - base.isEqual(to: value) - } -} - -// MARK: AnyAccessibilityValue.ConcreateBase - -extension AnyAccessibilityValue { - fileprivate struct ConcreteBase where Base: Codable, Base: AccessibilityValue { - var base: Base - } -} - -extension AnyAccessibilityValue.ConcreteBase: Codable {} -extension AnyAccessibilityValue.ConcreteBase: Equatable {} -extension AnyAccessibilityValue.ConcreteBase: AbstractAnyAccessibilityValue { - var localizedDescription: String? { base.localizedDescription } - var displayDescription: String? { base.displayDescription } - var value: Any { base.value } - var minValue: Any? { base.minValue } - var maxValue: Any? { base.maxValue } - var step: Any? { base.step } - var type: AnyAccessibilityValueType { Base.type } - func `as`(_: Value.Type) -> Value? where Value: AccessibilityValue { - base as? Value - } - - func isEqual(to value: AbstractAnyAccessibilityValue) -> Bool { - base == (value as? Self)?.base - } -} - -// MARK: AccessibilityBoundedNumber + Codable - -extension AccessibilityBoundedNumber: Codable { - private enum CodingKeys: CodingKey { - case number - case lowerBound - case upperBound - case stride - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - number = try container.decode(AccessibilityNumber.self, forKey: .number) - lowerBound = try container.decodeIfPresent(AccessibilityNumber.self, forKey: .lowerBound) - upperBound = try container.decodeIfPresent(AccessibilityNumber.self, forKey: .upperBound) - stride = try container.decodeIfPresent(AccessibilityNumber.self, forKey: .lowerBound) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(number, forKey: .number) - try container.encodeIfPresent(lowerBound, forKey: .lowerBound) - try container.encodeIfPresent(upperBound, forKey: .upperBound) - try container.encodeIfPresent(stride, forKey: .stride) - } -} - -// MARK: AccessibilitySliderValue + Codable - -extension AccessibilitySliderValue: Codable { - private enum CodingKeys: CodingKey { - case base - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - base = try container.decode(AccessibilityBoundedNumber.self, forKey: .base) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(base, forKey: .base) - } -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValueType.swift b/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValueType.swift deleted file mode 100644 index 30e9e2306..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/AnyAccessibilityValueType.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// AnyAccessibilityValueType.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete - -enum AnyAccessibilityValueType: UInt, Codable { - case int - case double - case bool - case string -// case disclosure -// case toggle - case slider -// case stepper -// case progress - case boundedNumber -// case headingLevel - case number -} diff --git a/Sources/OpenSwiftUI/Accessibility/internal/ClosedRange+AccessibilityNumeric.swift b/Sources/OpenSwiftUI/Accessibility/internal/ClosedRange+AccessibilityNumeric.swift deleted file mode 100644 index 0d5f319be..000000000 --- a/Sources/OpenSwiftUI/Accessibility/internal/ClosedRange+AccessibilityNumeric.swift +++ /dev/null @@ -1,17 +0,0 @@ -extension ClosedRange where Bound: Strideable { - var minimumValue: AccessibilityNumeric? { - guard let value = lowerBound as? AccessibilityNumeric, - value.isValidMinValue else { - return nil - } - return value - } - - var maximumValue: AccessibilityNumeric? { - guard let value = upperBound as? AccessibilityNumeric, - value.isValidMaxValue else { - return nil - } - return value - } -} diff --git a/Sources/OpenSwiftUI/Event/HitTest/HitTestingDebug.swift b/Sources/OpenSwiftUI/Event/HitTest/HitTestingDebug.swift index 7e07d5847..11e14926b 100644 --- a/Sources/OpenSwiftUI/Event/HitTest/HitTestingDebug.swift +++ b/Sources/OpenSwiftUI/Event/HitTest/HitTestingDebug.swift @@ -315,7 +315,6 @@ private enum ResponderBasedHitTestTracing: HitTestTracing { guard let responder = leaf.responderForHitTesting else { return nil } - // [AI] Both identity operands read the responder's host view. if let viewResponder = responder as? PlatformViewResponder, let hostView = viewResponder.hostView, hostView === viewResponder.hostView diff --git a/Sources/OpenSwiftUI/View/Control/Link/LinkDestination.swift b/Sources/OpenSwiftUI/View/Control/Link/LinkDestination.swift deleted file mode 100644 index 8f73a0a3c..000000000 --- a/Sources/OpenSwiftUI/View/Control/Link/LinkDestination.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// LinkDestination.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: Complete -// ID: 85AAA54365DC4DDF1DC8F2FECEF4501A - -import Foundation - -struct LinkDestination { - var configuration: Configuration - - @Environment(\.openURL) - private var openURL: OpenURLAction - - @Environment(\._openSensitiveURL) - private var openSensitiveURL: OpenURLAction - - func open() { - let openURLAction = configuration.isSensitive ? openSensitiveURL : openURL - openURLAction(configuration.url) - } -} - -extension LinkDestination { - struct Configuration: Codable { - var url: URL - var isSensitive: Bool - - init(url: URL, isSensitive: Bool) { - self.url = url - self.isSensitive = isSensitive - } - - private enum CodingKeys: CodingKey { - case url - case isSensitive - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - url = try container.decode(URL.self, forKey: .url) - isSensitive = try container.decode(Bool.self, forKey: .isSensitive) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(url, forKey: .url) - try container.encode(isSensitive, forKey: .isSensitive) - } - } -} diff --git a/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift b/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift index fd11bc824..6f37b5476 100644 --- a/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift +++ b/Sources/OpenSwiftUI/View/Control/Slider/Slider.swift @@ -577,7 +577,7 @@ extension Slider { _maximumValueLabel = maximumValueLabel hasCustomMinMaxValueLabels = customMinMaxValueLabels self.label = label - let accessibilitySliderValue = accessibilityValue.map { AccessibilitySliderValue(base: $0) } + let accessibilitySliderValue = accessibilityValue.map { AccessibilitySliderValue($0) } self.accessibilityValue = accessibilitySliderValue?.base } } diff --git a/Sources/OpenSwiftUI/View/LabelContent/AccessibilityLabeledContent.swift b/Sources/OpenSwiftUI/View/LabelContent/AccessibilityLabeledContent.swift index f5e42049e..f6fd37958 100644 --- a/Sources/OpenSwiftUI/View/LabelContent/AccessibilityLabeledContent.swift +++ b/Sources/OpenSwiftUI/View/LabelContent/AccessibilityLabeledContent.swift @@ -157,9 +157,6 @@ struct AccessibilityAttachmentModifier: PrimitiveViewModifier { } } -// FIXME -struct AccessibilityAttachment {} - extension AccessibilityAttachment { enum Tree { case leaf(AccessibilityAttachment) diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityActivationPoint.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityActivationPoint.swift new file mode 100644 index 000000000..b817f60fb --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityActivationPoint.swift @@ -0,0 +1,112 @@ +// +// AccessibilityActivationPoint.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package import Foundation +import OpenSwiftUI_SPI + +package struct AccessibilityActivationPointStorage: Equatable { + package var activate: AccessibilityActivationPoint.Location? + package var drag: [AccessibilityActivationPoint] + package var drop: [AccessibilityActivationPoint] + + package init() { + activate = nil + drag = [] + drop = [] + } + + package init(kind: AccessibilityActivationKind.RawValue, point: AccessibilityActivationPoint.Location) { + self.init() + switch kind { + case .activate: + activate = point + case let .drag(description): + description.assertUnstyled() + drag = [.init(location: point, description: description)] + case let .drop(description): + description.assertUnstyled() + drop = [.init(location: point, description: description)] + } + } +} + +extension AccessibilityActivationPointStorage: AccessibilityCombinable { + @discardableResult + package mutating func merge(with child: Self) -> Bool { + let result: Bool + if activate == nil { + activate = child.activate + result = true + } else { + result = !child.drag.isEmpty || child.drop.isEmpty + } + drag.append(contentsOf: child.drag) + drop.append(contentsOf: child.drop) + return result + } +} + +package struct AccessibilityActivationPoint: Equatable { + package enum Location: Equatable { + case point(CGPoint) + case unitPoint(UnitPoint) + case automatic + } + + package var location: Location + package var description: Text +} + +package struct AccessibilityActivationKind { + package enum RawValue: Equatable { + case activate + case drag(Text) + case drop(Text) + } + + package var rawValue: RawValue + + package init(rawValue: RawValue) { + self.rawValue = rawValue + } + + package static let `default` = Self(rawValue: .activate) +} + +extension AccessibilityActivationKind { + package static var defaultDescriptor: String { + #if canImport(Darwin) + AXOpenSwiftUIInteractionLocationDescriptorDefaultName() + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif + } +} + +extension AccessibilityActivationPoint.Location: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + switch self { + case let .point(point): + try encoder.messageField(1, point) + case let .unitPoint(point): + try encoder.messageField(2, point) + case .automatic: + break + } + } + + package init(from decoder: inout ProtobufDecoder) throws { + self = .automatic + while let field = try decoder.nextField() { + switch field.tag { + case 1: self = .point(try decoder.messageField(field)) + case 2: self = .unitPoint(try decoder.messageField(field)) + default: try decoder.skipField(field) + } + } + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift index 5a255bd89..80b52189c 100644 --- a/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityAnnouncementPriority.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: Blocked by Text.Style +// Status: Complete package import Foundation diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityChartDescriptor.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityChartDescriptor.swift new file mode 100644 index 000000000..f1ba1e836 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityChartDescriptor.swift @@ -0,0 +1,215 @@ +// +// AccessibilityChartDescriptor.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 7BC255353E40EE9AA85BDA85829B61DC (SwiftUICore) + +#if canImport(Accessibility) +public import Accessibility +#endif +import libAccessibilityPrivate +import Foundation +import OpenAttributeGraphShims +import OpenSwiftUI_SPI + +/// A type to generate an `AXChartDescriptor` object that you use to provide +/// information about a chart and its data for an accessible experience +/// in VoiceOver or other assistive technologies. +/// +/// Note that you may use the `@Environment` property wrapper inside the +/// implementation of your `AXChartDescriptorRepresentable`, in which case you +/// should implement `updateChartDescriptor`, which will be called when the +/// `Environment` changes. +/// +/// For example, to provide accessibility for a view that represents a chart, +/// you would first declare your chart descriptor representable type: +/// +/// struct MyChartDescriptorRepresentable: AXChartDescriptorRepresentable { +/// func makeChartDescriptor() -> AXChartDescriptor { +/// // Build and return your `AXChartDescriptor` here. +/// } +/// +/// func updateChartDescriptor(_ descriptor: AXChartDescriptor) { +/// // Update your chart descriptor with any new values. +/// } +/// } +/// +/// Then, provide an instance of your `AXChartDescriptorRepresentable` type to +/// your view using the `accessibilityChartDescriptor` modifier: +/// +/// var body: some View { +/// MyChartView() +/// .accessibilityChartDescriptor(MyChartDescriptorRepresentable()) +/// } +/// +@available(OpenSwiftUI_v3_0, *) +public protocol AXChartDescriptorRepresentable { + /// Create the `AXChartDescriptor` for this view, and return it. + /// + /// This will be called once per identity of your `View`. It will not be run + /// again unless the identity of your `View` changes. If you need to + /// update the `AXChartDescriptor` based on changes in your `View`, or in + /// the `Environment`, implement `updateChartDescriptor`. + /// This method will only be called if / when accessibility needs the + /// `AXChartDescriptor` of your view, for VoiceOver. + func makeChartDescriptor() -> AXChartDescriptor + + /// Update the existing `AXChartDescriptor` for your view, based on changes + /// in your view or in the `Environment`. + /// + /// This will be called as needed, when accessibility needs your + /// `AXChartDescriptor` for VoiceOver. It will only be called if the inputs + /// to your views, or a relevant part of the `Environment`, have changed. + func updateChartDescriptor(_ descriptor: AXChartDescriptor) +} + +@available(OpenSwiftUI_v3_0, *) +extension AXChartDescriptorRepresentable { + public func updateChartDescriptor(_ descriptor: AXChartDescriptor) { + _openSwiftUIEmptyStub() + } +} + +package class AccessibilityChartDescriptorStorage: Equatable { + package func resolve() throws -> AXChartDescriptor { + _openSwiftUIBaseClassAbstractMethod() + } + + package func isEqual(to other: AccessibilityChartDescriptorStorage) -> Bool { + false + } + + package static func == (lhs: AccessibilityChartDescriptorStorage, rhs: AccessibilityChartDescriptorStorage) -> Bool { + lhs.isEqual(to: rhs) + } +} + +final package class RepresentableChartDescriptorStorage: AccessibilityChartDescriptorStorage { + private var representable: any AXChartDescriptorRepresentable + + private var cachedDescriptor: AXChartDescriptor? + + private var needsUpdate: Bool + + private var generation: UInt32 + + package init(_ representable: any AXChartDescriptorRepresentable) { + self.representable = representable + cachedDescriptor = nil + needsUpdate = false + generation = 0 + } + + package func reset() { + generation = 0 + needsUpdate = false + cachedDescriptor = nil + } + + package func markNeedsUpdate(_ representable: any AXChartDescriptorRepresentable) { + self.representable = representable + generation.unsafeIncrement() + needsUpdate = true + } + + package override func resolve() throws -> AXChartDescriptor { + if let cachedDescriptor { + if needsUpdate { + representable.updateChartDescriptor(cachedDescriptor) + needsUpdate = false + } + return cachedDescriptor + } else { + let descriptor = representable.makeChartDescriptor() + cachedDescriptor = descriptor + needsUpdate = false + return descriptor + } + } + + package override func isEqual(to other: AccessibilityChartDescriptorStorage) -> Bool { + guard let other = other as? RepresentableChartDescriptorStorage else { + return false + } + return generation == other.generation && compareValues(representable, other.representable) + } +} + +package struct CodableAXChartDescriptor: Codable { + package var storage: AccessibilityChartDescriptorStorage + + package init(_ storage: AccessibilityChartDescriptorStorage) { + self.storage = storage + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let data = try container.decode(Data.self, forKey: .dictionaryData) + storage = AccessibilitySpecificChartDescriptorStorage(data: data) + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + let descriptor = try storage.resolve() + let data = try NSKeyedArchiver.archivedData( + withRootObject: descriptor.dictionaryRepresentation(), + requiringSecureCoding: true + ) + try container.encode(data, forKey: .dictionaryData) + } + + private enum CodingKeys: String, CodingKey { + case dictionaryData + } +} + +class AccessibilitySpecificChartDescriptorStorage: AccessibilityChartDescriptorStorage { + private var storage: Storage + + init(_ descriptor: AXChartDescriptor) { + storage = .resolved(descriptor) + } + + init(data: Data) { + storage = .data(data) + } + + override func resolve() throws -> AXChartDescriptor { + switch storage { + case let .data(data): + guard let dictionary = _AXOpenSwiftUIUnarchiveChartDescriptor(data) as? [AnyHashable: Any] else { + throw ChartDescriptorArchiveError.unarchiveFailed + } + let descriptor = AXChartDescriptor(dictionary: dictionary) + storage = .resolved(descriptor) + return descriptor + case let .resolved(descriptor): + return descriptor + } + } + + override func isEqual(to other: AccessibilityChartDescriptorStorage) -> Bool { + guard let other = other as? Self else { + return false + } + if case let (.data(lhs), .data(rhs)) = (storage, other.storage) { + return lhs == rhs + } else { + guard let lhs = try? resolve(), let rhs = try? other.resolve() else { + return false + } + return lhs.dictionaryRepresentation() as NSDictionary == rhs.dictionaryRepresentation() as NSDictionary + } + } + + private enum Storage { + case data(Data) + case resolved(AXChartDescriptor) + } + + enum ChartDescriptorArchiveError: Error { + case unarchiveFailed + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityChildBehaviorKind.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityChildBehaviorKind.swift new file mode 100644 index 000000000..c06944f04 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityChildBehaviorKind.swift @@ -0,0 +1,33 @@ +// +// AccessibilityChildBehaviorKind.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: B6DAE808DEA8C0752D7A012F5E22D918 (SwiftUICore) + +// MARK: - AccessibilityChildBehaviorKind + +package enum AccessibilityChildBehaviorKind: Codable { + case combine + case contain +} + +// MARK: - AccessibilityChildBehaviorKind + ProtobufEnum + +extension AccessibilityChildBehaviorKind: ProtobufEnum { + package var protobufValue: UInt { + switch self { + case .combine: 0 + case .contain: 1 + } + } + + package init?(protobufValue value: UInt) { + switch value { + case 0: self = .combine + case 1: self = .contain + default: return nil + } + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityCombinable.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCombinable.swift new file mode 100644 index 000000000..5039e886d --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCombinable.swift @@ -0,0 +1,52 @@ +// +// AccessibilityCombinable.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - AccessibilityCombinable + +package protocol AccessibilityCombinable { + @discardableResult + mutating func merge(with child: Self) -> Bool +} + +extension AccessibilityCombinable { + package func combined(with child: Self) -> Self { + var result = self + result.merge(with: child) + return result + } +} + +// MARK: - Optional + AccessibilityCombinable + +extension Optional: AccessibilityCombinable where Wrapped: AccessibilityCombinable { + @discardableResult + package mutating func merge(with child: Wrapped?) -> Bool { + switch (self, child) { + case (_, .none): + return false + case (.none, .some): + self = child + return true + case (.some(let lhs), .some(let rhs)): + var value = lhs + let result = value.merge(with: rhs) + self = value + return result + } + } +} + +// MARK: - Array + AccessibilityCombinable + +extension Array: AccessibilityCombinable { + @discardableResult + package mutating func merge(with child: [Element]) -> Bool { + let oldCount = count + append(contentsOf: child) + return count != oldCount + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityCore.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCore.swift index 3f96bf31a..b3793a353 100644 --- a/Sources/OpenSwiftUICore/Accessibility/AccessibilityCore.swift +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCore.swift @@ -3,10 +3,17 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: TODO +// Status: Complete + +public import Foundation +import OpenSwiftUI_SPI + +// MARK: - AccessibilityCore package enum AccessibilityCore {} +// MARK: - _ViewInputs/_GraphInputs + needsAccessibility + extension _GraphInputs { package var needsAccessibility: Bool { get { options.contains(.needsAccessibility) } @@ -21,25 +28,99 @@ extension _ViewInputs { } } +// MARK: - WithinAccessibilityRotor + package struct WithinAccessibilityRotor: ViewInputBoolFlag { - package init() {} + package init() { + _openSwiftUIEmptyStub() + } } extension _ViewInputs { @inline(__always) package var withinAccessibilityRotor: Bool { - needsAccessibility && self[WithinAccessibilityRotor.self] + get { self[WithinAccessibilityRotor.self] } + set { self[WithinAccessibilityRotor.self] = newValue } + } +} + +// MARK: - PlatformAccessibilityElementProtocol + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +#if canImport(ObjectiveC) +@objc +#endif +public protocol PlatformAccessibilityElementProtocol: NSObjectProtocol { + #if os(macOS) + @objc dynamic var knownRepresentedElement: PlatformAccessibilityElement { get } + @objc dynamic var rotorOwnerElement: PlatformAccessibilityElement { get } + #endif +} + +// MARK: - PlatformAccessibilityElement + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +public typealias PlatformAccessibilityElement = NSObject & PlatformAccessibilityElementProtocol + +// MARK: - NSObject + PlatformAccessibilityElementProtocol + +@_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) +extension NSObject: PlatformAccessibilityElementProtocol { + #if canImport(ObjectiveC) + @objc + #endif + dynamic open var knownRepresentedElement: PlatformAccessibilityElement { + self + } + + #if canImport(ObjectiveC) + @objc + #endif + dynamic open var rotorOwnerElement: PlatformAccessibilityElement { + self + } +} + +// MARK: - AccessibilityLayoutRole + +package enum AccessibilityLayoutRole: Hashable { + case stack + case grid +} + +// MARK: - Locale.bcp47LanguageCode + +extension Locale { + package var bcp47LanguageCode: String? { + guard let languageCode = language.languageCode?.identifier, + !languageCode.isEmpty else { + return nil + } + return language.maximalIdentifier } } -// TODO +// MARK: - AccessibilityCore + description extension AccessibilityCore { package static func description( for symbolName: String, in environment: EnvironmentValues ) -> String? { - _openSwiftUIUnimplementedWarning() - return nil + #if canImport(Darwin) + let description = soft_AXSSAccessibilityDescriptionForSymbolName( + symbolName, + environment.locale.identifier + ) + guard !description.isEmpty else { + return nil + } + return description.capitalized(with: environment.locale) + #else + _openSwiftUIPlatformUnimplementedFailure() + #endif } } diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityCustomAttributes.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCustomAttributes.swift new file mode 100644 index 000000000..6f259825d --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCustomAttributes.swift @@ -0,0 +1,207 @@ +// +// AccessibilityCustomAttributes.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: BF898C2367FAF9E98D04BBAD3CB75E24 (SwiftUICore) + +package import Foundation + +package struct AccessibilityCustomAttributes: Equatable { + package enum Value: Equatable { + case string(String) + case data(Data) + case nsNumber(NSNumber) + case nsValue(NSValue) + case date(Date) + case url(URL) + case attributedString(NSAttributedString) + case object(NSObject) + + package var displayDescription: String { + switch self { + case let .string(value): value + case let .data(value): value.description + case let .nsNumber(value): value.stringValue + case let .nsValue(value): value.description + case let .date(value): value.description + case let .url(value): value.absoluteString + case let .attributedString(value): value.string + case let .object(value): value.description + } + } + + package func axRepresentation() -> Any { + switch self { + case let .string(value): value as NSString + case let .data(value): value as NSData + case let .nsNumber(value): value + case let .nsValue(value): value + case let .date(value): value as NSDate + case let .url(value): value as NSURL + case let .attributedString(value): value + case let .object(value): value + } + } + } + + package init() { + attributes = [:] + } + + package init(_ attributeName: String, value: Value) { + attributes = [attributeName: value] + } + + private var attributes: AttributesDictionary + + package typealias AttributesDictionary = [String: Value] + + package var attributeNames: [String] { + Array(attributes.keys) + } + + package subscript(_ attributeName: String) -> Value? { + get { attributes[attributeName] } + set { attributes[attributeName] = newValue } + } + + package static func == (lhs: Self, rhs: Self) -> Bool { + guard lhs.attributes.count == rhs.attributes.count else { + return false + } + for (name, value) in lhs.attributes { + guard let other = rhs.attributes[name], value == other else { + return false + } + } + return true + } +} + +extension AccessibilityCustomAttributes: Collection { + package typealias Index = AttributesDictionary.Index + package typealias Element = AttributesDictionary.Element + + package var startIndex: Index { attributes.startIndex } + package var endIndex: Index { attributes.endIndex } + + package subscript(index: Index) -> Element { + attributes[index] + } + + package func index(after index: Index) -> Index { + attributes.index(after: index) + } +} + +extension AccessibilityCustomAttributes: CustomDebugStringConvertible { + package var debugDescription: String { + var result = "<" + for (name, value) in attributes { + result += "\(name): \(value)" + } + result += ">" + return result + } +} + +extension AccessibilityCustomAttributes: AccessibilityCombinable { + @discardableResult + package mutating func merge(with child: Self) -> Bool { + var changed = false + for (name, value) in child.attributes where attributes[name] == nil { + attributes[name] = value + changed = true + } + return changed + } +} + +extension AccessibilityCustomAttributes.Value: Codable { + private enum CodingKeys: String, CodingKey { + case string + case data + case nsNumber + case nsValue + case date + case url + case attributedString + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let value = try? container.decode(String.self, forKey: .string) { + self = .string(value) + } else if let value = try? container.decode(Data.self, forKey: .data) { + self = .data(value) + } else if let data = try? container.decode(Data.self, forKey: .nsNumber) { + let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) + guard let value = unarchiver.decodeObject(forKey: CodingKeys.nsNumber.rawValue) as? NSNumber else { + throw DecodingError.typeMismatch( + NSNumber.self, + .init(codingPath: decoder.codingPath, debugDescription: "Could not decode number!") + ) + } + self = .nsNumber(value) + } else if let data = try? container.decode(Data.self, forKey: .nsValue) { + let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) + guard let value = unarchiver.decodeObject(forKey: CodingKeys.nsValue.rawValue) as? NSValue else { + throw DecodingError.typeMismatch( + NSValue.self, + .init(codingPath: decoder.codingPath, debugDescription: "Could not decode value!") + ) + } + self = .nsValue(value) + } else if let value = try? container.decode(Date.self, forKey: .date) { + self = .date(value) + } else if let value = try? container.decode(URL.self, forKey: .url) { + self = .url(value) + } else if let data = try? container.decode(Data.self, forKey: .attributedString) { + let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data) + guard let value = unarchiver.decodeObject(forKey: CodingKeys.attributedString.rawValue) as? NSAttributedString else { + throw DecodingError.typeMismatch( + NSAttributedString.self, + .init(codingPath: decoder.codingPath, debugDescription: "Could not decode attributed string!") + ) + } + self = .attributedString(value) + } else { + throw DecodingError.typeMismatch( + AccessibilityCustomAttributes.self, + .init(codingPath: decoder.codingPath, debugDescription: "Could not decode custom attributes!") + ) + } + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .string(value): + try container.encode(value, forKey: .string) + case let .data(value): + try container.encode(value, forKey: .data) + case let .nsNumber(value): + let archiver = NSKeyedArchiver(requiringSecureCoding: true) + archiver.encode(value, forKey: CodingKeys.nsNumber.rawValue) + try container.encode(archiver.encodedData, forKey: .nsNumber) + case let .nsValue(value): + let archiver = NSKeyedArchiver(requiringSecureCoding: true) + archiver.encode(value, forKey: CodingKeys.nsValue.rawValue) + try container.encode(archiver.encodedData, forKey: .nsValue) + case let .date(value): + try container.encode(value, forKey: .date) + case let .url(value): + try container.encode(value, forKey: .url) + case let .attributedString(value): + let archiver = NSKeyedArchiver(requiringSecureCoding: true) + archiver.encode(value, forKey: CodingKeys.attributedString.rawValue) + try container.encode(archiver.encodedData, forKey: .attributedString) + case .object: + break + } + } +} + +extension AccessibilityCustomAttributes: Codable {} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityCustomContent.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCustomContent.swift new file mode 100644 index 000000000..742759437 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCustomContent.swift @@ -0,0 +1,253 @@ +// +// AccessibilityCustomContent.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: AB2166DCFF9943AF0BA6B5EE55765400 (SwiftUICore) + +#if canImport(Accessibility) +public import Accessibility +#endif + +// MARK: - AccessibilityCustomContentKey + +/// Key used to specify the identifier and label associated with +/// an entry of additional accessibility information. +/// +/// Use `AccessibilityCustomContentKey` and the associated modifiers taking +/// this value as a parameter in order to simplify clearing or replacing +/// entries of additional information that are manipulated from multiple places +/// in your code. +@available(OpenSwiftUI_v3_0, *) +public struct AccessibilityCustomContentKey { + enum Identifier: Equatable { + case string(String) + case text(Text) + } + + var identifier: Identifier + var label: Text + + /// Create an `AccessibilityCustomContentKey` with the specified label and + /// identifier. + /// + /// - Parameter label: Localized text describing to the user what + /// is contained in this additional information entry. For example: + /// "orientation". + /// - Parameter id: String used to identify the additional information entry + /// to OpenSwiftUI. Adding an entry will replace any previous value with the + /// same identifier. + public init(_ label: Text, id: String) { + identifier = .string(id) + self.label = label + } + + /// Create an `AccessibilityCustomContentKey` with the specified label and + /// identifier. + /// + /// - Parameter labelKey: Localized text describing to the user what + /// is contained in this additional information entry. For example: + /// "orientation". + /// - Parameter id: String used to identify the additional information entry + /// to OpenSwiftUI. Adding an entry will replace any previous value with the + /// same identifier. + public init(_ labelKey: LocalizedStringKey, id: String) { + self.init(Text(labelKey), id: id) + } + + /// Create an `AccessibilityCustomContentKey` with the specified label. + /// + /// - Parameter labelKey: Localized text describing to the user what + /// is contained in this additional information entry. For example: + /// "orientation". This will also be used as the identifier. + public init(_ labelKey: LocalizedStringKey) { + self.init(Text(labelKey)) + } + + package init(_ text: Text) { + identifier = .text(text) + label = text + } +} + +@available(*, unavailable) +extension AccessibilityCustomContentKey: Sendable {} + +@available(OpenSwiftUI_v3_0, *) +extension AccessibilityCustomContentKey: Equatable {} + +// MARK: - AccessibilityCustomContentEntry + +@_spi(Private) +@available(OpenSwiftUI_v5_0, *) +public struct AccessibilityCustomContentEntry: Equatable { + public typealias Importance = AXCustomContent.Importance + + enum Value: Equatable { + case text(Text, Importance) + case value(AnyAccessibilityValue, Importance) + case clear + } + + var key: AccessibilityCustomContentKey + var value: Value + + public init(_ key: AccessibilityCustomContentKey, value: Text, importance: Importance = .default) { + self.key = key + self.value = .text(value, importance) + } + + package init(_ key: AccessibilityCustomContentKey, value: V, importance: Importance = .default) + where V: AccessibilityValue & Codable { + self.key = key + self.value = .value(AnyAccessibilityValue(value), importance) + } + + package init(clearing key: AccessibilityCustomContentKey) { + self.key = key + value = .clear + } +} + +@_spi(Private) +@available(*, unavailable) +extension AccessibilityCustomContentEntry: Sendable {} + +// MARK: - AccessibilityCustomContentList + +package typealias AccessibilityCustomContentList = [AccessibilityCustomContentEntry] + +extension Array where Element == AccessibilityCustomContentEntry { + package func resolve(in environment: EnvironmentValues) -> [AXCustomContent] { + var result: [AXCustomContent] = [] + var identifiers: Set = [] + for entry in self { + let identifier = switch entry.key.identifier { + case let .string(string): string + case let .text(text): text.resolveString(in: environment) + } + guard !identifier.isEmpty, !identifiers.contains(identifier) else { + continue + } + identifiers.insert(identifier) + + let resolvedValue: String + let importance: Element.Importance + switch entry.value { + case let .text(value, valueImportance): + resolvedValue = value.resolveString(in: environment) + importance = valueImportance + case let .value(value, valueImportance): + resolvedValue = value.localizedDescription ?? "" + importance = valueImportance + case .clear: + continue + } + let content = AXCustomContent( + label: entry.key.label.resolveString(in: environment), + value: resolvedValue + ) + content.importance = importance + result.insert(content, at: 0) + } + return result + } +} + +// MARK: - CodableAccessibilityCustomContentList + +struct CodableAccessibilityCustomContentList: Codable { + private struct CodableEntry: Codable { + struct CodableKey: Codable { + var identifier: AccessibilityText? + var label: AccessibilityText? + + init(_ key: AccessibilityCustomContentKey, in environment: EnvironmentValues) { + switch key.identifier { + case let .string(string): + identifier = AccessibilityText(string) + case let .text(text): + identifier = AccessibilityText(text, environment: environment) + } + label = AccessibilityText(key.label, environment: environment) + } + } + + struct CodableValue: Codable { + enum Content: Codable { + case text(AccessibilityText) + case value(AnyAccessibilityValue) + } + + var clear: Bool = false + var content: Content? + @ProxyCodable var importance: AccessibilityCustomContentEntry.Importance? = nil + + init(_ value: AccessibilityCustomContentEntry.Value, in environment: EnvironmentValues) { + switch value { + case let .text(text, importance): + if let text = AccessibilityText(text, environment: environment) { + content = .text(text) + self.importance = importance + } + case let .value(value, importance): + content = .value(value) + self.importance = importance + case .clear: + clear = true + } + } + } + + var codableKey: CodableKey + var codableValue: CodableValue + } + + private var codableEntries: [CodableEntry] + + init(_ entries: AccessibilityCustomContentList, in environment: EnvironmentValues) { + codableEntries = entries.map { + CodableEntry( + codableKey: .init($0.key, in: environment), + codableValue: .init($0.value, in: environment) + ) + } + } + + var customContentList: AccessibilityCustomContentList { + codableEntries.compactMap { entry in + guard let identifier = entry.codableKey.identifier, let label = entry.codableKey.label else { + return nil + } + var key = AccessibilityCustomContentKey(identifier.text) + key.label = label.text + var result = AccessibilityCustomContentEntry(clearing: key) + if entry.codableValue.clear { + return result + } + guard let content = entry.codableValue.content, let importance = entry.codableValue.importance else { + return nil + } + switch content { + case let .text(text): + result.value = .text(text.text, importance) + case let .value(value): + result.value = .value(value, importance) + } + return result + } + } +} + +// MARK: - AXCustomContent.Importance + CodableByProxy + +extension AXCustomContent.Importance: CodableByProxy { + package var codingProxy: UInt { + rawValue + } + + package static func unwrap(codingProxy rawValue: UInt) -> Self { + Self(rawValue: rawValue)! + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityDataSeries.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityDataSeries.swift new file mode 100644 index 000000000..a2dae5df3 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityDataSeries.swift @@ -0,0 +1,202 @@ +// +// AccessibilityDataSeries.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: D54125E3A4CDE7D8F0604D8A75122DE8 (SwiftUICore) + +// MARK: - AccessibilityDataSeriesConfiguration + +@_spi(Private) +@available(OpenSwiftUI_v2_0, *) +public struct AccessibilityDataSeriesConfiguration { + public struct ValueDescription { + public var description: Text + public var effectiveValueRange: Range + + public init(description: Text, effectiveValueRange: Range) { + self.description = description + self.effectiveValueRange = effectiveValueRange + } + } + + public struct AxisConfiguration { + public var title: Text? + public var unitLabel: Text? + public var categoryLabels: [Text] + public var minimumValue: Double? + public var maximumValue: Double? + public var gridlinePositions: [Double] + public var values: [Double] + public var valueDescriptions: [ValueDescription] + + public init( + title: Text? = nil, + unitLabel: Text? = nil, + categoryLabels: [Text] = [], + minimumValue: Double? = nil, + maximumValue: Double? = nil, + gridlinePositions: [Double] = [], + values: [Double] = [], + valueDescriptions: [ValueDescription] = [] + ) { + self.title = title + self.unitLabel = unitLabel + self.categoryLabels = categoryLabels + self.minimumValue = minimumValue + self.maximumValue = maximumValue + self.gridlinePositions = gridlinePositions + self.values = values + self.valueDescriptions = valueDescriptions + } + } + + public enum DataSeriesType: Int, Codable { + case scatter + case line + case bar + } + + public var name: Text + public var type: DataSeriesType + public var supportsSonification: Bool + public var sonificationDuration: Double? + public var includesTrendlineInSonification: Bool + public var supportsSummarization: Bool + public var xAxisConfiguration: AxisConfiguration? + public var yAxisConfiguration: AxisConfiguration? + + public init( + name: Text, + type: DataSeriesType, + supportsSonification: Bool = false, + sonificationDuration: Double? = nil, + includesTrendlineInSonification: Bool = false, + supportsSummarization: Bool = false, + xAxisConfiguration: AxisConfiguration? = nil, + yAxisConfiguration: AxisConfiguration? = nil + ) { + self.name = name + self.type = type + self.supportsSonification = supportsSonification + self.sonificationDuration = sonificationDuration + self.includesTrendlineInSonification = includesTrendlineInSonification + self.supportsSummarization = supportsSummarization + self.xAxisConfiguration = xAxisConfiguration + self.yAxisConfiguration = yAxisConfiguration + } +} + +@_spi(Private) +@available(*, unavailable) +extension AccessibilityDataSeriesConfiguration.ValueDescription: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension AccessibilityDataSeriesConfiguration.AxisConfiguration: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension AccessibilityDataSeriesConfiguration.DataSeriesType: Sendable {} + +@_spi(Private) +@available(*, unavailable) +extension AccessibilityDataSeriesConfiguration: Sendable {} + +// MARK: - CodableAccessibilityDataSeriesConfiguration + +struct CodableAccessibilityDataSeriesConfiguration: Codable { + struct ValueDescription: Codable { + var description: CodableAccessibilityVersionStorage? + var effectiveValueRange: Range + } + + struct AxisConfiguration: Codable { + var title: CodableAccessibilityVersionStorage? + var unitLabel: CodableAccessibilityVersionStorage? + var categoryLabels: [CodableAccessibilityVersionStorage] + var minimumValue: Double? + var maximumValue: Double? + var gridlinePositions: [Double] + var values: [Double] + var valueDescriptions: [ValueDescription] + + init(_ configuration: AccessibilityDataSeriesConfiguration.AxisConfiguration, in environment: EnvironmentValues) { + title = configuration.title.flatMap { + .init(texts: [$0], in: environment, optional: false, idiom: nil) + } + unitLabel = configuration.unitLabel.flatMap { + .init(texts: [$0], in: environment, optional: false, idiom: nil) + } + categoryLabels = configuration.categoryLabels.compactMap { + .init(texts: [$0], in: environment, optional: false, idiom: nil) + } + minimumValue = configuration.minimumValue + maximumValue = configuration.maximumValue + gridlinePositions = configuration.gridlinePositions + values = configuration.values + valueDescriptions = configuration.valueDescriptions.map { + ValueDescription( + description: .init(texts: [$0.description], in: environment, optional: false, idiom: nil), + effectiveValueRange: $0.effectiveValueRange + ) + } + } + + var configuration: AccessibilityDataSeriesConfiguration.AxisConfiguration { + .init( + title: title?.text, + unitLabel: unitLabel?.text, + categoryLabels: categoryLabels.map { $0.text }, + minimumValue: minimumValue, + maximumValue: maximumValue, + gridlinePositions: gridlinePositions, + values: values, + valueDescriptions: valueDescriptions.map { + .init( + description: $0.description?.text ?? Text(verbatim: ""), + effectiveValueRange: $0.effectiveValueRange + ) + } + ) + } + } + + var name: CodableAccessibilityVersionStorage? + var type: AccessibilityDataSeriesConfiguration.DataSeriesType + var supportsSonification: Bool + var sonificationDuration: Double? + var includesTrendlineInSonification: Bool + var supportsSummarization: Bool + var xAxisConfiguration: AxisConfiguration? + var yAxisConfiguration: AxisConfiguration? + + init(_ configuration: AccessibilityDataSeriesConfiguration, in environment: EnvironmentValues) { + name = .init(texts: [configuration.name], in: environment, optional: false, idiom: nil) + type = configuration.type + supportsSonification = configuration.supportsSonification + sonificationDuration = configuration.sonificationDuration + includesTrendlineInSonification = configuration.includesTrendlineInSonification + supportsSummarization = configuration.supportsSummarization + xAxisConfiguration = configuration.xAxisConfiguration.map { + AxisConfiguration($0, in: environment) + } + yAxisConfiguration = configuration.yAxisConfiguration.map { + AxisConfiguration($0, in: environment) + } + } + + var configuration: AccessibilityDataSeriesConfiguration { + .init( + name: name?.text ?? Text(verbatim: ""), + type: type, + supportsSonification: supportsSonification, + sonificationDuration: sonificationDuration, + includesTrendlineInSonification: includesTrendlineInSonification, + supportsSummarization: supportsSummarization, + xAxisConfiguration: xAxisConfiguration?.configuration, + yAxisConfiguration: yAxisConfiguration?.configuration + ) + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift index 6c7d6271b..6181c4f14 100644 --- a/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityEnvironment.swift @@ -6,7 +6,7 @@ // Status: Complete // ID: 1E80A5D8CD82563C298D64AC1337E839 (SwiftUICore) -// MARK: - Accessibility Environment Values [WIP] +// MARK: - Accessibility Environment Values private struct AccessibilityEnabledKey: EnvironmentKey { static var defaultValue: Bool { false } diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityIdentifierStorage.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityIdentifierStorage.swift new file mode 100644 index 000000000..e4a344629 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityIdentifierStorage.swift @@ -0,0 +1,63 @@ +// +// AccessibilityIdentifierStorage.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - AccessibilityIdentifierStorage + +package struct AccessibilityIdentifierStorage: Equatable { + package enum Placement: Equatable { + case assign + case optional + case suffix + } + + let rawValue: String + + package let placement: Placement + + package init(_ value: String, placement: Placement = .assign) { + rawValue = value + self.placement = placement + } + + package var value: String? { + switch placement { + case .assign, .optional: + rawValue + case .suffix: + nil + } + } +} + +// MARK: - AccessibilityIdentifierStorage + AccessibilityCombinable + +extension AccessibilityIdentifierStorage: AccessibilityCombinable { + package mutating func merge(with child: AccessibilityIdentifierStorage) -> Bool { + switch (placement, child.placement) { + case (.optional, .assign), (.optional, .suffix), (.suffix, .assign): + self = child + return true + case (.assign, .suffix), (.suffix, .suffix): + self = [self, child].joined() ?? self + return true + default: + return false + } + } +} + +// MARK: - RangeReplaceableCollection + AccessibilityIdentifierStorage + +extension RangeReplaceableCollection where Element == AccessibilityIdentifierStorage { + package func joined(separator: String = ".") -> AccessibilityIdentifierStorage? { + let values = map { $0.rawValue }.filter { !$0.isEmpty } + guard !values.isEmpty else { + return nil + } + return AccessibilityIdentifierStorage(values.joined(separator: separator)) + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityLabelStorage.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityLabelStorage.swift new file mode 100644 index 000000000..c4039fb0a --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityLabelStorage.swift @@ -0,0 +1,55 @@ +// +// AccessibilityLabelStorage.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +// MARK: - AccessibilityLabelStorage + +package struct AccessibilityLabelStorage: Equatable { + package enum Placement: Equatable { + case prefix + case suffix + case assign + case optional + } + + package var texts: [Text] + + package var placement: Placement + + package init(texts: [Text], placement: Placement = .assign) { + self.texts = texts + self.placement = placement + } + + @discardableResult + package mutating func removing(_ text: Text) -> Bool { + guard let index = texts.firstIndex(of: text) else { + return false + } + texts.remove(at: index) + return true + } +} + +// MARK: - AccessibilityLabelStorage + AccessibilityCombinable + +extension AccessibilityLabelStorage: AccessibilityCombinable { + package mutating func merge(with child: AccessibilityLabelStorage) -> Bool { + switch (placement, child.placement) { + case (.optional, .prefix), (.optional, .suffix), (.optional, .assign): + self = child + return true + case (_, .optional), (.assign, .assign): + return false + case (_, .prefix), (.suffix, _): + texts.insert(contentsOf: child.texts, at: 0) + return true + case (_, .suffix), (.prefix, _): + texts.append(contentsOf: child.texts) + return true + } + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityNodeAttachment.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityNodeAttachment.swift new file mode 100644 index 000000000..aa31ef4c9 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityNodeAttachment.swift @@ -0,0 +1,514 @@ +// +// AccessibilityNodeAttachment.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: E6F55EC17684DEB3593A96D781D9FC84 (SwiftUICore) + +import libAccessibilityPrivate +import Foundation + +// MARK: - AccessibilityNodeAttachment + +package struct AccessibilityNodeAttachment { + package enum Storage { + case codable(CodableAccessibilityAttachmentStorage) + case attachment(AccessibilityAttachment) + } + + package enum Kind: Codable, Hashable { + case standard + case placeholder + } + + package var storage: Storage + package var createsNode: Bool + package var kind: Kind + + package func makeAttachment(with applicator: A.Type) -> AccessibilityAttachment + where A: AccessibilityPlatformPropertyApplicator { + switch storage { + case let .codable(storage): + AccessibilityAttachment(storage, applicator: applicator) + case let .attachment(attachment): + attachment + } + } + + package init( + _ attachment: AccessibilityAttachment, + platformStorage: AccessibilityPlatformPropertyStorage, + in environment: EnvironmentValues, + idiom: AnyInterfaceIdiom, + createsNode: Bool, + kind: Kind, + encode: Bool + ) { + storage = encode ? .codable(CodableAccessibilityAttachmentStorage( + attachment, + platformStorage: platformStorage, + in: environment, + idiom: idiom + )) : .attachment(attachment) + self.createsNode = createsNode + self.kind = kind + } +} + +extension AccessibilityNodeAttachment: ProtobufMessage { + private enum Error: Swift.Error { + case invalidStorage + } + + package func encode(to encoder: inout ProtobufEncoder) throws { + guard case let .codable(storage) = storage else { + throw Error.invalidStorage + } + try encoder.messageField(1, storage) + encoder.boolField(2, createsNode) + if encoder.archiveOptions.deploymentVersion < .v6 { + try encoder.codableField(3, kind) + } else { + encoder.enumField(4, kind) + } + } + + package init(from decoder: inout ProtobufDecoder) throws { + var storage: CodableAccessibilityAttachmentStorage? + createsNode = false + kind = .standard + while let field = try decoder.nextField() { + switch field.tag { + case 1: storage = try decoder.messageField(field) + case 2: createsNode = try decoder.boolField(field) + case 3: kind = try decoder.codableField(field) ?? .standard + case 4: kind = try decoder.enumField(field) ?? .standard + default: try decoder.skipField(field) + } + } + guard let storage else { + throw ProtobufDecoder.DecodingError.failed + } + self.storage = .codable(storage) + } +} + +extension AccessibilityNodeAttachment.Kind: ProtobufEnum { + package var protobufValue: UInt { + switch self { + case .standard: 0 + case .placeholder: 1 + } + } + + package init?(protobufValue value: UInt) { + switch value { + case 0: self = .standard + case 1: self = .placeholder + default: return nil + } + } +} + +// MARK: - AccessibilityPlatformPropertyStorage + +package struct AccessibilityPlatformPropertyStorage { + package var explicitRole: String? + package var explicitSubrole: String? + package var explicitTraits: CodableAccessibilityUIKitTraits? + + package init( + explicitRole: String? = nil, + explicitSubrole: String? = nil, + explicitTraits: CodableAccessibilityUIKitTraits? = nil + ) { + self.explicitRole = explicitRole + self.explicitSubrole = explicitSubrole + self.explicitTraits = explicitTraits + } +} + +package protocol AccessibilityPlatformPropertyApplicator { + static func apply(_ storage: AccessibilityPlatformPropertyStorage, to properties: inout AccessibilityProperties) +} + +// MARK: - CodableAccessibilityAttachmentStorage + +package struct CodableAccessibilityAttachmentStorage: ProtobufMessage { + package struct VBase { + var identifier: String? + var label: CodableAccessibilityVersionStorage? + var hint: CodableAccessibilityVersionStorage? + var roleDescription: CodableAccessibilityVersionStorage? + var visibility: AccessibilityNullableOptionSet + var traits: AccessibilityNullableOptionSet + var sortPriority: Double? + var explicitAutomationType: AXAutomationType? + var dataSeriesConfiguration: CodableAccessibilityDataSeriesConfiguration? + var linkDestination: LinkDestination.Configuration? + var customAttributes: AccessibilityCustomAttributes? + } + + package struct V2 { + var base: VBase + var textValue: ResolvedStyledText? + } + + package struct V3 { + var base: VBase + var platformStorage: AccessibilityPlatformPropertyStorage = .init() + var value: CodableAccessibilityValueStorage? + var inputLabels: [AccessibilityText]? + var activationPoint: AccessibilityActivationPoint.Location? + var customContentList: CodableAccessibilityCustomContentList? + var textHeadingLevel: AccessibilityHeadingLevel? + var textContentType: AccessibilityTextContentType? + var chartDescriptor: CodableAXChartDescriptor? + var locale: String? + var childBehaviorKind: AccessibilityChildBehaviorKind? + } + + var base: CodableAccessibilityVersionStorage + + package init( + _ attachment: AccessibilityAttachment, + platformStorage: AccessibilityPlatformPropertyStorage, + in environment: EnvironmentValues, + idiom: AnyInterfaceIdiom + ) { + let properties = attachment.properties + let base = VBase( + identifier: properties.identifierStorage.flatMap { + $0.placement == .optional ? nil : $0.value + }, + label: .init( + texts: properties.labelStorage?.texts, + in: environment, + optional: properties.labelStorage?.placement == .optional, + idiom: idiom + ), + hint: .init(texts: properties.hints, in: environment, optional: false, idiom: idiom), + roleDescription: .init( + texts: properties.roleDescription.map { [$0] }, + in: environment, + optional: false, + idiom: idiom + ), + visibility: properties.visibility, + traits: properties.traits, + sortPriority: properties.sortPriority, + explicitAutomationType: properties.explicitAutomationType, + dataSeriesConfiguration: properties.dataSeriesConfiguration.map { + CodableAccessibilityDataSeriesConfiguration($0, in: environment) + }, + linkDestination: properties.linkDestination, + customAttributes: properties.customAttributes + ) + switch CodableAccessibilityVersion.current { + case .v2: + let textValue: ResolvedStyledText? = { + guard let value = properties.value else { + return nil + } + let texts = value.description.text + guard let storage = AccessibilityCore.textsResolvedToAttributedText( + texts, + in: environment, + includeResolvableAttributes: false, + includeDefaultAttributes: true, + updateResolvableAttributes: false, + resolveSuffix: false, + idiom: nil + ) else { + return nil + } + return ResolvedStyledText.styledText( + storage: storage, + environment: environment, + isCollapsible: texts.contains { $0.isCollapsible() }, + writingMode: nil + ) + }() + self.base = .v2(V2(base: base, textValue: textValue)) + case .v3: + self.base = .v3(V3( + base: base, + platformStorage: platformStorage, + value: properties.value.map { + CodableAccessibilityValueStorage($0, in: environment, idiom: idiom) + }, + inputLabels: properties.inputLabels?.compactMap { + AccessibilityText($0, environment: environment) + }, + activationPoint: properties.activationPoint, + customContentList: properties.customContentList.isEmpty ? nil : + CodableAccessibilityCustomContentList(properties.customContentList, in: environment), + textHeadingLevel: properties.textHeadingLevel, + textContentType: properties.textContentType, + chartDescriptor: properties.chartDescriptor.map { CodableAXChartDescriptor($0) }, + locale: environment.locale.identifier, + childBehaviorKind: properties.childBehaviorKind + )) + } + } + + package func encode(to encoder: inout ProtobufEncoder) throws { + try base.encode(to: &encoder) + } + + package init(from decoder: inout ProtobufDecoder) throws { + base = try CodableAccessibilityVersionStorage(from: &decoder) + } +} + +extension CodableAccessibilityAttachmentStorage.VBase: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + if let identifier { try encoder.stringField(1, identifier) } + if let label { try encoder.messageField(2, label) } + if let hint { try encoder.messageField(3, hint) } + if let roleDescription { try encoder.messageField(4, roleDescription) } + try encoder.messageField(5, visibility) + try encoder.messageField(6, traits) + if let sortPriority { encoder.doubleField(7, sortPriority, defaultValue: nil) } + if let explicitAutomationType { + encoder.fixed64Field(8, explicitAutomationType.rawValue, defaultValue: nil) + } + if let dataSeriesConfiguration { try encoder.codableField(9, dataSeriesConfiguration) } + if let linkDestination { try encoder.codableField(10, linkDestination) } + if let customAttributes { try encoder.codableField(11, customAttributes) } + } + + package init(from decoder: inout ProtobufDecoder) throws { + var visibility: AccessibilityNullableOptionSet? + var traits: AccessibilityNullableOptionSet? + while let field = try decoder.nextField() { + switch field.tag { + case 1: identifier = try decoder.stringField(field) + case 2: label = try decoder.messageField(field) + case 3: hint = try decoder.messageField(field) + case 4: roleDescription = try decoder.messageField(field) + case 5: visibility = try decoder.messageField(field) + case 6: traits = try decoder.messageField(field) + case 7: sortPriority = try decoder.doubleField(field) + case 8: explicitAutomationType = AXAutomationType(rawValue: try decoder.fixed64Field(field)) + case 9: dataSeriesConfiguration = .some(try decoder.codableField(field)) + case 10: linkDestination = .some(try decoder.codableField(field)) + case 11: customAttributes = .some(try decoder.codableField(field)) + default: try decoder.skipField(field) + } + } + guard let visibility, let traits else { + throw ProtobufDecoder.DecodingError.failed + } + self.visibility = visibility + self.traits = traits + } +} + +extension CodableAccessibilityAttachmentStorage.V2: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + try encoder.messageField(1, base) + if let textValue { + try encoder.messageField(2, CodableResolvedStyledText(base: textValue)) + } + } + + package init(from decoder: inout ProtobufDecoder) throws { + var base: CodableAccessibilityAttachmentStorage.VBase? + while let field = try decoder.nextField() { + switch field.tag { + case 1: base = try decoder.messageField(field) + case 2: + let value: CodableResolvedStyledText = try decoder.messageField(field) + textValue = value.base + default: try decoder.skipField(field) + } + } + guard let base else { + throw ProtobufDecoder.DecodingError.failed + } + self.base = base + } +} + +extension CodableAccessibilityAttachmentStorage.V3: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + try encoder.messageField(1, base) + if let value { try encoder.codableField(2, value) } + if let inputLabels { + for label in inputLabels { + try encoder.messageField(3, label) + } + } + if let activationPoint { try encoder.messageField(4, activationPoint) } + if let customContentList { try encoder.codableField(5, customContentList) } + if let textContentType { encoder.enumField(6, textContentType) } + if let role = platformStorage.explicitRole { try encoder.stringField(7, role) } + if let subrole = platformStorage.explicitSubrole { try encoder.stringField(8, subrole) } + if let chartDescriptor { try encoder.codableField(9, chartDescriptor) } + if let locale { try encoder.stringField(10, locale) } + if let traits = platformStorage.explicitTraits { try encoder.messageField(11, traits) } + if let textHeadingLevel { encoder.enumField(12, textHeadingLevel) } + if let childBehaviorKind { + if encoder.archiveOptions.deploymentVersion < .v6 { + try encoder.codableField(13, childBehaviorKind) + } else { + encoder.enumField(14, childBehaviorKind) + } + } + } + + package init(from decoder: inout ProtobufDecoder) throws { + var base: CodableAccessibilityAttachmentStorage.VBase? + while let field = try decoder.nextField() { + switch field.tag { + case 1: base = try decoder.messageField(field) + case 2: value = .some(try decoder.codableField(field)) + case 3: + let label: AccessibilityText = try decoder.messageField(field) + if inputLabels == nil { inputLabels = [] } + inputLabels?.append(label) + case 4: activationPoint = try decoder.messageField(field) + case 5: customContentList = .some(try decoder.codableField(field)) + case 6: textContentType = try decoder.enumField(field) + case 7: platformStorage.explicitRole = try decoder.stringField(field) + case 8: platformStorage.explicitSubrole = try decoder.stringField(field) + case 9: chartDescriptor = .some(try decoder.codableField(field)) + case 10: locale = try decoder.stringField(field) + case 11: platformStorage.explicitTraits = try decoder.messageField(field) + case 12: textHeadingLevel = try decoder.enumField(field) + case 13: childBehaviorKind = try decoder.codableField(field) + case 14: childBehaviorKind = try decoder.enumField(field) + default: try decoder.skipField(field) + } + } + guard let base else { + throw ProtobufDecoder.DecodingError.failed + } + self.base = base + } +} + +// MARK: - AccessibilityAttachment + CodableAccessibilityAttachmentStorage + +extension AccessibilityAttachment { + package init(_ storage: CodableAccessibilityAttachmentStorage, applicator: A.Type) + where A: AccessibilityPlatformPropertyApplicator { + var properties = AccessibilityProperties() + let base: CodableAccessibilityAttachmentStorage.VBase + switch storage.base { + case let .v2(storage): + base = storage.base + properties.value = storage.textValue.map { + AccessibilityValueStorage(description: $0.accessibilityText) + } + case let .v3(storage): + base = storage.base + properties.value = storage.value?.accessibilityValue + properties.inputLabels = storage.inputLabels?.map { $0.text } ?? [] + properties.activationPointStorage = storage.activationPoint.map { + AccessibilityActivationPointStorage(kind: .activate, point: $0) + } + properties.customContentList = storage.customContentList?.customContentList ?? [] + properties.textHeadingLevel = storage.textHeadingLevel + properties.textContentType = storage.textContentType + properties.chartDescriptor = storage.chartDescriptor?.storage + properties.locale = storage.locale.map { Locale(identifier: $0) } + properties.childBehaviorKind = storage.childBehaviorKind + A.apply(storage.platformStorage, to: &properties) + } + properties.labelStorage = base.label.map { label in + let placement: AccessibilityLabelStorage.Placement + if case let .v3(text) = label, text.optional { + placement = .optional + } else { + placement = .assign + } + return AccessibilityLabelStorage(texts: [label.text], placement: placement) + } + properties.hints = base.hint.map { [$0.text] } ?? [] + properties.identifierStorage = base.identifier.map { AccessibilityIdentifierStorage($0) } + properties.visibility = base.visibility + properties.traits = base.traits + properties.sortPriority = base.sortPriority + properties.explicitAutomationType = base.explicitAutomationType + properties.roleDescription = base.roleDescription?.text + properties.dataSeriesConfiguration = base.dataSeriesConfiguration?.configuration + properties.linkDestination = base.linkDestination + properties.customAttributes = base.customAttributes + self.init(properties: properties) + } +} + +// MARK: - CodableAccessibilityUIKitTraits + +package struct CodableAccessibilityUIKitTraits { + package var removed: UInt64 + package var added: UInt64 + + package init(removed: UInt64 = 0, added: UInt64 = 0) { + self.removed = removed + self.added = added + } +} + +extension CodableAccessibilityUIKitTraits: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.fixed64Field(1, removed) + encoder.fixed64Field(2, added) + } + + package init(from decoder: inout ProtobufDecoder) throws { + self.init() + while let field = try decoder.nextField() { + switch field.tag { + case 1: removed = try decoder.fixed64Field(field) + case 2: added = try decoder.fixed64Field(field) + default: try decoder.skipField(field) + } + } + } +} + +// MARK: - CodableAccessibilityVersion + +enum CodableAccessibilityVersion: Sendable { + case v2 + case v3 + + static let current: CodableAccessibilityVersion = Semantics.AccessibilityCodableVersion3.isEnabled ? .v3 : .v2 +} + +// MARK: - CodableAccessibilityVersionStorage + +enum CodableAccessibilityVersionStorage { + case v2(V2) + case v3(V3) +} + +extension CodableAccessibilityVersionStorage: CodableByProtobuf {} + +extension CodableAccessibilityVersionStorage: ProtobufMessage { + func encode(to encoder: inout ProtobufEncoder) throws { + switch self { + case let .v2(value): try encoder.messageField(2, value) + case let .v3(value): try encoder.messageField(3, value) + } + } + + init(from decoder: inout ProtobufDecoder) throws { + var value: Self? + while let field = try decoder.nextField() { + switch field.tag { + case 2: value = .v2(try decoder.messageField(field)) + case 3: value = .v3(try decoder.messageField(field)) + default: try decoder.skipField(field) + } + } + guard let value else { + throw ProtobufDecoder.DecodingError.failed + } + self = value + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityNullableOptionSet.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityNullableOptionSet.swift new file mode 100644 index 000000000..67ed9c1fa --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityNullableOptionSet.swift @@ -0,0 +1,112 @@ +// +// AccessibilityNullableOptionSet.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 3F072AF2D37E4BBD060A42F9AFFB5042 (SwiftUICore?) + +package struct AccessibilityNullableOptionSet: Equatable, Hashable, Codable +where T: Codable & Hashable & OptionSet, T.RawValue: Codable & FixedWidthInteger & UnsignedInteger { + package private(set) var value: T + package private(set) var mask: T + + package init() { + value = T() + mask = T() + } + + package init(implying bits: T.Element...) { + value = T(bits) + mask = T() + } + + package init(adding bits: T.Element...) { + value = T(bits) + mask = value + } + + package init(removing bits: T.Element...) { + value = T() + mask = T(bits) + } + + package init(adding: T.Element..., removing: T.Element...) { + value = T(adding) + mask = value + mask.formUnion(T(removing)) + } + + package subscript(bit: T.Element, default defaultValue: Bool) -> Bool { + self[bit] ?? defaultValue + } + + package subscript(bit: T.Element) -> Bool? { + get { + if value.contains(bit) { + return true + } + return mask.contains(bit) ? false : nil + } + set { + if let newValue { + mask.insert(bit) + if newValue { + value.insert(bit) + } else { + value.remove(bit) + } + } else { + mask.remove(bit) + value.remove(bit) + } + } + } + + package var isDefault: Bool { + mask.isEmpty && value.isEmpty + } + + package func isSet(_ bit: T.Element) -> Bool { + mask.contains(bit) + } +} + +extension AccessibilityNullableOptionSet: CustomStringConvertible { + package var description: String { + "AccessibilityNullableOptionSet" + } +} + +extension AccessibilityNullableOptionSet: AccessibilityCombinable { + @discardableResult + package mutating func merge(with other: Self) -> Bool { + let newValue = other.value.subtracting(mask) + let newMask = other.mask.subtracting(mask) + mask.formUnion(other.mask) + value.subtract(newMask) + value.formUnion(newValue) + return newValue.isEmpty || newMask.isEmpty + } +} + +extension AccessibilityNullableOptionSet: ProtobufMessage { + package func encode(to encoder: inout ProtobufEncoder) throws { + encoder.uint64Field(1, UInt64(value.rawValue)) + encoder.uint64Field(2, UInt64(mask.rawValue)) + } + + package init(from decoder: inout ProtobufDecoder) throws { + self.init() + while let field = try decoder.nextField() { + switch field.tag { + case 1: + value = T(rawValue: T.RawValue(try decoder.uint64Field(field))) + case 2: + mask = T(rawValue: T.RawValue(try decoder.uint64Field(field))) + default: + try decoder.skipField(field) + } + } + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityNumeric.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityNumeric.swift new file mode 100644 index 000000000..2cd9d3f3f --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityNumeric.swift @@ -0,0 +1,160 @@ +// +// AccessibilityNumeric.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package import Foundation + +// MARK: - AccessibilityNumeric + +package protocol AccessibilityNumeric { + var isValidMinValue: Bool { get } + var isValidMaxValue: Bool { get } + func asNumber() -> AccessibilityNumber? +} + +extension AccessibilityNumeric where Self: FixedWidthInteger { + package var isValidMinValue: Bool { + guard !isNaN, isFinite else { + return false + } + return !Self.isSigned || Self.bitWidth == 8 || self > .min + } + + package var isValidMaxValue: Bool { + !isNaN && isFinite && self < .max + } +} + +extension AccessibilityNumeric where Self: BinaryFloatingPoint { + package var isValidMinValue: Bool { + !isNaN && !isSignalingNaN && isFinite && self > -Self.greatestFiniteMagnitude + } + + package var isValidMaxValue: Bool { + !isNaN && !isSignalingNaN && isFinite && self < Self.greatestFiniteMagnitude + } +} + +// MARK: - Standard Library + AccessibilityNumeric + +extension Int: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension Int8: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension Int16: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension Int32: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension Int64: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension UInt: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension UInt8: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension UInt16: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension UInt32: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension UInt64: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension Double: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +extension Float: AccessibilityNumeric { + package func asNumber() -> AccessibilityNumber? { + AccessibilityNumber(NSNumber(value: self)) + } +} + +// MARK: - ClosedRange + AccessibilityNumeric + +extension ClosedRange where Bound: Strideable { + package var minimumValue: (any AccessibilityNumeric)? { + guard let value = lowerBound as? any AccessibilityNumeric, value.isValidMinValue else { + return nil + } + return value + } + + package var maximumValue: (any AccessibilityNumeric)? { + guard let value = upperBound as? any AccessibilityNumeric, value.isValidMaxValue else { + return nil + } + return value + } +} + +// MARK: - AccessibilityValueStorage + Numeric + +extension AccessibilityValueStorage { + @_disfavoredOverload + package init( + _ value: V, + from lowerBound: V? = nil, + to upperBound: V? = nil, + description: Text? + ) where V: Numeric { + guard let number = (value as? any AccessibilityNumeric)?.asNumber() else { + self.init("\(value)", description: description) + return + } + if lowerBound == nil, upperBound == nil { + self.init(number, description: description) + } else { + self.init( + AccessibilityBoundedNumber( + value: number, + minValue: (lowerBound as? any AccessibilityNumeric)?.asNumber(), + maxValue: (upperBound as? any AccessibilityNumeric)?.asNumber() + ), + description: description + ) + } + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties+Keys.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties+Keys.swift new file mode 100644 index 000000000..a31f3b80e --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties+Keys.swift @@ -0,0 +1,206 @@ +// +// AccessibilityProperties+Keys.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package import Foundation +package import libAccessibilityPrivate + +// MARK: - AccessibilityProperties + Keys + +extension AccessibilityProperties { + package struct ActivationPointKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityActivationPointStorage.self + } + + package var activationPointStorage: AccessibilityActivationPointStorage? { + get { self[ActivationPointKey.self] } + set { self[ActivationPointKey.self] = newValue } + } + + package var activationPoint: AccessibilityActivationPoint.Location? { + activationPointStorage?.activate + } + + package struct ChartDescriptorKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityChartDescriptorStorage.self + } + + package var chartDescriptor: AccessibilityChartDescriptorStorage? { + get { self[ChartDescriptorKey.self] } + set { self[ChartDescriptorKey.self] = newValue } + } + + package struct ChildBehaviorKind: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityChildBehaviorKind.self + } + + package var childBehaviorKind: AccessibilityChildBehaviorKind? { + get { self[ChildBehaviorKind.self] } + set { self[ChildBehaviorKind.self] = newValue } + } + + package struct CustomContentListKey: AccessibilityPropertiesKey { + package static let defaultValue: AccessibilityCustomContentList = [] + } + + package var customContentList: AccessibilityCustomContentList { + get { self[CustomContentListKey.self] } + set { self[CustomContentListKey.self] = newValue } + } + + package struct CustomAttributesKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityCustomAttributes.self + } + + package var customAttributes: AccessibilityCustomAttributes? { + get { self[CustomAttributesKey.self] } + set { self[CustomAttributesKey.self] = newValue } + } + + package struct DataSeriesConfigurationKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityDataSeriesConfiguration.self + } + + package var dataSeriesConfiguration: AccessibilityDataSeriesConfiguration? { + get { self[DataSeriesConfigurationKey.self] } + set { self[DataSeriesConfigurationKey.self] = newValue } + } + + package struct AutomationTypeKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AXAutomationType.self + } + + package var explicitAutomationType: AXAutomationType? { + get { self[AutomationTypeKey.self] } + set { self[AutomationTypeKey.self] = newValue } + } + + package struct IdentifierKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityIdentifierStorage.self + } + + package var identifierStorage: AccessibilityIdentifierStorage? { + get { self[IdentifierKey.self] } + set { self[IdentifierKey.self] = newValue } + } + + package struct HintsKey: AccessibilityPropertiesKey { + package static let defaultValue: [Text] = [] + } + + package var hints: [Text] { + get { self[HintsKey.self] } + set { self[HintsKey.self] = newValue } + } + + package struct InputLabelsKey: AccessibilityOptionalPropertiesKey { + package static let valueType = [Text].self + } + + package var inputLabels: [Text]? { + get { self[InputLabelsKey.self] } + set { self[InputLabelsKey.self] = newValue } + } + + package struct LabelKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityLabelStorage.self + } + + package var labelStorage: AccessibilityLabelStorage? { + get { self[LabelKey.self] } + set { self[LabelKey.self] = newValue } + } + + package struct LocaleKey: AccessibilityOptionalPropertiesKey { + package static let valueType = Locale.self + } + + package var locale: Locale? { + get { self[LocaleKey.self] } + set { self[LocaleKey.self] = newValue } + } + + package struct LinkDestinationKey: AccessibilityOptionalPropertiesKey { + package static let valueType = LinkDestination.Configuration.self + } + + package var linkDestination: LinkDestination.Configuration? { + get { self[LinkDestinationKey.self] } + set { self[LinkDestinationKey.self] = newValue } + } + + package struct RoleDescriptionKey: AccessibilityOptionalPropertiesKey { + package static let valueType = Text.self + } + + package var roleDescription: Text? { + get { self[RoleDescriptionKey.self] } + set { self[RoleDescriptionKey.self] = newValue } + } + + package struct SortPriorityKey: AccessibilityOptionalPropertiesKey { + package static let valueType = Double.self + } + + package var sortPriority: Double? { + get { self[SortPriorityKey.self] } + set { self[SortPriorityKey.self] = newValue } + } + + package struct TextContentTypeKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityTextContentType.self + } + + package var textContentType: AccessibilityTextContentType? { + get { self[TextContentTypeKey.self] } + set { self[TextContentTypeKey.self] = newValue } + } + + package struct TextHeadingLevelKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityHeadingLevel.self + } + + package var textHeadingLevel: AccessibilityHeadingLevel? { + get { self[TextHeadingLevelKey.self] } + set { self[TextHeadingLevelKey.self] = newValue } + } + + package struct TraitsKey: AccessibilityPropertiesKey { + package static let defaultValue: AccessibilityTraitStorage = .init() + } + + package var traits: AccessibilityTraitStorage { + get { self[TraitsKey.self] } + set { self[TraitsKey.self] = newValue } + } + + package subscript(trait trait: AccessibilityTrait) -> Bool? { + get { traits[trait] } + set { traits[trait] = newValue } + } + + package subscript(trait trait: AccessibilityTrait, default defaultValue: Bool) -> Bool { + traits[trait, default: defaultValue] + } + + package struct ValueKey: AccessibilityOptionalPropertiesKey { + package static let valueType = AccessibilityValueStorage.self + } + + package var value: AccessibilityValueStorage? { + get { self[ValueKey.self] } + set { self[ValueKey.self] = newValue } + } + + package struct VisibilityKey: AccessibilityPropertiesKey { + package static let defaultValue: AccessibilityVisibilityStorage = .init() + } + + package var visibility: AccessibilityVisibilityStorage { + get { self[VisibilityKey.self] } + set { self[VisibilityKey.self] = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift index 14acd7ad7..8568c5084 100644 --- a/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift @@ -1,5 +1,238 @@ // // AccessibilityProperties.swift -// OpenSwiftUI +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package import OpenAttributeGraphShims + +// MARK: - AccessibilityProperties + +package struct AccessibilityProperties: Equatable { + private var storage: [ObjectIdentifier: any AnyAccessibilityPropertiesEntry] + + package var isEmpty: Bool { + storage.isEmpty + } + + package init(_ key: K.Type, _ value: K.PropertyValue) where K: AccessibilityPropertiesKey { + self.init(reserving: 1) + storage[ObjectIdentifier(K.self)] = AccessibilityPropertiesEntry(value) + } + + package init(_ key: K.Type, _ value: K.PropertyValue?) where K: AccessibilityPropertiesKey { + if let value { + self.init(key, value) + } else { + self.init() + } + } + + package init(reserving count: Int = 0) { + storage = Dictionary(minimumCapacity: count) + } + + package mutating func update(_ work: (inout [ObjectIdentifier: any AnyAccessibilityPropertiesEntry]) -> Void) { + work(&storage) + } + + package static func == (lhs: AccessibilityProperties, rhs: AccessibilityProperties) -> Bool { + guard lhs.storage.count == rhs.storage.count else { + return false + } + for (key, value) in lhs.storage { + guard let other = rhs.storage[key], value.isEqual(to: other) else { + return false + } + } + return true + } +} + +extension AccessibilityProperties { + package subscript(key: K.Type) -> K.PropertyValue where K: AccessibilityPropertiesKey { + get { + storage[ObjectIdentifier(K.self)]?.anyValue as? K.PropertyValue ?? K.defaultValue + } + set { + if K.isDefault(newValue) { + storage.removeValue(forKey: ObjectIdentifier(K.self)) + } else { + storage[ObjectIdentifier(K.self)] = AccessibilityPropertiesEntry(newValue) + } + } + } +} + +// MARK: - AnyAccessibilityPropertiesEntry + +package protocol AnyAccessibilityPropertiesEntry { + var anyValue: Any { get } + + func isEqual(to other: any AnyAccessibilityPropertiesEntry) -> Bool +} + +extension AnyAccessibilityPropertiesEntry { + package func isEqual(to other: any AnyAccessibilityPropertiesEntry) -> Bool { + false + } +} + +// MARK: - AccessibilityPropertiesEntry + +package struct AccessibilityPropertiesEntry: AnyAccessibilityPropertiesEntry, Equatable { + package var typedValue: V + + package init(_ typedValue: V) { + self.typedValue = typedValue + } + + package static func == (lhs: AccessibilityPropertiesEntry, rhs: AccessibilityPropertiesEntry) -> Bool { + compareValues(lhs.typedValue, rhs.typedValue) + } + + package var anyValue: Any { + typedValue + } + + package func isEqual(to other: any AnyAccessibilityPropertiesEntry) -> Bool { + guard let other = other as? Self else { + return false + } + return self == other + } +} + +extension AccessibilityPropertiesEntry: AccessibilityCombinable where V: AccessibilityCombinable { + @discardableResult + package mutating func merge(with child: AccessibilityPropertiesEntry) -> Bool { + typedValue.merge(with: child.typedValue) + } + + @discardableResult + package mutating func merge(with child: V) -> Bool { + typedValue.merge(with: child) + } +} + +// MARK: - AccessibilityPropertiesKey + +package protocol AccessibilityPropertiesKey { + associatedtype PropertyValue + + static var defaultValue: PropertyValue { get } + + static func isDefault(_ value: PropertyValue) -> Bool +} + +// MARK: - AccessibilityOptionalPropertiesKey + +package protocol AccessibilityOptionalPropertiesKey: AccessibilityPropertiesKey { + associatedtype NonOptionalPropertyValue where PropertyValue == NonOptionalPropertyValue? + + static var valueType: NonOptionalPropertyValue.Type { get } +} + +extension AccessibilityPropertiesKey { + package static func isDefault(_ value: PropertyValue) -> Bool { + false + } +} + +extension AccessibilityPropertiesKey where PropertyValue: Equatable { + package static func isDefault(_ value: PropertyValue) -> Bool { + value == defaultValue + } +} + +extension AccessibilityOptionalPropertiesKey { + package static func isDefault(_ value: PropertyValue) -> Bool { + value == nil + } + + package static var defaultValue: NonOptionalPropertyValue? { + nil + } +} + +// MARK: - AccessibilityAttachment + +package struct AccessibilityAttachment: Equatable { + package var properties: AccessibilityProperties + + package var platformElement: PlatformAccessibilityElement? + + package init(properties: AccessibilityProperties, platformElement: PlatformAccessibilityElement?) { + self.properties = properties + self.platformElement = platformElement + } + + package init(properties: AccessibilityProperties) { + self.init(properties: properties, platformElement: nil) + } + + package init() { + self.init(properties: AccessibilityProperties()) + } + + package var isEmpty: Bool { + properties.isEmpty && platformElement == nil + } + + package static func == (lhs: AccessibilityAttachment, rhs: AccessibilityAttachment) -> Bool { + if let element = lhs.platformElement, element !== rhs.platformElement { + return false + } + return lhs.properties == rhs.properties + } +} + +extension AccessibilityAttachment { + package static func properties(_ properties: AccessibilityProperties) -> AccessibilityAttachment { + AccessibilityAttachment(properties: properties) + } +} + +// MARK: - AccessibilityAttachmentToken + +package enum AccessibilityAttachmentToken: Hashable, Codable { + case attribute(AnyWeakAttribute) + case identifier(UInt32) + + package init(_ attribute: AnyAttribute) { + self = .attribute(AnyWeakAttribute(attribute)) + } + + package init(_ attribute: Attribute) { + self.init(attribute.identifier) + } + + package init(_ identifier: UInt32) { + self = .identifier(identifier) + } + + package var attribute: AnyAttribute? { + guard case let .attribute(attribute) = self else { + return nil + } + return attribute.attribute + } + + package func encode(to encoder: any Encoder) throws { + let value: UInt32 + switch self { + case let .attribute(attribute): + value = attribute.attribute?.rawValue ?? 0 + case let .identifier(identifier): + value = identifier + } + var container = encoder.singleValueContainer() + try container.encode(value) + } -package struct AccessibilityProperties: Equatable {} + package init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + self = .identifier(try container.decode(UInt32.self)) + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityTraits.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityTraits.swift new file mode 100644 index 000000000..d2c847b33 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityTraits.swift @@ -0,0 +1,350 @@ +// +// AccessibilityTraits.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package import OpenSwiftUI_SPI + +/// A set of accessibility traits that describe how an element behaves. +@available(OpenSwiftUI_v1_0, *) +public struct AccessibilityTraits: SetAlgebra, Sendable { + var traitSet: AccessibilityTraitSet + + /// The accessibility element is a button. + public static let isButton = Self(traitSet: .init(trait: .isButton)) + + /// The accessibility element is a header that divides content into + /// sections, like the title of a navigation bar. + public static let isHeader = Self(traitSet: .init(trait: .isHeader)) + + /// The accessibility element is currently selected. + public static let isSelected = Self(traitSet: .init(trait: .isSelected)) + + /// The accessibility element is a link. + public static let isLink = Self(traitSet: .init(trait: .isLink)) + + /// The accessibility element is a search field. + public static let isSearchField = Self(traitSet: .init(trait: .isSearchField)) + + /// The accessibility element is an image. + public static let isImage = Self(traitSet: .init(trait: .isImage)) + + /// The accessibility element plays its own sound when activated. + public static let playsSound = Self(traitSet: .init(trait: .playsSound)) + + /// The accessibility element behaves as a keyboard key. + public static let isKeyboardKey = Self(traitSet: .init(trait: .isKeyboardKey)) + + /// The accessibility element is a static text that cannot be + /// modified by the user. + public static let isStaticText = Self(traitSet: .init(trait: .isStaticText)) + + /// The accessibility element provides summary information when the + /// application starts. + /// + /// Use this trait to characterize an accessibility element that provides + /// a summary of current conditions, settings, or state, like the + /// temperature in the Weather app. + public static let isSummaryElement = Self(traitSet: .init(trait: .isSummaryElement)) + + /// The accessibility element frequently updates its label or value. + /// + /// Use this trait when you want an assistive technology to poll for + /// changes when it needs updated information. For example, you might use + /// this trait to characterize the readout of a stopwatch. + public static let updatesFrequently = Self(traitSet: .init(trait: .updatesFrequently)) + + /// The accessibility element starts a media session when it is activated. + /// + /// Use this trait to silence the audio output of an assistive technology, + /// such as VoiceOver, during a media session that should not be interrupted. + /// For example, you might use this trait to silence VoiceOver speech while + /// the user is recording audio. + public static let startsMediaSession = Self(traitSet: .init(trait: .startsMediaSession)) + + /// The accessibility element allows direct touch interaction for + /// VoiceOver users. + public static let allowsDirectInteraction = Self(traitSet: .init(trait: .allowsDirectInteraction)) + + /// The accessibility element causes an automatic page turn when VoiceOver + /// finishes reading the text within it. + public static let causesPageTurn = Self(traitSet: .init(trait: .causesPageTurn)) + + /// The accessibility element is modal. + /// + /// Use this trait to restrict which accessibility elements an assistive + /// technology can navigate. When a modal accessibility element is visible, + /// sibling accessibility elements that are not modal are ignored. + public static let isModal = Self(traitSet: .init(trait: .isModal)) + + /// The accessibility element is a toggle. + @available(OpenSwiftUI_v5_0, *) + public static let isToggle = Self(traitSet: .init(trait: .isToggle)) + + /// The accessibility element is a tab bar. + @available(OpenSwiftUI_v5_0, *) + public static let isTabBar = Self(traitSet: .init(trait: .isTabBar)) + + @_spi(UIFrameworks) + @available(OpenSwiftUI_v5_0, *) + public static let isTabButton = Self(traitSet: .init(trait: .isTabButton)) + + @_spi(UIFrameworks) + @available(OpenSwiftUI_v4_0, *) + public static let isBackButton = Self(traitSet: .init(trait: .isBackButton)) + + @_spi(UIFrameworks) + @available(OpenSwiftUI_v4_0, *) + public static let excludeFromItemChooser = Self(traitSet: .init(trait: .excludeFromItemChooser)) + + @_spi(UIFrameworks) + @available(OpenSwiftUI_v5_0, *) + public static let isSwitch = Self(traitSet: .init(trait: .isSwitch)) + + @_spi(Private) + @available(OpenSwiftUI_v6_0, *) + public static let isMathEquation = Self(traitSet: .init(trait: .isMathEquation)) + + private init(traitSet: AccessibilityTraitSet) { + self.traitSet = traitSet + } + + public init() { + traitSet = [] + } + + public func union(_ other: Self) -> Self { + Self(traitSet: traitSet.union(other.traitSet)) + } + + public mutating func formUnion(_ other: Self) { + traitSet.formUnion(other.traitSet) + } + + public func intersection(_ other: Self) -> Self { + Self(traitSet: traitSet.intersection(other.traitSet)) + } + + public mutating func formIntersection(_ other: Self) { + traitSet.formIntersection(other.traitSet) + } + + public func symmetricDifference(_ other: Self) -> Self { + Self(traitSet: traitSet.symmetricDifference(other.traitSet)) + } + + public mutating func formSymmetricDifference(_ other: Self) { + traitSet.formSymmetricDifference(other.traitSet) + } + + public func contains(_ member: Self) -> Bool { + traitSet.contains(member.traitSet) + } + + public mutating func insert(_ newMember: Self) -> (inserted: Bool, memberAfterInsert: Self) { + let result = traitSet.insert(newMember.traitSet) + return (result.inserted, .init(traitSet: result.memberAfterInsert)) + } + + public mutating func remove(_ member: Self) -> Self? { + traitSet.remove(member.traitSet).map { .init(traitSet: $0) } + } + + public mutating func update(with newMember: Self) -> Self? { + traitSet.update(with: newMember.traitSet).map { .init(traitSet: $0) } + } +} + +package enum AccessibilityTrait: UInt64, CaseIterable { + case isButton + case isHeader + case isSelected + case isLink + case isSearchField + case isImage + case playsSound + case isKeyboardKey + case isStaticText + case isSummaryElement + case updatesFrequently + case startsMediaSession + case allowsDirectInteraction + case causesPageTurn + case isModal + case isCheckbox + case isSwitch + case isRadioButton + case isRadioGroup + case isLabel + case isSingleSiblingWithGesture + case isInteractive + case isTabBar + case isTabButton + case isBackButton + case excludeFromItemChooser + case isControlGroup + case isPopupButton + case isToggle + case isMathEquation + + package var displayDescription: String? { + switch self { + case .isButton, .isHeader, .isSelected, .isLink, .isSearchField, + .isImage, .playsSound, .isKeyboardKey, .isStaticText, + .isSummaryElement, .updatesFrequently, .startsMediaSession, + .allowsDirectInteraction, .causesPageTurn, .isModal, + .isTabBar, .isTabButton, .isBackButton, .excludeFromItemChooser, + .isToggle: + return "." + String(describing: self) + default: + return nil + } + } + + package var uiTrait: AXOpenSwiftUITraits? { + switch self { + case .isButton: .button + case .isHeader: .header + case .isSelected: .selected + case .isLink: .link + case .isSearchField: .searchField + case .isImage: .image + case .playsSound: .playsSound + case .isKeyboardKey: .keyboardKey + case .isStaticText: .staticText + case .isSummaryElement: .summaryElement + case .updatesFrequently: .updatesFrequently + case .startsMediaSession: .startsMediaSession + case .allowsDirectInteraction: .allowsDirectInteraction + case .causesPageTurn: .causesPageTurn + case .isRadioButton: .radioButton + case .isTabBar: .tabBar + case .isTabButton: .tabButton + case .isBackButton: .backButton + case .isPopupButton: .popupButton + case .isToggle: .toggle + case .isMathEquation: .mathEquation + case .isModal, .isCheckbox, .isSwitch, .isRadioGroup, .isLabel, + .isSingleSiblingWithGesture, .isInteractive, + .excludeFromItemChooser, .isControlGroup: + nil + } + } + + package var isElementUITrait: Bool { + switch self { + case .isButton, .isHeader, .isLink, .isSearchField, .isImage, + .playsSound, .isKeyboardKey, .isStaticText, .isSummaryElement, + .startsMediaSession, .causesPageTurn, .isRadioButton, + .isTabButton, .isBackButton, .isPopupButton, .isToggle, + .isMathEquation: + true + default: + false + } + } + + package var isContainerUITrait: Bool { + switch self { + case .isSelected, .updatesFrequently, .allowsDirectInteraction, .isTabBar: + true + default: + false + } + } + + package var isInteractionUITrait: Bool { + switch self { + case .isSelected, .playsSound, .startsMediaSession, .causesPageTurn: + true + default: + false + } + } +} + +package struct AccessibilityTraitSet: OptionSet, Hashable, Codable { + package let rawValue: UInt64 + + package init(rawValue: UInt64) { + self.rawValue = rawValue + } + + package init(trait: AccessibilityTrait) { + rawValue = 1 << trait.rawValue + } + + package init(traits: [AccessibilityTrait]) { + rawValue = traits.reduce(0) { $0 + (1 << $1.rawValue) } + } +} + +package typealias AccessibilityTraitStorage = AccessibilityNullableOptionSet + +extension AccessibilityNullableOptionSet where T == AccessibilityTraitSet { + package init(adding traits: AccessibilityTraits) { + self.init(adding: traits.traitSet) + } + + package init(removing traits: AccessibilityTraits) { + self.init(removing: traits.traitSet) + } + + @_disfavoredOverload + package init(adding trait: AccessibilityTrait) { + self.init(adding: AccessibilityTraitSet(trait: trait)) + } + + @_disfavoredOverload + package init(adding traits: [AccessibilityTrait]) { + self.init(adding: AccessibilityTraitSet(traits: traits)) + } + + @_disfavoredOverload + package init(removing trait: AccessibilityTrait) { + self.init(removing: AccessibilityTraitSet(trait: trait)) + } + + @_disfavoredOverload + package init(removing traits: [AccessibilityTrait]) { + self.init(removing: AccessibilityTraitSet(traits: traits)) + } + + @_disfavoredOverload + package func isSet(_ trait: AccessibilityTrait) -> Bool { + isSet(AccessibilityTraitSet(trait: trait)) + } + + package subscript(_ trait: AccessibilityTrait) -> Bool? { + get { self[AccessibilityTraitSet(trait: trait)] } + set { self[AccessibilityTraitSet(trait: trait)] = newValue } + } + + package subscript(_ trait: AccessibilityTrait, default defaultValue: Bool) -> Bool { + self[AccessibilityTraitSet(trait: trait), default: defaultValue] + } +} + +extension AccessibilityProperties { + package var isTabBar: Bool? { + get { self[trait: .isTabBar] } + set { self[trait: .isTabBar] = newValue } + } + + package var isTabButton: Bool? { + get { self[trait: .isTabButton] } + set { self[trait: .isTabButton] = newValue } + } + + package var isBackButton: Bool? { + get { self[trait: .isBackButton] } + set { self[trait: .isBackButton] = newValue } + } + + package var excludeFromItemChooser: Bool? { + get { self[trait: .excludeFromItemChooser] } + set { self[trait: .excludeFromItemChooser] = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityValue.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityValue.swift new file mode 100644 index 000000000..7a7a56c87 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityValue.swift @@ -0,0 +1,681 @@ +// +// AccessibilityValue.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 9CDC06258A415BCDFCA123F203571FE0 (SwiftUICore) + +package import Foundation + +// MARK: - AnyAccessibilityValueType + +package enum AnyAccessibilityValueType: UInt, Codable { + case int + case double + case bool + case string + case disclosure + case toggle + case slider + case stepper + case progress + case boundedNumber + case number + case empty +} + +// MARK: - AccessibilityValue + +package protocol AccessibilityValue: Equatable { + associatedtype PlatformValue: AccessibilityPlatformSafe = Self + + var localizedDescription: String? { get } + var displayDescription: String? { get } + var value: PlatformValue { get } + var minValue: PlatformValue? { get } + var maxValue: PlatformValue? { get } + var step: PlatformValue? { get } + static var type: AnyAccessibilityValueType { get } +} + +extension AccessibilityValue { + package var localizedDescription: String? { nil } + package var minValue: PlatformValue? { nil } + package var maxValue: PlatformValue? { nil } + package var step: PlatformValue? { nil } + package var displayDescription: String? { nil } +} + +extension AccessibilityValue where PlatformValue: CustomStringConvertible { + package var displayDescription: String? { value.description } + package var localizedDescription: String? { value.description } +} + +extension AccessibilityValue where Self == PlatformValue { + package var value: Self { self } +} + +extension AccessibilityValue where Self: RawRepresentable { + package var value: RawValue { rawValue } +} + +extension AccessibilityValue where PlatformValue == NSNumber { + package var localizedDescription: String? { localizedNumericDescription } + package var displayDescription: String? { localizedDescription } + + package var localizedNumericDescription: String? { + let width = minValue.map { minValue in + maxValue.map { $0.doubleValue - minValue.doubleValue } ?? 0 + } ?? 0 + var number = value + let style: NumberFormatter.Style + if abs(width - 100) < Double.ulpOfOne { + number = (number.doubleValue / 100) as NSNumber + style = .percent + } else if abs(width - 1) < Double.ulpOfOne { + style = .percent + } else { + style = .decimal + } + return NumberFormatter.localizedString(from: number, number: style) + } +} + +// MARK: - AccessibilityValueByProxy + +package protocol AccessibilityValueByProxy: AccessibilityValue { + associatedtype Base: AccessibilityValue + var base: Base { get } +} + +extension AccessibilityValueByProxy { + package var localizedDescription: String? { base.localizedDescription } + package var displayDescription: String? { base.displayDescription } + package var value: Base.PlatformValue { base.value } + package var minValue: Base.PlatformValue? { base.minValue } + package var maxValue: Base.PlatformValue? { base.maxValue } + package var step: Base.PlatformValue? { base.step } +} + +// MARK: - Standard Library + AccessibilityValue + +extension Int: AccessibilityValue { + package static var type: AnyAccessibilityValueType { .int } +} + +extension Double: AccessibilityValue { + package static var type: AnyAccessibilityValueType { .double } +} + +extension Bool: AccessibilityValue { + package static var type: AnyAccessibilityValueType { .bool } +} + +extension String: AccessibilityValue { + package static var type: AnyAccessibilityValueType { .string } +} + +// MARK: - AccessibilityEmptyValue + +package struct AccessibilityEmptyValue: AccessibilityValue, Codable { + package static var type: AnyAccessibilityValueType { .empty } + + package var value: Never? { nil } + + package init() { + _openSwiftUIEmptyStub() + } +} + +// MARK: - AccessibilityBoundedNumber + +package struct AccessibilityBoundedNumber: AccessibilityValue, Codable { + package static var type: AnyAccessibilityValueType { .boundedNumber } + + package var number: AccessibilityNumber + package var lowerBound: AccessibilityNumber? + package var upperBound: AccessibilityNumber? + package var stride: AccessibilityNumber? + + package init( + value: AccessibilityNumber, + minValue: AccessibilityNumber? = nil, + maxValue: AccessibilityNumber? = nil, + step: AccessibilityNumber? = nil + ) { + number = value + lowerBound = minValue + upperBound = maxValue + stride = step + } + + package var value: NSNumber { number.value } + package var minValue: NSNumber? { lowerBound?.value } + package var maxValue: NSNumber? { upperBound?.value } + package var step: NSNumber? { stride?.value } +} + +extension AccessibilityBoundedNumber { + package init?(for value: T, in range: ClosedRange? = nil, by step: T.Stride? = nil) where T: Strideable { + let value = range.map { value.clamped(to: $0) } ?? value + guard let number = (value as? any AccessibilityNumeric)?.asNumber() else { + return nil + } + self.init( + value: number, + minValue: range?.minimumValue?.asNumber(), + maxValue: range?.maximumValue?.asNumber(), + step: (step as? any AccessibilityNumeric)?.asNumber() + ) + } +} + +// MARK: - AccessibilityDisclosureValue + +package enum AccessibilityDisclosureValue: UInt8, AccessibilityValue { + case collapsed + case expanded + + package init(_ isDisclosed: Bool) { + self = isDisclosed ? .expanded : .collapsed + } + + package static var type: AnyAccessibilityValueType { .disclosure } + + package var value: Bool? { + isUIKitBased() ? uiKitValue : appKitValue + } + + package var uiKitValue: Bool? { nil } + package var appKitValue: Bool? { platformDisclosureValue } + package var platformDisclosureValue: Bool { self == .expanded } + package var displayDescription: String? { nil } +} + +// MARK: - AccessibilityToggleValue + +package struct AccessibilityToggleValue: AccessibilityValue, Codable { + package static var type: AnyAccessibilityValueType { .toggle } + + package enum State: UInt8, Codable { + case off + case on + case mixed + } + + package var state: State + + package init(_ isOn: ToggleState) { + switch isOn { + case .on: state = .on + case .off: state = .off + case .mixed: state = .mixed + } + } + + package var value: Int { + isUIKitBased() ? uiKitValue : appKitValue + } + + package var uiKitValue: Int { Int(state.rawValue) } + + package var appKitValue: Int { + switch state { + case .off: 0 + case .on: 1 + case .mixed: -1 + } + } + + package var displayDescription: String? { + switch state { + case .off: "false" + case .on: "true" + case .mixed: "mixed" + } + } +} + +// MARK: - AccessibilityStepperValue + +package struct AccessibilityStepperValue: AccessibilityValueByProxy, Codable { + package static var type: AnyAccessibilityValueType { .stepper } + + package var base: AccessibilityBoundedNumber + + package init(_ base: AccessibilityBoundedNumber) { + self.base = base + } + + package init(value: AccessibilityNumber, minValue: AccessibilityNumber? = nil, maxValue: AccessibilityNumber? = nil) { + base = AccessibilityBoundedNumber(value: value, minValue: minValue, maxValue: maxValue) + } + + package var localizedDescription: String? { + NumberFormatter.localizedString(from: value, number: .decimal) + } + + package var displayDescription: String? { localizedDescription } +} + +// MARK: - AccessibilitySliderValue + +package struct AccessibilitySliderValue: AccessibilityValueByProxy, Codable { + package static var type: AnyAccessibilityValueType { .slider } + + package var base: AccessibilityBoundedNumber + + package init(_ base: AccessibilityBoundedNumber) { + self.base = base + } + + package init(value: AccessibilityNumber, minValue: AccessibilityNumber? = nil, maxValue: AccessibilityNumber? = nil) { + base = AccessibilityBoundedNumber(value: value, minValue: minValue, maxValue: maxValue) + } + + package init(value: V, minValue: V? = nil, maxValue: V? = nil) where V: BinaryFloatingPoint { + self.init( + value: AccessibilityNumber(floatingPoint: value), + minValue: minValue != nil ? AccessibilityNumber(floatingPoint: minValue!) : nil, + maxValue: maxValue != nil ? AccessibilityNumber(floatingPoint: maxValue!) : nil + ) + } +} + +// MARK: - AccessibilityProgressValue + +package struct AccessibilityProgressValue: AccessibilityValue, Codable { + package static var type: AnyAccessibilityValueType { .progress } + + package var percent: Double? + + package init(percent: Double?) { + self.percent = percent + } + + package var value: NSNumber { NSNumber(floatLiteral: percent ?? 0) } + package var minValue: NSNumber? { percent == nil ? nil : 0 } + package var maxValue: NSNumber? { percent == nil ? nil : 1 } + package var localizedDescription: String? { percent == nil ? nil : localizedNumericDescription } + package var displayDescription: String? { localizedDescription } +} + +// MARK: - AccessibilityNumber + +package struct AccessibilityNumber: AccessibilityValue, Codable { + package enum AccessibilityNumberArchiveError: Error { + case unarchiveFailed + } + + package static var type: AnyAccessibilityValueType { .number } + + private var base: NSNumber + + package init(_ base: NSNumber) { + self.base = base + } + + package var value: NSNumber { base } + + package init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let data = try container.decode(Data.self) + guard let base = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSNumber.self, from: data) else { + throw AccessibilityNumberArchiveError.unarchiveFailed + } + self.base = base + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + let data = try NSKeyedArchiver.archivedData(withRootObject: base, requiringSecureCoding: true) + try container.encode(data) + } +} + +extension AccessibilityNumber: ExpressibleByIntegerLiteral { + package typealias IntegerLiteralType = NSNumber.IntegerLiteralType + + package init(integerLiteral value: IntegerLiteralType) { + base = NSNumber(integerLiteral: value) + } +} + +extension AccessibilityNumber: ExpressibleByFloatLiteral { + package typealias FloatLiteralType = NSNumber.FloatLiteralType + + package init(floatLiteral value: FloatLiteralType) { + base = NSNumber(floatLiteral: value) + } + + package init(floatingPoint value: V) where V: BinaryFloatingPoint { + base = NSNumber(floatLiteral: Double(value)) + } +} + +// MARK: - ProxyCodable + AccessibilityValue + +extension ProxyCodable: AccessibilityValue where Value: AccessibilityValue { + package typealias PlatformValue = Value.PlatformValue + + package var localizedDescription: String? { + wrappedValue.localizedDescription + } + + package var displayDescription: String? { + wrappedValue.displayDescription + } + + package var value: PlatformValue { + wrappedValue.value + } + + package var minValue: PlatformValue? { + wrappedValue.minValue + } + + package var maxValue: PlatformValue? { + wrappedValue.maxValue + } + + package var step: PlatformValue? { + wrappedValue.step + } + + package static var type: AnyAccessibilityValueType { + Value.type + } +} + +// MARK: - AbstractAnyAccessibilityValue + +private protocol AbstractAnyAccessibilityValue: Codable { + var localizedDescription: String? { get } + var displayDescription: String? { get } + var value: Any { get } + var minValue: Any? { get } + var maxValue: Any? { get } + var step: Any? { get } + var type: AnyAccessibilityValueType { get } + func `as`(_ type: T.Type) -> T? where T: AccessibilityValue + func isEqual(to other: any AbstractAnyAccessibilityValue) -> Bool +} + +// MARK: - AnyAccessibilityValue + +package struct AnyAccessibilityValue: Equatable { + private var base: any AbstractAnyAccessibilityValue + + package init(_ value: V) where V: AccessibilityValue, V: Codable { + base = ConcreteBase(base: value) + } + + package var localizedDescription: String? { + base.localizedDescription + } + + package var displayDescription: String? { + base.displayDescription + } + + package var value: Any { + base.value + } + + package var minValue: Any? { + base.minValue + } + + package var maxValue: Any? { + base.maxValue + } + + package var step: Any? { + base.step + } + + package var type: AnyAccessibilityValueType { + base.type + } + + package func `as`(_ type: T.Type) -> T? where T: AccessibilityValue { + base.as(type) + } + + package static func == (lhs: AnyAccessibilityValue, rhs: AnyAccessibilityValue) -> Bool { + lhs.isEqual(to: rhs) + } +} + +extension AnyAccessibilityValue: AbstractAnyAccessibilityValue { + fileprivate func isEqual(to other: any AbstractAnyAccessibilityValue) -> Bool { + guard let other = other as? AnyAccessibilityValue else { + return false + } + return base.isEqual(to: other.base) + } +} + +extension AnyAccessibilityValue { + private struct ConcreteBase: AbstractAnyAccessibilityValue, Equatable where Value: AccessibilityValue, Value: Codable { + var base: Value + + var localizedDescription: String? { base.localizedDescription } + var displayDescription: String? { base.displayDescription } + var value: Any { base.value } + var minValue: Any? { base.minValue } + var maxValue: Any? { base.maxValue } + var step: Any? { base.step } + var type: AnyAccessibilityValueType { Value.type } + + func `as`(_ type: T.Type) -> T? where T: AccessibilityValue { + base as? T + } + + func isEqual(to other: any AbstractAnyAccessibilityValue) -> Bool { + (other as? Self)?.base == base + } + } +} + +// MARK: - AccessibilityValueStorage + +package struct AccessibilityValueStorage: Equatable { + package enum Description: Equatable { + case text([Text]) + case empty + + package var text: [Text] { + switch self { + case let .text(texts): texts + case .empty: [] + } + } + } + + package fileprivate(set) var value: AnyAccessibilityValue? = nil + + package private(set) var description: Description + + package init(_ value: V? = nil, description: Text? = nil) where V: AccessibilityValue, V: CodableByProxy { + self.value = value.map { AnyAccessibilityValue(ProxyCodable($0)) } + self.description = .text(description.map { [$0] } ?? []) + } + + package init(_ value: V? = nil, description: Text? = nil) where V: AccessibilityValue, V: Codable { + self.value = value.map { AnyAccessibilityValue($0) } + self.description = .text(description.map { [$0] } ?? []) + } + + package init(_ value: V, description: Description) where V: AccessibilityValue, V: Codable { + self.value = AnyAccessibilityValue(value) + self.description = description + } + + package init(description: Text? = nil) { + self.description = .text(description.map { [$0] } ?? []) + } + + package init(descriptions: [Text]) { + description = .text(descriptions) + } + + package var valueDescription: [Text] { + get { + if case let .text(texts) = description, !texts.isEmpty { + return texts + } + guard let description = value?.localizedDescription else { + return [] + } + return [Text(verbatim: description)] + } + set { + description = .text(newValue) + } + } +} + +// MARK: - AccessibilityValueStorage + Platform Values + +extension AccessibilityValueStorage { + package var platformMinValue: Any? { + value?.minValue + } + + package var platformMaxValue: Any? { + value?.maxValue + } + + package var platformNumberValue: NSNumber? { + value?.value as? NSNumber + } + + package var isBounded: Bool { + value?.minValue != nil || value?.maxValue != nil + } + + package var hasAllowedValues: Bool { + value?.step != nil && value?.minValue != nil && value?.maxValue != nil + } + + package var platformAllowedValues: [NSNumber]? { + guard let step = value?.step as? NSNumber, + let minValue = value?.minValue as? NSNumber, + let maxValue = value?.maxValue as? NSNumber else { + return nil + } + return stride( + from: minValue.decimalValue, + through: maxValue.decimalValue, + by: step.decimalValue + ).map { $0 as NSNumber } + } +} + +// MARK: - AccessibilityValueStorage + AccessibilityCombinable + +extension AccessibilityValueStorage: AccessibilityCombinable { + @discardableResult + package mutating func merge(with child: AccessibilityValueStorage) -> Bool { + var changed = false + if value == nil, let childValue = child.value { + value = childValue + changed = true + } + if case let .text(texts) = description, texts.isEmpty, + case let .text(childTexts) = child.description, !childTexts.isEmpty { + description = child.description + changed = true + } + return changed + } +} + +extension AccessibilityDisclosureValue: Codable {} + +// MARK: - AnyAccessibilityValue + Codable + +extension AnyAccessibilityValue: Codable { + private enum Keys: String, CodingKey { + case type + case value + } + + package init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Keys.self) + switch try container.decode(AnyAccessibilityValueType.self, forKey: .type) { + case .int: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .double: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .bool: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .string: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .disclosure: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .toggle: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .slider: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .stepper: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .progress: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .boundedNumber: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .number: + base = try container.decode(ConcreteBase.self, forKey: .value) + case .empty: + base = try container.decode(ConcreteBase.self, forKey: .value) + } + } + + package func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: Keys.self) + try container.encode(type, forKey: .type) + try base.encode(to: container.superEncoder(forKey: .value)) + } +} + +// MARK: - CodableAccessibilityValueStorage + +package struct CodableAccessibilityValueStorage: Codable { + var text: AccessibilityText? + var value: AnyAccessibilityValue? + + package init( + _ storage: AccessibilityValueStorage, + in environment: EnvironmentValues, + idiom: AnyInterfaceIdiom? = nil + ) { + text = AccessibilityText( + texts: storage.description.text, + environment: environment, + idiom: idiom + ) + value = storage.value + } + + package var accessibilityValue: AccessibilityValueStorage { + var storage = AccessibilityValueStorage(description: text?.text) + storage.value = value + return storage + } +} + +// MARK: - AccessibilityPlatformSafe + +package protocol AccessibilityPlatformSafe {} + +extension String: AccessibilityPlatformSafe {} +extension Double: AccessibilityPlatformSafe {} +extension Int: AccessibilityPlatformSafe {} +extension UInt: AccessibilityPlatformSafe {} +extension UInt8: AccessibilityPlatformSafe {} +extension Bool: AccessibilityPlatformSafe {} +extension NSNumber: AccessibilityPlatformSafe {} +extension Never: AccessibilityPlatformSafe {} +extension Optional: AccessibilityPlatformSafe where Wrapped: AccessibilityPlatformSafe {} diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityVisibility.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityVisibility.swift new file mode 100644 index 000000000..d451e3150 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityVisibility.swift @@ -0,0 +1,79 @@ +// +// AccessibilityVisibility.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package struct AccessibilityVisibility: OptionSet, Hashable, Codable { + package enum Resolved: Int, Codable { + case element + case containerElement + case container + case hidden + } + + package let rawValue: UInt32 + + package init(rawValue: UInt32) { + self.rawValue = rawValue + } + + package static let element = Self(rawValue: 1 << 0) + package static let container = Self(rawValue: 1 << 1) + package static let hidden = Self(rawValue: 1 << 2) + package static let transparent = Self(rawValue: 1 << 3) + package static let ignored = Self(rawValue: 1 << 4) + package static let host = Self(rawValue: 1 << 5) + package static let childrenIgnored = Self(rawValue: 1 << 6) + package static let stack = Self(rawValue: 1 << 7) +} + +extension AccessibilityVisibility { + package static var `default`: Self { [] } + + package static var containerElement: Self { [.element, .container] } +} + +extension AccessibilityVisibility { + package init(_ value: Resolved) { + switch value { + case .element: self = .element + case .containerElement: self = .containerElement + case .container: self = .container + case .hidden: self = .hidden + } + } +} + +package typealias AccessibilityVisibilityStorage = AccessibilityNullableOptionSet + +extension AccessibilityNullableOptionSet where T == AccessibilityVisibility { + package var resolvesToHidden: Bool { + self[.hidden] ?? value.contains(.transparent) + } + + package var resolved: AccessibilityVisibility.Resolved? { + if resolvesToHidden { + return .hidden + } else if value.contains(.ignored) { + return .container + } else if value.contains(.childrenIgnored) { + return .element + } else if value.contains(.element) { + return value.contains(.container) ? .containerElement : .element + } else if value.contains(.container) { + return .container + } else { + return nil + } + } + + package var shouldApplyPlatformElementOverride: (isAXElement: Bool, isAXElementsHidden: Bool) { + let specified = value.union(mask) + return ( + specified.contains(.element) || specified.contains(.container) || resolvesToHidden, + !specified.intersection([.hidden, .transparent]).isEmpty + ) + } +} diff --git a/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufEncoder.swift b/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufEncoder.swift index 0d87ec7cd..a120ade4a 100644 --- a/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufEncoder.swift +++ b/Sources/OpenSwiftUICore/Data/Protobuf/ProtobufEncoder.swift @@ -39,6 +39,10 @@ package struct ProtobufEncoder { package var archiveHost: (any AnyArchivedViewHost)? { userInfo[ArchivedViewCore.archivedViewHostKey] as? any AnyArchivedViewHost } + + package var archiveOptions: ArchivedViewInput.Value { + userInfo[ArchivedViewCore.archiveOptionsKey] as? ArchivedViewInput.Value ?? .init() + } /// Takes the encoded data. /// diff --git a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift index f0b9bef23..ee5e66e3a 100644 --- a/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift +++ b/Sources/OpenSwiftUICore/Render/DisplayList/DisplayList.swift @@ -1210,8 +1210,6 @@ extension DisplayList.Item { } } -package struct AccessibilityNodeAttachment {} - extension DisplayList.Item { func addDrawingGroup(contentSeed: DisplayList.Seed) { _openSwiftUIUnimplementedWarning() diff --git a/Sources/OpenSwiftUICore/Util/AccessibilityShims.swift b/Sources/OpenSwiftUICore/Util/AccessibilityShims.swift new file mode 100644 index 000000000..5099d922d --- /dev/null +++ b/Sources/OpenSwiftUICore/Util/AccessibilityShims.swift @@ -0,0 +1,119 @@ +// +// AccessibilityShims.swift +// OpenSwiftUICore + +#if !canImport(Accessibility) +public import Foundation + +// MARK: - AXChartDescriptor + +public final class AXChartDescriptor: NSObject { + private let dictionary: [AnyHashable: Any] + + init(dictionary: [AnyHashable: Any]) { + self.dictionary = dictionary + super.init() + } + + func dictionaryRepresentation() -> [AnyHashable: Any] { + dictionary + } +} + +func _AXOpenSwiftUIUnarchiveChartDescriptor(_ data: Data) -> Any? { + try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) +} + +// MARK: - AXCustomContent + +extension AXCustomContent { + public enum Importance: UInt, @unchecked Sendable { + case `default` = 0 + case high = 1 + } +} + +open class AXCustomContent: NSObject, NSCopying, NSSecureCoding { + private var _label: String? + private var _attributedLabel: NSAttributedString? + private var _value: String? + private var _attributedValue: NSAttributedString? + + public init(label: String, value: String) { + _label = label + _value = value + super.init() + } + + public init(attributedLabel label: NSAttributedString, attributedValue value: NSAttributedString) { + _attributedLabel = label.copy() as? NSAttributedString + _attributedValue = value.copy() as? NSAttributedString + super.init() + } + + open var label: String { + _label ?? _attributedLabel?.string ?? "" + } + + open var attributedLabel: NSAttributedString { + _attributedLabel ?? NSAttributedString(string: label) + } + + open var value: String { + _value ?? _attributedValue?.string ?? "" + } + + open var attributedValue: NSAttributedString { + _attributedValue ?? NSAttributedString(string: value) + } + + open var importance: Importance = .default + + open func copy(with zone: NSZone? = nil) -> Any { + // The platform implementation preserves object identity when copied. + self + } + + open class var supportsSecureCoding: Bool { + true + } + + public required init?(coder: NSCoder) { + _label = coder.decodeObject(of: NSString.self, forKey: "label") as String? + _attributedLabel = coder.decodeObject(of: NSAttributedString.self, forKey: "attributedLabel")?.copy() as? NSAttributedString + _value = coder.decodeObject(of: NSString.self, forKey: "value") as String? + _attributedValue = coder.decodeObject(of: NSAttributedString.self, forKey: "attributedValue")?.copy() as? NSAttributedString + importance = Importance(rawValue: UInt(bitPattern: coder.decodeInteger(forKey: "importance"))) ?? .default + super.init() + } + + open func encode(with coder: NSCoder) { + coder.encode(label, forKey: "label") + coder.encode(attributedLabel, forKey: "attributedLabel") + coder.encode(value, forKey: "value") + coder.encode(attributedValue, forKey: "attributedValue") + coder.encode(Int(importance.rawValue), forKey: "importance") + } + + open override func isEqual(_ object: Any?) -> Bool { + guard let other = object as? AXCustomContent else { + return false + } + return (label as NSString).isEqual(to: other.label) + && (value as NSString).isEqual(to: other.value) + && attributedLabel.isEqual(to: other.attributedLabel) + && attributedValue.isEqual(to: other.attributedValue) + && importance == other.importance + } + + open override var hash: Int { + var hasher = Hasher() + hasher.combine((label as NSString).hash) + hasher.combine((value as NSString).hash) + hasher.combine(attributedLabel.hash) + hasher.combine(attributedValue.hash) + hasher.combine(importance) + return hasher.finalize() + } +} +#endif diff --git a/Sources/OpenSwiftUICore/View/Control/Link.swift b/Sources/OpenSwiftUICore/View/Control/Link.swift new file mode 100644 index 000000000..2372a2e36 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Control/Link.swift @@ -0,0 +1,38 @@ +// +// Link.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 6FFE843582373F7ACAB4C773335A2472 (SwiftUICore) + +package import Foundation + +package struct LinkDestination: DynamicProperty { + package struct Configuration: Codable { + package var url: URL + package var isSensitive: Bool + + package init(url: URL, isSensitive: Bool = false) { + self.url = url + self.isSensitive = isSensitive + } + } + + @Environment(\.openURL) + private var openURL: OpenURLAction + + @Environment(\._openSensitiveURL) + private var openSensitiveURL: OpenURLAction + + package var configuration: Configuration + + package init(configuration: Configuration) { + self.configuration = configuration + } + + package func open() { + let openURLAction = configuration.isSensitive ? openSensitiveURL : openURL + openURLAction(configuration.url) + } +} diff --git a/Sources/OpenSwiftUICore/View/Text/Accessibility/Text+Accessibility.swift b/Sources/OpenSwiftUICore/View/Text/Accessibility/Text+Accessibility.swift index 875c51546..96e9240a1 100644 --- a/Sources/OpenSwiftUICore/View/Text/Accessibility/Text+Accessibility.swift +++ b/Sources/OpenSwiftUICore/View/Text/Accessibility/Text+Accessibility.swift @@ -185,6 +185,49 @@ extension AccessibilityText: ProtobufMessage { extension AccessibilityText: CodableByProtobuf {} +// MARK: - CodableAccessibilityVersionStorage + +extension CodableAccessibilityVersionStorage where V2 == CodableResolvedStyledText, V3 == AccessibilityText { + var text: Text { + switch self { + case let .v2(text): text.base.accessibilityText + case let .v3(text): text.text + } + } + + init?(texts: [Text]?, in environment: EnvironmentValues, optional: Bool, idiom: AnyInterfaceIdiom?) { + guard let texts else { + return nil + } + switch CodableAccessibilityVersion.current { + case .v2: + guard let storage = AccessibilityCore.textsResolvedToAttributedText( + texts, + in: environment, + includeResolvableAttributes: false, + includeDefaultAttributes: true, + updateResolvableAttributes: false, + resolveSuffix: false, + idiom: idiom + ) else { + return nil + } + let text = ResolvedStyledText.styledText( + storage: storage, + environment: environment, + isCollapsible: texts.contains { $0.isCollapsible() }, + writingMode: nil + ) + self = .v2(CodableResolvedStyledText(base: text)) + case .v3: + guard let text = AccessibilityText(texts: texts, environment: environment, optional: optional, idiom: idiom) else { + return nil + } + self = .v3(text) + } + } +} + // MARK: - AccessibilityTextAttributes package struct AccessibilityTextAttributes: Equatable { diff --git a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift index 86e56b4f9..6e198463b 100644 --- a/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift +++ b/Sources/OpenSwiftUICore/View/Text/Text/Text+View.swift @@ -1068,6 +1068,10 @@ extension ResolvedStyledText { struct CodableResolvedStyledText: ProtobufMessage { var base: ResolvedStyledText + init(base: ResolvedStyledText) { + self.base = base + } + init(from decoder: inout ProtobufDecoder) throws { _openSwiftUIUnimplementedFailure() } diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXChartDescriptor.h b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXChartDescriptor.h new file mode 100644 index 000000000..480bf6da0 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXChartDescriptor.h @@ -0,0 +1,28 @@ +// +// AXChartDescriptor.h +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef AXChartDescriptor_h +#define AXChartDescriptor_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +OPENSWIFTUI_EXTERN_C_BEGIN +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +OPENSWIFTUI_EXPORT +id _Nullable _AXOpenSwiftUIUnarchiveChartDescriptor(NSData *data); + +OPENSWIFTUI_ASSUME_NONNULL_END +OPENSWIFTUI_EXTERN_C_END + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ + +#endif /* AXChartDescriptor_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXChartDescriptor.m b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXChartDescriptor.m new file mode 100644 index 000000000..1f6427bc3 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXChartDescriptor.m @@ -0,0 +1,20 @@ +// +// AXChartDescriptor.m +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#include "AXChartDescriptor.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +id _Nullable _AXOpenSwiftUIUnarchiveChartDescriptor(NSData *data) { + // Preserve support for dictionaries archived without secure coding. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + return [NSKeyedUnarchiver unarchiveTopLevelObjectWithData:data error:NULL]; +#pragma clang diagnostic pop +} + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXOpenSwiftUITraits.c b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXOpenSwiftUITraits.c new file mode 100644 index 000000000..66702b42f --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXOpenSwiftUITraits.c @@ -0,0 +1,73 @@ +// +// AXOpenSwiftUITraits.c +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#include "AXOpenSwiftUITraits.h" + +const AXOpenSwiftUITraits AXOpenSwiftUITraitsNone = 0; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsButton = UINT64_C(1) << 0; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsLink = UINT64_C(1) << 1; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsImage = UINT64_C(1) << 2; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSelected = UINT64_C(1) << 3; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsPlaysSound = UINT64_C(1) << 4; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsKeyboardKey = UINT64_C(1) << 5; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsStaticText = UINT64_C(1) << 6; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSummaryElement = UINT64_C(1) << 7; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsNotEnabled = UINT64_C(1) << 8; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsUpdatesFrequently = UINT64_C(1) << 9; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSearchField = UINT64_C(1) << 10; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsStartsMediaSession = UINT64_C(1) << 11; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsAdjustable = UINT64_C(1) << 12; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsAllowsDirectInteraction = UINT64_C(1) << 13; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsCausesPageTurn = UINT64_C(1) << 14; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsTabBar = UINT64_C(1) << 15; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsHeader = UINT64_C(1) << 16; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsWebContent = UINT64_C(1) << 17; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsTextEntry = UINT64_C(1) << 18; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsPickerElement = UINT64_C(1) << 19; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsRadioButton = UINT64_C(1) << 20; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsIsEditing = UINT64_C(1) << 21; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsLaunchIcon = UINT64_C(1) << 22; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsStatusBarElement = UINT64_C(1) << 23; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSecureTextField = UINT64_C(1) << 24; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsInactive = UINT64_C(1) << 25; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsFooter = UINT64_C(1) << 26; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsBackButton = UINT64_C(1) << 27; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsTabButton = UINT64_C(1) << 28; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsAutoCorrectCandidate = UINT64_C(1) << 29; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsDeleteKey = UINT64_C(1) << 30; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSelectionDismissesItem = UINT64_C(1) << 31; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsVisited = UINT64_C(1) << 32; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsScrollable = UINT64_C(1) << 33; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSpacer = UINT64_C(1) << 34; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsTableIndex = UINT64_C(1) << 35; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsMap = UINT64_C(1) << 36; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsTextOperationsAvailable = UINT64_C(1) << 37; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsDraggable = UINT64_C(1) << 38; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsGesturePracticeRegion = UINT64_C(1) << 39; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsPopupButton = UINT64_C(1) << 40; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsAllowsNativeSliding = UINT64_C(1) << 41; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsMathEquation = UINT64_C(1) << 42; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByTable = UINT64_C(1) << 43; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByList = UINT64_C(1) << 44; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsTouchContainer = UINT64_C(1) << 45; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSupportsZoom = UINT64_C(1) << 46; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsTextArea = UINT64_C(1) << 47; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsBookContent = UINT64_C(1) << 48; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByLandmark = UINT64_C(1) << 49; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsFolderIcon = UINT64_C(1) << 50; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsReadOnly = UINT64_C(1) << 51; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsMenuItem = UINT64_C(1) << 52; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsToggle = UINT64_C(1) << 53; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsIgnoreItemChooser = UINT64_C(1) << 54; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsSupportsTrackingDetail = UINT64_C(1) << 55; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsAlert = UINT64_C(1) << 56; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByFieldset = UINT64_C(1) << 57; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsAllowsLayoutChangeInStatusBar = UINT64_C(1) << 58; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsWebInteractiveVideo = UINT64_C(1) << 59; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsProminentIcon = UINT64_C(1) << 60; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsGestureHandlerRegion = UINT64_C(1) << 61; +const AXOpenSwiftUITraits AXOpenSwiftUITraitsRemoveTraitsSentinel = UINT64_C(1) << 63; diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXOpenSwiftUITraits.h b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXOpenSwiftUITraits.h new file mode 100644 index 000000000..d2a11a610 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXOpenSwiftUITraits.h @@ -0,0 +1,84 @@ +// +// AXOpenSwiftUITraits.h +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef AXOpenSwiftUITraits_h +#define AXOpenSwiftUITraits_h + +#include "OpenSwiftUIBase.h" + +OPENSWIFTUI_EXTERN_C_BEGIN + +typedef uint64_t AXOpenSwiftUITraits OPENSWIFTUI_SWIFT_STRUCT; + +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsNone; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsButton; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsLink; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsImage; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSelected; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsPlaysSound; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsKeyboardKey; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsStaticText; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSummaryElement; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsNotEnabled; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsUpdatesFrequently; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSearchField; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsStartsMediaSession; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsAdjustable; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsAllowsDirectInteraction; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsCausesPageTurn; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsTabBar; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsHeader; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsWebContent; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsTextEntry; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsPickerElement; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsRadioButton; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsIsEditing; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsLaunchIcon; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsStatusBarElement; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSecureTextField; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsInactive; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsFooter; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsBackButton; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsTabButton; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsAutoCorrectCandidate; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsDeleteKey; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSelectionDismissesItem; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsVisited; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsScrollable; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSpacer; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsTableIndex; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsMap; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsTextOperationsAvailable; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsDraggable; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsGesturePracticeRegion; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsPopupButton; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsAllowsNativeSliding; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsMathEquation; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByTable; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByList; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsTouchContainer; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSupportsZoom; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsTextArea; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsBookContent; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByLandmark; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsFolderIcon; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsReadOnly; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsMenuItem; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsToggle; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsIgnoreItemChooser; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsSupportsTrackingDetail; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsAlert; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsContainedByFieldset; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsAllowsLayoutChangeInStatusBar; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsWebInteractiveVideo; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsProminentIcon; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsGestureHandlerRegion; +OPENSWIFTUI_EXPORT const AXOpenSwiftUITraits AXOpenSwiftUITraitsRemoveTraitsSentinel; + +OPENSWIFTUI_EXTERN_C_END + +#endif /* AXOpenSwiftUITraits_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXRuntime.h b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXRuntime.h new file mode 100644 index 000000000..28bf31a0a --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXRuntime.h @@ -0,0 +1,31 @@ +// +// AXRuntime.h +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef AXRuntime_h +#define AXRuntime_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +OPENSWIFTUI_EXTERN_C_BEGIN +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +OPENSWIFTUI_EXPORT +void *AXRuntimeLibrary(void); + +OPENSWIFTUI_EXPORT +NSString *AXOpenSwiftUIInteractionLocationDescriptorDefaultName(void); + +OPENSWIFTUI_ASSUME_NONNULL_END +OPENSWIFTUI_EXTERN_C_END + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ + +#endif /* AXRuntime_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXRuntime.m b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXRuntime.m new file mode 100644 index 000000000..a0e66244f --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AXRuntime.m @@ -0,0 +1,47 @@ +// +// AXRuntime.m +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#include "AXRuntime.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#include +#include + +void abort_report_np(const char *format, ...) __attribute__((noreturn, format(printf, 1, 2))); + +void *AXRuntimeLibrary(void) { + static void *frameworkLibrary; + static dispatch_once_t once; + dispatch_once(&once, ^{ + frameworkLibrary = dlopen( + "/System/Library/PrivateFrameworks/AXRuntime.framework/AXRuntime", + RTLD_LAZY | RTLD_LOCAL + ); + if (frameworkLibrary == NULL) { + abort_report_np("%s", dlerror()); + } + }); + return frameworkLibrary; +} + +NSString *AXOpenSwiftUIInteractionLocationDescriptorDefaultName(void) { + static NSString * __unsafe_unretained const *symbol; + static dispatch_once_t once; + dispatch_once(&once, ^{ + symbol = (NSString * __unsafe_unretained const *)dlsym( + AXRuntimeLibrary(), + "AXInteractionLocationDescriptorDefaultName" + ); + if (symbol == NULL) { + abort_report_np("%s", dlerror()); + } + }); + return *symbol; +} + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AccessibilitySharedSupport.h b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AccessibilitySharedSupport.h new file mode 100644 index 000000000..10f85e6a6 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AccessibilitySharedSupport.h @@ -0,0 +1,28 @@ +// +// AccessibilitySharedSupport.h +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef AccessibilitySharedSupport_h +#define AccessibilitySharedSupport_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +OPENSWIFTUI_EXTERN_C_BEGIN +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +OPENSWIFTUI_EXPORT +NSString *soft_AXSSAccessibilityDescriptionForSymbolName(NSString *symbolName, NSString *localeIdentifier); + +OPENSWIFTUI_ASSUME_NONNULL_END +OPENSWIFTUI_EXTERN_C_END + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ + +#endif /* AccessibilitySharedSupport_h */ diff --git a/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AccessibilitySharedSupport.m b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AccessibilitySharedSupport.m new file mode 100644 index 000000000..a1ea4d773 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Overlay/Accessibility/AccessibilitySharedSupport.m @@ -0,0 +1,64 @@ +// +// AccessibilitySharedSupport.m +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#include "AccessibilitySharedSupport.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#include +#include +#include + +void abort_report_np(const char *format, ...) __attribute__((noreturn, format(printf, 1, 2))); + +static void *AccessibilitySharedSupportLibraryCore(char **error) { + static void *frameworkLibrary; + if (frameworkLibrary == NULL) { + frameworkLibrary = dlopen( + "/System/Library/PrivateFrameworks/AccessibilitySharedSupport.framework/AccessibilitySharedSupport", + RTLD_LAZY | RTLD_LOCAL + ); + if (frameworkLibrary == NULL) { + *error = strdup(dlerror()); + } + } + return frameworkLibrary; +} + +static void *AccessibilitySharedSupportLibrary(void) { + char *error = NULL; + void *frameworkLibrary = AccessibilitySharedSupportLibraryCore(&error); + if (frameworkLibrary == NULL) { + abort_report_np("%s", error); + } + if (error != NULL) { + free(error); + } + return frameworkLibrary; +} + +static void *getAXSSAccessibilityDescriptionForSymbolNameSymbolLoc(void) { + static void *ptr; + if (ptr == NULL) { + ptr = dlsym( + AccessibilitySharedSupportLibrary(), + "AXSSAccessibilityDescriptionForSymbolName" + ); + } + return ptr; +} + +NSString *soft_AXSSAccessibilityDescriptionForSymbolName(NSString *symbolName, NSString *localeIdentifier) { + NSString *(*function)(NSString *, NSString *) = + (NSString *(*)(NSString *, NSString *))getAXSSAccessibilityDescriptionForSymbolNameSymbolLoc(); + if (function == NULL) { + abort_report_np("%s", dlerror()); + } + return function(symbolName, localeIdentifier); +} + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/Accessibility/AXAutomationType.h b/Sources/OpenSwiftUI_SPI/Shims/Accessibility/AXAutomationType.h new file mode 100644 index 000000000..4b0b7ac53 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/Accessibility/AXAutomationType.h @@ -0,0 +1,102 @@ +// +// AXAutomationType.h +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: WIP + +#ifndef AXAutomationType_h +#define AXAutomationType_h + +#include "OpenSwiftUIBase.h" + +// Names follow the corresponding automation roles. Values differ from XCUIElementType. +typedef OPENSWIFTUI_ENUM(uint64_t, AXAutomationType) { + AXAutomationTypeUnknown = 0, + AXAutomationTypeApplication = 1, + AXAutomationTypeWindow = 2, + AXAutomationTypeSheet = 3, + AXAutomationTypeDrawer = 4, + AXAutomationTypeGroup = 5, + AXAutomationTypeAlert = 6, + AXAutomationTypeDialog = 7, + AXAutomationTypeBannerNotification = 8, + AXAutomationTypeButton = 9, + AXAutomationTypeRadioButton = 10, + AXAutomationTypeRadioGroup = 11, + AXAutomationTypeCheckBox = 12, + AXAutomationTypeDisclosureTriangle = 13, + AXAutomationTypePopUpButton = 14, + AXAutomationTypeComboBox = 15, + AXAutomationTypeMenuButton = 16, + AXAutomationTypeToolbarButton = 17, + AXAutomationTypePopover = 18, + AXAutomationTypeKeyboard = 19, + AXAutomationTypeKey = 20, + AXAutomationTypeNavigationBar = 21, + AXAutomationTypeTabBar = 22, + AXAutomationTypeTabGroup = 23, + AXAutomationTypeToolbar = 24, + AXAutomationTypeStatusBar = 25, + AXAutomationTypeTable = 26, + AXAutomationTypeTableRow = 27, + AXAutomationTypeTableColumn = 28, + AXAutomationTypeOutline = 29, + AXAutomationTypeOutlineRow = 30, + AXAutomationTypeBrowser = 31, + AXAutomationTypeCollectionView = 32, + AXAutomationTypeSlider = 33, + AXAutomationTypePageIndicator = 34, + AXAutomationTypeProgressIndicator = 35, + AXAutomationTypeActivityIndicator = 36, + AXAutomationTypeSegmentedControl = 37, + AXAutomationTypePicker = 38, + AXAutomationTypePickerWheel = 39, + AXAutomationTypeSwitch = 40, + AXAutomationTypeToggle = 41, + AXAutomationTypeLink = 42, + AXAutomationTypeImage = 43, + AXAutomationTypeIcon = 44, + AXAutomationTypeSearchField = 45, + AXAutomationTypeScrollView = 46, + AXAutomationTypeScrollBar = 47, + AXAutomationTypeStaticText = 48, + AXAutomationTypeTextField = 49, + AXAutomationTypeSecureTextField = 50, + AXAutomationTypeDatePicker = 51, + AXAutomationTypeTextView = 52, + AXAutomationTypeMenu = 53, + AXAutomationTypeMenuItem = 54, + AXAutomationTypeMenuBar = 55, + AXAutomationTypeMenuBarItem = 56, + AXAutomationTypeMap = 57, + AXAutomationTypeWebView = 58, + AXAutomationTypeIncrementArrow = 59, + AXAutomationTypeDecrementArrow = 60, + AXAutomationTypeTimeline = 61, + AXAutomationTypeRatingIndicator = 62, + AXAutomationTypeValueIndicator = 63, + AXAutomationTypeSplitGroup = 64, + AXAutomationTypeSplitter = 65, + AXAutomationTypeRelevanceIndicator = 66, + AXAutomationTypeColorWell = 67, + AXAutomationTypeHelpTag = 68, + AXAutomationTypeMatte = 69, + AXAutomationTypeDockItem = 70, + AXAutomationTypeRuler = 71, + AXAutomationTypeRulerMarker = 72, + AXAutomationTypeGrid = 73, + AXAutomationTypeLevelIndicator = 74, + AXAutomationTypeCell = 75, + AXAutomationTypeLayoutArea = 76, + AXAutomationTypeLayoutItem = 77, + AXAutomationTypeHandle = 78, + AXAutomationTypeStepper = 79, + AXAutomationTypeTab = 80, + AXAutomationTypeTouchBar = 81, + AXAutomationTypeStatusItem = 82, + AXAutomationTypeOther = 83, + // TODO: Identify the distinct button roles at values 84 through 88. +}; + +#endif /* AXAutomationType_h */ diff --git a/Sources/OpenSwiftUI_SPI/Shims/Accessibility/AXChartDescriptor_Private.h b/Sources/OpenSwiftUI_SPI/Shims/Accessibility/AXChartDescriptor_Private.h new file mode 100644 index 000000000..bd0077639 --- /dev/null +++ b/Sources/OpenSwiftUI_SPI/Shims/Accessibility/AXChartDescriptor_Private.h @@ -0,0 +1,28 @@ +// +// AXChartDescriptor_Private.h +// OpenSwiftUI_SPI +// +// Audited for 6.5.4 +// Status: Complete + +#ifndef AXChartDescriptor_Private_h +#define AXChartDescriptor_Private_h + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +OPENSWIFTUI_ASSUME_NONNULL_BEGIN + +@interface AXChartDescriptor (OpenSwiftUI_SPI) +- (instancetype)initWithDictionary:(NSDictionary *)dictionary; +- (NSDictionary *)dictionaryRepresentation; +@end + +OPENSWIFTUI_ASSUME_NONNULL_END + +#endif /* OPENSWIFTUI_TARGET_OS_DARWIN */ + +#endif /* AXChartDescriptor_Private_h */ diff --git a/Sources/OpenSwiftUI_SPI/module.modulemap b/Sources/OpenSwiftUI_SPI/module.modulemap index 75a6ac57c..077100c90 100644 --- a/Sources/OpenSwiftUI_SPI/module.modulemap +++ b/Sources/OpenSwiftUI_SPI/module.modulemap @@ -58,6 +58,11 @@ module MobileGestaltPrivate { export * } +module libAccessibilityPrivate { + umbrella "Shims/Accessibility" + export * +} + module UIFoundation_Private { umbrella "Shims/UIFoundation" export *