From e32ca2856ad0e80a53c9e64f23918599c089df61 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 24 Aug 2025 16:43:45 +0800 Subject: [PATCH 1/4] Update SExpPrinter --- Sources/OpenSwiftUICore/Util/SExpPrinter.swift | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/Sources/OpenSwiftUICore/Util/SExpPrinter.swift b/Sources/OpenSwiftUICore/Util/SExpPrinter.swift index 1748a7f93..b3a7bfdcd 100644 --- a/Sources/OpenSwiftUICore/Util/SExpPrinter.swift +++ b/Sources/OpenSwiftUICore/Util/SExpPrinter.swift @@ -17,15 +17,8 @@ package struct SExpPrinter { } package mutating func end() -> String { - if depth == 0 { - output.append(")") - return output - } else { - depth -= 1 - indent.removeLast(2) - output.append(")") - return output - } + pop() + return output } package mutating func print(_ string: String, newline: Bool = true) { @@ -56,12 +49,10 @@ package struct SExpPrinter { } package mutating func pop() { - if depth == 0 { - output.append(")") - } else { + if depth != 0 { depth -= 1 indent.removeLast(2) - output.append(")") } + output.append(")") } } From 840a9311b351a8e71b79c7754db5087d387fca0e Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 24 Aug 2025 17:34:16 +0800 Subject: [PATCH 2/4] Add ScrapeableContent API --- .../AccessibilityProperties.swift | 5 + .../Util/Data/ScrapeableContent.swift | 201 ++++++++++++++++++ .../Util/Data/ScrapeableID.swift | 31 --- 3 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift create mode 100644 Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift delete mode 100644 Sources/OpenSwiftUICore/Util/Data/ScrapeableID.swift diff --git a/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift b/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift new file mode 100644 index 000000000..14acd7ad7 --- /dev/null +++ b/Sources/OpenSwiftUICore/Accessibility/AccessibilityProperties.swift @@ -0,0 +1,5 @@ +// +// AccessibilityProperties.swift +// OpenSwiftUI + +package struct AccessibilityProperties: Equatable {} diff --git a/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift b/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift new file mode 100644 index 000000000..0cbd50ad9 --- /dev/null +++ b/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift @@ -0,0 +1,201 @@ +// +// ScrapeableContent.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: WIP +// ID: 0EC4D15D4D4D8FD0340271BA6BA4D1B4 + +package import Foundation +package import OpenGraphShims + +// MARK: - ScrapeableID + +package struct ScrapeableID: Hashable { + package static let none: ScrapeableID = .init(value: 0) + + package init() { + value = numericCast(makeUniqueID()) + } + + let value: UInt32 + + private init(value: UInt32) { + self.value = value + } +} + +// MARK: - ViewInputs + Scrapeable + +extension _ViewInputs { + package var isScrapeable: Bool { + get { + guard needsGeometry else { + return false + } + return preferences.contains(DisplayList.Key.self) + } + set { + base.options.setValue(!newValue, for: .doNotScrape) + } + } + + private struct ScrapeableParentID: ViewInput { + static var defaultValue: ScrapeableID { .none } + } + + package var scrapeableParentID: ScrapeableID { + get { self[ScrapeableParentID.self] } + set { self[ScrapeableParentID.self] = newValue } + } +} + +// MARK: - ScrapeableAttribute + +package protocol ScrapeableAttribute: _AttributeBody { + static func scrapeContent(from ident: AnyAttribute) -> ScrapeableContent.Item? +} + +// MARK: - ScrapeableAttachmentViewModifier [WIP] + +private struct ScrapeableAttachmentViewModifier: ViewModifier { + var content: ScrapeableContent.Content? + + struct Attachment: Rule, ScrapeableAttribute { + @Attribute var content: ScrapeableContent.Content? + @Attribute var position: ViewOrigin + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + let localID: ScrapeableID + let parentID: ScrapeableID + + var value: Void { + _openSwiftUIUnimplementedFailure() + } + + static func scrapeContent(from ident: AnyAttribute) -> ScrapeableContent.Item? { + _openSwiftUIUnimplementedFailure() + } + } +} + +extension View { + package func scrapeableAttachment(_ content: ScrapeableContent.Content?) -> some View { + modifier(ScrapeableAttachmentViewModifier(content: content)) + } +} + +// MARK: - ScrapeableContent [WIP] + +package struct ScrapeableContent { + indirect package enum Content { + case text(Text, ResolvedStyledText, EnvironmentValues) + case image(Image, EnvironmentValues) + case platformView(AnyObject) + case accessibilityProperties(AccessibilityProperties, EnvironmentValues, AnyInterfaceIdiom) + case intelligenceProvider(Any) + case opacity(Double) + case userActivity(NSUserActivity) + case hidden + case presentationContainer + case presentationContainerChild + } + + package struct Item { + package var localID: ScrapeableID + package var parentID: ScrapeableID + package var content: ScrapeableContent.Content + package var size: CGSize + package var transform: ViewTransform + + package init( + _ content: ScrapeableContent.Content, + ids localID: ScrapeableID, + _ parentID: ScrapeableID, + position: Attribute, + size: Attribute, + transform: Attribute + ) { + _openSwiftUIUnimplementedFailure() + } + } + + final package class Node { + package let item: Item + + package private(set) var children: [ScrapeableContent.Node] + + init(item: Item, children: [ScrapeableContent.Node], moved: Bool = false) { + self.item = item + self.children = children + self.moved = moved + } + + private var moved = false + } + + package var nodes: [ScrapeableContent.Node] + package var children: [ScrapeableContent] + + package var isEmpty: Bool { + _openSwiftUIUnimplementedFailure() + } +} + +extension Subgraph { + private struct Map { + struct Key: Hashable { + var subgraph: Subgraph + } + + var map: [Key: [ScrapeableContent.Node]] = [:] + + func content(for subgraph: Subgraph, updated: inout Set) -> ScrapeableContent? { + _openSwiftUIUnimplementedFailure() + } + + func resolveParents(nodes: inout [ScrapeableContent.Node], children: inout [ScrapeableContent]) { + _openSwiftUIUnimplementedFailure() + } + } + + package func scrapeContent() -> ScrapeableContent { + _openSwiftUIUnimplementedFailure() + } +} + +extension ViewGraph { + final package func scrapeContent() -> ScrapeableContent { + _openSwiftUIUnimplementedFailure() + } +} + +extension ViewRendererHost { + package func scrapeContent() -> ScrapeableContent { + _openSwiftUIUnimplementedFailure() + } +} + +extension ScrapeableContent: CustomStringConvertible { + private func print(into printer: inout SExpPrinter) { + _openSwiftUIUnimplementedFailure() + } + + package var description: String { + var printer = SExpPrinter(tag: "(scrapeable-content") + print(into: &printer) + return printer.end() + } +} + +extension ScrapeableContent.Node: CustomStringConvertible { + final package var description: String { + _openSwiftUIUnimplementedFailure() + } +} + +extension ScrapeableContent.Item: CustomStringConvertible { + package var description: String { + _openSwiftUIUnimplementedFailure() + } +} diff --git a/Sources/OpenSwiftUICore/Util/Data/ScrapeableID.swift b/Sources/OpenSwiftUICore/Util/Data/ScrapeableID.swift deleted file mode 100644 index a78d93923..000000000 --- a/Sources/OpenSwiftUICore/Util/Data/ScrapeableID.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ScrapeableID.swift -// OpenSwiftUICore -// -// Audited for 6.5.4 -// Status: WIP - -import OpenGraphShims - -package struct ScrapeableID: Hashable { - - @inlinable - package init() { - value = numericCast(makeUniqueID()) - } - - package static let none = ScrapeableID(value: 0) - - let value: UInt32 - - private init(value: UInt32) { - self.value = value - } -} - -// FIXME -extension _ViewInputs { - package var scrapeableParentID: ScrapeableID { - .none - } -} From 59b0a2e47374ee43b5d0f13515cdcd417d6e3e9e Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 24 Aug 2025 20:15:04 +0800 Subject: [PATCH 3/4] Update ScrapeableContent implementation --- .../Util/Data/ScrapeableContent.swift | 133 ++++++++++++++++-- .../OpenSwiftUICore/Util/SExpPrinter.swift | 3 +- 2 files changed, 120 insertions(+), 16 deletions(-) diff --git a/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift b/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift index 0cbd50ad9..f91d4aa18 100644 --- a/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift +++ b/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift @@ -56,11 +56,36 @@ package protocol ScrapeableAttribute: _AttributeBody { static func scrapeContent(from ident: AnyAttribute) -> ScrapeableContent.Item? } -// MARK: - ScrapeableAttachmentViewModifier [WIP] +// MARK: - ScrapeableAttachmentViewModifier -private struct ScrapeableAttachmentViewModifier: ViewModifier { +private struct ScrapeableAttachmentViewModifier: MultiViewModifier, PrimitiveViewModifier { var content: ScrapeableContent.Content? + nonisolated static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + var inputs = inputs + if inputs.needsGeometry, inputs.preferences.requiresDisplayList { + let localID = ScrapeableID() + let attachment = Attribute( + Attachment( + content: modifier.value[keyPath: \.content], + position: inputs.position, + size: inputs.size, + transform: inputs.transform, + localID: localID, + parentID: inputs.scrapeableParentID + ) + ) + attachment.flags = [attachment.flags, .scrapeable] + inputs.scrapeableParentID = localID + } + let outputs = body(_Graph(), inputs) + return outputs + } + struct Attachment: Rule, ScrapeableAttribute { @Attribute var content: ScrapeableContent.Content? @Attribute var position: ViewOrigin @@ -74,7 +99,18 @@ private struct ScrapeableAttachmentViewModifier: ViewModifier { } static func scrapeContent(from ident: AnyAttribute) -> ScrapeableContent.Item? { - _openSwiftUIUnimplementedFailure() + let pointer = ident.info.body.assumingMemoryBound(to: Attachment.self) + guard let content = pointer.pointee.content else { + return nil + } + return .init( + content, + ids: pointer.pointee.localID, + pointer.pointee.parentID, + position: pointer.pointee.$position, + size: pointer.pointee.$size, + transform: pointer.pointee.$transform + ) } } } @@ -116,7 +152,11 @@ package struct ScrapeableContent { size: Attribute, transform: Attribute ) { - _openSwiftUIUnimplementedFailure() + self.localID = localID + self.parentID = parentID + self.content = content + self.size = size.value.value + self.transform = transform.value.withPosition(position.value) } } @@ -125,20 +165,21 @@ package struct ScrapeableContent { package private(set) var children: [ScrapeableContent.Node] - init(item: Item, children: [ScrapeableContent.Node], moved: Bool = false) { + private var moved: Bool + + init(item: Item, children: [ScrapeableContent.Node] = [], moved: Bool = false) { self.item = item self.children = children self.moved = moved } - - private var moved = false } package var nodes: [ScrapeableContent.Node] + package var children: [ScrapeableContent] package var isEmpty: Bool { - _openSwiftUIUnimplementedFailure() + nodes.isEmpty && children.isEmpty } } @@ -150,7 +191,21 @@ extension Subgraph { var map: [Key: [ScrapeableContent.Node]] = [:] + mutating func addItem(_ item: ScrapeableContent.Item, for subgraph: Subgraph) { + let key = Key(subgraph: subgraph) + var nodes = map[key] ?? [] + nodes.append(.init(item: item)) + map[key] = nodes + } + func content(for subgraph: Subgraph, updated: inout Set) -> ScrapeableContent? { + let (isInserted, m) = updated.insert(ObjectIdentifier(subgraph)) + guard isInserted else { + return nil + } + let key = Key(subgraph: subgraph) + let content = ScrapeableContent(nodes: map[key] ?? [], children: []) + // subgraph.childCount / OGSubgraphGetChildCount _openSwiftUIUnimplementedFailure() } @@ -160,25 +215,48 @@ extension Subgraph { } package func scrapeContent() -> ScrapeableContent { - _openSwiftUIUnimplementedFailure() + var map = Map() + forEach(.scrapeable) { attribute in + guard let attr = attribute._bodyType as? ScrapeableAttribute.Type, // FIXME + let item = attr.scrapeContent(from: attribute) else { + return + } + map.addItem(item, for: self) + } + var updated: Set = [] + return map.content(for: self, updated: &updated) ?? .init(nodes: [], children: []) } } extension ViewGraph { final package func scrapeContent() -> ScrapeableContent { - _openSwiftUIUnimplementedFailure() + rootSubgraph.scrapeContent() } } extension ViewRendererHost { package func scrapeContent() -> ScrapeableContent { - _openSwiftUIUnimplementedFailure() + updateViewGraph { viewGraph in + viewGraph.scrapeContent() + } } } +// MARK: - ScrapeableContent + CustomStringConvertible + extension ScrapeableContent: CustomStringConvertible { - private func print(into printer: inout SExpPrinter) { - _openSwiftUIUnimplementedFailure() + fileprivate func print(into printer: inout SExpPrinter) { + for node in nodes { + node.print(into: &printer) + } + guard !children.isEmpty else { + return + } + printer.push("children") + for child in children { + child.print(into: &printer) + } + printer.pop() } package var description: String { @@ -189,13 +267,38 @@ extension ScrapeableContent: CustomStringConvertible { } extension ScrapeableContent.Node: CustomStringConvertible { + fileprivate func print(into printer: inout SExpPrinter) { + item.print(into: &printer) + guard !children.isEmpty else { + return + } + printer.push("children") + for child in children { + child.print(into: &printer) + } + printer.pop() + } + final package var description: String { - _openSwiftUIUnimplementedFailure() + var printer = SExpPrinter(tag: "(scrapeable-content-node") + print(into: &printer) + return printer.end() } } extension ScrapeableContent.Item: CustomStringConvertible { + fileprivate func print(into printer: inout SExpPrinter) { + printer.push("item") + if size != .zero { + printer.print("#:size (\(size.width) \(size.height))", newline: false) + } + // TODO: switch Content + printer.pop() + } + package var description: String { - _openSwiftUIUnimplementedFailure() + var printer = SExpPrinter(tag: "(scrapeable-content-item") + print(into: &printer) + return printer.end() } } diff --git a/Sources/OpenSwiftUICore/Util/SExpPrinter.swift b/Sources/OpenSwiftUICore/Util/SExpPrinter.swift index b3a7bfdcd..97b7abfa2 100644 --- a/Sources/OpenSwiftUICore/Util/SExpPrinter.swift +++ b/Sources/OpenSwiftUICore/Util/SExpPrinter.swift @@ -23,7 +23,8 @@ package struct SExpPrinter { package mutating func print(_ string: String, newline: Bool = true) { if newline, depth != 0 { - output.append("\n\(indent)") + output.append("\n") + output.append(indent) } else { output.append(" ") } From 69c4c191d7e5e8c645eb4a6f65eee55274a18888 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 24 Aug 2025 20:20:49 +0800 Subject: [PATCH 4/4] Fix non-Darwin build issue --- .../Util/Data/ScrapeableContent.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift b/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift index f91d4aa18..4c2a29eb9 100644 --- a/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift +++ b/Sources/OpenSwiftUICore/Util/Data/ScrapeableContent.swift @@ -131,7 +131,9 @@ package struct ScrapeableContent { case accessibilityProperties(AccessibilityProperties, EnvironmentValues, AnyInterfaceIdiom) case intelligenceProvider(Any) case opacity(Double) + #if canImport(Darwin) case userActivity(NSUserActivity) + #endif case hidden case presentationContainer case presentationContainerChild @@ -187,6 +189,17 @@ extension Subgraph { private struct Map { struct Key: Hashable { var subgraph: Subgraph + + #if !canImport(Darwin) + // FIXME: Subgraph on non-Darwin platform does not conform to Hashable by default + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(subgraph)) + } + + static func == (a: Key, b: Key) -> Bool { + ObjectIdentifier(a.subgraph) == ObjectIdentifier(b.subgraph) + } + #endif } var map: [Key: [ScrapeableContent.Node]] = [:]