@swiftbrowser/swift 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/Package.swift +214 -0
  2. package/README.md +10 -2
  3. package/Sources/AVFoundation/AVFoundation.swift +131 -0
  4. package/Sources/AVFoundation/Asset.swift +487 -0
  5. package/Sources/AVFoundation/AudioSession.swift +144 -0
  6. package/Sources/AVFoundation/CMTime.swift +303 -0
  7. package/Sources/AVFoundation/Capture.swift +688 -0
  8. package/Sources/AVFoundation/Player.swift +749 -0
  9. package/Sources/AVFoundation/PlayerView.swift +117 -0
  10. package/Sources/AVKit/AVKit.swift +132 -0
  11. package/Sources/Accelerate/Accelerate.swift +122 -0
  12. package/Sources/AppIntents/AppIntents.swift +144 -0
  13. package/Sources/AppIntents/Parameters.swift +220 -0
  14. package/Sources/AppIntents/Shortcuts.swift +159 -0
  15. package/Sources/AudioToolbox/AudioToolbox.swift +55 -0
  16. package/Sources/AuthenticationServices/AuthenticationServices.swift +257 -0
  17. package/Sources/AuthenticationServices/Authorization.swift +251 -0
  18. package/Sources/Charts/Charts.swift +61 -0
  19. package/Sources/Combine/AnyCancellable.swift +65 -0
  20. package/Sources/Combine/AnyPublisher.swift +148 -0
  21. package/Sources/Combine/AnySubscriber.swift +230 -0
  22. package/Sources/Combine/Cancellable.swift +35 -0
  23. package/Sources/Combine/Codable.swift +29 -0
  24. package/Sources/Combine/CombineIdentifier.swift +45 -0
  25. package/Sources/Combine/Concurrency/ConcurrencyHelpers.swift +24 -0
  26. package/Sources/Combine/Concurrency/Future+Concurrency.swift +136 -0
  27. package/Sources/Combine/Concurrency/GENERATED-Publisher+Concurrency.swift +397 -0
  28. package/Sources/Combine/CurrentValueSubject.swift +252 -0
  29. package/Sources/Combine/CustomCombineIdentifierConvertible.swift +28 -0
  30. package/Sources/Combine/Future.swift +228 -0
  31. package/Sources/Combine/GENERATED-RootProtocols.swift +426 -0
  32. package/Sources/Combine/Helpers/ConduitBase.swift +40 -0
  33. package/Sources/Combine/Helpers/ConduitList.swift +59 -0
  34. package/Sources/Combine/Helpers/DebugHook.swift +177 -0
  35. package/Sources/Combine/Helpers/FilterProducer.swift +174 -0
  36. package/Sources/Combine/Helpers/Locking.swift +30 -0
  37. package/Sources/Combine/Helpers/PartialCompletion.swift +26 -0
  38. package/Sources/Combine/Helpers/ReduceProducer.swift +249 -0
  39. package/Sources/Combine/Helpers/SubjectSubscriber.swift +96 -0
  40. package/Sources/Combine/Helpers/SubscriberTap.swift +69 -0
  41. package/Sources/Combine/Helpers/SubscriptionStatus.swift +33 -0
  42. package/Sources/Combine/Helpers/SubscriptionTap.swift +33 -0
  43. package/Sources/Combine/Helpers/Utils.swift +30 -0
  44. package/Sources/Combine/Helpers/Violations.swift +33 -0
  45. package/Sources/Combine/ImmediateScheduler.swift +178 -0
  46. package/Sources/Combine/LICENSE +21 -0
  47. package/Sources/Combine/NotificationPublisher.swift +48 -0
  48. package/Sources/Combine/ObjectWillChange.swift +46 -0
  49. package/Sources/Combine/PassthroughSubject.swift +222 -0
  50. package/Sources/Combine/Publisher+Subscribe.swift +53 -0
  51. package/Sources/Combine/Publishers/Deferred.swift +48 -0
  52. package/Sources/Combine/Publishers/Empty.swift +54 -0
  53. package/Sources/Combine/Publishers/Fail.swift +42 -0
  54. package/Sources/Combine/Publishers/GENERATED-Publishers.Catch.swift +673 -0
  55. package/Sources/Combine/Publishers/GENERATED-Publishers.Encode.swift +388 -0
  56. package/Sources/Combine/Publishers/GENERATED-Publishers.MapKeyPath.swift +384 -0
  57. package/Sources/Combine/Publishers/Just.swift +313 -0
  58. package/Sources/Combine/Publishers/Optional.Publisher.swift +315 -0
  59. package/Sources/Combine/Publishers/Publishers.AllSatisfy.swift +217 -0
  60. package/Sources/Combine/Publishers/Publishers.AssertNoFailure.swift +158 -0
  61. package/Sources/Combine/Publishers/Publishers.Autoconnect.swift +193 -0
  62. package/Sources/Combine/Publishers/Publishers.Breakpoint.swift +231 -0
  63. package/Sources/Combine/Publishers/Publishers.Buffer.swift +356 -0
  64. package/Sources/Combine/Publishers/Publishers.Collect.swift +102 -0
  65. package/Sources/Combine/Publishers/Publishers.CollectByCount.swift +195 -0
  66. package/Sources/Combine/Publishers/Publishers.CompactMap.swift +241 -0
  67. package/Sources/Combine/Publishers/Publishers.Comparison.swift +371 -0
  68. package/Sources/Combine/Publishers/Publishers.Concatenate.swift +474 -0
  69. package/Sources/Combine/Publishers/Publishers.Contains.swift +293 -0
  70. package/Sources/Combine/Publishers/Publishers.Count.swift +87 -0
  71. package/Sources/Combine/Publishers/Publishers.Debounce.swift +332 -0
  72. package/Sources/Combine/Publishers/Publishers.Delay.swift +269 -0
  73. package/Sources/Combine/Publishers/Publishers.Drop.swift +155 -0
  74. package/Sources/Combine/Publishers/Publishers.DropUntilOutput.swift +283 -0
  75. package/Sources/Combine/Publishers/Publishers.DropWhile.swift +378 -0
  76. package/Sources/Combine/Publishers/Publishers.Filter.swift +248 -0
  77. package/Sources/Combine/Publishers/Publishers.First.swift +261 -0
  78. package/Sources/Combine/Publishers/Publishers.FlatMap.swift +533 -0
  79. package/Sources/Combine/Publishers/Publishers.HandleEvents.swift +260 -0
  80. package/Sources/Combine/Publishers/Publishers.IgnoreOutput.swift +109 -0
  81. package/Sources/Combine/Publishers/Publishers.Last.swift +252 -0
  82. package/Sources/Combine/Publishers/Publishers.MakeConnectable.swift +82 -0
  83. package/Sources/Combine/Publishers/Publishers.Map.swift +351 -0
  84. package/Sources/Combine/Publishers/Publishers.MapError.swift +140 -0
  85. package/Sources/Combine/Publishers/Publishers.MeasureInterval.swift +187 -0
  86. package/Sources/Combine/Publishers/Publishers.Multicast.swift +284 -0
  87. package/Sources/Combine/Publishers/Publishers.Output.swift +241 -0
  88. package/Sources/Combine/Publishers/Publishers.PrefixUntilOutput.swift +197 -0
  89. package/Sources/Combine/Publishers/Publishers.PrefixWhile.swift +176 -0
  90. package/Sources/Combine/Publishers/Publishers.Print.swift +206 -0
  91. package/Sources/Combine/Publishers/Publishers.ReceiveOn.swift +227 -0
  92. package/Sources/Combine/Publishers/Publishers.Reduce.swift +204 -0
  93. package/Sources/Combine/Publishers/Publishers.RemoveDuplicates.swift +264 -0
  94. package/Sources/Combine/Publishers/Publishers.ReplaceEmpty.swift +201 -0
  95. package/Sources/Combine/Publishers/Publishers.ReplaceError.swift +211 -0
  96. package/Sources/Combine/Publishers/Publishers.Retry.swift +274 -0
  97. package/Sources/Combine/Publishers/Publishers.Scan.swift +331 -0
  98. package/Sources/Combine/Publishers/Publishers.Sequence.swift +437 -0
  99. package/Sources/Combine/Publishers/Publishers.SetFailureType.swift +120 -0
  100. package/Sources/Combine/Publishers/Publishers.Share.swift +104 -0
  101. package/Sources/Combine/Publishers/Publishers.SubscribeOn.swift +197 -0
  102. package/Sources/Combine/Publishers/Publishers.SwitchToLatest.swift +444 -0
  103. package/Sources/Combine/Publishers/Publishers.Throttle.swift +359 -0
  104. package/Sources/Combine/Publishers/Publishers.Timeout.swift +301 -0
  105. package/Sources/Combine/Publishers/Publishers.Zip.swift +720 -0
  106. package/Sources/Combine/Publishers/Publishers.swift +13 -0
  107. package/Sources/Combine/Publishers/Record.swift +237 -0
  108. package/Sources/Combine/Publishers/Result.Publisher.swift +337 -0
  109. package/Sources/Combine/Result.swift +46 -0
  110. package/Sources/Combine/Scheduler.swift +89 -0
  111. package/Sources/Combine/Schedulers.swift +211 -0
  112. package/Sources/Combine/Subject+Void.swift +17 -0
  113. package/Sources/Combine/Subscriber+Void.swift +20 -0
  114. package/Sources/Combine/Subscribers/Subscribers.Assign.swift +175 -0
  115. package/Sources/Combine/Subscribers/Subscribers.Completion.swift +90 -0
  116. package/Sources/Combine/Subscribers/Subscribers.Demand.swift +476 -0
  117. package/Sources/Combine/Subscribers/Subscribers.Sink.swift +206 -0
  118. package/Sources/Combine/Subscribers/Subscribers.swift +9 -0
  119. package/Sources/Combine/Subscription.swift +22 -0
  120. package/Sources/Combine/Subscriptions.swift +42 -0
  121. package/Sources/Combine/SwiftUIObservation.swift +10 -0
  122. package/Sources/Combine/_Introspection.swift +107 -0
  123. package/Sources/CommonCrypto/CommonCrypto.swift +80 -0
  124. package/Sources/Compression/Compression.swift +365 -0
  125. package/Sources/Compression/Deflate.swift +407 -0
  126. package/Sources/Compression/Zlib.swift +179 -0
  127. package/Sources/CoreFoundationTypes/CoreFoundationTypes.swift +101 -0
  128. package/Sources/CoreGraphics/CoreGraphics.swift +411 -0
  129. package/Sources/CoreGraphics/ProjectionTransforms.swift +28 -0
  130. package/Sources/CoreHaptics/CoreHaptics.swift +399 -0
  131. package/Sources/CoreImage/CIColor.swift +41 -0
  132. package/Sources/CoreImage/CIContext.swift +31 -0
  133. package/Sources/CoreImage/CIDetector.swift +57 -0
  134. package/Sources/CoreImage/CIFilter.swift +243 -0
  135. package/Sources/CoreImage/CIFilterBuiltins.swift +136 -0
  136. package/Sources/CoreImage/CIImage.swift +162 -0
  137. package/Sources/CoreImage/QRCode.swift +393 -0
  138. package/Sources/CoreLocation/Coordinate.swift +30 -0
  139. package/Sources/CoreLocation/Location.swift +65 -0
  140. package/Sources/CoreML/Evaluation.swift +414 -0
  141. package/Sources/CoreML/FeatureValue.swift +266 -0
  142. package/Sources/CoreML/Model.swift +269 -0
  143. package/Sources/CoreML/MultiArray.swift +92 -0
  144. package/Sources/CoreML/Specification.swift +233 -0
  145. package/Sources/CoreText/CoreText.swift +64 -0
  146. package/Sources/CoreTransferable/CoreTransferable.swift +606 -0
  147. package/Sources/CryptoKit/ChaChaPoly.swift +258 -0
  148. package/Sources/CryptoKit/CryptoKit.swift +375 -0
  149. package/Sources/CryptoKit/Curve25519.swift +317 -0
  150. package/Sources/CryptoKit/P256.swift +321 -0
  151. package/Sources/FoundationBridge/AppStorage.swift +165 -0
  152. package/Sources/FoundationBridge/AssetResources.swift +90 -0
  153. package/Sources/FoundationBridge/AsyncBytes.swift +128 -0
  154. package/Sources/FoundationBridge/AsyncImage.swift +116 -0
  155. package/Sources/FoundationBridge/AttributedStrings.swift +16 -0
  156. package/Sources/FoundationBridge/Bundle.swift +119 -0
  157. package/Sources/FoundationBridge/CombineExports.swift +8 -0
  158. package/Sources/FoundationBridge/CoreFoundation.swift +77 -0
  159. package/Sources/FoundationBridge/Dispatch.swift +458 -0
  160. package/Sources/FoundationBridge/EnvironmentFoundation.swift +41 -0
  161. package/Sources/FoundationBridge/FileImporter.swift +128 -0
  162. package/Sources/FoundationBridge/FileSystem.swift +68 -0
  163. package/Sources/FoundationBridge/FoundationBridge.swift +29 -0
  164. package/Sources/FoundationBridge/HostResolution.swift +31 -0
  165. package/Sources/FoundationBridge/ItemProvider.swift +655 -0
  166. package/Sources/FoundationBridge/KeyValueObserving.swift +138 -0
  167. package/Sources/FoundationBridge/Links.swift +127 -0
  168. package/Sources/FoundationBridge/ListFormatter.swift +32 -0
  169. package/Sources/FoundationBridge/Locale.swift +86 -0
  170. package/Sources/FoundationBridge/Localization.swift +149 -0
  171. package/Sources/FoundationBridge/Locks.swift +50 -0
  172. package/Sources/FoundationBridge/Markdown.swift +527 -0
  173. package/Sources/FoundationBridge/MeasurementFormat.swift +95 -0
  174. package/Sources/FoundationBridge/NotificationCenter.swift +154 -0
  175. package/Sources/FoundationBridge/OpenURLHandlers.swift +60 -0
  176. package/Sources/FoundationBridge/Operation.swift +195 -0
  177. package/Sources/FoundationBridge/ProcessInfo.swift +32 -0
  178. package/Sources/FoundationBridge/RelativeDateTimeFormatter.swift +136 -0
  179. package/Sources/FoundationBridge/SceneStorage.swift +78 -0
  180. package/Sources/FoundationBridge/SecurityExports.swift +8 -0
  181. package/Sources/FoundationBridge/Selector.swift +226 -0
  182. package/Sources/FoundationBridge/StringComparators.swift +17 -0
  183. package/Sources/FoundationBridge/Table.swift +211 -0
  184. package/Sources/FoundationBridge/TextAttributes.swift +42 -0
  185. package/Sources/FoundationBridge/Timer.swift +53 -0
  186. package/Sources/FoundationBridge/UITextChecker.swift +30 -0
  187. package/Sources/FoundationBridge/URLSession.swift +718 -0
  188. package/Sources/FoundationBridge/UserDefaults.swift +183 -0
  189. package/Sources/FoundationBridge/WebAuthRedirects.swift +227 -0
  190. package/Sources/FoundationBridge/WebSocket.swift +187 -0
  191. package/Sources/FoundationModels/FoundationModels.swift +1098 -0
  192. package/Sources/ImageIO/GIF.swift +72 -0
  193. package/Sources/ImageIO/ImageIO.swift +248 -0
  194. package/Sources/ImageIO/ImageTypes.swift +28 -0
  195. package/Sources/LinkPresentation/LinkPresentation.swift +249 -0
  196. package/Sources/LocalAuthentication/LAContext.swift +86 -0
  197. package/Sources/MapKit/Geometry.swift +223 -0
  198. package/Sources/MapKit/Map.swift +638 -0
  199. package/Sources/MapKit/MapView.swift +229 -0
  200. package/Sources/MobileCoreServices/MobileCoreServices.swift +28 -0
  201. package/Sources/NaturalLanguage/LanguageIdentifier.swift +317 -0
  202. package/Sources/NaturalLanguage/NaturalLanguage.swift +135 -0
  203. package/Sources/OSLog/OSLog.swift +6 -0
  204. package/Sources/Photos/Photos.swift +572 -0
  205. package/Sources/PhotosUI/PhotosPicker.swift +248 -0
  206. package/Sources/PhotosUI/PhotosPickerItem.swift +261 -0
  207. package/Sources/QuickLook/QuickLook.swift +168 -0
  208. package/Sources/SafariServices/SafariServices.swift +106 -0
  209. package/Sources/Security/Security.swift +332 -0
  210. package/Sources/StoreKit/AppStore.swift +207 -0
  211. package/Sources/StoreKit/Products.swift +280 -0
  212. package/Sources/StoreKit/RequestReview.swift +28 -0
  213. package/Sources/StoreKit/StoreKit1.swift +486 -0
  214. package/Sources/StoreKit/StoreViews.swift +428 -0
  215. package/Sources/StoreKit/Transactions.swift +197 -0
  216. package/Sources/SwiftData/Macros.swift +58 -0
  217. package/Sources/SwiftData/ModelActor.swift +38 -0
  218. package/Sources/SwiftData/ModelContainer.swift +129 -0
  219. package/Sources/SwiftData/ModelContext.swift +451 -0
  220. package/Sources/SwiftData/PersistentModel.swift +238 -0
  221. package/Sources/SwiftData/Properties.swift +261 -0
  222. package/Sources/SwiftData/Query.swift +256 -0
  223. package/Sources/SwiftData/Schema.swift +192 -0
  224. package/Sources/SwiftData/Store.swift +146 -0
  225. package/Sources/SwiftData/StringPredicates.swift +125 -0
  226. package/Sources/SwiftDataMacros/EntryMacro.swift +82 -0
  227. package/Sources/SwiftDataMacros/GenerableMacro.swift +183 -0
  228. package/Sources/SwiftDataMacros/ModelActorMacro.swift +46 -0
  229. package/Sources/SwiftDataMacros/ModelMacro.swift +260 -0
  230. package/Sources/SwiftDataMacros/Plugin.swift +18 -0
  231. package/Sources/SwiftDataMacros/PredicateMacro.swift +1244 -0
  232. package/Sources/SwiftUI/API/Accessibility.swift +181 -0
  233. package/Sources/SwiftUI/API/AccessibilityExtras.swift +33 -0
  234. package/Sources/SwiftUI/API/AccessibilityFocus.swift +63 -0
  235. package/Sources/SwiftUI/API/Alert.swift +221 -0
  236. package/Sources/SwiftUI/API/AlignmentGuides.swift +65 -0
  237. package/Sources/SwiftUI/API/Animation.swift +285 -0
  238. package/Sources/SwiftUI/API/App.swift +87 -0
  239. package/Sources/SwiftUI/API/BindingCollections.swift +35 -0
  240. package/Sources/SwiftUI/API/BindingForEach.swift +27 -0
  241. package/Sources/SwiftUI/API/Bitmaps.swift +254 -0
  242. package/Sources/SwiftUI/API/ButtonStyles.swift +96 -0
  243. package/Sources/SwiftUI/API/Canvas.swift +337 -0
  244. package/Sources/SwiftUI/API/Color.swift +237 -0
  245. package/Sources/SwiftUI/API/ColorEffects.swift +52 -0
  246. package/Sources/SwiftUI/API/ColorPickerAndLabeledContent.swift +138 -0
  247. package/Sources/SwiftUI/API/ColorSpaces.swift +161 -0
  248. package/Sources/SwiftUI/API/Commands.swift +106 -0
  249. package/Sources/SwiftUI/API/ContainerBackground.swift +28 -0
  250. package/Sources/SwiftUI/API/ContentShape.swift +33 -0
  251. package/Sources/SwiftUI/API/ContentSizeCategory.swift +35 -0
  252. package/Sources/SwiftUI/API/ContentUnavailable.swift +101 -0
  253. package/Sources/SwiftUI/API/Controls.swift +300 -0
  254. package/Sources/SwiftUI/API/CustomLayout.swift +442 -0
  255. package/Sources/SwiftUI/API/CustomToolbarContent.swift +46 -0
  256. package/Sources/SwiftUI/API/Decorations.swift +92 -0
  257. package/Sources/SwiftUI/API/EditMode.swift +212 -0
  258. package/Sources/SwiftUI/API/Effects.swift +388 -0
  259. package/Sources/SwiftUI/API/Entry.swift +8 -0
  260. package/Sources/SwiftUI/API/Environment.swift +439 -0
  261. package/Sources/SwiftUI/API/EnvironmentSendable.swift +6 -0
  262. package/Sources/SwiftUI/API/Focus.swift +137 -0
  263. package/Sources/SwiftUI/API/Font.swift +131 -0
  264. package/Sources/SwiftUI/API/Gauge.swift +290 -0
  265. package/Sources/SwiftUI/API/Geometry.swift +89 -0
  266. package/Sources/SwiftUI/API/GeometryEffects.swift +232 -0
  267. package/Sources/SwiftUI/API/GeometryReader.swift +135 -0
  268. package/Sources/SwiftUI/API/GestureState.swift +65 -0
  269. package/Sources/SwiftUI/API/Gestures.swift +342 -0
  270. package/Sources/SwiftUI/API/Glass.swift +127 -0
  271. package/Sources/SwiftUI/API/Grid.swift +188 -0
  272. package/Sources/SwiftUI/API/GroupBoxAndControlGroup.swift +130 -0
  273. package/Sources/SwiftUI/API/KeyPress.swift +54 -0
  274. package/Sources/SwiftUI/API/LabelStyle.swift +124 -0
  275. package/Sources/SwiftUI/API/Layout.swift +220 -0
  276. package/Sources/SwiftUI/API/LazyStacks.swift +49 -0
  277. package/Sources/SwiftUI/API/LegacyAlert.swift +97 -0
  278. package/Sources/SwiftUI/API/Lists.swift +331 -0
  279. package/Sources/SwiftUI/API/Menu.swift +58 -0
  280. package/Sources/SwiftUI/API/Modifiers.swift +313 -0
  281. package/Sources/SwiftUI/API/MoreModifiers.swift +100 -0
  282. package/Sources/SwiftUI/API/Namespace.swift +44 -0
  283. package/Sources/SwiftUI/API/Navigation.swift +386 -0
  284. package/Sources/SwiftUI/API/NavigationDestinationBindings.swift +55 -0
  285. package/Sources/SwiftUI/API/ObservableObject.swift +293 -0
  286. package/Sources/SwiftUI/API/OnChange.swift +48 -0
  287. package/Sources/SwiftUI/API/Path.swift +543 -0
  288. package/Sources/SwiftUI/API/Phase8Controls.swift +411 -0
  289. package/Sources/SwiftUI/API/PhaseAnimator.swift +130 -0
  290. package/Sources/SwiftUI/API/Picker.swift +185 -0
  291. package/Sources/SwiftUI/API/PointerAndKeyboard.swift +77 -0
  292. package/Sources/SwiftUI/API/Popover.swift +41 -0
  293. package/Sources/SwiftUI/API/Preferences.swift +262 -0
  294. package/Sources/SwiftUI/API/Presentation.swift +122 -0
  295. package/Sources/SwiftUI/API/PresentationExtras.swift +60 -0
  296. package/Sources/SwiftUI/API/Primitives.swift +517 -0
  297. package/Sources/SwiftUI/API/Publishers.swift +107 -0
  298. package/Sources/SwiftUI/API/Redaction.swift +75 -0
  299. package/Sources/SwiftUI/API/RowActions.swift +84 -0
  300. package/Sources/SwiftUI/API/Scenes.swift +131 -0
  301. package/Sources/SwiftUI/API/ScrollModifiers.swift +132 -0
  302. package/Sources/SwiftUI/API/ScrollTargets.swift +104 -0
  303. package/Sources/SwiftUI/API/ScrollViewReader.swift +144 -0
  304. package/Sources/SwiftUI/API/SectionSupplements.swift +96 -0
  305. package/Sources/SwiftUI/API/Shape.swift +214 -0
  306. package/Sources/SwiftUI/API/ShapeStyle.swift +409 -0
  307. package/Sources/SwiftUI/API/ShapeTrim.swift +78 -0
  308. package/Sources/SwiftUI/API/Sharing.swift +193 -0
  309. package/Sources/SwiftUI/API/StackLayouts.swift +199 -0
  310. package/Sources/SwiftUI/API/State.swift +144 -0
  311. package/Sources/SwiftUI/API/StatusGaps.swift +121 -0
  312. package/Sources/SwiftUI/API/StrokeStyle.swift +30 -0
  313. package/Sources/SwiftUI/API/SymbolEffects.swift +203 -0
  314. package/Sources/SwiftUI/API/TabContent.swift +841 -0
  315. package/Sources/SwiftUI/API/Table.swift +382 -0
  316. package/Sources/SwiftUI/API/Tabs.swift +263 -0
  317. package/Sources/SwiftUI/API/Tasks.swift +35 -0
  318. package/Sources/SwiftUI/API/Text.swift +386 -0
  319. package/Sources/SwiftUI/API/TextDates.swift +61 -0
  320. package/Sources/SwiftUI/API/TextDecorations.swift +52 -0
  321. package/Sources/SwiftUI/API/TextFieldAxis.swift +103 -0
  322. package/Sources/SwiftUI/API/TextFieldCommit.swift +52 -0
  323. package/Sources/SwiftUI/API/Timeline.swift +212 -0
  324. package/Sources/SwiftUI/API/Toolbar.swift +251 -0
  325. package/Sources/SwiftUI/API/Transfer.swift +145 -0
  326. package/Sources/SwiftUI/API/UnevenRoundedRectangle.swift +66 -0
  327. package/Sources/SwiftUI/API/Video.swift +64 -0
  328. package/Sources/SwiftUI/API/View.swift +215 -0
  329. package/Sources/SwiftUI/API/ViewModifier.swift +43 -0
  330. package/Sources/SwiftUI/API/VisualEffect.swift +155 -0
  331. package/Sources/SwiftUI/API/Windows.swift +25 -0
  332. package/Sources/SwiftUI/Charts/Axis.swift +325 -0
  333. package/Sources/SwiftUI/Charts/AxisFormatsAndRanges.swift +68 -0
  334. package/Sources/SwiftUI/Charts/Chart.swift +660 -0
  335. package/Sources/SwiftUI/Charts/ChartContent.swift +376 -0
  336. package/Sources/SwiftUI/Charts/ChartProxy.swift +57 -0
  337. package/Sources/SwiftUI/Charts/Marks.swift +204 -0
  338. package/Sources/SwiftUI/Charts/Plottable.swift +99 -0
  339. package/Sources/SwiftUI/Charts/Scales.swift +326 -0
  340. package/Sources/SwiftUI/Localization/Localization.swift +498 -0
  341. package/Sources/SwiftUI/Localization/PluralRules.swift +127 -0
  342. package/Sources/SwiftUI/Runtime/Executor.swift +85 -0
  343. package/Sources/SwiftUI/Runtime/Exports.swift +101 -0
  344. package/Sources/SwiftUI/Runtime/ImageSource.swift +310 -0
  345. package/Sources/SwiftUI/Runtime/JSON.swift +143 -0
  346. package/Sources/SwiftUI/Runtime/JSONParser.swift +443 -0
  347. package/Sources/SwiftUI/Runtime/LazyRows.swift +99 -0
  348. package/Sources/SwiftUI/Runtime/Measurements.swift +140 -0
  349. package/Sources/SwiftUI/Runtime/Ops.swift +152 -0
  350. package/Sources/SwiftUI/Runtime/Reconciler.swift +219 -0
  351. package/Sources/SwiftUI/Runtime/Replay.swift +382 -0
  352. package/Sources/SwiftUI/Runtime/Runtime.swift +2146 -0
  353. package/Sources/SwiftUI/Runtime/Sameness.swift +152 -0
  354. package/Sources/SwiftUI/Runtime/ViewPath.swift +89 -0
  355. package/Sources/SwiftUI/Runtime/ViewTraits.swift +175 -0
  356. package/Sources/SwiftUI/Runtime/WebAuthHeadless.swift +49 -0
  357. package/Sources/Symbols/Symbols.swift +25 -0
  358. package/Sources/Translation/Translation.swift +423 -0
  359. package/Sources/UIKit/Activity.swift +213 -0
  360. package/Sources/UIKit/Alerts.swift +69 -0
  361. package/Sources/UIKit/AppDelegate.swift +222 -0
  362. package/Sources/UIKit/Appearance.swift +84 -0
  363. package/Sources/UIKit/ColorConversions.swift +70 -0
  364. package/Sources/UIKit/Controls.swift +225 -0
  365. package/Sources/UIKit/Device.swift +200 -0
  366. package/Sources/UIKit/Feedback.swift +65 -0
  367. package/Sources/UIKit/Font.swift +307 -0
  368. package/Sources/UIKit/Graphics.swift +204 -0
  369. package/Sources/UIKit/HostingController.swift +185 -0
  370. package/Sources/UIKit/Identity.swift +24 -0
  371. package/Sources/UIKit/Introspectable.swift +510 -0
  372. package/Sources/UIKit/Pasteboard.swift +363 -0
  373. package/Sources/UIKit/PhotoAlbum.swift +81 -0
  374. package/Sources/UIKit/RemoteNotifications.swift +15 -0
  375. package/Sources/UIKit/Representable.swift +288 -0
  376. package/Sources/UIKit/SafeArea.swift +12 -0
  377. package/Sources/UIKit/SceneDelegates.swift +132 -0
  378. package/Sources/UIKit/Scenes.swift +40 -0
  379. package/Sources/UIKit/ScrollView.swift +157 -0
  380. package/Sources/UIKit/ShortcutItems.swift +67 -0
  381. package/Sources/UIKit/SymbolConfiguration.swift +124 -0
  382. package/Sources/UIKit/TargetAction.swift +123 -0
  383. package/Sources/UIKit/TextPaste.swift +161 -0
  384. package/Sources/UIKit/TextView.swift +538 -0
  385. package/Sources/UIKit/UIKit.swift +292 -0
  386. package/Sources/UIKit/UIView.swift +855 -0
  387. package/Sources/UIKit/ViewTranslation.swift +311 -0
  388. package/Sources/UniformTypeIdentifiers/UTType.swift +196 -0
  389. package/Sources/UserNotifications/Content.swift +370 -0
  390. package/Sources/UserNotifications/NotificationCenter.swift +309 -0
  391. package/Sources/WeatherKit/WeatherKit.swift +228 -0
  392. package/Sources/WebKit/WebKit.swift +245 -0
  393. package/Sources/os/os.swift +376 -0
  394. package/Sources/zlib/zlib.swift +6 -0
  395. package/index.d.ts +2 -0
  396. package/index.mjs +16 -0
  397. package/manifest.d.mts +2 -0
  398. package/manifest.mjs +39 -0
  399. package/package.json +30 -4
  400. package/sync-sources.mjs +33 -0
@@ -0,0 +1,2146 @@
1
+ import Observation
2
+
3
+ #if canImport(WASILibc)
4
+ import WASILibc
5
+ #elseif canImport(Glibc)
6
+ import Glibc
7
+ #elseif canImport(Musl)
8
+ import Musl
9
+ #elseif canImport(Darwin)
10
+ import Darwin
11
+ #endif
12
+
13
+ /// A view that has been mounted. Composite views keep their `State` boxes here
14
+ /// so values survive the parent recreating the view struct.
15
+ @MainActor
16
+ final class _MountedNode {
17
+ var view: any View
18
+ var children: [_MountedNode] = []
19
+ var boxes: [any _AnyStateBox] = []
20
+ /// Identity within a keyed parent (`ForEach`).
21
+ var key: AnyHashable?
22
+ /// Position-and-type path used to restore state across hot reloads (ViewPath.swift).
23
+ var path: _ViewPath?
24
+ var environment = EnvironmentValues()
25
+ /// Handler supplied by the runtime (navigation links and screens).
26
+ var handler: ((_JSON) -> Void)?
27
+ var navTitle: String?
28
+ var navDisplayMode: String?
29
+ var depth = 0
30
+ var appeared = false
31
+ /// `.onDisappear`: run when the node leaves the tree.
32
+ var disappearAction: (@MainActor () -> Void)?
33
+ /// `.animation(_:value:)`: the last tracked value and a counter bumped on change.
34
+ var animationValue: Any?
35
+ var animationToken = 0
36
+ /// The render pass in which `animationToken` was last bumped.
37
+ var animationTokenPass = -1
38
+ /// Sheets: the mounted sheet content (second child) and its detents.
39
+ var sheetDetents: [String]?
40
+ /// `.task`: the running task and the id it was started for.
41
+ var task: Task<Void, Never>?
42
+ var taskID: AnyHashable?
43
+ var taskStarted = false
44
+ /// `GeometryReader`: the size the renderer last reported.
45
+ var geometrySize = CGSize.zero
46
+ /// A custom shape: the path it drew last (Shape.swift).
47
+ var pathState: _PathState?
48
+ /// The rows a lazily built keyed group has mounted so far (LazyRows.swift).
49
+ var lazyLimit = _LazyRows.batch
50
+ /// A `Menu` or context menu whose items are built (the page opened it).
51
+ var menuOpen = false
52
+ /// Phase 12: this stack is a `.scrollTargetLayout()`; its props carry `scrollTarget`.
53
+ var scrollTargetLayout = false
54
+ /// `TabView` / `Picker`: option tags in order, and the selected index.
55
+ var optionTags: [AnyHashable?] = []
56
+ var selectedIndex = 0
57
+ /// `NavigationStack`: destinations registered by descendants, kept across renders.
58
+ var destinations: [ObjectIdentifier: (Any) -> (any View)?] = [:]
59
+ var pickerOptions: [String] = []
60
+ var pickerLabel = ""
61
+ /// `NavigationSplitView`: the sidebar's `List(selection:)` binding, registered
62
+ /// each pass while the sidebar is reconciled.
63
+ var splitSelection: _ListSelection?
64
+ /// `NavigationStack` nested in another stack's screen: rendered in place.
65
+ /// Also a sidebar `TabView`, which renders its split view in place.
66
+ var navFlattened = false
67
+ /// `navscreen` / `List`: the edit mode this node owns (`EditButton` toggles it).
68
+ var editMode = EditMode.inactive
69
+ /// `NavigationStack` / compact `NavigationSplitView`: the stack context its
70
+ /// screens and links share. Kept across passes: a clean pass skips the
71
+ /// phase that recomputes `detailShown`, and screen handlers capture it.
72
+ var navContext: _NavContext?
73
+
74
+ // Incremental rendering (Phase 9). A render re-runs a view's body only
75
+ // when the node is `dirty` (its own `@State`, observed object or
76
+ // `@Observable` read changed) or an ancestor re-ran (its view value is
77
+ // new); a clean node whose descendants are all clean is skipped whole,
78
+ // and the elements it produced last time are reused.
79
+ weak var parent: _MountedNode?
80
+ /// This node's body must run again.
81
+ var dirty = true
82
+ /// A descendant is dirty (the chain from the root to every dirty node).
83
+ var subtreeDirty = false
84
+ /// The elements this node produced last time, with the sheets and alerts
85
+ /// its subtree presented (they are collected as side effects).
86
+ var cachedElements: [_Element]?
87
+ var cachedSheets: [_Element] = []
88
+ var cachedAlerts: [_Element] = []
89
+
90
+ // Skipping unchanged views (Phase 16). When an ancestor re-runs, this
91
+ // node gets a new view value; if it is the same as `view` and so is
92
+ // everything the node received from its parent, the node is treated as
93
+ // clean, like SwiftUI does for a view whose stored properties are equal.
94
+ /// What the parent passed down the last time this node was reconciled.
95
+ var received: _ReceivedContext?
96
+ /// This node or a descendant writes to its ancestors while reconciled
97
+ /// (a navigation title, a destination, detents, a list selection, a stack
98
+ /// or tab structure): a parent that re-runs resets what they set and
99
+ /// expects them to set it again, so they are never skipped that way.
100
+ var writesContext = false
101
+
102
+ /// The preference values this subtree produced (see Preferences.swift),
103
+ /// kept for passes that skip it.
104
+ var preferences = _PreferenceValues()
105
+ /// What an `onPreferenceChange` saw last.
106
+ var preferenceSeen: Any?
107
+
108
+ init(view: any View) {
109
+ self.view = view
110
+ }
111
+ }
112
+
113
+ /// The enclosing `NavigationStack`, shared by everything inside it.
114
+ @MainActor
115
+ final class _NavContext {
116
+ var box: _StateBox<[_PushedScreen]>
117
+ var path: _PathBinding?
118
+ /// The stack's mounted node, which owns the destination registry.
119
+ unowned let node: _MountedNode
120
+
121
+ init(box: _StateBox<[_PushedScreen]>, path: _PathBinding?, node: _MountedNode) {
122
+ self.box = box
123
+ self.path = path
124
+ self.node = node
125
+ }
126
+
127
+ func push(_ view: any View) { box.value.append(_PushedScreen(view: view)) }
128
+
129
+ /// Pushes a value: onto the path binding when there is one, otherwise as
130
+ /// a resolved destination view.
131
+ func push(value: AnyHashable) {
132
+ if let path {
133
+ path.set(path.get() + [value])
134
+ } else {
135
+ push(destination(for: value))
136
+ }
137
+ }
138
+
139
+ func register(_ registration: any _DestinationRegistering) {
140
+ node.destinations[registration._destinationType] = { registration._destination(for: $0) }
141
+ }
142
+
143
+ func destination(for value: AnyHashable) -> any View {
144
+ let type = ObjectIdentifier(Swift.type(of: value.base))
145
+ if let builder = node.destinations[type], let view = builder(value.base) { return view }
146
+ return Text("No navigationDestination(for: \(Swift.type(of: value.base)).self)")
147
+ }
148
+
149
+ /// Compact `NavigationSplitView`: set to the split view's node. While
150
+ /// `detailShown`, depth 1 is the detail screen, whose pop clears the
151
+ /// sidebar's selection; pushed screens follow it.
152
+ var splitNode: _MountedNode?
153
+ var detailShown = false
154
+
155
+ /// Pops so that `depth` screens below the popped one remain (`depth` 1 pops to the root).
156
+ func pop(to depth: Int) {
157
+ if let splitNode {
158
+ if detailShown, depth <= 1 {
159
+ box.value.removeAll()
160
+ splitNode.splitSelection?.set(nil)
161
+ return
162
+ }
163
+ let pushedDepth = depth - (detailShown ? 1 : 0)
164
+ guard pushedDepth >= 1, box.value.count >= pushedDepth else { return }
165
+ box.value.removeSubrange((pushedDepth - 1)...)
166
+ return
167
+ }
168
+ let pathCount = path?.get().count ?? 0
169
+ if depth <= pathCount, let path {
170
+ path.set(Array(path.get().prefix(depth - 1)))
171
+ box.value.removeAll()
172
+ return
173
+ }
174
+ let pushedDepth = depth - pathCount
175
+ guard pushedDepth >= 1, box.value.count >= pushedDepth else { return }
176
+ box.value.removeSubrange((pushedDepth - 1)...)
177
+ }
178
+ }
179
+
180
+ struct _ReconcileContext {
181
+ var environment: EnvironmentValues
182
+ /// `Type/index:Type/...` from the root (ViewPath.swift); keys state snapshots.
183
+ var path: _ViewPath
184
+ var navStack: _NavContext?
185
+ var screen: _MountedNode?
186
+ /// The nearest enclosing presented sheet's node (for `presentationDetents`).
187
+ var sheet: _MountedNode?
188
+ /// The `NavigationSplitView` whose sidebar is being reconciled: its
189
+ /// `List(selection:)` registers with it.
190
+ var split: _MountedNode?
191
+ /// Inside a `TabView` tab that is not selected: titles stay put.
192
+ var inactiveTab = false
193
+ /// Reconciling the rows of a `List` or lazy stack (LazyRows.swift): a
194
+ /// keyed group here builds its rows as they come into view.
195
+ var lazyRows: Axis?
196
+ }
197
+
198
+ /// What a node received from its parent, to tell whether a new pass passes
199
+ /// the same thing (`_MountedNode.received`). Objects are kept by identity.
200
+ struct _ReceivedContext {
201
+ var environment: EnvironmentValues
202
+ var path: _ViewPath
203
+ var navStack: ObjectIdentifier?
204
+ var screen: ObjectIdentifier?
205
+ var sheet: ObjectIdentifier?
206
+ var split: ObjectIdentifier?
207
+ var inactiveTab: Bool
208
+ var lazyRows: Axis?
209
+
210
+ @MainActor init(_ context: _ReconcileContext) {
211
+ environment = context.environment
212
+ path = context.path
213
+ navStack = context.navStack.map(ObjectIdentifier.init)
214
+ screen = context.screen.map(ObjectIdentifier.init)
215
+ sheet = context.sheet.map(ObjectIdentifier.init)
216
+ split = context.split.map(ObjectIdentifier.init)
217
+ inactiveTab = context.inactiveTab
218
+ lazyRows = context.lazyRows
219
+ }
220
+
221
+ /// Same objects, path and tab state, and the same environment.
222
+ @MainActor func matches(_ context: _ReconcileContext) -> Bool {
223
+ path.matches(context.path)
224
+ && navStack == context.navStack.map(ObjectIdentifier.init)
225
+ && screen == context.screen.map(ObjectIdentifier.init)
226
+ && sheet == context.sheet.map(ObjectIdentifier.init)
227
+ && split == context.split.map(ObjectIdentifier.init)
228
+ && inactiveTab == context.inactiveTab
229
+ && lazyRows == context.lazyRows
230
+ && environment._isSame(as: context.environment)
231
+ }
232
+ }
233
+
234
+ /// The identity of a value the runtime re-creates on every pass for a node
235
+ /// (`_RuntimeIdentified`): the node and what the value is for.
236
+ struct _RuntimeKey: Hashable, Sendable {
237
+ let node: ObjectIdentifier
238
+ let role: String
239
+ }
240
+
241
+ /// Owns the mounted tree, schedules renders and routes events.
242
+ @MainActor
243
+ public final class _Runtime {
244
+ public static let shared = _Runtime()
245
+
246
+ private(set) var host: any _RenderHost = _BufferedHost.shared
247
+ private var rootView: (any View)?
248
+ private var root: _MountedNode?
249
+ /// The off-screen element measuring views for `_ViewMeasurements` (Measurements.swift).
250
+ private var measuringRoot: _MountedNode?
251
+ /// Set when measurements arrive: the next render re-runs every body, none skipped as unchanged.
252
+ private var renderEverythingPass = false
253
+ private let reconciler = _Reconciler()
254
+ private var needsRender = false
255
+ /// The next render re-runs every body (environment changes, `invalidate()`).
256
+ private var fullRender = true
257
+ private var rendering = false
258
+ private var pendingAppear: [@MainActor () -> Void] = []
259
+ /// `.onDisappear` actions of the nodes the last pass removed, run after it like the appear actions.
260
+ private var pendingDisappear: [@MainActor () -> Void] = []
261
+ /// Values the renderer controls, such as the color scheme.
262
+ private var baseEnvironment = EnvironmentValues() {
263
+ didSet { EnvironmentValues._page = baseEnvironment }
264
+ }
265
+ /// `.preferredColorScheme` seen in the last pass (sticky), and what the host was told.
266
+ private var preferredColorScheme: ColorScheme?
267
+ private var sentColorScheme: ColorScheme?
268
+ /// State values from a previous build, applied as views first mount.
269
+ private var restoredState: [String: _JSON] = [:]
270
+ /// The transaction `withAnimation` (or an animated binding's write) has
271
+ /// open while its body runs: `.some(nil)` when it disables animations. Shared
272
+ /// by every runtime instance because `withAnimation` is a free function.
273
+ private static var openTransaction: Animation?? = nil
274
+ /// Animation for this runtime's next render pass: the open transaction's,
275
+ /// recorded when a change made inside it invalidates this runtime. A
276
+ /// transaction whose body changes nothing animates nothing later.
277
+ private var passAnimation: Animation?
278
+ /// Counts render passes (`_MountedNode.animationTokenPass`).
279
+ private var renderPass = 0
280
+ /// Sheet elements collected during the current element pass, emitted after the tree.
281
+ private var presentedSheets: [_Element] = []
282
+ /// Alert elements collected during the current element pass, emitted after
283
+ /// the sheets so that an alert presented from a sheet covers it.
284
+ private var presentedAlerts: [_Element] = []
285
+
286
+ init() {}
287
+
288
+ // MARK: Entry points
289
+
290
+ /// Called from `App.main()`.
291
+ public func launch<A: App>(_ app: A) {
292
+ watchApp(app)
293
+ guard let scene = app.body as? any _RootScene else {
294
+ fatalError("\(A.Body.self) is not a scene the runtime can present. Use WindowGroup.")
295
+ }
296
+ let headless = _environment("SB_HEADLESS") == "1"
297
+ isHeadless = headless
298
+ if headless, let text = _environment("SB_RESPONSES"), let json = try? _JSON.parse(text), case .array(let items) = json {
299
+ headlessResponses = items
300
+ }
301
+ if let state = _environment("SB_STATE"), let json = try? _JSON.parse(state), case .object(let values) = json {
302
+ restoredState = values
303
+ }
304
+ mount(scene._rootView, host: headless ? _HeadlessHost() : _BufferedHost.shared)
305
+ _flushAfterUpdate()
306
+
307
+ if headless {
308
+ // A script that names an element not on screen stops the run.
309
+ if !replay(events: _environment("SB_EVENTS") ?? "") { exit(1) }
310
+ if let limit = _environment("SB_RUN_MS").flatMap(Double.init), limit > 0 {
311
+ runHeadlessClock(upTo: limit)
312
+ }
313
+ }
314
+ }
315
+
316
+ /// Re-evaluates the app's `body` when the app's own `@State` changes or,
317
+ /// for an app with `@Environment` properties, when the environment the
318
+ /// host reports changes (`scenePhase`, the color scheme…), as SwiftUI
319
+ /// does: `.onChange(of: scenePhase)` on a scene and bindings to the app's
320
+ /// state (`TabView(selection: $selectedTab)`) work. The new root view
321
+ /// replaces the old and its node renders again. An app with neither is
322
+ /// evaluated once, as before.
323
+ private var refreshApp: (() -> Void)?
324
+
325
+ private func watchApp<A: App>(_ app: A) {
326
+ let properties = Mirror(reflecting: app).children.map(\.value)
327
+ let environment = properties.filter { $0 is any _EnvironmentProperty || $0 is any _EnvironmentReading }
328
+ // `@StateObject`s are observed by the views they are handed to.
329
+ let boxes = properties.filter { !($0 is any _ObservingProperty) }.compactMap { ($0 as? any _StateProperty)?._box }
330
+ guard !environment.isEmpty || !boxes.isEmpty else { return }
331
+ let refresh = { [unowned self] in
332
+ for property in environment {
333
+ (property as? any _EnvironmentProperty)?._resolve(in: baseEnvironment)
334
+ (property as? any _EnvironmentReading)?._resolve(in: baseEnvironment)
335
+ }
336
+ guard let scene = app.body as? any _RootScene else { return }
337
+ rootView = scene._rootView
338
+ if let root { invalidate(root) }
339
+ }
340
+ refreshApp = refresh
341
+ for box in boxes {
342
+ box._onChange = { [weak self] in self?.refreshApp?() }
343
+ }
344
+ for property in environment {
345
+ (property as? any _EnvironmentProperty)?._resolve(in: baseEnvironment)
346
+ (property as? any _EnvironmentReading)?._resolve(in: baseEnvironment)
347
+ }
348
+ }
349
+
350
+ /// Puts `object` in every view's environment, under its type: SwiftUI does
351
+ /// so with an app's scene delegate that is `Observable` or an
352
+ /// `ObservableObject` (UIKit's AppDelegate.swift connects the scene).
353
+ public func _addRootEnvironmentObject(_ object: AnyObject) {
354
+ baseEnvironment._setAnyObject(object)
355
+ refreshApp?()
356
+ if let root { invalidate(root) }
357
+ }
358
+
359
+ /// `launch` without the headless setup, for tests.
360
+ func _launchForTesting<A: App>(_ app: A, host: any _RenderHost) {
361
+ watchApp(app)
362
+ guard let scene = app.body as? any _RootScene else { fatalError("\(A.Body.self) is not a presentable scene") }
363
+ mount(scene._rootView, host: host)
364
+ }
365
+
366
+ /// Sends the app's defaults to the host (a `defaults` op), which keeps them
367
+ /// for the next launch. Called by `UserDefaults` (Sources/FoundationBridge).
368
+ public func storeDefaults(_ values: _JSON) {
369
+ host.apply([.defaults(values)])
370
+ }
371
+
372
+ /// Sends one saved batch of a SwiftData store to the host (a `swiftdata`
373
+ /// op), which writes it to IndexedDB and hands the store back at the next
374
+ /// launch (`SB_SWIFTDATA`). Called by `ModelContext.save()` (Sources/SwiftData).
375
+ public func storeSwiftData(_ change: _JSON) {
376
+ host.apply([.swiftData(change)])
377
+ }
378
+
379
+ /// Keys of the image sources the host has been sent (`imagedata` ops).
380
+ private var sentImageSources: Set<String> = []
381
+
382
+ /// Sends a bitmap's source to the host unless it has it already. Called
383
+ /// before an element that draws it is emitted, and by `ShareLink` before
384
+ /// it shares one, so the host always has the bytes a recipe names.
385
+ public func _sendImageSource(_ source: _ImageSource) {
386
+ guard sentImageSources.insert(source.key).inserted else { return }
387
+ host.apply([.imageData(source.json)])
388
+ }
389
+
390
+ // MARK: After-update actions
391
+
392
+ /// One-shot actions to run once the current event, executor run or launch
393
+ /// has rendered: SwiftData's autosave saves there, after the changes an
394
+ /// event made and before the host gets control back.
395
+ private var afterUpdate: [@MainActor () -> Void] = []
396
+
397
+ /// Runs `action` at the end of the current event (or executor run), after
398
+ /// its render pass.
399
+ public func _afterUpdate(_ action: @escaping @MainActor () -> Void) {
400
+ afterUpdate.append(action)
401
+ }
402
+
403
+ /// Runs the actions `_afterUpdate` queued, and renders what they changed.
404
+ public func _flushAfterUpdate() {
405
+ var rounds = 0
406
+ while !afterUpdate.isEmpty, rounds < 16 {
407
+ rounds += 1
408
+ let actions = afterUpdate
409
+ afterUpdate.removeAll()
410
+ for action in actions { action() }
411
+ renderIfNeeded()
412
+ }
413
+ }
414
+
415
+ // MARK: Host requests (Phase 13)
416
+
417
+ private var isHeadless = false
418
+ /// Headless mode: canned answers from `SB_RESPONSES` (a JSON array; each
419
+ /// item names a `url`, or a `prefix`, and carries `status` / `headers` /
420
+ /// `body` or `bodyBase64` for a fetch, `width` / `height` for an image;
421
+ /// or carries a `pick` for a `pickFiles` request).
422
+ private var headlessResponses: [_JSON] = []
423
+ private var pendingRequests: [Int: CheckedContinuation<_JSON, any Error>] = [:]
424
+ private var nextRequestID = 1
425
+
426
+ /// Asks the host for something only it can do (a `fetch`, an `image`
427
+ /// probe; see docs/ops-protocol.md, "Phase 13") and waits for its
428
+ /// `response` event. Called by `URLSession` and `AsyncImage`
429
+ /// (Sources/FoundationBridge). Headless runs answer from `SB_RESPONSES`
430
+ /// at once, or fail, so an app's network paths replay deterministically;
431
+ /// a caller that knows its own headless answer passes it as `headless`
432
+ /// (the stand-in frameworks: UserNotifications).
433
+ public func _hostRequest(kind: String, params: _JSON, headless: _JSON? = nil) async throws -> _JSON {
434
+ let id = nextRequestID
435
+ nextRequestID += 1
436
+ return try await withCheckedThrowingContinuation { continuation in
437
+ pendingRequests[id] = continuation
438
+ host.apply([.request(id: id, kind: kind, params: params)])
439
+ if isHeadless {
440
+ if let headless { complete(request: id, with: ["value": headless]) }
441
+ else { answerHeadless(id: id, kind: kind, params: params) }
442
+ }
443
+ }
444
+ }
445
+
446
+ /// Completes the request `id` from a `response` event: `value` on
447
+ /// success, `error` (a message) on failure.
448
+ private func complete(request id: Int, with fields: [String: _JSON]) {
449
+ guard let continuation = pendingRequests.removeValue(forKey: id) else { return }
450
+ if case .string(let message)? = fields["error"] {
451
+ continuation.resume(throwing: _HostRequestError(message: message))
452
+ } else {
453
+ continuation.resume(returning: fields["value"] ?? .null)
454
+ }
455
+ }
456
+
457
+ private func answerHeadless(id: Int, kind: String, params: _JSON) {
458
+ if kind == "share" {
459
+ // Nothing to share with headless; the op in the log is the record.
460
+ complete(request: id, with: ["value": .object(["outcome": .string("headless")])])
461
+ return
462
+ }
463
+ if kind == "pickFiles" {
464
+ complete(request: id, with: ["value": headlessPick()])
465
+ return
466
+ }
467
+ if kind == "scanner" {
468
+ complete(request: id, with: ["value": headlessScan(params: params)])
469
+ return
470
+ }
471
+ if kind == "openURL" {
472
+ // No tabs headless: the op in the log is the record.
473
+ complete(request: id, with: ["value": .null])
474
+ return
475
+ }
476
+ if kind == "media" {
477
+ // AVFoundation (docs/ops-protocol.md, "Video and audio"): a file
478
+ // becomes a stand-in URL; a probe reads `duration` / `width` /
479
+ // `height` from the `SB_RESPONSES` entry for its URL.
480
+ complete(request: id, with: Self._headlessMedia(params: params, fixtures: headlessResponses, serial: id))
481
+ return
482
+ }
483
+ if kind == "websocket" {
484
+ // No sockets headless: the open fails, as it does offline.
485
+ complete(request: id, with: ["error": .string("headless: WebSockets are not available")])
486
+ return
487
+ }
488
+ if kind == "webAuth" {
489
+ // A sign-in window: a fixture plays the provider (WebAuthHeadless.swift).
490
+ complete(request: id, with: Self._headlessWebAuth(params: params, fixtures: headlessResponses))
491
+ return
492
+ }
493
+ guard case .object(let request) = params, case .string(let url)? = request["url"] else {
494
+ complete(request: id, with: ["error": .string("malformed request")])
495
+ return
496
+ }
497
+ let fixture = headlessResponses.first { item in
498
+ guard case .object(let fields) = item else { return false }
499
+ if case .string(let exact)? = fields["url"], exact == url { return true }
500
+ if case .string(let prefix)? = fields["prefix"], url.hasPrefix(prefix) { return true }
501
+ return false
502
+ }
503
+ guard case .object(let fields)? = fixture else {
504
+ complete(request: id, with: ["error": .string("headless: no SB_RESPONSES entry for \(url)")])
505
+ return
506
+ }
507
+ switch kind {
508
+ case "fetch":
509
+ var body = ""
510
+ if case .string(let text)? = fields["body"] { body = _base64(Array(text.utf8)) }
511
+ if case .string(let encoded)? = fields["bodyBase64"] { body = encoded }
512
+ let status: _JSON = fields["status"] ?? .number(200)
513
+ let headers: _JSON = fields["headers"] ?? .object([:])
514
+ complete(request: id, with: ["value": .object(["status": status, "headers": headers, "body": .string(body), "url": .string(url)])])
515
+ case "image":
516
+ guard let width = fields["width"], let height = fields["height"] else {
517
+ complete(request: id, with: ["error": .string("headless: SB_RESPONSES entry for \(url) has no width/height")])
518
+ return
519
+ }
520
+ complete(request: id, with: ["value": .object(["width": width, "height": height])])
521
+ default:
522
+ complete(request: id, with: ["error": .string("headless: unknown request kind \(kind)")])
523
+ }
524
+ }
525
+
526
+ /// `SB_RESPONSES` entries with a `pick` (a list of files), used in order.
527
+ private var headlessPicksUsed = 0
528
+
529
+ /// A `pickFiles` answer (`PhotosPicker`; docs/ops-protocol.md, "Photos
530
+ /// picker") from the next `SB_RESPONSES` entry with a `pick`: each file's
531
+ /// `name`, `type`, `body` (text) or `bodyBase64`, and for a picture
532
+ /// `width` / `height` (its `url` defaults to a data URL). With no entry
533
+ /// left, the viewer dismissed the chooser: no items.
534
+ private func headlessPick() -> _JSON {
535
+ let picks = headlessResponses.compactMap { item -> [_JSON]? in
536
+ guard case .object(let fields) = item, case .array(let files)? = fields["pick"] else { return nil }
537
+ return files
538
+ }
539
+ guard headlessPicksUsed < picks.count else { return .object(["items": .array([])]) }
540
+ let files = picks[headlessPicksUsed]
541
+ headlessPicksUsed += 1
542
+ let items = files.map { file -> _JSON in
543
+ guard case .object(let fields) = file else { return .null }
544
+ var item = fields
545
+ item.removeValue(forKey: "body")
546
+ item.removeValue(forKey: "bodyBase64")
547
+ var data = ""
548
+ if case .string(let text)? = fields["body"] { data = _base64(Array(text.utf8)) }
549
+ if case .string(let encoded)? = fields["bodyBase64"] { data = encoded }
550
+ item["data"] = .string(data)
551
+ if item["url"] == nil, case .string(let type)? = fields["type"], fields["width"] != nil {
552
+ item["url"] = .string("data:\(type);base64,\(data)")
553
+ }
554
+ return .object(item)
555
+ }
556
+ return .object(["items": .array(items)])
557
+ }
558
+
559
+ /// `SB_RESPONSES` entries with a `scan` (a code), used in order.
560
+ private var headlessScansUsed = 0
561
+
562
+ /// A `scanner` answer (AVFoundation's capture sessions; docs/ops-protocol.md,
563
+ /// "Code scanning"): no camera for a `probe`; for a `scan`, the next
564
+ /// `SB_RESPONSES` entry's `scan` (`string`, and optionally `format`,
565
+ /// `corners`, `image`), or the viewer closing the camera when none is left.
566
+ private func headlessScan(params: _JSON) -> _JSON {
567
+ if case .object(let fields) = params, case .string("probe")? = fields["action"] { return .object(["camera": .bool(false)]) }
568
+ let scans = headlessResponses.compactMap { item -> _JSON? in
569
+ guard case .object(let fields) = item, let scan = fields["scan"] else { return nil }
570
+ return scan
571
+ }
572
+ guard headlessScansUsed < scans.count else { return .object(["cancelled": .bool(true)]) }
573
+ headlessScansUsed += 1
574
+ return scans[headlessScansUsed - 1]
575
+ }
576
+
577
+ /// Drives the executor with a virtual clock: runs jobs, jumps to the next
578
+ /// timer, and stops when none remain or the clock would pass `limit` ms.
579
+ private func runHeadlessClock(upTo limit: Double) {
580
+ let executor = _HostExecutor.shared
581
+ var guardCount = 0
582
+ while guardCount < 10_000 {
583
+ guardCount += 1
584
+ let next = runJobsAndRender()
585
+ if next < 0 { break }
586
+ let nextMs = Double(executor.nowNs + UInt64(next)) / 1_000_000
587
+ if nextMs > limit { break }
588
+ executor.nowNs += UInt64(next)
589
+ host.apply([.event(.object(["type": .string("clock"), "ms": .number(nextMs.rounded())]))])
590
+ }
591
+ }
592
+
593
+ /// Mounts `view` as the root and performs the first render.
594
+ func mount(_ view: any View, host: any _RenderHost) {
595
+ self.host = host
596
+ sentImageSources = []
597
+ rootView = view
598
+ root = nil
599
+ measuringRoot = nil
600
+ render()
601
+ }
602
+
603
+ /// A tap on the element with this id.
604
+ public func dispatch(tap id: Int) {
605
+ dispatch(event: .object(["type": .string("tap"), "id": .number(Double(id))]))
606
+ }
607
+
608
+ /// Handles one renderer event (see docs/ops-protocol.md) and renders if needed.
609
+ public func dispatch(event: _JSON) {
610
+ handle(event)
611
+ renderIfNeeded()
612
+ _flushAfterUpdate()
613
+ }
614
+
615
+ /// Applies an event's state changes without rendering. A `batch` event
616
+ /// (`{"type": "batch", "events": [...]}`) applies each of its events, so
617
+ /// one host layout pass that measured several `geometry` and `layout`
618
+ /// elements costs one render instead of one per element.
619
+ private func handle(_ event: _JSON) {
620
+ guard case .object(let fields) = event, case .string(let type)? = fields["type"] else { return }
621
+ let id: Int? = if case .number(let n)? = fields["id"] { Int(n) } else { nil }
622
+
623
+ switch type {
624
+ case "batch":
625
+ if case .array(let events)? = fields["events"] { events.forEach(handle) }
626
+ case "tap":
627
+ // A tap with a location (`onTapGesture { point in }`) carries the
628
+ // point in the element's coordinates; any other tap is `.null`.
629
+ if let id {
630
+ if let x = fields["x"], let y = fields["y"] {
631
+ reconciler.handlers[id]?(.object(["x": x, "y": y]))
632
+ } else {
633
+ reconciler.handlers[id]?(.null)
634
+ }
635
+ }
636
+ case "text", "toggle", "select", "step", "slide", "date", "link":
637
+ // `link`: a tap on a link in a text's runs; the value is its URL.
638
+ if let id, let value = fields["value"] { reconciler.handlers[id]?(value) }
639
+ case "geometry":
640
+ if let id, let width = fields["width"], let height = fields["height"] {
641
+ reconciler.handlers[id]?(.object(["width": width, "height": height]))
642
+ }
643
+ case "layout":
644
+ // Host measurement of a custom `Layout`'s children; the handler
645
+ // parses the whole event (see CustomLayout.swift).
646
+ if let id { reconciler.handlers[id]?(event) }
647
+ case "drag", "longpress", "delete", "refresh", "focus", "submit", "video", "lazy", "menu":
648
+ // Phase 8: the handler receives the whole event and reads `type`
649
+ // (a `listrow` handles both `tap` and `delete`; Phase 12: a
650
+ // `textfield` handles `text`, `focus` and `submit`; a `video`
651
+ // reads its `event`, docs/ops-protocol.md, "Video and audio").
652
+ if let id { reconciler.handlers[id]?(event) }
653
+ case "response":
654
+ // Phase 13: the host's answer to a `request` op.
655
+ if let id { complete(request: id, with: fields) }
656
+ case "environment":
657
+ if case .string(let scheme)? = fields["colorScheme"] {
658
+ baseEnvironment.colorScheme = scheme == "dark" ? .dark : .light
659
+ invalidate()
660
+ }
661
+ if case .string(let size)? = fields["dynamicTypeSize"], let value = DynamicTypeSize(name: size) {
662
+ baseEnvironment.dynamicTypeSize = value
663
+ invalidate()
664
+ }
665
+ if case .string(let name)? = fields["horizontalSizeClass"], let value = UserInterfaceSizeClass(name: name) {
666
+ baseEnvironment.horizontalSizeClass = value
667
+ invalidate()
668
+ }
669
+ if case .string(let name)? = fields["scenePhase"], let value = ScenePhase(name: name) {
670
+ baseEnvironment.scenePhase = value
671
+ invalidate()
672
+ }
673
+ // The app's `@Environment` properties read the new values.
674
+ refreshApp?()
675
+ default:
676
+ break
677
+ }
678
+ }
679
+
680
+ /// A render soon (a view to measure was queued outside of one).
681
+ func _scheduleRender() {
682
+ if !rendering { needsRender = true }
683
+ }
684
+
685
+ /// A render that re-evaluates the whole tree and re-runs every body, even
686
+ /// those whose view is unchanged (new measurements, Measurements.swift).
687
+ func _renderEverything() {
688
+ renderEverythingPass = true
689
+ invalidate()
690
+ }
691
+
692
+ /// Schedules a render that re-evaluates the whole tree.
693
+ func invalidate() {
694
+ needsRender = true
695
+ fullRender = true
696
+ recordTransaction()
697
+ }
698
+
699
+ /// Schedules a render that re-evaluates `node`'s subtree (and whatever
700
+ /// else is dirty); everything else is reused.
701
+ func invalidate(_ node: _MountedNode) {
702
+ markDirty(node)
703
+ needsRender = true
704
+ recordTransaction()
705
+ }
706
+
707
+ /// A change made inside a transaction takes its animation to the next pass.
708
+ private func recordTransaction() {
709
+ if let open = Self.openTransaction { passAnimation = open }
710
+ }
711
+
712
+ private func markDirty(_ node: _MountedNode) {
713
+ node.dirty = true
714
+ node.cachedElements = nil
715
+ var ancestor = node.parent
716
+ while let current = ancestor, !current.subtreeDirty {
717
+ current.subtreeDirty = true
718
+ current.cachedElements = nil
719
+ ancestor = current.parent
720
+ }
721
+ }
722
+
723
+ /// Opens a transaction for the changes `body` makes: they animate with
724
+ /// `animation` (none when it is nil), and changes after it do not.
725
+ func runInTransaction<Result>(_ animation: Animation?, _ body: () throws -> Result) rethrows -> Result {
726
+ let outer = beginTransaction(animation)
727
+ defer { endTransaction(outer) }
728
+ return try body()
729
+ }
730
+
731
+ /// Opens a transaction; returns the one it replaces, for `endTransaction`.
732
+ func beginTransaction(_ animation: Animation?) -> Animation?? {
733
+ let outer = Self.openTransaction
734
+ Self.openTransaction = .some(animation)
735
+ return outer
736
+ }
737
+
738
+ func endTransaction(_ outer: Animation??) {
739
+ Self.openTransaction = outer
740
+ }
741
+
742
+ /// Whether this pass may animate `node`'s changes: the transaction has an
743
+ /// animation, or an `.animation(_:value:)` above it saw its value change.
744
+ private func passAnimates(_ node: _MountedNode) -> Bool {
745
+ if passAnimation != nil { return true }
746
+ var ancestor: _MountedNode? = node
747
+ while let current = ancestor {
748
+ if current.animationTokenPass == renderPass { return true }
749
+ ancestor = current.parent
750
+ }
751
+ return false
752
+ }
753
+
754
+ /// Advances the executor's clock, runs queued jobs and due timers, renders
755
+ /// if anything changed, and returns milliseconds until the next timer (-1: none).
756
+ public func runJobs(nowMilliseconds: Double) -> Double {
757
+ let executor = _HostExecutor.shared
758
+ executor.nowNs = UInt64(max(0, nowMilliseconds) * 1_000_000)
759
+ let next = runJobsAndRender()
760
+ return next < 0 ? -1 : Double(next) / 1_000_000
761
+ }
762
+
763
+ /// Runs the queued jobs and renders what they changed. A render can start
764
+ /// tasks of its own (an `onChange` action's `Task`, such as one loading
765
+ /// the item a `PhotosPicker` answer just selected); they run in the same
766
+ /// call instead of waiting for the next event or timer. Returns the
767
+ /// executor's delay until its next timer (nanoseconds, -1: none).
768
+ private func runJobsAndRender() -> Int64 {
769
+ let executor = _HostExecutor.shared
770
+ var next: Int64 = -1
771
+ var rounds = 0
772
+ repeat {
773
+ rounds += 1
774
+ next = executor.runJobs()
775
+ renderIfNeeded()
776
+ _flushAfterUpdate()
777
+ } while executor.hasQueuedJobs && rounds < 16
778
+ return next
779
+ }
780
+
781
+ func renderIfNeeded() {
782
+ if needsRender, !rendering { render() }
783
+ }
784
+
785
+ // MARK: Rendering
786
+
787
+ private func render() {
788
+ guard let rootView else { return }
789
+ rendering = true
790
+ needsRender = false
791
+ renderPass += 1
792
+ _ViewMeasurements.shared.runtime = self
793
+ // An app's own `ShapeStyle` resolves in the page's environment, a
794
+ // global: the runtime rendering now is the page's (tests mount
795
+ // several runtimes, in parallel suites, each with its own scheme).
796
+ EnvironmentValues._page = baseEnvironment
797
+
798
+ let context = _ReconcileContext(environment: baseEnvironment, path: _ViewPath(root: type(of: rootView)))
799
+ let force = fullRender
800
+ fullRender = false
801
+ let node = reconcile(root, with: rootView, context: context, force: force)
802
+ root = node
803
+ // The views measured for synchronous size questions, asked while the
804
+ // bodies above ran (Measurements.swift).
805
+ let measuring = _ViewMeasurements.shared.pending
806
+ let measuringNode: _MountedNode? = measuring.isEmpty ? nil : reconcile(
807
+ measuringRoot, with: _MeasuringView(entries: measuring),
808
+ context: _ReconcileContext(environment: baseEnvironment, path: _ViewPath(root: _MeasuringView.self)), force: true)
809
+ measuringRoot = measuringNode
810
+ renderEverythingPass = false
811
+ presentedSheets = []
812
+ presentedAlerts = []
813
+ barFree.nextPass()
814
+ // Bodies are tracked per node (see `reconcileChildren`); an observable
815
+ // read while producing elements (rare) falls back to a full render.
816
+ let tree = withObservationTracking {
817
+ elements(of: node) + presentedSheets + presentedAlerts + (measuringNode.map { elements(of: $0) } ?? [])
818
+ } onChange: { [weak self] in
819
+ MainActor.assumeIsolated { self?.invalidate() }
820
+ }
821
+ let animation = passAnimation?._json
822
+ passAnimation = nil
823
+ var ops = reconciler.update(with: tree, animation: animation)
824
+ if preferredColorScheme != sentColorScheme {
825
+ // Phase 12: `.preferredColorScheme` reaches the host as a root update.
826
+ sentColorScheme = preferredColorScheme
827
+ ops.insert(.update(id: 0, props: .object(["colorScheme": preferredColorScheme.map { .string($0 == .dark ? "dark" : "light") } ?? .null])), at: 0)
828
+ }
829
+ host.apply(ops)
830
+ rendering = false
831
+ _ViewMeasurements.shared.renderFinished()
832
+
833
+ let disappearing = pendingDisappear
834
+ pendingDisappear.removeAll()
835
+ for action in disappearing { action() }
836
+ let appearing = pendingAppear
837
+ pendingAppear.removeAll()
838
+ for action in appearing { action() }
839
+ renderIfNeeded()
840
+ }
841
+
842
+ /// Re-evaluates the mounted tree against `view`, reusing nodes (and their
843
+ /// state) whose view type is unchanged at the same position or key.
844
+ /// `force`: the parent re-ran, so `view` is a new value and this node's
845
+ /// body must run (and its children are forced in turn), unless the value
846
+ /// and everything else the parent passed are the same as last time
847
+ /// (`canSkip`, Phase 16). Otherwise `view` is the node's own stored view
848
+ /// and the body runs only when the node is dirty; a clean node with no
849
+ /// dirty descendant is returned as is.
850
+ private func reconcile(_ existing: _MountedNode?, with view: any View, context: _ReconcileContext,
851
+ force: Bool) -> _MountedNode {
852
+ var force = force
853
+ if force, let existing, canSkip(existing, for: view, in: context) {
854
+ // The parent re-ran but passed the same view and context: the node
855
+ // is clean, and only its dirty descendants (if any) re-run.
856
+ force = false
857
+ skippedViews += 1
858
+ }
859
+ if let existing, !force, !existing.dirty, !existing.subtreeDirty {
860
+ return existing
861
+ }
862
+ let received = _ReceivedContext(context)
863
+ var context = context
864
+ let node: _MountedNode
865
+ let clean: Bool
866
+ let properties: [Any]?
867
+ let boxes: [any _AnyStateBox]
868
+ let path = context.path
869
+
870
+ if let existing, type(of: existing.view) == type(of: view) {
871
+ node = existing
872
+ clean = !force && !existing.dirty
873
+ if clean {
874
+ // Same view value, same state: no reflection, no body.
875
+ properties = nil
876
+ boxes = existing.boxes
877
+ } else {
878
+ properties = _reflectedProperties(of: view)
879
+ boxes = stateBoxes(in: properties)
880
+ for (old, new) in zip(existing.boxes, boxes) where old !== new {
881
+ new._adopt(old)
882
+ }
883
+ }
884
+ } else {
885
+ node = _MountedNode(view: view)
886
+ clean = false
887
+ properties = _reflectedProperties(of: view)
888
+ boxes = stateBoxes(in: properties)
889
+ restoreState(into: boxes, at: path)
890
+ }
891
+ node.dirty = false
892
+ node.subtreeDirty = false
893
+ // A clean node reached here has a dirty descendant, whose elements are
894
+ // part of its own. Its cache was cleared when the descendant was marked,
895
+ // unless that happened during the last pass (an `onChange` action
896
+ // setting state while its body ran), which cached its elements again
897
+ // after the mark.
898
+ node.cachedElements = nil
899
+ node.view = view
900
+ node.path = path
901
+ node.received = received
902
+ if !clean {
903
+ node.boxes = boxes
904
+ for box in boxes {
905
+ // Weak: a `StateObject`'s publisher keeps this closure for as long
906
+ // as the object lives, which may be longer than the runtime (tests).
907
+ box._onChange = { [weak self, weak node] in
908
+ if let node { self?.invalidate(node) }
909
+ }
910
+ }
911
+ }
912
+
913
+ let traits = _ViewTraits.of(view)
914
+ // Lazy rows (LazyRows.swift): a lazy container's content is its rows
915
+ // through groups, sections and views with a body; any other view is
916
+ // part of one row, whose groups are built with it.
917
+ let lazyRowsHere = context.lazyRows
918
+ if traits.contains(.lazyRows), let container = view as? any _LazyRowsContainer {
919
+ context.lazyRows = container._lazyAxis
920
+ } else if context.lazyRows != nil, !traits.contains(.group), !traits.contains(.section),
921
+ !traits.isDisjoint(with: .bodiless) {
922
+ context.lazyRows = nil
923
+ }
924
+ if traits.contains(.environmentModifying), let modifier = view as? any _EnvironmentModifying {
925
+ modifier._apply(to: &context.environment)
926
+ }
927
+ if traits.contains(.preferredColorScheme), let preferred = view as? any _PreferredColorSchemeModifying, !context.inactiveTab {
928
+ preferredColorScheme = preferred._preferredColorScheme
929
+ }
930
+ if context.environment._scrollTargetLayout, traits.contains(.scrollTargetContainer) {
931
+ // This stack's children are the snap targets; nested stacks are not.
932
+ node.scrollTargetLayout = true
933
+ context.environment._scrollTargetLayout = false
934
+ }
935
+ if traits.contains(.navigationTitle), let title = view as? any _NavigationTitleModifying, let screen = context.screen, !context.inactiveTab {
936
+ if let value = title._title { screen.navTitle = value }
937
+ if let mode = title._displayMode { screen.navDisplayMode = mode }
938
+ }
939
+ if traits.contains(.onDisappearing), let disappear = view as? any _OnDisappearing {
940
+ node.disappearAction = disappear._disappearAction
941
+ }
942
+ if traits.contains(.onAppearing), let appear = view as? any _OnAppearing, !node.appeared {
943
+ node.appeared = true
944
+ if let action = appear._action { pendingAppear.append(action) }
945
+ }
946
+ if traits.contains(.detents), let detents = view as? any _DetentsModifying, let sheet = context.sheet {
947
+ sheet.sheetDetents = detents._detents
948
+ }
949
+ if traits.contains(.animated), let animated = view as? any _AnimatedView {
950
+ if node === existing, animated._changed(since: node.animationValue) {
951
+ node.animationToken += 1
952
+ node.animationTokenPass = renderPass
953
+ }
954
+ node.animationValue = animated._value
955
+ }
956
+ if traits.contains(.destinationRegistering), let registration = view as? any _DestinationRegistering {
957
+ context.navStack?.register(registration)
958
+ }
959
+ if traits.contains(.splitSidebar), let sidebar = view as? _SplitSidebarView {
960
+ context.split = sidebar.split
961
+ }
962
+ if traits.contains(.listSelection), let list = view as? any _ListSelectionProviding {
963
+ if var selection = list._selection {
964
+ selection.identity = _RuntimeKey(node: ObjectIdentifier(node), role: "selection:\(selection.snapshot())")
965
+ context.environment._listSelection = selection
966
+ context.split?.splitSelection = selection
967
+ }
968
+ // A list outside a navigation screen owns its rows' edit mode.
969
+ if context.environment.editMode == nil {
970
+ context.environment.editMode = editModeBinding(for: node)
971
+ }
972
+ }
973
+ if traits.contains(.taskStarting), let starting = view as? any _TaskStarting {
974
+ if !node.taskStarted || node.taskID != starting._taskID {
975
+ node.task?.cancel()
976
+ node.taskStarted = true
977
+ node.taskID = starting._taskID
978
+ let action = starting._taskAction
979
+ node.task = Task { await action() }
980
+ }
981
+ }
982
+
983
+ // Decide how to produce the children first, because navigation views
984
+ // adjust the environment that must be resolved before any body runs.
985
+ var makeChildren: () -> [any View]
986
+ /// A second batch of children, produced after the first was reconciled
987
+ /// (a compact split view decides on its detail screen from the
988
+ /// selection its sidebar registered).
989
+ var makeMoreChildren: (() -> [any View])?
990
+ var keys: [AnyHashable]?
991
+ node.handler = nil
992
+ node.navFlattened = false
993
+
994
+ if traits.contains(.navigationSplit), let split = view as? any _NavigationSplitHost {
995
+ // Descendants fill these in as they are reconciled; a clean pass
996
+ // (only some descendants re-run) keeps what the skipped ones set.
997
+ if !clean { node.splitSelection = nil }
998
+ let sidebar = _SplitSidebarView(content: split._sidebar, split: node)
999
+ if context.environment.horizontalSizeClass == .regular {
1000
+ // Two columns, each its own stack; they are not nested in a
1001
+ // screen even when the split view is, so they are not flattened.
1002
+ context.navStack = nil
1003
+ context.screen = nil
1004
+ let detail = split._detail
1005
+ makeChildren = {
1006
+ let sidebarStack = NavigationStack {
1007
+ _NavigationTitleView(content: AnyView(sidebar), title: nil, displayMode: "large")
1008
+ }
1009
+ let detailStack: any View = detail is any _NavigationStackView
1010
+ ? detail : NavigationStack { AnyView(detail) }
1011
+ return [sidebarStack, detailStack]
1012
+ }
1013
+ } else {
1014
+ // One stack rooted at the sidebar; the detail is pushed while
1015
+ // the sidebar's selection is set, and deeper links push after it.
1016
+ let nav = navContext(for: node, box: split._pushedBox, path: nil, clean: clean)
1017
+ nav.splitNode = node
1018
+ context.navStack = nav
1019
+ makeChildren = { [_NavScreenView(content: sidebar, depth: 0)] }
1020
+ let detail = split._detail
1021
+ makeMoreChildren = {
1022
+ nav.detailShown = node.splitSelection?.get() != nil
1023
+ let base = nav.detailShown ? 1 : 0
1024
+ let detailScreens = nav.detailShown ? [_NavScreenView(content: detail, depth: 1)] : []
1025
+ let pushedScreens = nav.box.value.enumerated().map { index, pushed in
1026
+ _NavScreenView(content: pushed.view, depth: base + index + 1)
1027
+ }
1028
+ return detailScreens + pushedScreens
1029
+ }
1030
+ }
1031
+ } else if traits.contains(.navigationStack), let stack = view as? any _NavigationStackView, context.screen != nil, context.navStack != nil {
1032
+ // Nested in another stack's screen: the root renders in place, its
1033
+ // destinations and links use the enclosing stack, its path is ignored.
1034
+ node.navFlattened = true
1035
+ makeChildren = { [stack._root] }
1036
+ } else if traits.contains(.navigationStack), let stack = view as? any _NavigationStackView {
1037
+ let nav = navContext(for: node, box: stack._pushedBox, path: stack._path, clean: clean)
1038
+ context.navStack = nav
1039
+ makeChildren = {
1040
+ let pathScreens = (nav.path?.get() ?? []).enumerated().map { index, value in
1041
+ _NavScreenView(content: _PathScreenValue(value: value), depth: index + 1)
1042
+ }
1043
+ let pushedScreens = nav.box.value.enumerated().map { index, pushed in
1044
+ _NavScreenView(content: pushed.view, depth: pathScreens.count + index + 1)
1045
+ }
1046
+ return [_NavScreenView(content: stack._root, depth: 0)] + pathScreens + pushedScreens
1047
+ }
1048
+ } else if traits.contains(.navScreen), let screen = view as? _NavScreenView {
1049
+ context.screen = node
1050
+ // A screen inside an unselected tab still owns its own title.
1051
+ context.inactiveTab = false
1052
+ if !clean {
1053
+ node.navTitle = nil
1054
+ node.navDisplayMode = nil
1055
+ }
1056
+ node.depth = screen.depth
1057
+ if screen.depth > 0, let nav = context.navStack {
1058
+ let depth = screen.depth
1059
+ node.handler = { _ in nav.pop(to: depth) }
1060
+ context.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "pop\(depth)")) {
1061
+ nav.pop(to: depth)
1062
+ }
1063
+ }
1064
+ // Each screen owns the edit mode of its content (`EditButton`,
1065
+ // `List.editing`), as on iOS.
1066
+ context.environment.editMode = editModeBinding(for: node)
1067
+ let nav = context.navStack
1068
+ makeChildren = {
1069
+ // Path screens resolve their value through the stack's
1070
+ // registrations, which the root screen filled in just before.
1071
+ if let pathValue = screen.content as? _PathScreenValue, let nav {
1072
+ return [nav.destination(for: pathValue.value)]
1073
+ }
1074
+ return [screen.content]
1075
+ }
1076
+ } else if traits.contains(.navigationLink), let link = view as? any _NavigationLinkView {
1077
+ let destination = link._destination
1078
+ let value = traits.contains(.navigationValueLink) ? (view as? any _NavigationValueLinkView)?._value : nil
1079
+ let nav = context.navStack
1080
+ // In a split view's sidebar a value link selects the row of a
1081
+ // `List(selection:)` instead of pushing.
1082
+ let selection = context.environment._splitSidebar ? context.environment._listSelection : nil
1083
+ node.handler = { _ in
1084
+ if let value, let selection {
1085
+ selection.set(value)
1086
+ } else if let value {
1087
+ nav?.push(value: value)
1088
+ } else {
1089
+ nav?.push(destination)
1090
+ }
1091
+ }
1092
+ makeChildren = { (view as? any _HostView)?._content ?? [] }
1093
+ } else if traits.contains(.tabView), let tabs = view as? any _TabViewHost {
1094
+ let roots = _flattenChildren(tabs._tabContent)
1095
+ node.optionTags = roots.map { _tag(of: $0) }
1096
+ node.selectedIndex = tabs._selectedIndex(tags: node.optionTags)
1097
+ let tags = node.optionTags
1098
+ node.handler = { payload in
1099
+ if case .number(let index) = payload { tabs._select(index: Int(index), tags: tags) }
1100
+ }
1101
+ let selected = node.selectedIndex
1102
+ if _tabViewIsSidebar(context.environment) {
1103
+ // Regular-width `.sidebarAdaptable`: a split view whose sidebar
1104
+ // lists the tabs (Tabs.swift); the tab view adds no element.
1105
+ node.navFlattened = true
1106
+ makeChildren = { [_tabSidebar(roots: roots, selected: selected) { tabs._select(index: $0, tags: tags) }] }
1107
+ } else {
1108
+ makeChildren = {
1109
+ roots.enumerated().map { index, root in
1110
+ _TabScreenView(root: root, isSelected: index == selected)
1111
+ }
1112
+ }
1113
+ }
1114
+ } else if traits.contains(.tabScreen), let tab = view as? _TabScreenView {
1115
+ if !tab.isSelected { context.inactiveTab = true }
1116
+ makeChildren = { [tab.content] }
1117
+ } else if traits.contains(.picker), let picker = view as? any _PickerHost {
1118
+ let options = _flattenPickerOptions(picker._options)
1119
+ node.optionTags = options.map { $0.tag }
1120
+ node.selectedIndex = picker._selectedIndex(tags: node.optionTags)
1121
+ node.pickerOptions = options.map { _textContent(of: $0.view) }
1122
+ node.pickerLabel = _textContent(of: picker._labelView)
1123
+ let tags = node.optionTags
1124
+ node.handler = { payload in
1125
+ if case .number(let index) = payload { picker._select(index: Int(index), tags: tags) }
1126
+ }
1127
+ makeChildren = { [] }
1128
+ } else if traits.contains(.geometryReading), let reader = view as? any _GeometryReading {
1129
+ let size = node.geometrySize
1130
+ node.handler = { [unowned self] payload in
1131
+ guard case .object(let fields) = payload,
1132
+ case .number(let width)? = fields["width"],
1133
+ case .number(let height)? = fields["height"] else { return }
1134
+ let reported = CGSize(width: width, height: height)
1135
+ if reported != node.geometrySize {
1136
+ node.geometrySize = reported
1137
+ self.invalidate(node)
1138
+ }
1139
+ }
1140
+ makeChildren = { [reader._content(for: size)] }
1141
+ } else if traits.contains(.sheet), let sheet = view as? any _SheetPresenting {
1142
+ // The base is always a child; the sheet content is a second child
1143
+ // while presented, so its state survives re-renders.
1144
+ let isPresented = sheet._isPresented
1145
+ let onDismiss = sheet._onDismiss
1146
+ if !clean { node.sheetDetents = nil }
1147
+ node.handler = { _ in
1148
+ isPresented.wrappedValue = false
1149
+ onDismiss?()
1150
+ }
1151
+ // Read in the tracked closure: a binding into an `@Observable`
1152
+ // model presents when the model changes.
1153
+ makeChildren = { isPresented.wrappedValue ? [sheet._base, sheet._sheetContent] : [sheet._base] }
1154
+ } else if traits.contains(.alert), let alert = view as? any _AlertPresenting {
1155
+ // Like a sheet: the actions are a second child while presented. A
1156
+ // tap on the alert itself dismisses without running an action.
1157
+ let isPresented = alert._isPresented
1158
+ node.handler = { _ in isPresented.wrappedValue = false }
1159
+ makeChildren = { isPresented.wrappedValue ? [alert._base, alert._alertActions] : [alert._base] }
1160
+ } else if traits.contains(.preferenceDecorating), let decorating = view as? any _PreferenceValueDecorating {
1161
+ // The decoration is made from the base's values, so the base
1162
+ // reconciles first (and the decoration is made again every pass).
1163
+ makeChildren = { [decorating._base] }
1164
+ makeMoreChildren = { [unowned node] in
1165
+ [decorating._decoration(from: node.children.first?.preferences ?? _PreferenceValues())]
1166
+ }
1167
+ } else if traits.contains(.group), let group = view as? any _GroupView {
1168
+ keys = group._keys
1169
+ makeChildren = { group._children }
1170
+ if let axis = lazyRowsHere, let allKeys = keys, allKeys.count > node.lazyLimit,
1171
+ let requests = context.environment._scrollReader?.requests {
1172
+ // `proxy.scrollTo(id)` where the rows carry their own ids
1173
+ // (`.id(item.id)`): the rows up to the target are built first.
1174
+ for request in requests {
1175
+ if let index = allKeys.firstIndex(of: request.target), index >= node.lazyLimit {
1176
+ node.lazyLimit = index + _LazyRows.batch
1177
+ }
1178
+ }
1179
+ }
1180
+ if let axis = lazyRowsHere, let allKeys = keys, allKeys.count > node.lazyLimit {
1181
+ // The first rows, and a spacer for the rest until the page
1182
+ // sees it near the visible area.
1183
+ let limit = node.lazyLimit
1184
+ keys = Array(allKeys.prefix(limit)) + [AnyHashable(_LazySpacerKey(built: limit))]
1185
+ let spacer = _LazyRowsSpacer(axis: axis, remaining: allKeys.count - limit) { [weak self, weak node] in
1186
+ guard let self, let node, node.lazyLimit == limit else { return }
1187
+ node.lazyLimit = limit + _LazyRows.batch
1188
+ self.invalidate(node)
1189
+ }
1190
+ makeChildren = { Array(group._children.prefix(limit)) + [spacer] }
1191
+ }
1192
+ } else if traits.contains(.lazyMenu), let menu = view as? any _LazyMenuHost {
1193
+ // Lazy menus (LazyRows.swift): the items only while open.
1194
+ node.handler = { [weak self, weak node] payload in
1195
+ guard let self, let node, case .object(let fields) = payload,
1196
+ case .bool(let open)? = fields["open"], node.menuOpen != open else { return }
1197
+ node.menuOpen = open
1198
+ self.invalidate(node)
1199
+ }
1200
+ let open = isHeadless || node.menuOpen
1201
+ makeChildren = { open ? [menu._menuBase, menu._menuItems] : [menu._menuBase] }
1202
+ } else if traits.contains(.host), let hostView = view as? any _HostView {
1203
+ if let dependent = view as? any _EnvironmentContent {
1204
+ // A button under a custom style (ButtonStyles.swift).
1205
+ let environment = context.environment
1206
+ makeChildren = { dependent._content(in: environment) }
1207
+ } else {
1208
+ makeChildren = { hostView._content }
1209
+ }
1210
+ } else {
1211
+ makeChildren = { [self.body(of: view)] }
1212
+ }
1213
+
1214
+ if node.handler == nil, traits.contains(.refreshing), let refreshing = view as? any _Refreshing {
1215
+ node.handler = refreshing._refresh(in: context.environment).handler
1216
+ }
1217
+ if node.handler == nil, traits.contains(.environmentHandling), let handling = view as? any _EnvironmentHandling {
1218
+ node.handler = handling._handler(in: context.environment)
1219
+ }
1220
+ if traits.contains(.sizeReporting)
1221
+ || (traits.contains(.pathDrawing) && (view as? any _PathDrawing)?._drawsPath == true) {
1222
+ // The element reports its laid-out size, like a GeometryReader's;
1223
+ // an anchor preference resolves against it, a custom shape draws
1224
+ // its path for it.
1225
+ node.handler = { [unowned self] payload in
1226
+ guard case .object(let fields) = payload,
1227
+ case .number(let width)? = fields["width"],
1228
+ case .number(let height)? = fields["height"] else { return }
1229
+ let reported = CGSize(width: width, height: height)
1230
+ if reported != node.geometrySize {
1231
+ node.geometrySize = reported
1232
+ self.invalidate(node)
1233
+ }
1234
+ }
1235
+ }
1236
+
1237
+ node.environment = context.environment
1238
+ let decorating = traits.contains(.preferenceDecorating)
1239
+ if clean && !decorating {
1240
+ // The body's output is the children already mounted (the view
1241
+ // value is unchanged); only the dirty ones among them re-run.
1242
+ let kept = node.children
1243
+ makeChildren = { kept.map(\.view) }
1244
+ makeMoreChildren = nil
1245
+ keys = kept.allSatisfy { $0.key != nil } && !kept.isEmpty ? kept.map { $0.key! } : nil
1246
+ } else {
1247
+ resolveEnvironment(in: properties, with: context.environment, owner: node)
1248
+ }
1249
+ var phases = [makeChildren]
1250
+ if let makeMoreChildren { phases.append(makeMoreChildren) }
1251
+ reconcileChildren(of: node, phases: phases, keys: keys, context: context, force: !clean, forceLaterPhases: decorating)
1252
+
1253
+ // Preferences flow up: the children's values in order, then this view's own.
1254
+ var preferences = _PreferenceValues()
1255
+ for child in node.children where !child.preferences.isEmpty { preferences.merge(child.preferences) }
1256
+ if traits.contains(.preferenceWriting), let writer = view as? any _PreferenceWriting {
1257
+ writer._write(&preferences, measured: node.geometrySize)
1258
+ }
1259
+ node.preferences = preferences
1260
+ if traits.contains(.preferenceReading), let reader = view as? any _PreferenceReading {
1261
+ let (seen, action) = reader._read(preferences, last: node.preferenceSeen)
1262
+ node.preferenceSeen = seen
1263
+ // After the pass, like onAppear: the action usually sets state.
1264
+ if let action { pendingAppear.append(action) }
1265
+ }
1266
+ node.writesContext = !traits.isDisjoint(with: .contextWriting) || node.children.contains { $0.writesContext }
1267
+ return node
1268
+ }
1269
+
1270
+ /// Whether a node whose parent re-ran can be treated as clean: it is not
1271
+ /// dirty itself, nothing in its subtree writes to its ancestors, and the
1272
+ /// parent passed the same view value and the same context as last time.
1273
+ private func canSkip(_ existing: _MountedNode, for view: any View, in context: _ReconcileContext) -> Bool {
1274
+ guard !renderEverythingPass, !existing.dirty, !existing.writesContext, let received = existing.received,
1275
+ type(of: existing.view) == type(of: view) else { return false }
1276
+ let key = ObjectIdentifier(type(of: view))
1277
+ if incomparableTypes.contains(key) { return false }
1278
+ // Only views with a body of their own are compared. The shim's
1279
+ // containers and primitives (stacks, modifiers, text) are cheap to
1280
+ // re-run, and their values hold their whole content, so comparing
1281
+ // them would walk the subtree once per level; the views with bodies
1282
+ // inside them are compared on their own.
1283
+ if !_ViewTraits.of(view).isDisjoint(with: .bodiless) { return false }
1284
+ guard _Sameness.sameView(existing.view, view) else {
1285
+ // A type whose values never compare equal (it stores a closure or
1286
+ // a binding) is not compared again.
1287
+ if !_Sameness.canEverBeSame(view) { incomparableTypes.insert(key) }
1288
+ return false
1289
+ }
1290
+ return received.matches(context)
1291
+ }
1292
+
1293
+ /// See `_hoistBarItems`.
1294
+ private var barFree = _BarFreeTokens()
1295
+ /// View types whose values hold a closure or binding: never the same.
1296
+ private var incomparableTypes: Set<ObjectIdentifier> = []
1297
+ /// Forced nodes treated as clean because their view was unchanged (for tests).
1298
+ private(set) var skippedViews = 0
1299
+
1300
+ /// The stack context of a `NavigationStack` / split node, created once and
1301
+ /// pointed at the current view's storage on every pass that re-ran the body.
1302
+ private func navContext(for node: _MountedNode, box: _StateBox<[_PushedScreen]>, path: _PathBinding?,
1303
+ clean: Bool) -> _NavContext {
1304
+ if let nav = node.navContext {
1305
+ if !clean {
1306
+ nav.box = box
1307
+ nav.path = path
1308
+ }
1309
+ return nav
1310
+ }
1311
+ let nav = _NavContext(box: box, path: path, node: node)
1312
+ node.navContext = nav
1313
+ return nav
1314
+ }
1315
+
1316
+ /// Reconciles the node's children phase by phase: each phase's views are
1317
+ /// produced only after the previous phase's children were reconciled.
1318
+ /// Keys apply to the first phase's views.
1319
+ /// `forceLaterPhases`: the views of phases after the first are new values
1320
+ /// even on a clean pass (a preference decoration made from the first).
1321
+ private func reconcileChildren(of node: _MountedNode, phases: [() -> [any View]],
1322
+ keys: [AnyHashable]?, context: _ReconcileContext, force: Bool,
1323
+ forceLaterPhases: Bool = false) {
1324
+ let old = node.children
1325
+ var oldByKey: [AnyHashable: _MountedNode] = [:]
1326
+ for child in old {
1327
+ if let key = child.key { oldByKey[key] = child }
1328
+ }
1329
+ defer {
1330
+ let kept = Set(node.children.map(ObjectIdentifier.init))
1331
+ for dropped in old where !kept.contains(ObjectIdentifier(dropped)) {
1332
+ unmount(dropped)
1333
+ }
1334
+ }
1335
+
1336
+ var children: [_MountedNode] = []
1337
+ for (phase, makeViews) in phases.enumerated() {
1338
+ let force = force || (forceLaterPhases && phase > 0)
1339
+ // A later phase may read what the earlier ones mounted.
1340
+ if phase > 0 { node.children = children }
1341
+ // The body (and a `ForEach`'s row closures) run under observation
1342
+ // tracking: an `@Observable` property read here marks this node
1343
+ // dirty when it changes, and nothing else re-renders.
1344
+ let views: [any View] = force
1345
+ ? withObservationTracking(makeViews) { [weak self, weak node] in
1346
+ MainActor.assumeIsolated { if let node { self?.invalidate(node) } }
1347
+ }
1348
+ : makeViews()
1349
+ for childView in views {
1350
+ let index = children.count
1351
+ let key = phase == 0 ? keys?[index] : nil
1352
+ let existing: _MountedNode?
1353
+ if let key {
1354
+ existing = oldByKey.removeValue(forKey: key)
1355
+ } else if index < old.count, old[index].key == nil {
1356
+ existing = old[index]
1357
+ } else {
1358
+ existing = nil
1359
+ }
1360
+ var childContext = context
1361
+ childContext.path = context.path.child(key.map(_ViewPath.Step.key) ?? .index(index), type(of: childView))
1362
+ if index == 1, let sheet = node.view as? any _SheetPresenting {
1363
+ let isPresented = sheet._isPresented
1364
+ let onDismiss = sheet._onDismiss
1365
+ childContext.sheet = node
1366
+ childContext.navStack = nil
1367
+ childContext.screen = nil
1368
+ childContext.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "sheet")) {
1369
+ isPresented.wrappedValue = false
1370
+ onDismiss?()
1371
+ }
1372
+ }
1373
+ if index == 1, let alert = node.view as? any _AlertPresenting {
1374
+ let isPresented = alert._isPresented
1375
+ childContext.environment.dismiss = DismissAction(identity: _RuntimeKey(node: ObjectIdentifier(node), role: "alert")) {
1376
+ isPresented.wrappedValue = false
1377
+ }
1378
+ }
1379
+ let child = reconcile(existing, with: childView, context: childContext, force: force)
1380
+ child.key = key
1381
+ child.parent = node
1382
+ children.append(child)
1383
+ }
1384
+ }
1385
+ node.children = children
1386
+ }
1387
+
1388
+ /// The `editMode` environment binding a `navscreen` or `List` node owns.
1389
+ private func editModeBinding(for node: _MountedNode) -> Binding<EditMode> {
1390
+ Binding(
1391
+ // The mode is part of the identity: the binding reads the node's
1392
+ // live mode, so the environment of a pass must record which mode
1393
+ // it carried for a toggle to re-run the views below.
1394
+ identity: _RuntimeKey(node: ObjectIdentifier(node), role: "editMode:\(node.editMode)"),
1395
+ get: { [weak node] in MainActor.assumeIsolated { node?.editMode ?? .inactive } },
1396
+ set: { [weak self, weak node] mode in
1397
+ MainActor.assumeIsolated {
1398
+ guard let node, node.editMode != mode else { return }
1399
+ node.editMode = mode
1400
+ self?.invalidate(node)
1401
+ }
1402
+ }
1403
+ )
1404
+ }
1405
+
1406
+ /// Cancels tasks in a subtree that left the tree, and queues its
1407
+ /// `.onDisappear` actions (innermost first, as SwiftUI calls them).
1408
+ private func unmount(_ node: _MountedNode) {
1409
+ node.task?.cancel()
1410
+ node.task = nil
1411
+ node.children.forEach(unmount)
1412
+ if let action = node.disappearAction {
1413
+ node.disappearAction = nil
1414
+ pendingDisappear.append(action)
1415
+ }
1416
+ }
1417
+
1418
+ private func body(of view: any View) -> any View {
1419
+ func open<V: View>(_ view: V) -> any View { view.body }
1420
+ return open(view)
1421
+ }
1422
+
1423
+ /// The property-wrapper values of a view that the runtime acts on
1424
+ /// (`@State`, `@StateObject`, `@ObservedObject`, `@Environment`,
1425
+ /// `@EnvironmentObject`), or `nil` when its type declares none.
1426
+ ///
1427
+ /// Reflection is the most expensive step of a render, and most views in a
1428
+ /// tree are the shim's own modifiers and primitives, which declare no
1429
+ /// such properties. The first instance of each type is reflected and
1430
+ /// each stored property checked (`_RuntimeProperty`); the wrappers are
1431
+ /// concrete struct types, so which properties are wrappers is a property
1432
+ /// of the type, not of the instance. A struct type's answer is kept as a
1433
+ /// plan: none (it is never reflected again), or the positions to read,
1434
+ /// so later passes reflect it without checking each property again.
1435
+ private func _reflectedProperties(of view: any View) -> [Any]? {
1436
+ // `ModifiedContent` stores its `ViewModifier`, whose own `@State` and
1437
+ // `@Environment` properties are managed like the view's.
1438
+ // A custom `ButtonStyle` (ButtonStyles.swift) too.
1439
+ let values = runtimeProperties(of: view, lookInsideModifiers: _ViewTraits.of(view).contains(.holdsModifier))
1440
+ return values.isEmpty ? nil : values
1441
+ }
1442
+
1443
+ /// Which stored properties of a struct type the runtime reads: the
1444
+ /// wrappers', then (for `_HoldsModifier` views) the stored modifiers or
1445
+ /// styles whose own wrappers it reads, in that order.
1446
+ private struct PropertyPlan {
1447
+ var wrappers: [Int]
1448
+ var modifiers: [Int]
1449
+ static let none = PropertyPlan(wrappers: [], modifiers: [])
1450
+ var isNone: Bool { wrappers.isEmpty && modifiers.isEmpty }
1451
+ }
1452
+
1453
+ /// Per struct type: its plan. Per other type: `.none` when it declares no wrappers.
1454
+ private var propertyPlans: [ObjectIdentifier: PropertyPlan] = [:]
1455
+
1456
+ private func runtimeProperties(of value: Any, lookInsideModifiers: Bool) -> [Any] {
1457
+ let type = type(of: value)
1458
+ let key = ObjectIdentifier(type)
1459
+ let plan = propertyPlans[key]
1460
+ if let plan, plan.isNone { return [] }
1461
+ let mirrored = Mirror(reflecting: value).children
1462
+ if let plan {
1463
+ // A struct's children are random access: only the planned ones are read (and boxed).
1464
+ func child(_ offset: Int) -> Any { mirrored[mirrored.index(mirrored.startIndex, offsetBy: offset)].value }
1465
+ var values = plan.wrappers.map(child)
1466
+ for offset in plan.modifiers { values += runtimeProperties(of: child(offset), lookInsideModifiers: false) }
1467
+ return values
1468
+ }
1469
+ let children = Array(mirrored)
1470
+ var wrappers: [Int] = []
1471
+ var modifiers: [Int] = []
1472
+ var values: [Any] = []
1473
+ var inner: [Any] = []
1474
+ for (index, child) in children.enumerated() {
1475
+ // One lookup per property type (`_RuntimeProperty`, ViewTraits.swift).
1476
+ if child.value is any _RuntimeProperty {
1477
+ wrappers.append(index)
1478
+ values.append(child.value)
1479
+ } else if lookInsideModifiers, child.value is any ViewModifier || child.value is any ButtonStyle {
1480
+ let found = runtimeProperties(of: child.value, lookInsideModifiers: false)
1481
+ if !found.isEmpty { modifiers.append(index) }
1482
+ inner += found
1483
+ }
1484
+ }
1485
+ values += inner
1486
+ if values.isEmpty {
1487
+ propertyPlans[key] = PropertyPlan.none
1488
+ } else if _ViewTraits.isStruct(type) {
1489
+ // Not an enum's: its children are its case's payload, which differ by case.
1490
+ propertyPlans[key] = PropertyPlan(wrappers: wrappers, modifiers: modifiers)
1491
+ }
1492
+ return values
1493
+ }
1494
+
1495
+ private func stateBoxes(in properties: [Any]?) -> [any _AnyStateBox] {
1496
+ guard let properties else { return [] }
1497
+ return properties.compactMap { ($0 as? any _StateProperty)?._box }
1498
+ }
1499
+
1500
+ /// Resolves `@Environment`/`@EnvironmentObject` properties and subscribes
1501
+ /// `@ObservedObject`/`@EnvironmentObject` properties so that their
1502
+ /// object's `objectWillChange` invalidates this runtime. Resolution runs
1503
+ /// first because an environment object is observed through its resolved value.
1504
+ private func resolveEnvironment(in properties: [Any]?, with environment: EnvironmentValues, owner node: _MountedNode) {
1505
+ guard let properties else { return }
1506
+ for value in properties {
1507
+ (value as? any _EnvironmentProperty)?._resolve(in: environment)
1508
+ (value as? any _EnvironmentReading)?._resolve(in: environment)
1509
+ (value as? any _ObservingProperty)?._observe(owner: node) { [weak self, weak node] in
1510
+ if let node { self?.invalidate(node) }
1511
+ }
1512
+ }
1513
+ }
1514
+
1515
+ // MARK: Elements
1516
+
1517
+ private func elements(of node: _MountedNode) -> [_Element] {
1518
+ if let cached = node.cachedElements {
1519
+ presentedSheets += node.cachedSheets
1520
+ presentedAlerts += node.cachedAlerts
1521
+ return cached
1522
+ }
1523
+ let sheetsBefore = presentedSheets.count
1524
+ let alertsBefore = presentedAlerts.count
1525
+ let produced = produceElements(of: node)
1526
+ node.cachedElements = produced
1527
+ node.cachedSheets = Array(presentedSheets[sheetsBefore...])
1528
+ node.cachedAlerts = Array(presentedAlerts[alertsBefore...])
1529
+ return produced
1530
+ }
1531
+
1532
+ private func produceElements(of node: _MountedNode) -> [_Element] {
1533
+ var produced: [_Element]
1534
+ let traits = _ViewTraits.of(node.view)
1535
+ if traits.contains(.navScreen) {
1536
+ // `.toolbar` / `.searchable` emit `toolbar` and `searchfield`
1537
+ // elements wherever they were applied; the renderer wants them as
1538
+ // the screen's own children, after the content.
1539
+ var bar: [_Element] = []
1540
+ let content = node.children.flatMap(elements(of:)).map { _hoistBarItems(from: $0, into: &bar, barFree: &barFree) }
1541
+ produced = [_Element(
1542
+ kind: "navscreen",
1543
+ props: .object([
1544
+ "title": .string(node.navTitle ?? ""),
1545
+ "displayMode": .string(node.navDisplayMode ?? "automatic"),
1546
+ "depth": .number(Double(node.depth)),
1547
+ ]),
1548
+ children: content + bar,
1549
+ handler: node.handler
1550
+ )]
1551
+ } else if traits.contains(.sheet), let sheet = node.view as? any _SheetPresenting {
1552
+ // Base inline; the presented content becomes a root-level `sheet` element.
1553
+ produced = node.children.first.map(elements(of:)) ?? []
1554
+ if node.children.count > 1 {
1555
+ let detents = sheet._fullScreen ? ["full"] : (node.sheetDetents ?? ["large"])
1556
+ presentedSheets.append(_Element(
1557
+ kind: "sheet",
1558
+ props: .object(["detents": .array(detents.map(_JSON.string))]),
1559
+ children: elements(of: node.children[1]),
1560
+ handler: node.handler,
1561
+ key: AnyHashable(_SheetKey(path: node.path?.text ?? ""))
1562
+ ))
1563
+ }
1564
+ } else if traits.contains(.alert), let alert = node.view as? any _AlertPresenting {
1565
+ // Base inline; the actions become a root-level `alert` element whose
1566
+ // buttons also dismiss when tapped.
1567
+ produced = node.children.first.map(elements(of:)) ?? []
1568
+ if node.children.count > 1 {
1569
+ let isPresented = alert._isPresented
1570
+ let buttons = elements(of: node.children[1]).map { _dismissing($0, with: isPresented) }
1571
+ presentedAlerts.append(_Element(
1572
+ kind: "alert",
1573
+ props: .object([
1574
+ "title": .string(alert._alertTitle),
1575
+ "message": alert._alertMessage.map(_JSON.string) ?? .null,
1576
+ "style": .string(alert._alertStyle),
1577
+ ]),
1578
+ children: buttons,
1579
+ handler: node.handler,
1580
+ key: AnyHashable(_AlertKey(path: node.path?.text ?? ""))
1581
+ ))
1582
+ }
1583
+ } else if traits.contains(.navigationSplit) {
1584
+ // Regular: `navsplit` over the two column stacks. Compact: one
1585
+ // `navstack` whose screens are the sidebar, the detail and what
1586
+ // the detail pushed.
1587
+ let regular = node.environment.horizontalSizeClass == .regular
1588
+ produced = [_Element(
1589
+ kind: regular ? "navsplit" : "navstack",
1590
+ props: .object([:]),
1591
+ children: node.children.flatMap(elements(of:)),
1592
+ handler: nil
1593
+ )]
1594
+ } else if traits.contains(.navigationStack), node.navFlattened {
1595
+ produced = node.children.flatMap(elements(of:))
1596
+ } else if traits.contains(.tabView), node.navFlattened {
1597
+ produced = node.children.flatMap(elements(of:))
1598
+ } else if traits.contains(.tabView) {
1599
+ produced = [_Element(
1600
+ kind: "tabview",
1601
+ props: .object(["selected": .number(Double(node.selectedIndex))]),
1602
+ children: node.children.flatMap(elements(of:)),
1603
+ handler: node.handler
1604
+ )]
1605
+ } else if traits.contains(.picker) {
1606
+ produced = [_Element(
1607
+ kind: "picker",
1608
+ props: .object([
1609
+ "style": .string(node.environment._pickerStyle),
1610
+ "label": .string(node.pickerLabel),
1611
+ "options": .array(node.pickerOptions.map(_JSON.string)),
1612
+ "selected": .number(Double(node.selectedIndex)),
1613
+ ]),
1614
+ children: [],
1615
+ handler: node.handler
1616
+ )]
1617
+ } else if traits.contains(.animated), let animated = node.view as? any _AnimatedView {
1618
+ var style: [String: _JSON] = ["animationToken": .number(Double(node.animationToken))]
1619
+ if let animation = animated._animation { style["animation"] = animation._json }
1620
+ produced = [_Element(
1621
+ kind: "styled",
1622
+ props: .object(["style": .object(style)]),
1623
+ children: node.children.flatMap(elements(of:)),
1624
+ handler: nil
1625
+ )]
1626
+ } else if traits.contains(.host), !traits.contains(.group), let hostView = node.view as? any _HostView {
1627
+ let kind = hostView._kind
1628
+ var children = childElements(of: node)
1629
+ if kind == "list" { children = wrapListRows(children, in: node) }
1630
+ var props = hostView._props(in: node.environment)
1631
+ if traits.contains(.pathDrawing), let drawing = node.view as? any _PathDrawing, drawing._drawsPath {
1632
+ let state = node.pathState ?? _PathState()
1633
+ node.pathState = state
1634
+ props = drawing._pathProps(props, size: node.geometrySize, state: state, animated: passAnimates(node))
1635
+ }
1636
+ if traits.contains(.lazyMenu), !isHeadless, case .object(var fields) = props {
1637
+ fields["deferred"] = .bool(!node.menuOpen)
1638
+ props = .object(fields)
1639
+ }
1640
+ if kind == "image", let image = hostView as? Image { image._sendBitmapSource(through: self) }
1641
+ if kind == "text", let rich = hostView as? _RichText { rich._sendBitmapSources(through: self) }
1642
+ if node.scrollTargetLayout, case .object(var fields) = props {
1643
+ // Phase 12: this stack's children are the scroll view's snap targets.
1644
+ fields["scrollTarget"] = .bool(true)
1645
+ props = .object(fields)
1646
+ }
1647
+ // A custom layout reads its children's `.layoutValue`s (CustomLayout.swift).
1648
+ if kind == "layout", let receiving = node.view as? any _LayoutValuesReceiving {
1649
+ receiving._receive(childLayoutValues: children.map { $0.layoutValues ?? [:] })
1650
+ }
1651
+ var element = _Element(
1652
+ kind: kind,
1653
+ props: props,
1654
+ children: children,
1655
+ handler: node.handler ?? hostView._handler
1656
+ )
1657
+ if traits.contains(.navigationValueLink), let link = node.view as? any _NavigationValueLinkView, let value = link._value {
1658
+ element.row = _RowMark(linkValue: value, linkHandler: element.handler)
1659
+ }
1660
+ // Wrappers that leave the row's content as their (first) child pass
1661
+ // its row mark up, so `Text("x").tag(1).padding()` stays selectable.
1662
+ let transparent = kind == "styled" || kind == "gesture" || kind == "tapgesture"
1663
+ ? children.count == 1 : kind == "swipeactions" || kind == "contextmenu"
1664
+ if transparent, let inner = children.first?.row {
1665
+ element.row = (element.row ?? _RowMark()).merged(with: inner)
1666
+ }
1667
+ // Modifiers outside a `.layoutValue` (`.zIndex`, `.offset`, an
1668
+ // overlay) leave the value on the view the layout sees.
1669
+ if kind == "styled" || kind == "gesture" || kind == "tapgesture" || kind == "decorated"
1670
+ || kind == "contextmenu" || kind == "swipeactions", let inner = children.first?.layoutValues {
1671
+ element.layoutValues = inner
1672
+ }
1673
+ produced = [element]
1674
+ } else {
1675
+ produced = childElements(of: node)
1676
+ }
1677
+
1678
+ if traits.contains(.tagProviding), let tagged = node.view as? any _TagProviding {
1679
+ let tag = tagged._tag
1680
+ for index in produced.indices where produced[index].row?.tag == nil {
1681
+ produced[index].row = (produced[index].row ?? _RowMark()).merged(with: _RowMark(tag: tag))
1682
+ }
1683
+ }
1684
+ if traits.contains(.rowEditingDisabling), let disabling = node.view as? any _RowEditingDisabling, disabling._deleteDisabled {
1685
+ for index in produced.indices {
1686
+ produced[index].row = (produced[index].row ?? _RowMark()).merged(with: _RowMark(deleteDisabled: true))
1687
+ }
1688
+ }
1689
+ if traits.contains(.layoutValueProviding), let providing = node.view as? any _LayoutValueProviding {
1690
+ // The value closest to the view wins: an inner one is already set.
1691
+ let (key, value) = providing._layoutValue
1692
+ for index in produced.indices where produced[index].layoutValues?[key] == nil {
1693
+ produced[index].layoutValues = (produced[index].layoutValues ?? [:]).merging([key: value]) { inner, _ in inner }
1694
+ }
1695
+ }
1696
+ if traits.contains(.accessibilityProviding), let providing = node.view as? any _AccessibilityProviding {
1697
+ let mark = providing._accessibility
1698
+ for index in produced.indices {
1699
+ produced[index].accessibility = (produced[index].accessibility ?? _AccessibilityMark()).merged(with: mark)
1700
+ }
1701
+ }
1702
+ if traits.contains(.tabScreen), let tab = node.view as? _TabScreenView, !tab.isSelected {
1703
+ // An unselected tab's toolbars do not reach the enclosing screen.
1704
+ var dropped: [_Element] = []
1705
+ produced = produced.map { _hoistBarItems(from: $0, into: &dropped, barFree: &barFree) }
1706
+ }
1707
+
1708
+ if let key = node.key {
1709
+ for index in produced.indices where produced[index].key == nil {
1710
+ produced[index].key = AnyHashable(_ElementKey(base: key, index: index))
1711
+ }
1712
+ }
1713
+ return produced
1714
+ }
1715
+
1716
+ // MARK: State snapshot (hot reload)
1717
+
1718
+ /// The elements of a node's children, in order. The elements a keyed
1719
+ /// group (`ForEach`) produces carry its ids as keys; two keyed groups
1720
+ /// under the same parent may use the same ids (`ForEach(0..<n)` twice in
1721
+ /// a `ZStack`), so each group's keys are qualified by its position among
1722
+ /// the siblings. Without that the element diff matched the two groups'
1723
+ /// elements to each other on alternate passes, swapping their props.
1724
+ private func childElements(of node: _MountedNode) -> [_Element] {
1725
+ let delete = _ViewTraits.of(node.view).contains(.deletableGroup) ? (node.view as? any _DeletableGroup)?._delete : nil
1726
+ return node.children.enumerated().flatMap { position, child -> [_Element] in
1727
+ var items = elements(of: child)
1728
+ // A `ForEach` id is the row's implicit tag; an `onDelete` group's
1729
+ // child at `position` is the row at that offset of its data.
1730
+ if let key = child.key {
1731
+ for index in items.indices where items[index].row?.tag == nil {
1732
+ items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(tag: key))
1733
+ }
1734
+ }
1735
+ if let delete {
1736
+ for index in items.indices where items[index].row?.delete == nil {
1737
+ items[index].row = (items[index].row ?? _RowMark()).merged(with: _RowMark(delete: { delete(position) }))
1738
+ }
1739
+ }
1740
+ if child.key == nil, _ViewTraits.of(child.view).contains(.group), let group = child.view as? any _GroupView, group._keys != nil {
1741
+ for index in items.indices {
1742
+ if let key = items[index].key {
1743
+ items[index].key = AnyHashable(_GroupKey(position: position, key: key))
1744
+ }
1745
+ }
1746
+ }
1747
+ return items
1748
+ }
1749
+ }
1750
+
1751
+ /// Wraps a `list`'s rows (also those of its `section`s) that can be selected
1752
+ /// or deleted in `listrow` elements (docs/ops-protocol.md, Phase 8). A row
1753
+ /// is selectable when the list has a selection and the row's `.tag`,
1754
+ /// `ForEach` id or `NavigationLink(value:)` is of the selection's type;
1755
+ /// deletable when its `ForEach` has `onDelete` and the row is not
1756
+ /// `deleteDisabled`. The row's key moves to the wrapper.
1757
+ private func wrapListRows(_ elements: [_Element], in node: _MountedNode) -> [_Element] {
1758
+ let selection = node.environment._listSelection
1759
+ let editMode = node.environment.editMode
1760
+ return elements.map { element in
1761
+ if element.kind == "section" {
1762
+ var section = element
1763
+ section.children = wrapListRows(section.children, in: node)
1764
+ return section
1765
+ }
1766
+ guard let mark = element.row else { return element }
1767
+ let value = mark.linkValue ?? mark.tag
1768
+ let selectable: Bool = if let selection, let value { selection.accepts(value) } else { false }
1769
+ let deletable = mark.delete != nil && !mark.deleteDisabled
1770
+ guard selectable || deletable else { return element }
1771
+ let selected = selectable && selection!.contains(value!)
1772
+
1773
+ var inner = element
1774
+ inner.key = nil
1775
+ inner.row = nil
1776
+ let handler: (_JSON) -> Void = { payload in
1777
+ if case .object(let fields) = payload, case .string("delete")? = fields["type"] {
1778
+ if deletable { mark.delete?() }
1779
+ return
1780
+ }
1781
+ guard selectable, let selection, let value else {
1782
+ mark.linkHandler?(.null)
1783
+ return
1784
+ }
1785
+ let editing = editMode?.wrappedValue.isEditing ?? false
1786
+ if mark.linkValue != nil, !editing {
1787
+ // A value link pushes (or, in a split sidebar, selects) as before.
1788
+ mark.linkHandler?(.null)
1789
+ } else if selection.isMultiple || editing {
1790
+ selection.toggle(value)
1791
+ } else {
1792
+ selection.set(value)
1793
+ }
1794
+ }
1795
+ return _Element(
1796
+ kind: "listrow",
1797
+ props: .object([
1798
+ "selectable": .bool(selectable),
1799
+ "selected": .bool(selected),
1800
+ "deletable": .bool(deletable),
1801
+ ]),
1802
+ children: [inner],
1803
+ handler: handler,
1804
+ key: element.key
1805
+ )
1806
+ }
1807
+ }
1808
+
1809
+ /// Primitive `@State` values keyed by view path, for `SB_STATE`.
1810
+ public func snapshot() -> _JSON {
1811
+ var values: [String: _JSON] = [:]
1812
+ func walk(_ node: _MountedNode) {
1813
+ for (index, box) in node.boxes.enumerated() {
1814
+ if let json = _primitiveJSON(box._anyValue) {
1815
+ values["\(node.path?.text ?? "")#\(index)"] = json
1816
+ }
1817
+ }
1818
+ node.children.forEach(walk)
1819
+ }
1820
+ if let root { walk(root) }
1821
+ return .object(values)
1822
+ }
1823
+
1824
+ private func restoreState(into boxes: [any _AnyStateBox], at path: _ViewPath) {
1825
+ guard !restoredState.isEmpty, !boxes.isEmpty else { return }
1826
+ let text = path.text
1827
+ for (index, box) in boxes.enumerated() {
1828
+ guard let saved = restoredState["\(text)#\(index)"] else { continue }
1829
+ switch (box._anyValue, saved) {
1830
+ case (is Int, .number(let n)) where n == n.rounded(): box._anyValue = Int(n)
1831
+ case (is Double, .number(let n)): box._anyValue = n
1832
+ case (is String, .string(let s)): box._anyValue = s
1833
+ case (is Bool, .bool(let b)): box._anyValue = b
1834
+ default: break
1835
+ }
1836
+ }
1837
+ }
1838
+
1839
+ // MARK: Headless replay
1840
+
1841
+ /// Replays an event script (Sources/SwiftUI/Runtime/Replay.swift): events
1842
+ /// separated by commas or line breaks, such as `tap:"Add"`, `tap:12`,
1843
+ /// `text:"Name":Milk`, `toggle:"Done":true`, `select:"Sort":"Title"`,
1844
+ /// `env:dark`, `type:xxxLarge`, `size:regular`, `step:"8 hours":1`,
1845
+ /// `slide:7:0.35`, `date:7:1700000000`, `drag:12:changed:40x-3`,
1846
+ /// `longpress:12`, `delete:"Coffee"`, `refresh:40`, `back`, `dismiss`.
1847
+ /// Each is echoed, with the id its target resolved to, before its ops.
1848
+ /// Returns false, after printing an error op, at the first event that
1849
+ /// cannot be parsed or whose selector does not name exactly one element.
1850
+ @discardableResult
1851
+ func replay(events script: String) -> Bool {
1852
+ for raw in _EventScript.split(script) {
1853
+ // `wait:500`: the virtual clock moves on 500 ms, running what comes due
1854
+ // (work an event queued, a timer), before the next event.
1855
+ if raw.hasPrefix("wait:"), let ms = Double(String(raw.dropFirst(5)).trimmingSpaces()), ms >= 0 {
1856
+ runHeadlessClock(upTo: Double(_HostExecutor.shared.nowNs) / 1_000_000 + ms)
1857
+ continue
1858
+ }
1859
+ let event: [String: _JSON]
1860
+ do {
1861
+ event = try scriptEvent(raw)
1862
+ } catch {
1863
+ print(_JSON.object(["op": .string("error"), "message": .string(error.message)]).serialized)
1864
+ return false
1865
+ }
1866
+ host.apply([.event(.object(event))])
1867
+ dispatch(event: .object(event))
1868
+ }
1869
+ return true
1870
+ }
1871
+
1872
+ func scriptEvent(_ raw: String) throws(_ScriptError) -> [String: _JSON] {
1873
+ let parsed = try _EventScript.parse(raw)
1874
+ let unrecognized = _ScriptError(message: "unrecognized event \(raw)")
1875
+ func number(_ text: String?) throws(_ScriptError) -> _JSON {
1876
+ guard let text, let value = Double(text) else { throw unrecognized }
1877
+ return .number(value)
1878
+ }
1879
+ func size(_ text: String?) throws(_ScriptError) -> _JSON {
1880
+ let dims = (text ?? "").split(separator: "x").compactMap { Double(String($0)) }
1881
+ guard dims.count == 2 else { throw unrecognized }
1882
+ return .object(["width": .number(dims[0]), "height": .number(dims[1])])
1883
+ }
1884
+
1885
+ switch parsed.type {
1886
+ case "back", "dismiss":
1887
+ guard parsed.target == nil, parsed.value == nil else { throw unrecognized }
1888
+ let target = parsed.type == "back" ? try reconciler.backTarget() : try reconciler.dismissTarget()
1889
+ return ["type": .string("tap"), "id": .number(Double(target.id))]
1890
+ case "env", "type", "size":
1891
+ guard let value = parsed.value, !value.isEmpty else { throw unrecognized }
1892
+ let field = ["env": "colorScheme", "type": "dynamicTypeSize", "size": "horizontalSizeClass"][parsed.type]!
1893
+ return ["type": .string("environment"), field: .string(value)]
1894
+ default:
1895
+ break
1896
+ }
1897
+
1898
+ var element: _Reconciler.Rendered?
1899
+ let id: Int
1900
+ switch parsed.target {
1901
+ case .id(let number)?:
1902
+ id = number
1903
+ case .selector(let selector)?:
1904
+ let resolved = try reconciler.resolve(selector, for: parsed.type)
1905
+ element = resolved
1906
+ id = resolved.id
1907
+ case nil:
1908
+ throw unrecognized
1909
+ }
1910
+ var event: [String: _JSON] = ["type": .string(parsed.type), "id": .number(Double(id))]
1911
+ let value = parsed.value
1912
+
1913
+ switch parsed.type {
1914
+ case "tap", "longpress", "delete", "refresh":
1915
+ guard value == nil else { throw unrecognized }
1916
+ case "text":
1917
+ guard let value else { throw unrecognized }
1918
+ event["value"] = .string(value)
1919
+ case "toggle":
1920
+ guard value == "true" || value == "false" else { throw unrecognized }
1921
+ event["value"] = .bool(value == "true")
1922
+ case "select":
1923
+ if parsed.valueQuoted, let option = value {
1924
+ guard let target = element ?? reconciler.element(id: id) else { throw unrecognized }
1925
+ event["value"] = .number(Double(try reconciler.optionIndex(option, of: target)))
1926
+ } else {
1927
+ event["value"] = try number(value)
1928
+ }
1929
+ case "step", "slide", "date":
1930
+ event["value"] = try number(value)
1931
+ case "geometry":
1932
+ guard case .object(let dims) = try size(value) else { throw unrecognized }
1933
+ event["width"] = dims["width"]
1934
+ event["height"] = dims["height"]
1935
+ case "drag":
1936
+ // drag:12:changed:40x-3 (phase and translation)
1937
+ let rest = (value ?? "").split(separator: ":", maxSplits: 1).map { String($0).trimmingSpaces() }
1938
+ guard rest.count == 2 else { throw unrecognized }
1939
+ event["phase"] = .string(rest[0])
1940
+ event["translation"] = try size(rest[1])
1941
+ default:
1942
+ throw unrecognized
1943
+ }
1944
+ return event
1945
+ }
1946
+ }
1947
+
1948
+ /// A host request failed: the host could not perform it (a network error,
1949
+ /// a missing headless fixture). `URLSession` maps it onto `URLError`.
1950
+ public struct _HostRequestError: Error, CustomStringConvertible, Sendable {
1951
+ public let message: String
1952
+ public init(message: String) { self.message = message }
1953
+ public var description: String { message }
1954
+ }
1955
+
1956
+ /// Standard base64 (RFC 4648, padded). The runtime has no Foundation.
1957
+ public func _base64(_ bytes: [UInt8]) -> String {
1958
+ let table = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".utf8)
1959
+ var out: [UInt8] = []
1960
+ out.reserveCapacity((bytes.count + 2) / 3 * 4)
1961
+ var index = 0
1962
+ while index + 3 <= bytes.count {
1963
+ let n = (UInt32(bytes[index]) << 16) | (UInt32(bytes[index + 1]) << 8) | UInt32(bytes[index + 2])
1964
+ out += [table[Int(n >> 18 & 63)], table[Int(n >> 12 & 63)], table[Int(n >> 6 & 63)], table[Int(n & 63)]]
1965
+ index += 3
1966
+ }
1967
+ let rest = bytes.count - index
1968
+ if rest == 1 {
1969
+ let n = UInt32(bytes[index]) << 16
1970
+ out += [table[Int(n >> 18 & 63)], table[Int(n >> 12 & 63)], 61, 61]
1971
+ } else if rest == 2 {
1972
+ let n = (UInt32(bytes[index]) << 16) | (UInt32(bytes[index + 1]) << 8)
1973
+ out += [table[Int(n >> 18 & 63)], table[Int(n >> 12 & 63)], table[Int(n >> 6 & 63)], 61]
1974
+ }
1975
+ return String(decoding: out, as: UTF8.self)
1976
+ }
1977
+
1978
+ /// Views that run an action when first mounted.
1979
+ protocol _OnAppearing: _TraitBearing {
1980
+ var _action: (@MainActor () -> Void)? { get }
1981
+ }
1982
+
1983
+ extension _OnAppearView: _OnAppearing {
1984
+ var _action: (@MainActor () -> Void)? { action }
1985
+ }
1986
+
1987
+ /// Views that run an action when they leave the tree.
1988
+ protocol _OnDisappearing: _TraitBearing {
1989
+ var _disappearAction: (@MainActor () -> Void)? { get }
1990
+ }
1991
+
1992
+ extension _OnDisappearView: _OnDisappearing {
1993
+ var _disappearAction: (@MainActor () -> Void)? { action }
1994
+ }
1995
+
1996
+ func _primitiveJSON(_ value: Any) -> _JSON? {
1997
+ switch value {
1998
+ case let v as Int: .number(Double(v))
1999
+ case let v as Double: .number(v)
2000
+ case let v as String: .string(v)
2001
+ case let v as Bool: .bool(v)
2002
+ default: nil
2003
+ }
2004
+ }
2005
+
2006
+ /// The WASI environment (`SB_*` variables the host sets); nil when unset.
2007
+ public func _environment(_ name: String) -> String? {
2008
+ guard let value = getenv(name) else { return nil }
2009
+ return String(cString: value)
2010
+ }
2011
+
2012
+ /// Identity for a sheet element at the root, so re-renders keep its id.
2013
+ struct _SheetKey: Hashable {
2014
+ let path: String
2015
+ }
2016
+
2017
+ /// Identity for an alert element at the root.
2018
+ struct _AlertKey: Hashable {
2019
+ let path: String
2020
+ }
2021
+
2022
+ /// The element with every `button` in its subtree made to also set
2023
+ /// `isPresented` to false when tapped (an alert's action buttons dismiss it).
2024
+ private func _dismissing(_ element: _Element, with isPresented: Binding<Bool>) -> _Element {
2025
+ var copy = element
2026
+ if element.kind == "button" {
2027
+ let action = element.handler
2028
+ copy.handler = { payload in
2029
+ action?(payload)
2030
+ isPresented.wrappedValue = false
2031
+ }
2032
+ }
2033
+ copy.children = element.children.map { _dismissing($0, with: isPresented) }
2034
+ return copy
2035
+ }
2036
+
2037
+ // MARK: - Tab and picker helpers
2038
+
2039
+ /// Flattens structural groups (tuples, optionals, conditionals, ForEach) into
2040
+ /// the views they contribute, keeping ForEach ids as tags when asked.
2041
+ @MainActor
2042
+ func _flattenChildren(_ view: any View, keys: Bool) -> [(view: any View, tag: AnyHashable?)] {
2043
+ // A `Text` is a group (of its one rendering) but one view to its container.
2044
+ if let group = view as? any _GroupView, !(view is Text),
2045
+ !(view is any _TabItemProviding), !(view is any _TagProviding) {
2046
+ let children = group._children
2047
+ let ids = keys ? group._keys : nil
2048
+ return children.enumerated().flatMap { index, child -> [(view: any View, tag: AnyHashable?)] in
2049
+ let nested = _flattenChildren(child, keys: keys)
2050
+ guard let ids else { return nested }
2051
+ return nested.map { ($0.view, $0.tag ?? ids[index]) }
2052
+ }
2053
+ }
2054
+ return [(view, _tag(of: view))]
2055
+ }
2056
+
2057
+ @MainActor
2058
+ func _flattenChildren(_ view: any View) -> [any View] {
2059
+ _flattenChildren(view, keys: false).map(\.view)
2060
+ }
2061
+
2062
+ /// The `.tag` applied to a view, looking through other wrappers.
2063
+ @MainActor
2064
+ func _tag(of view: any View) -> AnyHashable? {
2065
+ if let tagged = view as? any _TagProviding { return tagged._tag }
2066
+ if let item = view as? any _TabItemProviding { return _tag(of: item._tabContent) }
2067
+ if let group = view as? any _GroupView, group._children.count == 1, !(view is TupleView<Any>) {
2068
+ return _tag(of: group._children[0])
2069
+ }
2070
+ return nil
2071
+ }
2072
+
2073
+ /// The `.tabItem` label applied to a view, looking through other wrappers.
2074
+ @MainActor
2075
+ func _tabLabel(of view: any View) -> (any View)? {
2076
+ if let item = view as? any _TabItemProviding { return item._tabLabel }
2077
+ if let tagged = view as? any _TagProviding { return _tabLabel(of: tagged._tagged) }
2078
+ if let group = view as? any _GroupView, group._children.count == 1 {
2079
+ return _tabLabel(of: group._children[0])
2080
+ }
2081
+ return nil
2082
+ }
2083
+
2084
+ /// Removes `toolbar` and `searchfield` elements from anywhere under `element`
2085
+ /// (appending them to `bar`) and returns the element without them.
2086
+ private func _hoistBarItems(from element: _Element, into bar: inout [_Element],
2087
+ barFree: inout _BarFreeTokens) -> _Element {
2088
+ if barFree.contains(element.token) { return element }
2089
+ var kept: [_Element] = []
2090
+ var changed = false
2091
+ for child in element.children {
2092
+ if child.kind == "toolbar" || child.kind == "searchfield" {
2093
+ bar.append(child)
2094
+ changed = true
2095
+ } else {
2096
+ let hoisted = _hoistBarItems(from: child, into: &bar, barFree: &barFree)
2097
+ if hoisted.token != child.token { changed = true }
2098
+ kept.append(hoisted)
2099
+ }
2100
+ }
2101
+ // Unchanged subtrees keep their element (and token), so the reconciler
2102
+ // can skip them; a modified copy is a new element.
2103
+ guard changed else {
2104
+ barFree.insert(element.token)
2105
+ return element
2106
+ }
2107
+ var copy = element
2108
+ copy.children = kept
2109
+ copy.token = _Element.nextToken()
2110
+ return copy
2111
+ }
2112
+
2113
+ /// Tokens of element subtrees known to hold no `toolbar` or `searchfield`,
2114
+ /// which `_hoistBarItems` does not walk again. A clean node's cached elements
2115
+ /// keep their tokens from pass to pass, so a screen whose content changed in
2116
+ /// one place walks only the elements produced anew. Holds the tokens met in
2117
+ /// this pass and the previous one.
2118
+ struct _BarFreeTokens {
2119
+ private var current: Set<Int> = []
2120
+ private var previous: Set<Int> = []
2121
+
2122
+ mutating func contains(_ token: Int) -> Bool {
2123
+ if current.contains(token) { return true }
2124
+ guard previous.contains(token) else { return false }
2125
+ current.insert(token)
2126
+ return true
2127
+ }
2128
+
2129
+ mutating func insert(_ token: Int) { current.insert(token) }
2130
+
2131
+ mutating func nextPass() {
2132
+ swap(&current, &previous)
2133
+ current.removeAll(keepingCapacity: true)
2134
+ }
2135
+ }
2136
+
2137
+ /// A picker's options, looking through `Section`s (their headers are dropped).
2138
+ @MainActor
2139
+ func _flattenPickerOptions(_ options: any View) -> [(view: any View, tag: AnyHashable?)] {
2140
+ _flattenChildren(options, keys: true).flatMap { entry -> [(view: any View, tag: AnyHashable?)] in
2141
+ if let section = entry.view as? any _SectionView {
2142
+ return _flattenPickerOptions(section._sectionContent)
2143
+ }
2144
+ return [entry]
2145
+ }
2146
+ }