Skip to content

Commit c280be1

Browse files
authored
[Feature] Implement CustomRecursiveStringConvertible (#687)
1 parent 8e2a554 commit c280be1

File tree

7 files changed

+679
-8
lines changed

7 files changed

+679
-8
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ Example/ReferenceImages/**/*.png
2323
.docs
2424
gh-pages
2525
.swift_pd_guess
26-
.claude/commands/guess_filename.md
26+
.claude/commands/guess_filename.md
27+
/.augments

Sources/OpenSwiftUI/Util/OpenSwiftUIGlue.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,17 @@ final public class OpenSwiftUIGlue2: CoreGlue2 {
104104
#endif
105105
}
106106

107-
override public func configureEmptyEnvironment(_ environment: inout EnvironmentValues) {
108-
environment.configureForPlatform(traitCollection: nil)
109-
}
110-
111107
override public final func configureDefaultEnvironment(_: inout EnvironmentValues) {
112108
#if os(iOS) || os(visionOS)
113109
#else
114110
// TODO
115111
#endif
116112
}
117113

114+
override public func configureEmptyEnvironment(_ environment: inout EnvironmentValues) {
115+
environment.configureForPlatform(traitCollection: nil)
116+
}
117+
118118
override public final func makeRootView(base: AnyView, rootFocusScope: Namespace.ID) -> AnyView {
119119
AnyView(base.safeAreaInsets(.zero, next: nil))
120120
}

Sources/OpenSwiftUICore/Test/TestApp.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public struct _TestApp {
4747

4848
package static let defaultEnvironment: EnvironmentValues = {
4949
var environment = EnvironmentValues()
50-
CoreGlue2.shared.configureDefaultEnvironment(&environment)
50+
CoreGlue2.shared.configureEmptyEnvironment(&environment)
5151
// TODO: Font: "HelveticaNeue"
5252
environment.displayScale = 2.0
5353
environment.setTestSystemColorDefinition()

Sources/OpenSwiftUICore/Util/CoreGlue.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,11 @@ open class CoreGlue2: NSObject {
226226
_openSwiftUIBaseClassAbstractMethod()
227227
}
228228

229-
open func configureEmptyEnvironment(_: inout EnvironmentValues) {
229+
open func configureDefaultEnvironment(_: inout EnvironmentValues) {
230230
_openSwiftUIBaseClassAbstractMethod()
231231
}
232232

233-
open func configureDefaultEnvironment(_: inout EnvironmentValues) {
233+
open func configureEmptyEnvironment(_: inout EnvironmentValues) {
234234
_openSwiftUIBaseClassAbstractMethod()
235235
}
236236

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
//
2+
// CustomRecursiveStringConvertible.swift
3+
// OpenSwiftUICore
4+
//
5+
// Status: Complete
6+
// Audited for 6.5.4
7+
// ID: 2DFA09903A864CB0F038E089ECDB7AF8 (SwiftUICore)
8+
9+
import Foundation
10+
11+
// MARK: - CustomRecursiveStringConvertible
12+
13+
package protocol CustomRecursiveStringConvertible {
14+
var descriptionName: String { get }
15+
16+
var descriptionAttributes: [(name: String, value: String)] { get }
17+
18+
var defaultDescriptionAttributes: Set<DefaultDescriptionAttribute> { get }
19+
20+
var descriptionChildren: [any CustomRecursiveStringConvertible] { get }
21+
22+
var hideFromDescription: Bool { get }
23+
}
24+
25+
extension CustomRecursiveStringConvertible {
26+
package var defaultDescriptionAttributes: Set<DefaultDescriptionAttribute> {
27+
DefaultDescriptionAttribute.all
28+
}
29+
30+
package var descriptionChildren: [any CustomRecursiveStringConvertible] {
31+
[]
32+
}
33+
34+
package var hideFromDescription: Bool {
35+
false
36+
}
37+
}
38+
39+
extension CustomRecursiveStringConvertible {
40+
package var descriptionName: String {
41+
recursiveDescriptionName(Self.self)
42+
}
43+
44+
package var descriptionAttributes: [(name: String, value: String)] {
45+
[]
46+
}
47+
48+
package var recursiveDescription: String {
49+
_recursiveDescription(indent: 0, rounded: false)
50+
}
51+
52+
package var roundedRecursiveDescription: String {
53+
_recursiveDescription(indent: 0, rounded: true)
54+
}
55+
56+
package func _recursiveDescription(
57+
indent: Int,
58+
rounded: Bool
59+
) -> String {
60+
let indentString = repeatElement(" ", count: indent).joined()
61+
var attributes = descriptionAttributes
62+
if rounded {
63+
attributes = attributes.roundedAttributes()
64+
}
65+
attributes.append(contentsOf: indent == 0 ? topLevelAttributes : [])
66+
let sortedAttributes = attributes.sorted(by: \.name)
67+
let attributeString = sortedAttributes.isEmpty ? "" : " " + sortedAttributes
68+
.map {
69+
let escapedName = $0.name
70+
.components(separatedBy: .whitespacesAndNewlines)
71+
.joined(separator: "_")
72+
.escapeXML()
73+
let escapedValue = $0.value.escapeXML()
74+
return #"\#(escapedName)="\#(escapedValue)""#
75+
}
76+
.joined(separator: " ")
77+
let escapedName = descriptionName
78+
.components(separatedBy: .whitespacesAndNewlines)
79+
.joined(separator: "_")
80+
.escapeXML()
81+
let mark = "\(indentString)<\(escapedName)\(attributeString)"
82+
if descriptionChildren.isEmpty {
83+
return "\(mark) />\n"
84+
} else {
85+
var result = "\(mark)>\n"
86+
for child in descriptionChildren {
87+
guard !child.hideFromDescription else { continue }
88+
result.append(child._recursiveDescription(indent: indent &+ 1, rounded: rounded))
89+
}
90+
result.append("\(indentString)")
91+
result.append("</\(escapedName)>\n")
92+
return result
93+
}
94+
}
95+
96+
package var topLevelAttributes: [(name: String, value: String)] {
97+
guard _TestApp.isIntending(to: .includeStatusBar),
98+
let isHidden = CoreGlue2.shared.isStatusBarHidden()
99+
else { return [] }
100+
return [(name: "statusBar", value: isHidden ? "hidden" : "visible")]
101+
}
102+
}
103+
104+
// MARK: - BridgeStringConvertible
105+
106+
package protocol BridgeStringConvertible {
107+
var bridgeDescriptionChildren: [any CustomRecursiveStringConvertible] { get }
108+
}
109+
110+
extension BridgeStringConvertible {
111+
package var bridgeDescriptionChildren: [any CustomRecursiveStringConvertible] { [] }
112+
}
113+
114+
// MARK: - recursiveDescriptionName
115+
116+
package func recursiveDescriptionName(_ type: any Any.Type) -> String {
117+
var name = "\(type)"
118+
if name.first == "(" {
119+
var substring = name.dropFirst()
120+
if let spaceIndex = substring.firstIndex(of: " ") {
121+
substring.removeSubrange(spaceIndex...)
122+
}
123+
name = String(substring)
124+
}
125+
if let angleIndex = name.firstIndex(of: "<") {
126+
name = String(name[..<angleIndex])
127+
}
128+
return name
129+
}
130+
131+
// MARK: - String + Extension
132+
133+
extension String {
134+
package func tupleOfDoubles() -> [(label: String, value: Double)]? {
135+
guard let first, first == "(",
136+
let last, last == ")"
137+
else { return nil }
138+
139+
func decomposeTuple() -> (labels: [String], values: [String]) {
140+
let inner = dropFirst().dropLast()
141+
let parts = inner.split(separator: ",", omittingEmptySubsequences: true)
142+
var labels: [String] = []
143+
var values: [String] = []
144+
for part in parts {
145+
if let colonIndex = part.firstIndex(of: ":") {
146+
let label = String(part[..<colonIndex]).trimmingCharacters(in: .whitespaces)
147+
let value = String(part[part.index(after: colonIndex)...]).trimmingCharacters(in: .whitespaces)
148+
labels.append(label)
149+
values.append(value)
150+
} else {
151+
labels.append("")
152+
values.append(part.trimmingCharacters(in: .whitespaces))
153+
}
154+
}
155+
return (labels: labels, values: values)
156+
}
157+
158+
let (labels, values) = decomposeTuple()
159+
var doubles: [Double] = []
160+
for valueString in values {
161+
guard let value = Double(valueString) else {
162+
return nil
163+
}
164+
doubles.append(value)
165+
}
166+
guard labels.count == doubles.count else { return nil }
167+
return zip(labels, doubles).map { (label: $0, value: $1) }
168+
}
169+
170+
fileprivate func escapeXML() -> String {
171+
var result = ""
172+
result.reserveCapacity(count)
173+
for char in self {
174+
switch char {
175+
case "\"": result.append("&quot;")
176+
case "&": result.append("&amp;")
177+
case "'": result.append("&apos;")
178+
case "<": result.append("&lt;")
179+
case ">": result.append("&gt;")
180+
case "\n": result.append("\\n")
181+
case "\r": result.append("\\r")
182+
default: result.append(char)
183+
}
184+
}
185+
return result
186+
}
187+
}
188+
189+
// MARK: - Sequence.roundedAttributes [?]
190+
191+
extension Sequence where Element == (name: String, value: String) {
192+
package func roundedAttributes() -> [(name: String, value: String)] {
193+
map { (name, value) in
194+
if let doubleValue = Double(value) {
195+
let rounded = round(doubleValue * 256.0) / 256.0
196+
return (name: name, value: rounded.description)
197+
} else if let tupleValues = value.tupleOfDoubles() {
198+
let roundedTuple = tupleValues.map { (label: $0.label, value: round($0.value * 256.0) / 256.0) }
199+
if roundedTuple.count == 4,
200+
name.range(of: "color", options: .caseInsensitive) != nil
201+
{
202+
let floats = roundedTuple.map { Float($0.value) }
203+
if let colorName = colorNameForColorComponents(floats[0], floats[1], floats[2], floats[3]) {
204+
return (name: name, value: colorName)
205+
}
206+
}
207+
let parts: [String] = roundedTuple.map { item in
208+
if item.label.isEmpty {
209+
return "\(item.value)"
210+
} else {
211+
return "\(item.label): \(item.value)"
212+
}
213+
}
214+
return (name: name, value: "(" + parts.joined(separator: ", ") + ")")
215+
} else {
216+
return (name, value)
217+
}
218+
}
219+
}
220+
}
221+
222+
// MARK: - Color.Resolved.name
223+
224+
extension Color.Resolved {
225+
package var name: String? {
226+
@inline(__always)
227+
func quantize(_ value: Float) -> Float {
228+
round(value * 256.0) / 256.0
229+
}
230+
return colorNameForColorComponents(
231+
quantize(linearRed),
232+
quantize(linearGreen),
233+
quantize(linearBlue),
234+
quantize(opacity)
235+
)
236+
}
237+
}
238+
239+
private func colorNameForColorComponents(_ r: Float, _ g: Float, _ b: Float, _ a: Float) -> String? {
240+
if r == 0 && g == 0 && b == 0 {
241+
if a == 0 {
242+
return "clear"
243+
} else if a == 1 {
244+
return "black"
245+
}
246+
}
247+
if r == 1 && g == 1 && b == 1 && a == 1 {
248+
return "white"
249+
} else if r == 8.0 / 256.0 && g == 8.0 / 256.0 && b == 8.0 / 256.0 && a == 1 {
250+
return "gray"
251+
} else if r == 1 && g == 0 && b == 0 && a == 1 {
252+
return "red"
253+
} else if r == 1 && g == 11.0 / 256.0 && b == 11.0 / 256.0 && a == 1 {
254+
return "system-red"
255+
} else if r == 1 && g == 15.0 / 256.0 && b == 11.0 / 256.0 && a == 1 {
256+
return "system-red-dark"
257+
} else if r == 0 && g == 1 && b == 0 && a == 1 {
258+
return "green"
259+
} else if r == 0 && g == 0 && b == 1 && a == 1 {
260+
return "blue"
261+
} else if r == 1 && g == 1 && b == 0 && a == 1 {
262+
return "yellow"
263+
} else if r == 55.0 / 256.0 && g == 0 && b == 55.0 / 256.0 && a == 1 {
264+
return "purple"
265+
} else if r == 1 && g == 55.0 / 256.0 && b == 0 && a == 1 {
266+
return "orange"
267+
} else if r == 0 && g == 1 && b == 1 && a == 1 {
268+
return "teal"
269+
} else if r == 55.0 / 256.0 && g == 55.0 / 256.0 && b == 1 && a == 1 {
270+
return "indigo"
271+
} else if r == 1 && g == 0 && b == 55.0 / 256.0 && a == 1 {
272+
return "pink"
273+
} else if r == 12.0 / 256.0 && g == 12.0 / 256.0 && b == 14.0 / 256.0 && a == 64.0 / 256.0 {
274+
return "brown"
275+
} else if r == 12.0 / 256.0 && g == 12.0 / 256.0 && b == 14.0 / 256.0 && a == 76.0 / 256.0 {
276+
return "placeholder-text"
277+
} else {
278+
return nil
279+
}
280+
}

0 commit comments

Comments
 (0)