diff --git a/Package.resolved b/Package.resolved index 6499b6007..b1bbb869b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -25,7 +25,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenCoreGraphics", "state" : { "branch" : "main", - "revision" : "1a1b9ad092ed373a3c94df7b589e3e099da93699" + "revision" : "706d964f419ce293fe1c7afa9b784de0d91f7e9a" } }, { diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityCore.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCore.swift new file mode 100644 index 000000000..e2085e6cd --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityCore.swift @@ -0,0 +1,33 @@ +// +// AccessibilityCore.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: TODO + +package enum AccessibilityCore {} + +extension _GraphInputs { + package var needsAccessibility: Bool { + get { options.contains(.needsAccessibility) } + set { options.setValue(newValue, for: .needsAccessibility) } + } +} + +extension _ViewInputs { + package var needsAccessibility: Bool { + get { base.needsAccessibility } + set { base.needsAccessibility = newValue } + } +} + +package struct WithinAccessibilityRotor: ViewInputBoolFlag { + package init() {} +} + +extension _ViewInputs { + @inline(__always) + package var withinAccessibilityRotor: Bool { + needsAccessibility && self[WithinAccessibilityRotor.self] + } +} diff --git a/Sources/OpenSwiftUICore/Data/Util/StrongHash.swift b/Sources/OpenSwiftUICore/Data/Util/StrongHash.swift index 7da1a7bef..fee7caabd 100644 --- a/Sources/OpenSwiftUICore/Data/Util/StrongHash.swift +++ b/Sources/OpenSwiftUICore/Data/Util/StrongHash.swift @@ -12,6 +12,7 @@ import CommonCrypto #endif import Foundation +import OpenAttributeGraphShims import OpenRenderBoxShims package protocol StronglyHashable { @@ -149,9 +150,15 @@ package struct StrongHasher { } package mutating func combineType(_ type: any Any.Type) { -// let signature = OAGTypeGetSignature -// CC_SHA1_Update(&state, signature, 20) - preconditionFailure("Blocked by latest OAGTypeGetSignature") + let signature = Metadata(type).signature + _ = withUnsafePointer(to: signature.bytes) { ptr in + #if OPENSWIFTUI_SWIFT_CRYPTO + // TODO: Auditd signature API design + _openSwiftUIUnimplementedFailure() + #else + CC_SHA1_Update(&state, ptr, 20) + #endif + } } } diff --git a/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainer.swift b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainer.swift new file mode 100644 index 000000000..ceefc80ba --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainer.swift @@ -0,0 +1,358 @@ +// +// DynamicContainer.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Blocked by DynamicPreferenceCombiner and DynamicContainerInfo +// ID: E7D4CD2D59FB8C77D6C7E9C534464C17 (SwiftUICore) + +package import OpenAttributeGraphShims + +// MARK: - DynamicContainerID + +package struct DynamicContainerID: Hashable, Comparable { + package var uniqueId: UInt32 + + package var viewIndex: Int32 + + package init(uniqueId: UInt32, viewIndex: Int32) { + self.uniqueId = uniqueId + self.viewIndex = viewIndex + } + + package static func < (lhs: DynamicContainerID, rhs: DynamicContainerID) -> Bool { + lhs.uniqueId < rhs.uniqueId || (lhs.uniqueId == rhs.uniqueId && lhs.viewIndex < rhs.viewIndex) + } +} + +// MARK: - DynamicContainer + +package struct DynamicContainer { + package typealias ID = DynamicContainerID + + package struct Info: Equatable { + package private(set) var items: [DynamicContainer.ItemInfo] = [] + + package private(set) var indexMap: [UInt32: Int] = [:] + + private(set) var displayMap: [UInt32]? + + private(set) var removedCount: Int = .zero + + private(set) var unusedCount: Int = .zero + + private(set) var allUnary: Bool = true + + private(set) var seed: UInt32 = .zero + + func viewIndex(id: ID) -> Int? { + guard let value = indexMap[id.uniqueId] else { + return nil + } + return value + Int(id.viewIndex) + } + + func item(for subgraph: Subgraph) -> ItemInfo? { + items.first { info in + info.subgraph.isAncestor(of: subgraph) + } + } + + @inline(__always) + subscript(_ index: Int) -> ItemInfo { + precondition(index >= 0 && index < items.count, "invalid view index") + return items[index] + } + + package static func == (lhs: DynamicContainer.Info, rhs: DynamicContainer.Info) -> Bool { + lhs.seed == rhs.seed + } + } + + package class ItemInfo { + fileprivate let subgraph: Subgraph + + final package let uniqueId: UInt32 + + fileprivate let viewCount: Int32 + + fileprivate let outputs: _ViewOutputs + + fileprivate let needsTransitions: Bool + + fileprivate var listener: DynamicAnimationListener? + + fileprivate var zIndex: Double = .zero + + fileprivate var removalOrder: UInt32 = .zero + + fileprivate(set) var precedingViewCount: Int32 = .zero + + fileprivate var resetSeed: UInt32 = .zero + + final package private(set) var phase: TransitionPhase? + + package init( + subgraph: Subgraph, + uniqueId: UInt32, + viewCount: Int32, + phase: TransitionPhase, + needsTransitions: Bool, + outputs: _ViewOutputs + ) { + self.subgraph = subgraph + self.uniqueId = uniqueId + self.viewCount = viewCount + self.outputs = outputs + self.needsTransitions = needsTransitions + self.phase = phase + } + + package var list: Attribute? { nil } + + package var id: ViewList.ID? { nil } + + final package func `for`(_ type: A.Type) -> DynamicContainer._ItemInfo where A: DynamicContainerAdaptor { + unsafeDowncast(self, to: DynamicContainer._ItemInfo.self) + } + + @inline(__always) + var count: Int32 { + viewCount + precedingViewCount + } + } + + final package class _ItemInfo: DynamicContainer.ItemInfo where Adaptor: DynamicContainerAdaptor { + init( + item: Adaptor.Item, + itemLayout: Adaptor.ItemLayout, + subgraph: Subgraph, + uniqueId: UInt32, + viewCount: Int32, + phase: TransitionPhase, + needsTransitions: Bool, + outputs: _ViewOutputs + ) { + self.item = item + self.itemLayout = itemLayout + super.init( + subgraph: subgraph, + uniqueId: uniqueId, + viewCount: viewCount, + phase: phase, + needsTransitions: needsTransitions, + outputs: outputs + ) + } + + package private(set) var item: Adaptor.Item + + package let itemLayout: Adaptor.ItemLayout + + override package var list: Attribute? { + item.list + } + + override package var id: _ViewList_ID? { + item.viewID + } + } + + package static func makeContainer( + adaptor: Adaptor, + inputs: _ViewInputs + ) -> (Attribute, _ViewOutputs) where Adaptor: DynamicContainerAdaptor { + var outputs = _ViewOutputs() + for key in inputs.preferences.keys { + func project(_ key: K.Type) where K: PreferenceKey { + outputs[key] = Attribute(DynamicPreferenceCombiner(info: .init())) + } + project(key) + } + let asyncSignal = Attribute(value: ()) + let info = Attribute(DynamicContainerInfo( + asyncSignal: asyncSignal, + adaptor: adaptor, + inputs: inputs, + outputs: outputs + )) + info.addInput(asyncSignal, options: ._4, token: 0) + info.flags = .transactional + outputs.forEachPreference { key, identifier in + func project(_ key: K.Type) where K: PreferenceKey { + identifier.mutateBody( + as: DynamicPreferenceCombiner.self, + invalidating: true + ) { combiner in + combiner.$info = info + } + } + project(key) + } + return (info, outputs) + } +} + +// MARK: - DynamicAnimationListener + +private class DynamicAnimationListener: AnimationListener, @unchecked Sendable { + weak var viewGraph: ViewGraph? + let asyncSignal: WeakAttribute + var count: Int + + init(viewGraph: ViewGraph?, asyncSignal: WeakAttribute) { + self.viewGraph = viewGraph + self.asyncSignal = asyncSignal + self.count = 0 + } + + override func animationWasAdded() { + count &+= 1 + } + + override func animationWasRemoved() { + count &-= 1 + guard count == 0, let viewGraph else { + return + } + viewGraph.continueTransaction { [asyncSignal] in + asyncSignal.attribute?.invalidateValue() + } + } +} + +// MARK: - DynamicPreferenceCombiner [WIP] + +private struct DynamicPreferenceCombiner: Rule, AsyncAttribute, CustomStringConvertible where K: PreferenceKey { + @OptionalAttribute + var info: DynamicContainer.Info? + + var value: K.Value { + // TODO: + _openSwiftUIUnimplementedWarning() + return K.defaultValue + } + + var description: String { + "∪+ \(K.readableName)" + } +} + +// MARK: - DynamicContainerInfo [WIP] + +struct DynamicContainerInfo: StatefulRule, AsyncAttribute where Adapter: DynamicContainerAdaptor { // FIXME + @Attribute + var asyncSignal: Void + + var adaptor: Adapter + + let inputs: _ViewInputs + + let outputs: _ViewOutputs + + let parentSubgraph: Subgraph + + var info: DynamicContainer.Info + + var lastUniqueId: UInt32 + + var lastRemoved: UInt32 + + var lastResetSeed: UInt32 + + var needsPhaseUpdate: Bool + + init( + asyncSignal: Attribute, + adaptor: Adapter, + inputs: _ViewInputs, + outputs: _ViewOutputs, + info: DynamicContainer.Info = .init(), + lastUniqueId: UInt32 = 0, + lastRemoved: UInt32 = 0, + lastResetSeed: UInt32 = .max, + needsPhaseUpdate: Bool = false + ) { + self._asyncSignal = asyncSignal + self.adaptor = adaptor + self.inputs = inputs + self.outputs = outputs + self.parentSubgraph = Subgraph.current! + self.info = info + self.lastUniqueId = lastUniqueId + self.lastRemoved = lastRemoved + self.lastResetSeed = lastResetSeed + self.needsPhaseUpdate = needsPhaseUpdate + } + + typealias Value = DynamicContainer.Info + + func updateValue() { + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - DynamicViewPhase + +private struct DynamicViewPhase: Rule, AsyncAttribute { + @Attribute + var info: DynamicContainer.Info + + @Attribute + var phase: ViewPhase + + let uniqueId: UInt32 + + var value: ViewPhase { + var phase = phase + guard let index = info.indexMap[uniqueId] else { + return phase + } + let resetSeed = info.items[index].resetSeed + let isBeingRemoved = info.items[index].phase == .didDisappear + phase.resetSeed += resetSeed + phase.isBeingRemoved = phase.isBeingRemoved || isBeingRemoved + return phase + } +} + +// MARK: - DynamicTransaction + +private struct DynamicTransaction: StatefulRule, AsyncAttribute { + @Attribute + var info: DynamicContainer.Info + + @Attribute + var transaction: Transaction + + let uniqueId: UInt32 + + var wasRemoved: Bool + + typealias Value = Transaction + + mutating func updateValue() { + guard let index = info.indexMap[uniqueId], + let transitionPhase = info.items[index].phase + else { + value = Transaction() + return + } + var transaction = transaction + let oldWasRemoved = wasRemoved + wasRemoved = false + switch transitionPhase { + case .willAppear: + transaction.animation = nil + transaction.disablesAnimations = true + case .identity: + break + case .didDisappear: + if !oldWasRemoved, let listener = info.items[index].listener { + transaction.addAnimationListener(listener) + } + wasRemoved = true + } + value = transaction + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainerAdaptor.swift b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainerAdaptor.swift new file mode 100644 index 000000000..fde695a22 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainerAdaptor.swift @@ -0,0 +1,62 @@ +// +// DynamicContainerAdaptor.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package import OpenAttributeGraphShims + +package protocol DynamicContainerAdaptor { + associatedtype Item: DynamicContainerItem + + associatedtype Items + + static var maxUnusedItems: Int { get } + + mutating func updatedItems() -> Items? + + func foreachItem(items: Items, _ body: (Item) -> Void) + + static func containsItem(_ items: Items, _ item: Item) -> Bool + + associatedtype ItemLayout + + func makeItemLayout( + item: Item, + uniqueId: UInt32, + inputs: _ViewInputs, + containerInfo: Attribute, + containerInputs: (inout _ViewInputs) -> Void + ) -> (_ViewOutputs, ItemLayout) + + func removeItemLayout(uniqueId: UInt32, itemLayout: ItemLayout) +} + +extension DynamicContainerAdaptor where Item == Items { + @inline(__always) + package func foreachItem(items: Items, _ body: (Item) -> Void) { + body(items) + } + + @inline(__always) + package static func containsItem(_ items: Items, _ item: Item) -> Bool { + items.matchesIdentity(of: item) + } +} + +extension DynamicContainerAdaptor where Item == Items.Element, Items: Collection { + @inline(__always) + package func foreachItem(items: Items, _ body: (Item) -> Void) { + items.forEach(body) + } + + @inline(__always) + package static func containsItem(_ items: Items, _ item: Item) -> Bool { + items.contains { $0.matchesIdentity(of: item) } + } +} + +extension DynamicContainerAdaptor { + package static var maxUnusedItems: Int { .zero } +} diff --git a/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainerItem.swift b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainerItem.swift new file mode 100644 index 000000000..b2d84e6d9 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicContainerItem.swift @@ -0,0 +1,40 @@ +// +// DynamicContainerItem.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package import OpenAttributeGraphShims + +package protocol DynamicContainerItem { + var count: Int { get } + + var needsTransitions: Bool { get } + + var zIndex: Double { get } + + func matchesIdentity(of other: Self) -> Bool + + static var supportsReuse: Bool { get } + + func canBeReused(by other: Self) -> Bool + + var list: Attribute? { get } + + var viewID: ViewList.ID? { get } +} + +extension DynamicContainerItem { + package var needsTransitions: Bool { false } + + package var zIndex: Double { .zero } + + package static var supportsReuse: Bool { false } + + package func canBeReused(by other: Self) -> Bool { false } + + package var list: Attribute? { nil } + + package var viewID: ViewList.ID? { nil } +} diff --git a/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicLayoutMap.swift b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicLayoutMap.swift new file mode 100644 index 000000000..acc2210f1 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicLayoutMap.swift @@ -0,0 +1,117 @@ +// +// DynamicLayoutMap.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +package struct DynamicLayoutMap { + private var map: [(id: DynamicContainerID, value: LayoutProxyAttributes)] + package var sortedArray: [LayoutProxyAttributes] + package var sortedSeed: UInt32 + + package init() { + map = [] + sortedArray = [] + sortedSeed = .zero + } + + package init( + map: [(id: DynamicContainerID, value: LayoutProxyAttributes)], + sortedArray: [LayoutProxyAttributes] = [LayoutProxyAttributes](), + sortedSeed: UInt32 = 0 as UInt32 + ) { + self.map = map + self.sortedArray = sortedArray + self.sortedSeed = sortedSeed + } + + package subscript(id: DynamicContainerID) -> LayoutProxyAttributes { + get { + map.first(where: { $0.id == id })?.value ?? .init() + } + set { + let index = map.firstIndex(where: { $0.id == id }) + if let index { + if newValue.isEmpty { + map.remove(at: index) + } else { + map[index].value = newValue + } + } else { + if !newValue.isEmpty { + map.insert((id, newValue), at: 0) + } + } + sortedSeed = .zero + } + } + + package mutating func remove(uniqueId: UInt32) { + guard !map.isEmpty else { + return + } + let index = map.partitionPoint { (id, value) in + DynamicContainerID(uniqueId: uniqueId, viewIndex: 0) <= id + } + var endIndex = index + guard index != map.count else { + return + } + while endIndex != map.count { + let indexUniqueId = map[index].id.uniqueId + guard indexUniqueId == uniqueId else { + break + } + endIndex &+= 1 + } + map.removeSubrange(index ..< endIndex) + sortedSeed = .zero + } + + package mutating func attributes( + info: DynamicContainer.Info + ) -> [LayoutProxyAttributes] { + guard sortedSeed != info.seed else { + return sortedArray + } + let allUnary = info.allUnary + sortedArray.removeAll(keepingCapacity: true) + var activeCount = info.items.count - (info.unusedCount + info.removedCount) + let lastActiveIndex = activeCount - 1 + if lastActiveIndex >= 0, !allUnary { + let lastActiveItem = info.items[lastActiveIndex] + activeCount &+= Int(lastActiveItem.count) + } + for index in 0 ..< activeCount { + let targetIndex: Int + if allUnary { + targetIndex = index + } else { + var itemIndex = 0 + while index >= Int(info[itemIndex].count) { + itemIndex &+= 1 + } + targetIndex = itemIndex + } + let viewIndex = index - numericCast(info[targetIndex].precedingViewCount) + let uniqueId = info[targetIndex].uniqueId + let targetId = DynamicContainerID(uniqueId: uniqueId, viewIndex: Int32(viewIndex)) + var attributes = LayoutProxyAttributes() + if !map.isEmpty { + let valueIndex = map.partitionPoint { (id, value) in + targetId <= id + } + if valueIndex != map.count { + let value = map[valueIndex] + if value.id == targetId { + attributes = value.value + } + } + } + sortedArray.append(attributes) + } + sortedSeed = info.seed + return sortedArray + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicLayoutView.swift b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicLayoutView.swift new file mode 100644 index 000000000..48d160793 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicLayoutView.swift @@ -0,0 +1,403 @@ +// +// DynamicLayoutView.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Blocked by Scrollable and ContentTransition +// ID: FF3C661D9D8317A1C8FE2B7FD4EDE12C (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Layout + makeDynamicView + +extension Layout { + static func makeDynamicView( + root: _GraphValue, + inputs: _ViewInputs, + properties: LayoutProperties, + list: Attribute + ) -> _ViewOutputs { + var inputs = inputs + let requiresScrollable = inputs.preferences.requiresScrollable + let requiresScrollTargetRoleContent = inputs.preferences.requiresScrollTargetRoleContent + let scrollTargetRole = inputs.scrollTargetRole + let scrollTargetRemovePreference = inputs.scrollTargetRemovePreference + let withinAccessibilityRotor = inputs.withinAccessibilityRotor + var childComputer: Attribute? + let childGeometry: OptionalAttribute<[ViewGeometry]> + let needLayout = inputs.requestsLayoutComputer || inputs.needsGeometry + if needLayout || requiresScrollable || withinAccessibilityRotor { + let layoutComputer = Attribute( + DynamicLayoutComputer( + layout: root.value, + environment: inputs.environment, + containerInfo: .init(), + layoutMap: .init() + ) + ) + childComputer = layoutComputer + childGeometry = .init(Attribute( + LayoutChildGeometries( + parentSize: inputs.size, + parentPosition: inputs.position, + layoutComputer: layoutComputer + ) + )) + } else { + childGeometry = .init() + } + var childInputs = inputs + childInputs.requestsLayoutComputer = false + + if requiresScrollTargetRoleContent && scrollTargetRemovePreference { + inputs.preferences.requiresScrollTargetRoleContent = false + inputs.preferences.requiresScrollStateRequest = false + } + if scrollTargetRole.attribute != nil { + childInputs.base.scrollTargetRole = .init() + childInputs.base.scrollTargetRemovePreference = true + childInputs.base.setScrollPosition(storage: nil, kind: .scrollContent) + childInputs.base.setScrollPositionAnchor(.init(), kind: .scrollContent) + } + func mapMutator(thunk: (inout DynamicLayoutMap) -> ()) -> () { + guard let childComputer else { return } + childComputer.mutateBody( + as: DynamicLayoutComputer.self, + invalidating: true + ) { computer in + thunk(&computer.layoutMap) + } + } + var (containerInfo, outputs) = DynamicContainer.makeContainer( + adaptor: DynamicLayoutViewAdaptor( + items: list, + childGeometries: childGeometry, + mutateLayoutMap: mapMutator(thunk:) + ), + inputs: childInputs + ) + if let childComputer { + childComputer.mutateBody( + as: DynamicLayoutComputer.self, + invalidating: true + ) { computer in + computer.$containerInfo = containerInfo + } + } + if requiresScrollable || scrollTargetRole.attribute == nil || withinAccessibilityRotor { + // TODO: Scrollable related + } + if inputs.requestsLayoutComputer, let childComputer { + outputs.layoutComputer = childComputer + } + return outputs + } +} + +// MARK: - DynamicLayoutViewChildGeometry + +private struct DynamicLayoutViewChildGeometry: StatefulRule, AsyncAttribute { + @Attribute var containerInfo: DynamicContainer.Info + @Attribute var childGeometries: [ViewGeometry] + let id: DynamicContainerID + + typealias Value = ViewGeometry + + func updateValue() { + guard let index = containerInfo.viewIndex(id: id), index < childGeometries.count else { + if !hasValue { + value = .zero + } + return + } + value = childGeometries[index] + } +} + +// MARK: - DynamicLayoutViewAdaptor [WIP] ContentTransition + +struct DynamicLayoutViewAdaptor: DynamicContainerAdaptor { + private struct MakeTransition: TransitionVisitor { + var containerInfo: Attribute + var uniqueId: UInt32 + var item: DynamicViewListItem + var inputs: _ViewInputs + var makeElt: (_ViewInputs) -> _ViewOutputs + var outputs: _ViewOutputs? + var isArchived: Bool + + mutating func visit(_ transition: T) where T: Transition { + let helper = TransitionHelper( + list: .init(item.list), + info: containerInfo, + uniqueID: uniqueId, + transition: transition, + phase: .identity + ) + if isArchived { + makeArchivedTransition(helper: helper) + } else { + let transition = Attribute( + ViewListTransition(helper: helper) + ) + let makeElt = self.makeElt + outputs = T.makeView( + view: .init(transition), + inputs: inputs, + body: { _, inputs in + makeElt(inputs) + } + ) + } + } + + mutating func makeArchivedTransition(helper: TransitionHelper) where T: Transition { + guard helper.transition.hasContentTransition else { + outputs = makeElt(inputs) + return + } + // TODO: archived transition + _openSwiftUIUnimplementedFailure() + } + } + + struct ItemLayout { + var release: ViewList.Elements.Release? + } + + @Attribute var items: ViewList + @OptionalAttribute var childGeometries: [ViewGeometry]? + var mutateLayoutMap: ((inout DynamicLayoutMap) -> ()) -> () + + func updatedItems() -> ViewList? { + let (items, itemsChanged) = $items.changedValue() + guard itemsChanged else { + return nil + } + return items + } + + func foreachItem( + items: any ViewList, + _ body: (DynamicViewListItem) -> Void + ) { + var index = 0 + items.applySublists(from: &index, list: $items) { sublist in + body(.init( + id: sublist.id, + elements: sublist.elements, + traits: sublist.traits, + list: sublist.list + )) + return true + } + } + + static func containsItem( + _ items: any ViewList, + _ item: DynamicViewListItem + ) -> Bool { + var index = 0 + let result = items.applySublists(from: &index, list: nil) { sublist in + sublist.id != item.id + } + return !result + } + + func makeItemLayout( + item: DynamicViewListItem, + uniqueId: UInt32, + inputs: _ViewInputs, + containerInfo: Attribute, + containerInputs: (inout _ViewInputs) -> () + ) -> (_ViewOutputs, DynamicLayoutViewAdaptor.ItemLayout) { + let isArchived = inputs.archivedView.isArchived + let traits = item.traits + let transition: AnyTransition? + if let t = traits.optionalTransition(ignoringIdentity: !isArchived) { + let prefersCrossFadeTransitions = Graph.withoutUpdate { + inputs.environment.value.accessibilityPrefersCrossFadeTransitions + } + transition = t.adjustedForAccessibility(prefersCrossFade: prefersCrossFadeTransitions) + } else { + transition = nil + } + var containerID = DynamicContainerID(uniqueId: uniqueId, viewIndex: 0) + let outputs = item.elements.makeAllElements(inputs: inputs) { elementInputs, body in + var elementInputs = elementInputs + if elementInputs.needsGeometry { + let childGeometry = Attribute( + DynamicLayoutViewChildGeometry( + containerInfo: containerInfo, + childGeometries: $childGeometries!, + id: containerID + ) + ) + elementInputs.size = childGeometry.size() + elementInputs.position = childGeometry.origin() + } + let outputs: _ViewOutputs + if let transition { + var makeTransition = MakeTransition( + containerInfo: containerInfo, + uniqueId: uniqueId, + item: item, + inputs: elementInputs, + makeElt: body, + isArchived: isArchived + ) + transition.visitBase(applying: &makeTransition) + outputs = makeTransition.outputs! + } else { + outputs = body(elementInputs) + } + mutateLayoutMap({ + $0[containerID] = LayoutProxyAttributes( + layoutComputer: .init(outputs.layoutComputer), + traitsList: .init(item.list) + ) + }) + containerID.viewIndex &+= 1 + return outputs + } + return (outputs ?? .init(), .init(release: item.elements.retain())) + } + + func removeItemLayout(uniqueId: UInt32, itemLayout: ItemLayout) { + mutateLayoutMap({ $0.remove(uniqueId: uniqueId)}) + } +} + +// MARK: - DynamicLayoutComputer + +private struct DynamicLayoutComputer: StatefulRule, AsyncAttribute, CustomStringConvertible where L: Layout { + @Attribute var layout: L + @Attribute var environment: EnvironmentValues + @OptionalAttribute var containerInfo: DynamicContainer.Info? + + var layoutMap: DynamicLayoutMap + + typealias Value = LayoutComputer + + mutating func updateValue() { + updateLayoutComputer( + layout: layout, + environment: $environment, + attributes: layoutMap.attributes(info: containerInfo!) + ) + } + + var description: String { + "\(L.self) → LayoutComputer" + } +} + +// TODO: DynamicLayoutScrollable + +struct DynamicLayoutScrollable {} + +// TODO: - ViewListContentTransition + +// FIXME +struct ContentTransitionEffect {} + +private struct ViewListContentTransition: StatefulRule, AsyncAttribute where T: Transition { + var helper: TransitionHelper + @Attribute var size: ViewSize + @Attribute var environment: EnvironmentValues + + init( + helper: TransitionHelper, + size: Attribute, + environment: Attribute + ) { + self.helper = helper + self._size = size + self._environment = environment + } + + typealias Value = ContentTransitionEffect + + func updateValue() { + _openSwiftUIUnimplementedFailure() + } +} + +// MARK: - ViewListArchivedAnimation + +private struct ViewListArchivedAnimation: Rule, AsyncAttribute { + struct Effect: _RendererEffect { + var animation: Animation? + var value: StrongHash? + + func effectValue(size: CGSize) -> DisplayList.Effect { + guard let value else { + return .identity + } + return .interpolatorAnimation( + .init( + value: value, + animation: animation + ) + ) + } + } + + @OptionalAttribute var traitsList: (any ViewList)? + + var value: Effect { + guard let traitsList else { + return .init() + } + guard let trait = traitsList.traits.archivedAnimationTrait else { + return .init() + } + return Effect(animation: trait.animation, value: trait.hash) + } +} + +// MARK: - TransitionHelper + +private struct TransitionHelper where T: Transition { + @OptionalAttribute var list: (any ViewList)? + @Attribute var info: DynamicContainer.Info + let uniqueID: UInt32 + var transition: T + var phase: TransitionPhase + + mutating func update() -> Bool { + var changed = false + if let index = info.indexMap[uniqueID] { + let itemInfo = info.items[index] + if let itemPhase = itemInfo.phase { + changed = phase != itemPhase + phase = itemPhase + } + } + guard phase != .didDisappear else { + return changed + } + let traits = list?.traits ?? .init() + traits.transition.base(as: T.self).map { + transition = $0 + changed = true + } + return changed + } +} + +// MARK: - ViewListTransition + +private struct ViewListTransition: StatefulRule, AsyncAttribute where T: Transition { + var helper: TransitionHelper + + typealias Value = T.Body + + mutating func updateValue() { + let changed = helper.update() + guard changed || !hasValue else { + return + } + value = helper.transition.body(content: .init(), phase: helper.phase) + } +} diff --git a/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicViewListItem.swift b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicViewListItem.swift new file mode 100644 index 000000000..0f5278ec8 --- /dev/null +++ b/Sources/OpenSwiftUICore/Layout/Dynamic/DynamicViewListItem.swift @@ -0,0 +1,33 @@ +// +// DynamicViewListItem.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete + +import OpenAttributeGraphShims + +struct DynamicViewListItem: DynamicContainerItem { + var id: ViewList.ID + var elements: ViewList.Elements + var traits: ViewTraitCollection + var list: Attribute? + + var count: Int { + elements.count + } + + var needsTransitions: Bool { + traits.optionalTransition() != nil + } + + var zIndex: Double { + traits.zIndex + } + + func matchesIdentity(of other: DynamicViewListItem) -> Bool { + list == other.list && id == other.id + } + + var viewID: _ViewList_ID? { id } +} diff --git a/Sources/OpenSwiftUICore/Layout/UnaryLayout.swift b/Sources/OpenSwiftUICore/Layout/LayoutView.swift similarity index 96% rename from Sources/OpenSwiftUICore/Layout/UnaryLayout.swift rename to Sources/OpenSwiftUICore/Layout/LayoutView.swift index 7939fdc32..4c3dc53c8 100644 --- a/Sources/OpenSwiftUICore/Layout/UnaryLayout.swift +++ b/Sources/OpenSwiftUICore/Layout/LayoutView.swift @@ -1,9 +1,9 @@ // -// UnaryLayout.swift +// LayoutView.swift // OpenSwiftUICore // // Status: Blocked by makeDynamicView -// ID: A7DFBD5AC47BCDAAE5525781FBD33CF6 (SwiftUICore?) +// ID: A7DFBD5AC47BCDAAE5525781FBD33CF6 (SwiftUICore) package import Foundation package import OpenAttributeGraphShims @@ -167,15 +167,6 @@ extension Layout { return outputs } } - - static func makeDynamicView( - root: _GraphValue, - inputs: _ViewInputs, - properties: LayoutProperties, - list: Attribute - ) -> _ViewOutputs { - _openSwiftUIUnimplementedFailure() - } } package struct LayoutChildGeometries: Rule, AsyncAttribute { diff --git a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift index 0b5467617..8b97a13ee 100644 --- a/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift +++ b/Sources/OpenSwiftUICore/Util/AttributeGraphAdditions.swift @@ -7,6 +7,13 @@ package import OpenAttributeGraphShims +// FIXME +extension OAGInputOptions { + package static var _4: OAGInputOptions { + .init(rawValue: 1 << 2) + } +} + // MARK: - Defaultable [6.5.4] package protocol Defaultable { diff --git a/Sources/OpenSwiftUICore/View/Scroll/ScrollPosition+Modifiers.swift b/Sources/OpenSwiftUICore/View/Scroll/ScrollPosition+Modifiers.swift new file mode 100644 index 000000000..02ce3ce22 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Scroll/ScrollPosition+Modifiers.swift @@ -0,0 +1,57 @@ +// +// ScrollPosition+Modifiers.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: TODO +// ID: E7547C80DE3C7109A44F15E50A35C84F (SwiftUICore) + +package import OpenAttributeGraphShims + +package enum ScrollPositionStorage { + case binding(Attribute>) + case value(Attribute) +} + +package enum ScrollStateInputKind { + case scrollView + case scrollContent +} + +extension _GraphInputs { + private struct ContentScrollPositionKey: GraphInput { + static var defaultValue: ScrollPositionStorage? { nil } + } + + private struct ScrollPositionKey: GraphInput { + static var defaultValue: ScrollPositionStorage? { nil } + } + + private struct ScrollPositionAnchorKey: GraphInput { + static let defaultValue: OptionalAttribute = .init() + } + + private struct ContentScrollPositionAnchorKey: GraphInput { + static let defaultValue: OptionalAttribute = .init() + } + + package mutating func setScrollPosition( + storage: ScrollPositionStorage?, + kind: ScrollStateInputKind + ) { + switch kind { + case .scrollView: self[ScrollPositionKey.self] = storage + case .scrollContent: self[ContentScrollPositionKey.self] = storage + } + } + + package mutating func setScrollPositionAnchor( + _ anchor: OptionalAttribute, + kind: ScrollStateInputKind + ) { + switch kind { + case .scrollView: self[ScrollPositionAnchorKey.self] = anchor + case .scrollContent: self[ContentScrollPositionAnchorKey.self] = anchor + } + } +} diff --git a/Sources/OpenSwiftUICore/View/Scroll/ScrollPosition.swift b/Sources/OpenSwiftUICore/View/Scroll/ScrollPosition.swift new file mode 100644 index 000000000..bbec1c96b --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Scroll/ScrollPosition.swift @@ -0,0 +1,5 @@ +// +// ScrollPosition.swift +// OpenSwiftUICore + +public struct ScrollPosition {} diff --git a/Sources/OpenSwiftUICore/View/Scroll/ScrollStateRequest.swift b/Sources/OpenSwiftUICore/View/Scroll/ScrollStateRequest.swift new file mode 100644 index 000000000..207ef4e3b --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Scroll/ScrollStateRequest.swift @@ -0,0 +1,37 @@ +// +// ScrollStateRequest.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: TODO + +// MARK: - ScrollStateRequest TODO +package protocol ScrollStateRequest {} + +// TODO + +// MARK: - UpdateScrollStateRequestKey + +package struct UpdateScrollStateRequestKey: PreferenceKey { + package typealias Value = [any ScrollStateRequest] + + package static let defaultValue: Value = [] + + package static func reduce(value: inout Value, nextValue: () -> Value) { + value.append(contentsOf: nextValue()) + } +} + +extension PreferencesInputs { + @inline(__always) + package var requiresScrollStateRequest: Bool { + get { contains(UpdateScrollStateRequestKey.self) } + set { + if newValue { + add(UpdateScrollStateRequestKey.self) + } else { + remove(UpdateScrollStateRequestKey.self) + } + } + } +} diff --git a/Sources/OpenSwiftUICore/View/Scroll/ScrollTarget.swift b/Sources/OpenSwiftUICore/View/Scroll/ScrollTarget.swift new file mode 100644 index 000000000..aec6ee1b5 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Scroll/ScrollTarget.swift @@ -0,0 +1,200 @@ +// +// ScrollTarget.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: D49197C3D3C61F0DA0F0CF1D72D0077A (SwiftUICore) + +package import OpenAttributeGraphShims +public import OpenCoreGraphicsShims + +/// A type defining the target in which a scroll view should try and scroll to. +@available(OpenSwiftUI_v5_0, *) +public struct ScrollTarget { + + /// The rect that a scrollable view should try and have contained. + public var rect: CGRect + + /// The anchor to which the rect should be aligned within the visible + /// region of the scrollable view. + public var anchor: UnitPoint? + + package init(rect: CGRect, anchor: UnitPoint? = nil) { + self.rect = rect + self.anchor = anchor + } +} + +@available(OpenSwiftUI_v6_0, *) +extension ScrollTarget: Hashable, Equatable {} + +@available(*, unavailable) +extension ScrollTarget: Sendable {} + +// MARK: - ScrollTargetConfiguration + +package struct ScrollTargetConfiguration { + package var animation: Animation? + package var requiresVisibility: Bool + package var preservesVelocity: Bool + + package init(transaction: Transaction) { + if transaction.animation != nil, !transaction.disablesAnimations { + animation = transaction.animation + } else { + animation = nil + } + requiresVisibility = transaction._scrollToRequiresCompleteVisibility + preservesVelocity = transaction.scrollPositionUpdatePreservesVelocity + } +} + +// MARK: - ScrollTargetRole + +package struct ScrollTargetRole { + package enum Role { + case container + case target + } + + package var role: ScrollTargetRole.Role + + package static var container: ScrollTargetRole { + .init(role: .container) + } + + package static var target: ScrollTargetRole { + .init(role: .target) + } +} + +extension ScrollTargetRole { + package typealias TargetCollection = [ScrollTargetRole.Role: [any ScrollableCollection]] + + package struct Key: PreferenceKey { + package typealias Value = ScrollTargetRole.TargetCollection + + package static let defaultValue: Value = [:] + + package static func reduce( + value: inout Value, + nextValue: () -> Value + ) { + // TO BE VERIFY + value.merge(nextValue()) { old, new in new } + } + } + + package struct ContentKey: PreferenceKey { + package typealias Value = ScrollTargetRole.TargetCollection + + package static let defaultValue: Value = [:] + + package static func reduce( + value: inout Value, + nextValue: () -> Value + ) { + // TO BE VERIFY + value.merge(nextValue()) { old, new in new } + } + } + + package struct SetLayout: Rule { + @Attribute var role: ScrollTargetRole.Role? + @Attribute var collection: any ScrollableCollection + + package init( + role: Attribute, + collection: Attribute + ) { + self._role = role + self._collection = collection + } + + package var value: (inout ScrollTargetRole.TargetCollection) -> Void { + { targetCollection in + guard let role else { return } + var colelctions = targetCollection[role] ?? [] + colelctions.append(collection) + targetCollection[role] = colelctions + } + } + } +} + +extension PreferencesInputs { + @inline(__always) + package var requiresScrollTargetRoleContent: Bool { + get { + contains(ScrollTargetRole.ContentKey.self) + } + set { + if newValue { + add(ScrollTargetRole.ContentKey.self) + } else { + remove(ScrollTargetRole.ContentKey.self) + } + } + } +} + +extension Transaction { + private struct IsScrollStateValueUpdateKey: TransactionKey { + static var defaultValue: Bool { false } + } + + @inline(__always) + package var isScrollStateValueUpdate: Bool { + get { self[IsScrollStateValueUpdateKey.self] } + set { self[IsScrollStateValueUpdateKey.self] = newValue } + } +} + +extension _GraphInputs { + private struct ScrollTargetRoleKey: GraphInput { + static let defaultValue: OptionalAttribute = .init() + } + + package var scrollTargetRole: OptionalAttribute { + get { self[ScrollTargetRoleKey.self] } + set { self[ScrollTargetRoleKey.self] = newValue } + } + + private struct RemovePreferenceInput: GraphInput { + static var defaultValue: Bool { false } + } + + package var scrollTargetRemovePreference: Bool { + get { self[RemovePreferenceInput.self] } + set { self[RemovePreferenceInput.self] = newValue } + } +} + +extension _ViewInputs { + package var scrollTargetRole: OptionalAttribute { + base.scrollTargetRole + } + + package var scrollTargetRemovePreference: Bool { + base.scrollTargetRemovePreference + } +} + +extension Transaction { + private struct ScrollToRequiresCompleteVisibility: TransactionKey { + static var defaultValue: Bool { false } + } + + package var _scrollToRequiresCompleteVisibility: Bool { + get { self[ScrollToRequiresCompleteVisibility.self] } + set { self[ScrollToRequiresCompleteVisibility.self] = newValue } + } + + @_spi(Internal) + @available(OpenSwiftUI_v4_0, *) + public var scrollToRequiresCompleteVisibility: Bool { + get { _scrollToRequiresCompleteVisibility } + set { _scrollToRequiresCompleteVisibility = newValue } + } +} diff --git a/Sources/OpenSwiftUICore/View/Scroll/Scrollable.swift b/Sources/OpenSwiftUICore/View/Scroll/Scrollable.swift new file mode 100644 index 000000000..867573cb3 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Scroll/Scrollable.swift @@ -0,0 +1,43 @@ +// +// Scrollable.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 425A368F5B4FB640C2ED9A96D72B5AF3 + +// MARK: - Scrollable [WIP] + +package protocol Scrollable {} + +package protocol ScrollableContainer: Scrollable {} + +package protocol ScrollableCollection : Scrollable {} + +// MARK: ScrollablePreferenceKey + +package struct ScrollablePreferenceKey: PreferenceKey { + package typealias Value = [any Scrollable] + + package static let defaultValue: Value = [] + + package static func reduce(value: inout Value, nextValue: () -> Value) { + value.append(contentsOf: nextValue()) + } +} + +extension PreferencesInputs { + @inline(__always) + package var requiresScrollable: Bool { + get { + contains(ScrollablePreferenceKey.self) + } + set { + if newValue { + add(ScrollablePreferenceKey.self) + } else { + remove(ScrollablePreferenceKey.self) + } + } + } +} diff --git a/Sources/OpenSwiftUICore/View/Scroll/Transaction+Scroll.swift b/Sources/OpenSwiftUICore/View/Scroll/Transaction+Scroll.swift new file mode 100644 index 000000000..724128b23 --- /dev/null +++ b/Sources/OpenSwiftUICore/View/Scroll/Transaction+Scroll.swift @@ -0,0 +1,155 @@ +// +// Transaction+Scroll.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 3107437717620AB5FD95CF7D87A21F58 (SwiftUICore?) + +// MARK: - Transaction + v4 + +extension Transaction { + private struct DisabledPageScrollAnimationKey: TransactionKey { + static var defaultValue: Bool { false } + } + + @_spi(Private) + @available(OpenSwiftUI_v4_0, *) + public var disablesPageScrollAnimations: Bool { + get { self[DisabledPageScrollAnimationKey.self] } + set { self[DisabledPageScrollAnimationKey.self] = newValue } + } +} + +// MARK: - Transaction + v5 + +@available(OpenSwiftUI_v5_0, *) +extension Transaction { + private struct ScrollTargetAnchorKey: TransactionKey { + static var defaultValue: UnitPoint? { + nil + } + } + + /// The preferred alignment of the view within a scroll view's visible + /// region when scrolling to a view. + /// + /// Use this API in conjunction with a + /// ``ScrollViewProxy/scrollTo(_:anchor)`` or when updating the binding + /// provided to a ``View/scrollPosition(id:anchor:)``. + /// + /// @Binding var position: Item.ID? + /// + /// var body: some View { + /// ScrollView { + /// LazyVStack { + /// ForEach(items) { item in + /// ItemView(item) + /// } + /// } + /// .scrollTargetLayout() + /// } + /// .scrollPosition(id: $position) + /// .safeAreaInset(edge: .bottom) { + /// Button("Scroll To Bottom") { + /// withAnimation { + /// withTransaction(\.scrollTargetAnchor, .bottom) { + /// position = items.last?.id + /// } + /// } + /// } + /// } + /// } + /// + /// When used with the ``View/scrollPosition(id:anchor:)`` modifier, + /// this value will be preferred over the anchor specified in the + /// modifier for the current transaction. + public var scrollTargetAnchor: UnitPoint? { + get { self[ScrollTargetAnchorKey.self] } + set { self[ScrollTargetAnchorKey.self] = newValue } + } + + package var _disablesPageScrollAnimations: Bool { + get { disablesPageScrollAnimations } + set { disablesPageScrollAnimations = newValue } + } + + package var isPageScrollAnimated: Bool { + animation != nil && !disablesAnimations && !disablesPageScrollAnimations + } + + private struct ScrollPreservesVelocityKey: TransactionKey { + static var defaultValue: Bool { false } + } + + package var scrollPositionUpdatePreservesVelocity: Bool { + get { self[ScrollPreservesVelocityKey.self] } + set { self[ScrollPreservesVelocityKey.self] = newValue } + } +} + + +// MARK: - ScrollContentOffsetAdjustmentBehavior + +/// A type that defines the different kinds of content offset adjusting +/// behaviors a scroll view can have. +@available(OpenSwiftUI_v6_0, *) +public struct ScrollContentOffsetAdjustmentBehavior { + + enum Role { + case automatic + case enabled + case disabled + } + + var role: Role + + /// The automatic behavior. + /// + /// A scroll view may automatically adjust its content offset + /// based on the current context. The absolute offset may be adjusted + /// to keep content in relatively the same place. For example, + /// when scrolled to the bottom, a scroll view may keep the bottom + /// edge scrolled to the bottom when the overall size of its content + /// changes. + public static var automatic: ScrollContentOffsetAdjustmentBehavior { + .init(role: .automatic) + } + + /// The disabled behavior. + /// + /// A scroll view will not adjust its content offset. + public static var disabled: ScrollContentOffsetAdjustmentBehavior { + .init(role: .disabled) + } +} + +@available(*, unavailable) +extension ScrollContentOffsetAdjustmentBehavior: Sendable {} + +// MARK: - Transaction + v6 + +extension Transaction { + private struct ScrollContentAdjustmentBehaviorKey: TransactionKey { + static var defaultValue: ScrollContentOffsetAdjustmentBehavior { + .automatic + } + } + + /// The behavior a scroll view will have regarding content offset + /// adjustments for the current transaction. + /// + /// A scroll view may automatically adjust its content offset + /// based on the current context. The absolute offset may be adjusted + /// to keep content in relatively the same place. For example, + /// when scrolled to the bottom, a scroll view may keep the bottom + /// edge scrolled to the bottom when the overall size of its content + /// changes. + /// + /// Use this property to disable these kinds of adjustments when needed. + @available(OpenSwiftUI_v6_0, *) + public var scrollContentOffsetAdjustmentBehavior: ScrollContentOffsetAdjustmentBehavior { + get { self[ScrollContentAdjustmentBehaviorKey.self] } + set { self[ScrollContentAdjustmentBehaviorKey.self] = newValue } + } +}