@apollohg/react-native-rich-text-editor 2.0.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 (494) hide show
  1. package/LICENSE +160 -0
  2. package/README.md +259 -0
  3. package/RUST-STANDARD-LIBRARY-NOTICES.html +8266 -0
  4. package/ReactNativeProseEditor.podspec +59 -0
  5. package/THIRD_PARTY_NOTICES.md +31155 -0
  6. package/android/build.gradle +188 -0
  7. package/android/consumer-rules.pro +18 -0
  8. package/android/expo/build.gradle +17 -0
  9. package/android/src/debug/java/com/apollohg/editor/viewer/PreparedProseDrawInstrumentation.kt +7 -0
  10. package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
  11. package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
  12. package/android/src/main/java/com/apollohg/editor/AndroidApiCompat.kt +19 -0
  13. package/android/src/main/java/com/apollohg/editor/AndroidCollaborationTransport.kt +860 -0
  14. package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +91 -0
  15. package/android/src/main/java/com/apollohg/editor/CodeHighlighting.kt +141 -0
  16. package/android/src/main/java/com/apollohg/editor/DecodedBitmapBudget.kt +250 -0
  17. package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +133 -0
  18. package/android/src/main/java/com/apollohg/editor/EditorBlockSpacing.kt +51 -0
  19. package/android/src/main/java/com/apollohg/editor/EditorBoxDrawable.kt +16 -0
  20. package/android/src/main/java/com/apollohg/editor/EditorBoxDrawing.kt +234 -0
  21. package/android/src/main/java/com/apollohg/editor/EditorCheckboxSpan.kt +87 -0
  22. package/android/src/main/java/com/apollohg/editor/EditorCodeHighlighting.kt +120 -0
  23. package/android/src/main/java/com/apollohg/editor/EditorDecorationLayer.java +18 -0
  24. package/android/src/main/java/com/apollohg/editor/EditorDocumentLayout.kt +626 -0
  25. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1042 -0
  26. package/android/src/main/java/com/apollohg/editor/EditorEditTextAnnotations.kt +191 -0
  27. package/android/src/main/java/com/apollohg/editor/EditorEditTextCaret.kt +100 -0
  28. package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +101 -0
  29. package/android/src/main/java/com/apollohg/editor/EditorEditTextComposition.kt +220 -0
  30. package/android/src/main/java/com/apollohg/editor/EditorEditTextConfiguration.kt +316 -0
  31. package/android/src/main/java/com/apollohg/editor/EditorEditTextCorrection.kt +303 -0
  32. package/android/src/main/java/com/apollohg/editor/EditorEditTextDeferredUpdates.kt +327 -0
  33. package/android/src/main/java/com/apollohg/editor/EditorEditTextDeletion.kt +258 -0
  34. package/android/src/main/java/com/apollohg/editor/EditorEditTextDiagnostics.kt +83 -0
  35. package/android/src/main/java/com/apollohg/editor/EditorEditTextDragDrop.kt +67 -0
  36. package/android/src/main/java/com/apollohg/editor/EditorEditTextExternalComposition.kt +357 -0
  37. package/android/src/main/java/com/apollohg/editor/EditorEditTextHardwareInput.kt +277 -0
  38. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +280 -0
  39. package/android/src/main/java/com/apollohg/editor/EditorEditTextInputLifecycle.kt +183 -0
  40. package/android/src/main/java/com/apollohg/editor/EditorEditTextInputPreparation.kt +79 -0
  41. package/android/src/main/java/com/apollohg/editor/EditorEditTextInputTraits.kt +384 -0
  42. package/android/src/main/java/com/apollohg/editor/EditorEditTextLayout.kt +100 -0
  43. package/android/src/main/java/com/apollohg/editor/EditorEditTextNativeMutation.kt +243 -0
  44. package/android/src/main/java/com/apollohg/editor/EditorEditTextPlaceholder.kt +154 -0
  45. package/android/src/main/java/com/apollohg/editor/EditorEditTextRenderPatch.kt +347 -0
  46. package/android/src/main/java/com/apollohg/editor/EditorEditTextRendering.kt +313 -0
  47. package/android/src/main/java/com/apollohg/editor/EditorEditTextRustCommands.kt +281 -0
  48. package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +352 -0
  49. package/android/src/main/java/com/apollohg/editor/EditorEditTextState.kt +113 -0
  50. package/android/src/main/java/com/apollohg/editor/EditorEditTextTaskMarkers.kt +92 -0
  51. package/android/src/main/java/com/apollohg/editor/EditorEditTextTextCommit.kt +327 -0
  52. package/android/src/main/java/com/apollohg/editor/EditorEditTextToolbar.kt +84 -0
  53. package/android/src/main/java/com/apollohg/editor/EditorEditTextUpdates.kt +245 -0
  54. package/android/src/main/java/com/apollohg/editor/EditorGlyphHeightCursorDrawable.kt +42 -0
  55. package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +13 -0
  56. package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +923 -0
  57. package/android/src/main/java/com/apollohg/editor/EditorInputCoordinates.kt +191 -0
  58. package/android/src/main/java/com/apollohg/editor/EditorInputCorrection.kt +107 -0
  59. package/android/src/main/java/com/apollohg/editor/EditorInputCursorGeometry.kt +128 -0
  60. package/android/src/main/java/com/apollohg/editor/EditorInputSnapshots.kt +134 -0
  61. package/android/src/main/java/com/apollohg/editor/EditorInputSurroundingDeletion.kt +321 -0
  62. package/android/src/main/java/com/apollohg/editor/EditorKeyboardToolbarView.kt +851 -0
  63. package/android/src/main/java/com/apollohg/editor/EditorMentionStyle.kt +192 -0
  64. package/android/src/main/java/com/apollohg/editor/EditorReconciliationWatcher.kt +46 -0
  65. package/android/src/main/java/com/apollohg/editor/EditorStyleSheet.kt +303 -0
  66. package/android/src/main/java/com/apollohg/editor/EditorStyleSheetSpans.kt +253 -0
  67. package/android/src/main/java/com/apollohg/editor/EditorSurfaceInputConnection.kt +67 -0
  68. package/android/src/main/java/com/apollohg/editor/EditorTextDecorationDrawing.kt +143 -0
  69. package/android/src/main/java/com/apollohg/editor/EditorTextSurface.kt +712 -0
  70. package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceDragDrop.kt +94 -0
  71. package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +708 -0
  72. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +650 -0
  73. package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +912 -0
  74. package/android/src/main/java/com/apollohg/editor/EditorV2AdapterMutations.kt +402 -0
  75. package/android/src/main/java/com/apollohg/editor/EditorV2AdapterRendering.kt +123 -0
  76. package/android/src/main/java/com/apollohg/editor/EditorV2AdapterSelection.kt +203 -0
  77. package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +120 -0
  78. package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +87 -0
  79. package/android/src/main/java/com/apollohg/editor/EditorV2Registry.kt +97 -0
  80. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +409 -0
  81. package/android/src/main/java/com/apollohg/editor/ExpoAutoGrowStyleSizePublisher.kt +34 -0
  82. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +266 -0
  83. package/android/src/main/java/com/apollohg/editor/ImeTextCoordinateMapper.kt +102 -0
  84. package/android/src/main/java/com/apollohg/editor/KeyboardToolbarImeAnimationController.kt +101 -0
  85. package/android/src/main/java/com/apollohg/editor/MentionSuggestionChipView.kt +234 -0
  86. package/android/src/main/java/com/apollohg/editor/NativeCollaborationTransportRegistry.kt +493 -0
  87. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +813 -0
  88. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewAtoms.kt +68 -0
  89. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewBinding.kt +126 -0
  90. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewDiagnostics.kt +208 -0
  91. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewErrors.kt +146 -0
  92. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewEvents.kt +150 -0
  93. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewExternalComposition.kt +39 -0
  94. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewFocus.kt +176 -0
  95. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewLifecycle.kt +302 -0
  96. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewMentions.kt +259 -0
  97. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewNativeActions.kt +179 -0
  98. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewOutsideTap.kt +214 -0
  99. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewPendingUpdates.kt +376 -0
  100. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +234 -0
  101. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewPropertyRetries.kt +197 -0
  102. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewToolbar.kt +285 -0
  103. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewTypes.kt +28 -0
  104. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewUpdates.kt +271 -0
  105. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +687 -0
  106. package/android/src/main/java/com/apollohg/editor/NativeEditorOutsideTapDispatcher.kt +380 -0
  107. package/android/src/main/java/com/apollohg/editor/NativeEditorV2Numbers.kt +45 -0
  108. package/android/src/main/java/com/apollohg/editor/NativeEditorViewRegistry.kt +372 -0
  109. package/android/src/main/java/com/apollohg/editor/NativeImagePipeline.kt +22 -0
  110. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +193 -0
  111. package/android/src/main/java/com/apollohg/editor/NativeToolbarIcons.kt +177 -0
  112. package/android/src/main/java/com/apollohg/editor/NativeToolbarItem.kt +274 -0
  113. package/android/src/main/java/com/apollohg/editor/OkHttpCollaborationSocket.kt +102 -0
  114. package/android/src/main/java/com/apollohg/editor/OrderedListMarkerFormatter.kt +130 -0
  115. package/android/src/main/java/com/apollohg/editor/PointerTransparentView.java +18 -0
  116. package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +128 -0
  117. package/android/src/main/java/com/apollohg/editor/ProseViewerView.kt +854 -0
  118. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +314 -0
  119. package/android/src/main/java/com/apollohg/editor/RenderBlockSpans.kt +314 -0
  120. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +224 -0
  121. package/android/src/main/java/com/apollohg/editor/RenderBridgeBlockStyles.kt +407 -0
  122. package/android/src/main/java/com/apollohg/editor/RenderBridgeElements.kt +544 -0
  123. package/android/src/main/java/com/apollohg/editor/RenderBridgeInlineAndAtoms.kt +631 -0
  124. package/android/src/main/java/com/apollohg/editor/RenderBridgeParagraphs.kt +215 -0
  125. package/android/src/main/java/com/apollohg/editor/RenderImageDecoder.kt +748 -0
  126. package/android/src/main/java/com/apollohg/editor/RenderImageLoader.kt +900 -0
  127. package/android/src/main/java/com/apollohg/editor/RenderLayoutConstants.kt +117 -0
  128. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +337 -0
  129. package/android/src/main/java/com/apollohg/editor/RenderParagraphSpans.kt +182 -0
  130. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +951 -0
  131. package/android/src/main/java/com/apollohg/editor/SharedNativeImagePipeline.kt +78 -0
  132. package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +261 -0
  133. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1656 -0
  134. package/android/src/main/java/com/apollohg/editor/viewer/FabricAttachmentSidecars.kt +80 -0
  135. package/android/src/main/java/com/apollohg/editor/viewer/FabricLeaseHandleBridge.kt +59 -0
  136. package/android/src/main/java/com/apollohg/editor/viewer/FallbackSelectionGeometry.kt +450 -0
  137. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseDrawingView.kt +678 -0
  138. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseInstrumentation.kt +608 -0
  139. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayout.kt +186 -0
  140. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayoutCache.kt +393 -0
  141. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseLayoutRegistry.kt +828 -0
  142. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseTheme.kt +316 -0
  143. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseViewerManager.kt +900 -0
  144. package/android/src/main/java/com/apollohg/editor/viewer/PreparedProseViewerPackage.kt +16 -0
  145. package/android/src/main/java/com/apollohg/editor/viewer/PreparedViewerAtoms.kt +89 -0
  146. package/android/src/main/java/com/apollohg/editor/viewer/ViewerCodeHighlighting.kt +91 -0
  147. package/android/src/main/java/com/apollohg/editor/viewer/ViewerDocument.kt +453 -0
  148. package/android/src/main/java/com/apollohg/editor/viewer/ViewerFontEnvironment.kt +282 -0
  149. package/android/src/main/java/com/apollohg/editor/viewer/ViewerImagePipeline.kt +460 -0
  150. package/android/src/main/jni/CMakeLists.txt +85 -0
  151. package/android/src/main/jni/PreparedProseViewerMeasurementsManager.cpp +334 -0
  152. package/android/src/main/jni/ReactNativeProseEditorSpec.h +17 -0
  153. package/android/src/main/jni/reactnativeproseeditor.h +6 -0
  154. package/android/src/main/res/values/attrs.xml +6 -0
  155. package/android/src/release/java/com/apollohg/editor/viewer/PreparedProseDrawInstrumentation.kt +9 -0
  156. package/app.plugin.js +62 -0
  157. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseMeasurementsManager.h +62 -0
  158. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerComponentDescriptor.h +30 -0
  159. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerShadowNode.cpp +192 -0
  160. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerShadowNode.h +46 -0
  161. package/common/cpp/react/renderer/components/PreparedProseViewer/PreparedProseViewerState.h +145 -0
  162. package/dist/AtomHost.d.ts +17 -0
  163. package/dist/AtomHost.js +135 -0
  164. package/dist/DefaultAtomChip.d.ts +2 -0
  165. package/dist/DefaultAtomChip.js +22 -0
  166. package/dist/EditorAddon.d.ts +32 -0
  167. package/dist/EditorAddon.js +29 -0
  168. package/dist/EditorAddonNormalization.d.ts +6 -0
  169. package/dist/EditorAddonNormalization.js +48 -0
  170. package/dist/EditorMentionThemeNormalization.d.ts +8 -0
  171. package/dist/EditorMentionThemeNormalization.js +22 -0
  172. package/dist/EditorMentionThemeValidation.d.ts +12 -0
  173. package/dist/EditorMentionThemeValidation.js +95 -0
  174. package/dist/EditorStyleSheet.d.ts +4 -0
  175. package/dist/EditorStyleSheet.js +10 -0
  176. package/dist/EditorStyleSheetNormalization.d.ts +3 -0
  177. package/dist/EditorStyleSheetNormalization.js +226 -0
  178. package/dist/EditorStyleSheetTypes.d.ts +151 -0
  179. package/dist/EditorStyleSheetTypes.js +2 -0
  180. package/dist/EditorTheme.d.ts +117 -0
  181. package/dist/EditorTheme.js +41 -0
  182. package/dist/EditorToolbar.d.ts +4 -0
  183. package/dist/EditorToolbar.js +17 -0
  184. package/dist/EditorToolbarComponent.d.ts +13 -0
  185. package/dist/EditorToolbarComponent.js +24 -0
  186. package/dist/EditorToolbarItems.d.ts +9 -0
  187. package/dist/EditorToolbarItems.js +58 -0
  188. package/dist/EditorToolbarRegistry.d.ts +59 -0
  189. package/dist/EditorToolbarRegistry.js +175 -0
  190. package/dist/EditorToolbarTypes.d.ts +273 -0
  191. package/dist/EditorToolbarTypes.js +2 -0
  192. package/dist/EditorToolbarVisuals.d.ts +154 -0
  193. package/dist/EditorToolbarVisuals.js +228 -0
  194. package/dist/EditorUpdateRevision.d.ts +8 -0
  195. package/dist/EditorUpdateRevision.js +22 -0
  196. package/dist/ExternalTextComposition.d.ts +45 -0
  197. package/dist/ExternalTextComposition.js +537 -0
  198. package/dist/ImageLoadingPolicy.d.ts +54 -0
  199. package/dist/ImageLoadingPolicy.js +60 -0
  200. package/dist/NativeEditorBoundaryError.d.ts +111 -0
  201. package/dist/NativeEditorBoundaryError.js +268 -0
  202. package/dist/NativeEditorBridge.d.ts +9 -0
  203. package/dist/NativeEditorBridge.js +37 -0
  204. package/dist/NativeEditorCollaborationTransport.d.ts +154 -0
  205. package/dist/NativeEditorCollaborationTransport.js +275 -0
  206. package/dist/NativeEditorCreateConfig.d.ts +16 -0
  207. package/dist/NativeEditorCreateConfig.js +249 -0
  208. package/dist/NativeEditorCreateJson.d.ts +76 -0
  209. package/dist/NativeEditorCreateJson.js +474 -0
  210. package/dist/NativeEditorCreateValidation.d.ts +32 -0
  211. package/dist/NativeEditorCreateValidation.js +150 -0
  212. package/dist/NativeEditorDocumentBridge.d.ts +72 -0
  213. package/dist/NativeEditorDocumentBridge.js +305 -0
  214. package/dist/NativeEditorDocumentHandle.d.ts +70 -0
  215. package/dist/NativeEditorDocumentHandle.js +89 -0
  216. package/dist/NativeEditorLocalAwareness.d.ts +34 -0
  217. package/dist/NativeEditorLocalAwareness.js +153 -0
  218. package/dist/NativeEditorNativeModule.d.ts +48 -0
  219. package/dist/NativeEditorNativeModule.js +34 -0
  220. package/dist/NativeEditorRenderNormalization.d.ts +22 -0
  221. package/dist/NativeEditorRenderNormalization.js +274 -0
  222. package/dist/NativeEditorResultNormalization.d.ts +108 -0
  223. package/dist/NativeEditorResultNormalization.js +400 -0
  224. package/dist/NativeEditorTypes.d.ts +318 -0
  225. package/dist/NativeEditorTypes.js +15 -0
  226. package/dist/NativeEditorV2Decimal.d.ts +5 -0
  227. package/dist/NativeEditorV2Decimal.js +18 -0
  228. package/dist/NativeProseViewer.d.ts +122 -0
  229. package/dist/NativeProseViewer.js +130 -0
  230. package/dist/NativeRichTextEditor.d.ts +3 -0
  231. package/dist/NativeRichTextEditor.js +6 -0
  232. package/dist/ResourceLimits.d.ts +100 -0
  233. package/dist/ResourceLimits.js +80 -0
  234. package/dist/RichTextEditor.d.ts +4 -0
  235. package/dist/RichTextEditor.js +25 -0
  236. package/dist/RichTextEditorNativeTypes.d.ts +122 -0
  237. package/dist/RichTextEditorNativeTypes.js +2 -0
  238. package/dist/RichTextEditorNativeView.d.ts +12 -0
  239. package/dist/RichTextEditorNativeView.js +15 -0
  240. package/dist/RichTextEditorSerialization.d.ts +53 -0
  241. package/dist/RichTextEditorSerialization.js +283 -0
  242. package/dist/RichTextEditorTypes.d.ts +281 -0
  243. package/dist/RichTextEditorTypes.js +2 -0
  244. package/dist/ViewerConfiguration.d.ts +22 -0
  245. package/dist/ViewerConfiguration.js +6 -0
  246. package/dist/YjsCollaboration.d.ts +146 -0
  247. package/dist/YjsCollaboration.js +496 -0
  248. package/dist/addons.d.ts +193 -0
  249. package/dist/addons.js +122 -0
  250. package/dist/atomConstants.d.ts +3 -0
  251. package/dist/atomConstants.js +6 -0
  252. package/dist/atomInstances.d.ts +35 -0
  253. package/dist/atomInstances.js +79 -0
  254. package/dist/atomPolicy.d.ts +4 -0
  255. package/dist/atomPolicy.js +47 -0
  256. package/dist/atomUpdates.d.ts +2 -0
  257. package/dist/atomUpdates.js +38 -0
  258. package/dist/atoms.d.ts +92 -0
  259. package/dist/atoms.js +222 -0
  260. package/dist/attributeValidation.d.ts +4 -0
  261. package/dist/attributeValidation.js +97 -0
  262. package/dist/contentExpression.d.ts +6 -0
  263. package/dist/contentExpression.js +335 -0
  264. package/dist/index.d.ts +21 -0
  265. package/dist/index.js +78 -0
  266. package/dist/listMarkers.d.ts +15 -0
  267. package/dist/listMarkers.js +21 -0
  268. package/dist/schemaDefinition.d.ts +143 -0
  269. package/dist/schemaDefinition.js +206 -0
  270. package/dist/schemaDocument.d.ts +17 -0
  271. package/dist/schemaDocument.js +69 -0
  272. package/dist/schemaDocumentConstruction.d.ts +5 -0
  273. package/dist/schemaDocumentConstruction.js +125 -0
  274. package/dist/schemaNormalization.d.ts +29 -0
  275. package/dist/schemaNormalization.js +313 -0
  276. package/dist/schemaPresets.d.ts +68 -0
  277. package/dist/schemaPresets.js +198 -0
  278. package/dist/schemaResolution.d.ts +9 -0
  279. package/dist/schemaResolution.js +211 -0
  280. package/dist/schemas.d.ts +5 -0
  281. package/dist/schemas.js +28 -0
  282. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +37 -0
  283. package/dist/specs/PreparedProseViewerNativeComponent.js +6 -0
  284. package/dist/useEditorToolbarInteractions.d.ts +12 -0
  285. package/dist/useEditorToolbarInteractions.js +218 -0
  286. package/dist/useEditorToolbarItems.d.ts +8 -0
  287. package/dist/useEditorToolbarItems.js +280 -0
  288. package/dist/useEditorToolbarPresentation.d.ts +4 -0
  289. package/dist/useEditorToolbarPresentation.js +203 -0
  290. package/dist/useEditorToolbarState.d.ts +63 -0
  291. package/dist/useEditorToolbarState.js +108 -0
  292. package/dist/useFocusPreservingFrames.d.ts +17 -0
  293. package/dist/useFocusPreservingFrames.js +108 -0
  294. package/dist/useNativeEditor.d.ts +66 -0
  295. package/dist/useNativeEditor.js +370 -0
  296. package/dist/useRichTextEditorCommands.d.ts +21 -0
  297. package/dist/useRichTextEditorCommands.js +299 -0
  298. package/dist/useRichTextEditorEvents.d.ts +18 -0
  299. package/dist/useRichTextEditorEvents.js +216 -0
  300. package/dist/useRichTextEditorMentions.d.ts +11 -0
  301. package/dist/useRichTextEditorMentions.js +316 -0
  302. package/dist/useRichTextEditorPresentation.d.ts +5 -0
  303. package/dist/useRichTextEditorPresentation.js +178 -0
  304. package/dist/useRichTextEditorState.d.ts +180 -0
  305. package/dist/useRichTextEditorState.js +405 -0
  306. package/dist/useRichTextEditorUpdates.d.ts +10 -0
  307. package/dist/useRichTextEditorUpdates.js +308 -0
  308. package/dist/useViewerAtoms.d.ts +38 -0
  309. package/dist/useViewerAtoms.js +288 -0
  310. package/expo-module.config.json +13 -0
  311. package/ios/EditorAddons.swift +354 -0
  312. package/ios/EditorCodeHighlighting.swift +184 -0
  313. package/ios/EditorCore.xcframework/Info.plist +44 -0
  314. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  315. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  316. package/ios/EditorInteractionOverlays.swift +697 -0
  317. package/ios/EditorLayoutManager.swift +971 -0
  318. package/ios/EditorNodeTypes.swift +37 -0
  319. package/ios/EditorStyleSheet.swift +461 -0
  320. package/ios/EditorTextView+AttributedPatch.swift +305 -0
  321. package/ios/EditorTextView+Binding.swift +61 -0
  322. package/ios/EditorTextView+Caret.swift +333 -0
  323. package/ios/EditorTextView+Commands.swift +203 -0
  324. package/ios/EditorTextView+Delegate.swift +65 -0
  325. package/ios/EditorTextView+Diagnostics.swift +225 -0
  326. package/ios/EditorTextView+DragDrop.swift +290 -0
  327. package/ios/EditorTextView+ExternalComposition.swift +364 -0
  328. package/ios/EditorTextView+Images.swift +354 -0
  329. package/ios/EditorTextView+InputTraits.swift +200 -0
  330. package/ios/EditorTextView+KeyboardInput.swift +318 -0
  331. package/ios/EditorTextView+Layout.swift +245 -0
  332. package/ios/EditorTextView+MarkedText.swift +267 -0
  333. package/ios/EditorTextView+NativeInputLifecycle.swift +372 -0
  334. package/ios/EditorTextView+NativeMutation.swift +471 -0
  335. package/ios/EditorTextView+Reconciliation.swift +97 -0
  336. package/ios/EditorTextView+RenderPatch.swift +573 -0
  337. package/ios/EditorTextView+Rendering.swift +539 -0
  338. package/ios/EditorTextView+Selection.swift +542 -0
  339. package/ios/EditorTextView.swift +953 -0
  340. package/ios/EditorTheme.swift +626 -0
  341. package/ios/EditorV2Adapter+Commands.swift +204 -0
  342. package/ios/EditorV2Adapter+Envelope.swift +238 -0
  343. package/ios/EditorV2Adapter+Mutation.swift +392 -0
  344. package/ios/EditorV2Adapter+Registry.swift +185 -0
  345. package/ios/EditorV2Adapter+RenderState.swift +360 -0
  346. package/ios/EditorV2Adapter+RenderValidation.swift +420 -0
  347. package/ios/EditorV2Adapter+Selection.swift +329 -0
  348. package/ios/EditorV2Adapter+TextCommands.swift +430 -0
  349. package/ios/EditorV2Adapter.swift +376 -0
  350. package/ios/EditorV2Shadow.swift +353 -0
  351. package/ios/Generated_editor_core.swift +2378 -0
  352. package/ios/NativeCodeHighlighting.swift +159 -0
  353. package/ios/NativeCollaborationSocket.swift +144 -0
  354. package/ios/NativeCollaborationTransport+Protocol.swift +258 -0
  355. package/ios/NativeCollaborationTransport+Socket.swift +379 -0
  356. package/ios/NativeCollaborationTransport+Validation.swift +103 -0
  357. package/ios/NativeCollaborationTransport.swift +322 -0
  358. package/ios/NativeCollaborationTransportRegistry.swift +521 -0
  359. package/ios/NativeEditorExpoView+AccessoryRetry.swift +219 -0
  360. package/ios/NativeEditorExpoView+Binding.swift +268 -0
  361. package/ios/NativeEditorExpoView+Diagnostics.swift +66 -0
  362. package/ios/NativeEditorExpoView+Events.swift +131 -0
  363. package/ios/NativeEditorExpoView+Focus.swift +169 -0
  364. package/ios/NativeEditorExpoView+MentionRetry.swift +288 -0
  365. package/ios/NativeEditorExpoView+Mentions.swift +288 -0
  366. package/ios/NativeEditorExpoView+Properties.swift +300 -0
  367. package/ios/NativeEditorExpoView+Toolbar.swift +86 -0
  368. package/ios/NativeEditorExpoView+Updates.swift +312 -0
  369. package/ios/NativeEditorExpoView.swift +349 -0
  370. package/ios/NativeEditorModule.swift +856 -0
  371. package/ios/NativeEditorViewRegistry.swift +226 -0
  372. package/ios/NativeToolbar+ButtonStyle.swift +214 -0
  373. package/ios/NativeToolbar+Buttons.swift +228 -0
  374. package/ios/NativeToolbar+Diagnostics.swift +114 -0
  375. package/ios/NativeToolbar+Menu.swift +101 -0
  376. package/ios/NativeToolbar+Models.swift +519 -0
  377. package/ios/NativeToolbar+Placeholder.swift +39 -0
  378. package/ios/NativeToolbar.swift +612 -0
  379. package/ios/OrderedListMarkerFormatter.swift +103 -0
  380. package/ios/PositionBridge.swift +596 -0
  381. package/ios/ProseViewerView.swift +462 -0
  382. package/ios/RenderBridge+Atoms.swift +288 -0
  383. package/ios/RenderBridge+Attachments.swift +246 -0
  384. package/ios/RenderBridge+BlockBoundaries.swift +313 -0
  385. package/ios/RenderBridge+Style.swift +343 -0
  386. package/ios/RenderBridge.swift +704 -0
  387. package/ios/RichTextEditorView.swift +930 -0
  388. package/ios/SharedNativeImagePipeline+Cache.swift +189 -0
  389. package/ios/SharedNativeImagePipeline+Transport.swift +328 -0
  390. package/ios/SharedNativeImagePipeline.swift +533 -0
  391. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +436 -0
  392. package/ios/Viewer/CoreTextProseLayoutEngine+Models.swift +322 -0
  393. package/ios/Viewer/CoreTextProseLayoutEngine.swift +635 -0
  394. package/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.h +4 -0
  395. package/ios/Viewer/Fabric/PREPPreparedProseViewerComponentView.mm +767 -0
  396. package/ios/Viewer/Fabric/PreparedProseMeasurementsManager.mm +118 -0
  397. package/ios/Viewer/NativeSVGImageDecoder.h +9 -0
  398. package/ios/Viewer/NativeSVGImageDecoder.mm +106 -0
  399. package/ios/Viewer/NativeSVGPreflight.h +11 -0
  400. package/ios/Viewer/NativeSVGPreflight.mm +219 -0
  401. package/ios/Viewer/PreparedProseDrawingView.swift +639 -0
  402. package/ios/Viewer/PreparedProseInstrumentation.swift +387 -0
  403. package/ios/Viewer/PreparedProseLayout.swift +326 -0
  404. package/ios/Viewer/PreparedProseLayoutCache.swift +766 -0
  405. package/ios/Viewer/PreparedProseLayoutRegistry+Artifacts.swift +377 -0
  406. package/ios/Viewer/PreparedProseLayoutRegistry+Fabric.swift +582 -0
  407. package/ios/Viewer/PreparedProseLayoutRegistry+FabricOwnership.swift +128 -0
  408. package/ios/Viewer/PreparedProseLayoutRegistry.swift +381 -0
  409. package/ios/Viewer/ViewerDocument.swift +575 -0
  410. package/ios/Viewer/ViewerFontEnvironment.swift +283 -0
  411. package/ios/Viewer/ViewerImagePipeline.swift +478 -0
  412. package/ios/editor_coreFFI/editor_coreFFI.h +1023 -0
  413. package/ios/editor_coreFFI/module.modulemap +7 -0
  414. package/package.json +151 -0
  415. package/react-native.config.js +17 -0
  416. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  417. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  418. package/rust/android/x86/libeditor_core.so +0 -0
  419. package/rust/android/x86_64/libeditor_core.so +0 -0
  420. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +3146 -0
  421. package/src/AtomHost.tsx +180 -0
  422. package/src/DefaultAtomChip.tsx +27 -0
  423. package/src/EditorAddon.ts +65 -0
  424. package/src/EditorAddonNormalization.ts +49 -0
  425. package/src/EditorMentionThemeNormalization.ts +25 -0
  426. package/src/EditorMentionThemeValidation.ts +133 -0
  427. package/src/EditorStyleSheet.ts +9 -0
  428. package/src/EditorStyleSheetNormalization.ts +219 -0
  429. package/src/EditorStyleSheetTypes.ts +189 -0
  430. package/src/EditorTheme.ts +196 -0
  431. package/src/EditorToolbar.tsx +31 -0
  432. package/src/EditorToolbarComponent.tsx +23 -0
  433. package/src/EditorToolbarItems.tsx +64 -0
  434. package/src/EditorToolbarRegistry.tsx +254 -0
  435. package/src/EditorToolbarTypes.tsx +322 -0
  436. package/src/EditorToolbarVisuals.tsx +270 -0
  437. package/src/EditorUpdateRevision.ts +26 -0
  438. package/src/ExternalTextComposition.ts +658 -0
  439. package/src/ImageLoadingPolicy.ts +125 -0
  440. package/src/NativeEditorBoundaryError.ts +345 -0
  441. package/src/NativeEditorBridge.ts +93 -0
  442. package/src/NativeEditorCollaborationTransport.ts +493 -0
  443. package/src/NativeEditorCreateConfig.ts +326 -0
  444. package/src/NativeEditorCreateJson.ts +579 -0
  445. package/src/NativeEditorCreateValidation.ts +178 -0
  446. package/src/NativeEditorDocumentBridge.ts +485 -0
  447. package/src/NativeEditorDocumentHandle.ts +168 -0
  448. package/src/NativeEditorLocalAwareness.ts +168 -0
  449. package/src/NativeEditorNativeModule.ts +97 -0
  450. package/src/NativeEditorRenderNormalization.ts +320 -0
  451. package/src/NativeEditorResultNormalization.ts +469 -0
  452. package/src/NativeEditorTypes.ts +366 -0
  453. package/src/NativeEditorV2Decimal.ts +13 -0
  454. package/src/NativeProseViewer.tsx +356 -0
  455. package/src/NativeRichTextEditor.tsx +33 -0
  456. package/src/ResourceLimits.ts +187 -0
  457. package/src/RichTextEditor.tsx +27 -0
  458. package/src/RichTextEditorNativeTypes.tsx +144 -0
  459. package/src/RichTextEditorNativeView.tsx +21 -0
  460. package/src/RichTextEditorSerialization.tsx +381 -0
  461. package/src/RichTextEditorTypes.tsx +327 -0
  462. package/src/ViewerConfiguration.ts +23 -0
  463. package/src/YjsCollaboration.ts +692 -0
  464. package/src/addons.ts +311 -0
  465. package/src/atomConstants.ts +3 -0
  466. package/src/atomInstances.ts +123 -0
  467. package/src/atomPolicy.ts +47 -0
  468. package/src/atomUpdates.ts +52 -0
  469. package/src/atoms.ts +379 -0
  470. package/src/attributeValidation.ts +116 -0
  471. package/src/contentExpression.ts +338 -0
  472. package/src/index.ts +295 -0
  473. package/src/listMarkers.ts +18 -0
  474. package/src/schemaDefinition.ts +395 -0
  475. package/src/schemaDocument.ts +92 -0
  476. package/src/schemaDocumentConstruction.ts +148 -0
  477. package/src/schemaNormalization.ts +367 -0
  478. package/src/schemaPresets.ts +250 -0
  479. package/src/schemaResolution.ts +242 -0
  480. package/src/schemas.ts +43 -0
  481. package/src/specs/PreparedProseViewerNativeComponent.ts +40 -0
  482. package/src/useEditorToolbarInteractions.tsx +329 -0
  483. package/src/useEditorToolbarItems.tsx +390 -0
  484. package/src/useEditorToolbarPresentation.tsx +483 -0
  485. package/src/useEditorToolbarState.tsx +172 -0
  486. package/src/useFocusPreservingFrames.ts +171 -0
  487. package/src/useNativeEditor.ts +534 -0
  488. package/src/useRichTextEditorCommands.tsx +478 -0
  489. package/src/useRichTextEditorEvents.tsx +342 -0
  490. package/src/useRichTextEditorMentions.tsx +441 -0
  491. package/src/useRichTextEditorPresentation.tsx +563 -0
  492. package/src/useRichTextEditorState.tsx +620 -0
  493. package/src/useRichTextEditorUpdates.tsx +403 -0
  494. package/src/useViewerAtoms.tsx +427 -0
@@ -0,0 +1,930 @@
1
+ import UIKit
2
+
3
+ enum EditorHeightBehavior: String {
4
+ case fixed
5
+ case autoGrow
6
+ }
7
+
8
+ // MARK: - RichTextEditorView (Fabric Host)
9
+
10
+ final class AtomHostContainerView: UIView {
11
+ let atomKey: String
12
+ private(set) var hostedView: UIView?
13
+ weak var editorView: RichTextEditorView?
14
+
15
+ private var boundsObservation: NSKeyValueObservation?
16
+ private var isDetachingHostedView = false
17
+ private var lastReportedHeight: CGFloat?
18
+
19
+ init(atomKey: String) {
20
+ self.atomKey = atomKey
21
+ super.init(frame: .zero)
22
+ clipsToBounds = false
23
+ isUserInteractionEnabled = true
24
+ }
25
+
26
+ required init?(coder: NSCoder) {
27
+ fatalError("init(coder:) has not been implemented")
28
+ }
29
+
30
+ func host(_ view: UIView) {
31
+ if hostedView !== view {
32
+ _ = detachHostedView()
33
+ }
34
+ view.removeFromSuperview()
35
+ hostedView = view
36
+ addSubview(view)
37
+ boundsObservation = view.observe(\.bounds, options: [.new]) { [weak self] _, _ in
38
+ self?.reportHeightIfNeeded()
39
+ }
40
+ setNeedsLayout()
41
+ reportHeightIfNeeded()
42
+ }
43
+
44
+ @discardableResult
45
+ func detachHostedView() -> UIView? {
46
+ boundsObservation = nil
47
+ guard let hostedView else { return nil }
48
+ self.hostedView = nil
49
+ lastReportedHeight = nil
50
+ isDetachingHostedView = true
51
+ hostedView.removeFromSuperview()
52
+ isDetachingHostedView = false
53
+ return hostedView
54
+ }
55
+
56
+ override func layoutSubviews() {
57
+ super.layoutSubviews()
58
+ reportHeightIfNeeded()
59
+ guard let hostedView, lastReportedHeight != nil else { return }
60
+ let height = hostedView.bounds.height
61
+ let frame = CGRect(x: 0, y: 0, width: bounds.width, height: height)
62
+ if hostedView.frame != frame {
63
+ hostedView.frame = frame
64
+ }
65
+ reportHeightIfNeeded()
66
+ }
67
+
68
+ override func willRemoveSubview(_ subview: UIView) {
69
+ super.willRemoveSubview(subview)
70
+ guard !isDetachingHostedView, subview === hostedView else { return }
71
+ boundsObservation = nil
72
+ hostedView = nil
73
+ lastReportedHeight = nil
74
+ editorView?.atomHostContainerDidLoseHostedView(self)
75
+ }
76
+
77
+ private func reportHeightIfNeeded() {
78
+ guard let height = hostedView?.bounds.height,
79
+ height.isFinite,
80
+ height >= 0,
81
+ height > 0 || (hostedView?.bounds.width ?? 0) > 0,
82
+ lastReportedHeight.map({ abs($0 - height) > 0.5 }) ?? true
83
+ else { return }
84
+ lastReportedHeight = height
85
+ editorView?.atomHostContainer(self, didMeasureHeight: height)
86
+ }
87
+ }
88
+
89
+ /// The top-level container view that a Fabric component would own.
90
+ ///
91
+ /// Hosts the EditorTextView. In a full Fabric integration, this would be
92
+ /// a `RCTViewComponentView` subclass registered via the component descriptor.
93
+ ///
94
+ /// For now, this is a plain UIView that can be used in a UIKit context
95
+ /// and serves as the integration point for the future Fabric component.
96
+ final class RichTextEditorView: UIView {
97
+
98
+ struct HostedLayoutTrace {
99
+ let intrinsicContentSizeNanos: UInt64
100
+ let intrinsicContentSizeCount: Int
101
+ let measuredEditorHeightNanos: UInt64
102
+ let measuredEditorHeightCount: Int
103
+ let layoutSubviewsNanos: UInt64
104
+ let layoutSubviewsCount: Int
105
+ let refreshOverlaysNanos: UInt64
106
+ let refreshOverlaysCount: Int
107
+ let overlayScheduleRequestCount: Int
108
+ let overlayScheduleExecuteCount: Int
109
+ let overlayScheduleSkipCount: Int
110
+ let onHeightMayChangeNanos: UInt64
111
+ let onHeightMayChangeCount: Int
112
+ }
113
+
114
+ // MARK: - Properties
115
+
116
+ /// The editor text view that handles input interception.
117
+ let textView: EditorTextView
118
+ private let defaultImageLoadOwner = RenderImageLoadOwner(policy: .default)
119
+ var imageLoadOwner: RenderImageLoadOwner {
120
+ get { textView.imageLoadOwner ?? defaultImageLoadOwner }
121
+ set {
122
+ guard textView.imageLoadOwner !== newValue else { return }
123
+ textView.imageLoadOwner?.cancelAll()
124
+ textView.imageLoadOwner = newValue
125
+ textView.imageLoadingPolicyDidChange()
126
+ }
127
+ }
128
+ private let remoteSelectionOverlayView = RemoteSelectionOverlayView()
129
+ private let taskListMarkerTapOverlayView = TaskListMarkerTapOverlayView()
130
+ private let imageTapOverlayView = ImageTapOverlayView()
131
+ private let imageResizeOverlayView = ImageResizeOverlayView()
132
+ var onHeightMayChange: ((CGFloat) -> Void)?
133
+ var onAtomContentWidthChange: ((CGFloat) -> Void)?
134
+ private var lastAutoGrowWidth: CGFloat = 0
135
+ private var cachedAutoGrowMeasuredHeight: CGFloat = 0
136
+ private var atomRenderConfiguration: AtomRenderConfiguration?
137
+ private var atomHostContainers: [String: AtomHostContainerView] = [:]
138
+ private var measuredAtomHeights: [String: CGFloat] = [:]
139
+ private var fallbackAtomHeights: [String: CGFloat] = [:]
140
+ private var lastAtomContentWidth: CGFloat = 0
141
+ private var lastAtomPositions: [[String: AnyHashable]] = []
142
+ private var lastAtomViewport = CGRect.zero
143
+ private(set) var atomLayoutInvalidationCountForTesting = 0
144
+ private var remoteSelections: [RemoteSelectionDecoration] = []
145
+ private var initialUpdateJSONForNextEditorBind: String?
146
+ private var overlayRefreshScheduled = false
147
+ var captureHostedLayoutTraceForTesting = false
148
+ private var hostedLayoutTraceNanos = (
149
+ intrinsicContentSize: UInt64(0),
150
+ measuredEditorHeight: UInt64(0),
151
+ layoutSubviews: UInt64(0),
152
+ refreshOverlays: UInt64(0),
153
+ onHeightMayChange: UInt64(0)
154
+ )
155
+ private var hostedLayoutTraceCounts = (
156
+ intrinsicContentSize: 0,
157
+ measuredEditorHeight: 0,
158
+ layoutSubviews: 0,
159
+ refreshOverlays: 0,
160
+ overlayScheduleRequest: 0,
161
+ overlayScheduleExecute: 0,
162
+ overlayScheduleSkip: 0,
163
+ onHeightMayChange: 0
164
+ )
165
+ var allowImageResizing = true {
166
+ didSet {
167
+ guard oldValue != allowImageResizing else { return }
168
+ textView.allowImageResizing = allowImageResizing
169
+ textView.refreshSelectionVisualState()
170
+ imageTapOverlayView.isHidden = editorId == 0 || !allowImageResizing
171
+ imageResizeOverlayView.refresh()
172
+ }
173
+ }
174
+
175
+ var heightBehavior: EditorHeightBehavior = .fixed {
176
+ didSet {
177
+ guard oldValue != heightBehavior else { return }
178
+ textView.heightBehavior = heightBehavior
179
+ textView.updateAutoGrowHostHeight(heightBehavior == .autoGrow ? bounds.height : 0)
180
+ if heightBehavior != .autoGrow {
181
+ cachedAutoGrowMeasuredHeight = 0
182
+ }
183
+ invalidateIntrinsicContentSize()
184
+ setNeedsLayout()
185
+ if heightBehavior == .autoGrow {
186
+ let measuredHeight = measuredEditorHeight()
187
+ if measuredHeight > 0 {
188
+ cachedAutoGrowMeasuredHeight = measuredHeight
189
+ onHeightMayChange?(measuredHeight)
190
+ } else {
191
+ onHeightMayChange?(0)
192
+ }
193
+ } else {
194
+ onHeightMayChange?(0)
195
+ }
196
+ remoteSelectionOverlayView.refresh()
197
+ imageResizeOverlayView.refresh()
198
+ }
199
+ }
200
+
201
+ /// The Rust editor instance ID. Setting this binds/unbinds the editor.
202
+ var editorId: UInt64 = 0 {
203
+ didSet {
204
+ guard oldValue != editorId else { return }
205
+ textView.discardTransientNativeInputForEditorRebind()
206
+ if editorId != 0 {
207
+ let initialUpdateJSON = initialUpdateJSONForNextEditorBind
208
+ initialUpdateJSONForNextEditorBind = nil
209
+ textView.bindEditor(id: editorId, initialUpdateJSON: initialUpdateJSON)
210
+ } else {
211
+ initialUpdateJSONForNextEditorBind = nil
212
+ textView.unbindEditor()
213
+ }
214
+ remoteSelectionOverlayView.update(
215
+ selections: remoteSelections,
216
+ editorId: editorId
217
+ )
218
+ imageTapOverlayView.isHidden = editorId == 0 || !allowImageResizing
219
+ imageResizeOverlayView.refresh()
220
+ }
221
+ }
222
+
223
+ func bindEditor(id: UInt64, initialUpdateJSON: String?) {
224
+ guard editorId != id else { return }
225
+ initialUpdateJSONForNextEditorBind = initialUpdateJSON
226
+ editorId = id
227
+ }
228
+
229
+ // MARK: - Initialization
230
+
231
+ override init(frame: CGRect) {
232
+ textView = EditorTextView(frame: .zero, textContainer: nil)
233
+ super.init(frame: frame)
234
+ setupView()
235
+ }
236
+
237
+ required init?(coder: NSCoder) {
238
+ textView = EditorTextView(frame: .zero, textContainer: nil)
239
+ super.init(coder: coder)
240
+ setupView()
241
+ }
242
+
243
+ private func setupView() {
244
+ // Add the text view as a subview. These views always track the host bounds,
245
+ // so manual layout is cheaper than driving them through Auto Layout.
246
+ textView.imageLoadOwner = defaultImageLoadOwner
247
+ remoteSelectionOverlayView.bind(textView: textView)
248
+ taskListMarkerTapOverlayView.bind(editorView: self)
249
+ imageTapOverlayView.bind(editorView: self)
250
+ imageResizeOverlayView.bind(editorView: self)
251
+ textView.allowImageResizing = allowImageResizing
252
+ imageTapOverlayView.isHidden = editorId == 0 || !allowImageResizing
253
+ textView.onHeightMayChange = { [weak self] measuredHeight in
254
+ guard let self, self.heightBehavior == .autoGrow else { return }
255
+ let startedAt = DispatchTime.now().uptimeNanoseconds
256
+ self.cachedAutoGrowMeasuredHeight = measuredHeight
257
+ self.invalidateIntrinsicContentSize()
258
+ self.onHeightMayChange?(measuredHeight)
259
+ self.recordHostedLayoutTrace(
260
+ durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
261
+ keyPath: .onHeightMayChange
262
+ )
263
+ }
264
+ textView.onViewportMayChange = { [weak self] in
265
+ self?.layoutManagedSubviews()
266
+ self?.refreshOverlaysIfNeeded()
267
+ self?.emitAtomContentWidthIfAvailable()
268
+ }
269
+ textView.onSelectionOrContentMayChange = { [weak self] in
270
+ self?.scheduleRefreshOverlaysIfNeeded()
271
+ }
272
+ addSubview(textView)
273
+ addSubview(remoteSelectionOverlayView)
274
+ addSubview(taskListMarkerTapOverlayView)
275
+ // Image touches must stay inside the scroll view's gesture hierarchy.
276
+ textView.addSubview(imageTapOverlayView)
277
+ addSubview(imageResizeOverlayView)
278
+ layoutManagedSubviews()
279
+ }
280
+
281
+ override var intrinsicContentSize: CGSize {
282
+ let startedAt = DispatchTime.now().uptimeNanoseconds
283
+ defer {
284
+ recordHostedLayoutTrace(
285
+ durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
286
+ keyPath: .intrinsicContentSize
287
+ )
288
+ }
289
+ guard heightBehavior == .autoGrow else {
290
+ return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
291
+ }
292
+
293
+ let measuredHeight = measuredEditorHeight()
294
+ guard measuredHeight > 0 else {
295
+ return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
296
+ }
297
+ return CGSize(width: UIView.noIntrinsicMetric, height: measuredHeight)
298
+ }
299
+
300
+ override func layoutSubviews() {
301
+ let startedAt = DispatchTime.now().uptimeNanoseconds
302
+ defer {
303
+ recordHostedLayoutTrace(
304
+ durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
305
+ keyPath: .layoutSubviews
306
+ )
307
+ }
308
+ super.layoutSubviews()
309
+ updateStyleContentMask()
310
+ layoutManagedSubviews()
311
+ layoutAtomHostContainers()
312
+ refreshOverlaysIfNeeded()
313
+ guard heightBehavior == .autoGrow else { return }
314
+ textView.updateAutoGrowHostHeight(bounds.height)
315
+ let currentWidth = bounds.width.rounded(.towardZero)
316
+ guard currentWidth != lastAutoGrowWidth else { return }
317
+ lastAutoGrowWidth = currentWidth
318
+ cachedAutoGrowMeasuredHeight = 0
319
+ invalidateIntrinsicContentSize()
320
+ }
321
+
322
+ // MARK: - Configuration
323
+
324
+ /// Configure the editor's appearance.
325
+ ///
326
+ /// - Parameters:
327
+ /// - font: Base font for unstyled text.
328
+ /// - textColor: Default text color.
329
+ /// - backgroundColor: Background color for the text view.
330
+ func configure(
331
+ font: UIFont = .systemFont(ofSize: 16),
332
+ textColor: UIColor = .label,
333
+ backgroundColor: UIColor = .systemBackground
334
+ ) {
335
+ textView.baseFont = font
336
+ textView.baseTextColor = textColor
337
+ textView.baseBackgroundColor = backgroundColor
338
+ textView.font = font
339
+ textView.textColor = textColor
340
+ textView.backgroundColor = backgroundColor
341
+ }
342
+
343
+ @discardableResult
344
+ func applyTheme(_ theme: EditorTheme?) -> Bool {
345
+ guard textView.applyTheme(theme) else { return false }
346
+ let cornerRadius = theme?.borderRadius ?? 0
347
+ layer.cornerRadius = cornerRadius
348
+ clipsToBounds = cornerRadius > 0
349
+ updateStyleContentMask()
350
+ refreshOverlays()
351
+ return true
352
+ }
353
+
354
+ private func updateStyleContentMask() {
355
+ guard let box = textView.theme?.styleSheet?.box("content"), box.radii.contains(where: { $0 > 0 }) else {
356
+ layer.mask = nil
357
+ return
358
+ }
359
+ let mask = (layer.mask as? CAShapeLayer) ?? CAShapeLayer()
360
+ mask.path = box.path(in: bounds).cgPath
361
+ layer.mask = mask
362
+ }
363
+
364
+ @discardableResult
365
+ func applyAtomRenderConfiguration(_ configuration: AtomRenderConfiguration?) -> Bool {
366
+ let previousConfiguration = atomRenderConfiguration
367
+ atomRenderConfiguration = configuration
368
+ guard textView.applyAtomRenderConfiguration(resolvedAtomRenderConfiguration()) else {
369
+ atomRenderConfiguration = previousConfiguration
370
+ return false
371
+ }
372
+ layoutAtomHostContainers()
373
+ refreshOverlays()
374
+ return true
375
+ }
376
+
377
+ func mountAtomChild(_ child: UIView, atomKey: String) {
378
+ if let existing = atomHostContainers[atomKey] {
379
+ if existing.hostedView === child {
380
+ if child.superview !== existing {
381
+ existing.host(child)
382
+ }
383
+ layoutAtomHostContainers()
384
+ return
385
+ }
386
+ _ = existing.detachHostedView()
387
+ existing.removeFromSuperview()
388
+ }
389
+
390
+ let container = AtomHostContainerView(atomKey: atomKey)
391
+ container.editorView = self
392
+ atomHostContainers[atomKey] = container
393
+ textView.addSubview(container)
394
+ container.host(child)
395
+ layoutAtomHostContainers()
396
+ }
397
+
398
+ @discardableResult
399
+ func unmountAtomChild(_ child: UIView) -> Bool {
400
+ guard let entry = atomHostContainers.first(where: { $0.value.hostedView === child }) else {
401
+ return false
402
+ }
403
+ removeAtomHostContainer(for: entry.key, removeChild: true)
404
+ return true
405
+ }
406
+
407
+ func atomHostContainer(for atomKey: String) -> AtomHostContainerView? {
408
+ atomHostContainers[atomKey]
409
+ }
410
+
411
+ func measuredAtomHeight(for atomKey: String) -> CGFloat? {
412
+ measuredAtomHeights[atomKey]
413
+ }
414
+
415
+ func emitAtomContentWidthIfAvailable(force: Bool = false) {
416
+ let width = atomContentWidth()
417
+ let positions = atomLayoutPositions()
418
+ let viewport = textView.bounds
419
+ guard width > 0,
420
+ force || abs(width - lastAtomContentWidth) > 0.5
421
+ || positions != lastAtomPositions || viewport != lastAtomViewport
422
+ else { return }
423
+ lastAtomContentWidth = width
424
+ lastAtomPositions = positions
425
+ lastAtomViewport = viewport
426
+ onAtomContentWidthChange?(width)
427
+ }
428
+
429
+ func atomLayoutPositions() -> [[String: AnyHashable]] {
430
+ let padding = textView.textContainer.lineFragmentPadding
431
+ return atomAttachmentEntries().sorted { $0.value.1.location < $1.value.1.location }.map { key, entry in
432
+ let (attachment, range) = entry
433
+ textView.layoutManager.ensureLayout(forCharacterRange: range)
434
+ let glyphs = textView.layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
435
+ let rect = textView.layoutManager.boundingRect(forGlyphRange: glyphs, in: textView.textContainer)
436
+ return [
437
+ "key": key,
438
+ "x": Double(textView.textContainerInset.left + padding),
439
+ "y": Double(textView.textContainerInset.top + rect.minY),
440
+ "height": Double(attachment.reservedHeight)
441
+ ]
442
+ }
443
+ }
444
+
445
+ fileprivate func atomHostContainer(
446
+ _ container: AtomHostContainerView,
447
+ didMeasureHeight height: CGFloat
448
+ ) {
449
+ setAtomHeight(key: container.atomKey, height: height)
450
+ }
451
+
452
+ fileprivate func atomHostContainerDidLoseHostedView(_ container: AtomHostContainerView) {
453
+ guard atomHostContainers[container.atomKey] === container else { return }
454
+ removeAtomHostContainer(for: container.atomKey, removeChild: false)
455
+ }
456
+
457
+ func setAtomHeight(key atomKey: String, height: CGFloat) {
458
+ let previousMeasuredHeight = measuredAtomHeights[atomKey]
459
+ guard height.isFinite,
460
+ height >= 0,
461
+ measuredAtomHeights[atomKey].map({ abs($0 - height) > 0.5 }) ?? true
462
+ else { return }
463
+
464
+ measuredAtomHeights[atomKey] = height
465
+ textView.atomRenderConfiguration = resolvedAtomRenderConfiguration()
466
+ guard let entry = atomAttachmentEntry(for: atomKey) else { return }
467
+ let fallbackHeight = atomRenderConfiguration?.estimatedHeights[entry.attachment.nodeType]
468
+ ?? (previousMeasuredHeight == nil && entry.attachment.reservedHeight > 0
469
+ ? entry.attachment.reservedHeight
470
+ : nil)
471
+ if let fallbackHeight {
472
+ fallbackAtomHeights[atomKey] = fallbackAtomHeights[atomKey] ?? fallbackHeight
473
+ }
474
+ updateAtomAttachment(entry, height: height)
475
+ }
476
+
477
+ private func removeAtomHostContainer(for atomKey: String, removeChild: Bool) {
478
+ guard let container = atomHostContainers.removeValue(forKey: atomKey) else { return }
479
+ container.editorView = nil
480
+ let child = container.detachHostedView()
481
+ container.removeFromSuperview()
482
+ if removeChild {
483
+ child?.removeFromSuperview()
484
+ }
485
+
486
+ measuredAtomHeights.removeValue(forKey: atomKey)
487
+ textView.atomRenderConfiguration = resolvedAtomRenderConfiguration()
488
+ if let entry = atomAttachmentEntry(for: atomKey) {
489
+ let fallbackHeight = fallbackAtomHeights.removeValue(forKey: atomKey)
490
+ ?? atomRenderConfiguration?.estimatedHeights[entry.attachment.nodeType]
491
+ if let fallbackHeight {
492
+ updateAtomAttachment(entry, height: fallbackHeight)
493
+ }
494
+ }
495
+ }
496
+
497
+ private func resolvedAtomRenderConfiguration() -> AtomRenderConfiguration? {
498
+ guard let atomRenderConfiguration else { return nil }
499
+ var heights = atomRenderConfiguration.measuredHeights
500
+ heights.merge(measuredAtomHeights) { _, measured in measured }
501
+ return AtomRenderConfiguration(
502
+ registeredNodeTypes: atomRenderConfiguration.registeredNodeTypes,
503
+ estimatedHeights: atomRenderConfiguration.estimatedHeights,
504
+ measuredHeights: heights
505
+ )
506
+ }
507
+
508
+ private func updateAtomAttachment(
509
+ _ entry: (attachment: AtomBlockAttachment, range: NSRange),
510
+ height: CGFloat
511
+ ) {
512
+ guard abs(entry.attachment.reservedHeight - height) > 0.5 else { return }
513
+ entry.attachment.reservedHeight = height
514
+ atomLayoutInvalidationCountForTesting += 1
515
+ textView.layoutManager.invalidateLayout(
516
+ forCharacterRange: entry.range,
517
+ actualCharacterRange: nil
518
+ )
519
+ textView.layoutManager.invalidateDisplay(forCharacterRange: entry.range)
520
+ textView.setNeedsLayout()
521
+ setNeedsLayout()
522
+ layoutAtomHostContainers()
523
+ }
524
+
525
+ private func atomAttachmentEntry(
526
+ for atomKey: String
527
+ ) -> (attachment: AtomBlockAttachment, range: NSRange)? {
528
+ guard textView.textStorage.length > 0 else { return nil }
529
+ var match: (attachment: AtomBlockAttachment, range: NSRange)?
530
+ textView.textStorage.enumerateAttribute(
531
+ .attachment,
532
+ in: NSRange(location: 0, length: textView.textStorage.length),
533
+ options: [.longestEffectiveRangeNotRequired]
534
+ ) { value, range, stop in
535
+ guard let attachment = value as? AtomBlockAttachment,
536
+ attachment.atomKey == atomKey
537
+ else { return }
538
+ match = (attachment, range)
539
+ stop.pointee = true
540
+ }
541
+ return match
542
+ }
543
+
544
+ private func atomAttachmentEntries() -> [String: (AtomBlockAttachment, NSRange)] {
545
+ guard textView.textStorage.length > 0 else { return [:] }
546
+ var entries: [String: (AtomBlockAttachment, NSRange)] = [:]
547
+ textView.textStorage.enumerateAttribute(
548
+ .attachment,
549
+ in: NSRange(location: 0, length: textView.textStorage.length),
550
+ options: [.longestEffectiveRangeNotRequired]
551
+ ) { value, range, _ in
552
+ guard let attachment = value as? AtomBlockAttachment else { return }
553
+ entries[attachment.atomKey] = (attachment, range)
554
+ }
555
+ return entries
556
+ }
557
+
558
+ private func atomContentWidth() -> CGFloat {
559
+ let padding = textView.textContainer.lineFragmentPadding
560
+ return max(0, textView.textContainer.size.width - (padding * 2))
561
+ }
562
+
563
+ private func layoutAtomHostContainers() {
564
+ emitAtomContentWidthIfAvailable()
565
+ guard !atomHostContainers.isEmpty else { return }
566
+ let entries = atomAttachmentEntries()
567
+ let padding = textView.textContainer.lineFragmentPadding
568
+ let width = atomContentWidth()
569
+
570
+ for (atomKey, container) in atomHostContainers {
571
+ guard let (attachment, characterRange) = entries[atomKey] else {
572
+ container.isHidden = true
573
+ continue
574
+ }
575
+ textView.layoutManager.ensureLayout(forCharacterRange: characterRange)
576
+ let glyphRange = textView.layoutManager.glyphRange(
577
+ forCharacterRange: characterRange,
578
+ actualCharacterRange: nil
579
+ )
580
+ let attachmentRect = textView.layoutManager.boundingRect(
581
+ forGlyphRange: glyphRange,
582
+ in: textView.textContainer
583
+ )
584
+ let frame = CGRect(
585
+ x: textView.textContainerInset.left + padding,
586
+ y: textView.textContainerInset.top + attachmentRect.minY,
587
+ width: width,
588
+ height: attachment.reservedHeight
589
+ )
590
+ if container.frame != frame {
591
+ container.frame = frame
592
+ container.setNeedsLayout()
593
+ }
594
+ textView.bringSubviewToFront(container)
595
+ container.isHidden = false
596
+ }
597
+ }
598
+
599
+ func setRemoteSelections(_ selections: [RemoteSelectionDecoration]) {
600
+ remoteSelections = selections
601
+ remoteSelectionOverlayView.update(
602
+ selections: selections,
603
+ editorId: editorId
604
+ )
605
+ }
606
+
607
+ func refreshRemoteSelections() {
608
+ guard remoteSelectionOverlayView.hasSelectionsOrVisibleDecorations else { return }
609
+ remoteSelectionOverlayView.refresh()
610
+ }
611
+
612
+ func currentCaretRect() -> CGRect? {
613
+ guard let selectedTextRange = textView.selectedTextRange else { return nil }
614
+ let rect = textView.caretRect(for: selectedTextRange.end)
615
+ guard rect.height > 0 else { return nil }
616
+ return textView.convert(rect, to: self)
617
+ }
618
+
619
+ func remoteSelectionOverlaySubviewsForTesting() -> [UIView] {
620
+ remoteSelectionOverlayView.subviews.filter { !$0.isHidden }
621
+ }
622
+
623
+ func resetHostedLayoutTraceForTesting() {
624
+ hostedLayoutTraceNanos = (
625
+ intrinsicContentSize: 0,
626
+ measuredEditorHeight: 0,
627
+ layoutSubviews: 0,
628
+ refreshOverlays: 0,
629
+ onHeightMayChange: 0
630
+ )
631
+ hostedLayoutTraceCounts = (
632
+ intrinsicContentSize: 0,
633
+ measuredEditorHeight: 0,
634
+ layoutSubviews: 0,
635
+ refreshOverlays: 0,
636
+ overlayScheduleRequest: 0,
637
+ overlayScheduleExecute: 0,
638
+ overlayScheduleSkip: 0,
639
+ onHeightMayChange: 0
640
+ )
641
+ }
642
+
643
+ func lastHostedLayoutTraceForTesting() -> HostedLayoutTrace {
644
+ HostedLayoutTrace(
645
+ intrinsicContentSizeNanos: hostedLayoutTraceNanos.intrinsicContentSize,
646
+ intrinsicContentSizeCount: hostedLayoutTraceCounts.intrinsicContentSize,
647
+ measuredEditorHeightNanos: hostedLayoutTraceNanos.measuredEditorHeight,
648
+ measuredEditorHeightCount: hostedLayoutTraceCounts.measuredEditorHeight,
649
+ layoutSubviewsNanos: hostedLayoutTraceNanos.layoutSubviews,
650
+ layoutSubviewsCount: hostedLayoutTraceCounts.layoutSubviews,
651
+ refreshOverlaysNanos: hostedLayoutTraceNanos.refreshOverlays,
652
+ refreshOverlaysCount: hostedLayoutTraceCounts.refreshOverlays,
653
+ overlayScheduleRequestCount: hostedLayoutTraceCounts.overlayScheduleRequest,
654
+ overlayScheduleExecuteCount: hostedLayoutTraceCounts.overlayScheduleExecute,
655
+ overlayScheduleSkipCount: hostedLayoutTraceCounts.overlayScheduleSkip,
656
+ onHeightMayChangeNanos: hostedLayoutTraceNanos.onHeightMayChange,
657
+ onHeightMayChangeCount: hostedLayoutTraceCounts.onHeightMayChange
658
+ )
659
+ }
660
+
661
+ func imageResizeOverlayRectForTesting() -> CGRect? {
662
+ imageResizeOverlayView.visibleRectForTesting
663
+ }
664
+
665
+ func imageTapOverlayInterceptsPointForTesting(_ point: CGPoint) -> Bool {
666
+ imageTapOverlayView.interceptsPointForTesting(convert(point, to: imageTapOverlayView))
667
+ }
668
+
669
+ func taskListMarkerTapOverlayInterceptsPointForTesting(_ point: CGPoint) -> Bool {
670
+ taskListMarkerTapOverlayView.interceptsPointForTesting(
671
+ convert(point, to: taskListMarkerTapOverlayView)
672
+ )
673
+ }
674
+
675
+ @discardableResult
676
+ func tapTaskListMarkerOverlayForTesting(at point: CGPoint) -> Bool {
677
+ taskListMarkerTapOverlayView.handleTapForTesting(
678
+ convert(point, to: taskListMarkerTapOverlayView)
679
+ )
680
+ }
681
+
682
+ @discardableResult
683
+ func tapImageOverlayForTesting(at point: CGPoint) -> Bool {
684
+ imageTapOverlayView.handleTapForTesting(convert(point, to: imageTapOverlayView))
685
+ }
686
+
687
+ func imageResizePreviewHasImageForTesting() -> Bool {
688
+ imageResizeOverlayView.previewHasImageForTesting
689
+ }
690
+
691
+ func refreshSelectionVisualStateForTesting() {
692
+ textView.refreshSelectionVisualState()
693
+ }
694
+
695
+ func imageResizeOverlayInterceptsPointForTesting(_ point: CGPoint) -> Bool {
696
+ imageResizeOverlayView.interceptsPointForTesting(convert(point, to: imageResizeOverlayView))
697
+ }
698
+
699
+ func maximumImageWidthForTesting() -> CGFloat {
700
+ textView.maximumRenderableImageWidth()
701
+ }
702
+
703
+ func resizeSelectedImageForTesting(width: CGFloat, height: CGFloat) {
704
+ imageResizeOverlayView.simulateResizeForTesting(width: width, height: height)
705
+ }
706
+
707
+ func previewResizeSelectedImageForTesting(width: CGFloat, height: CGFloat) {
708
+ imageResizeOverlayView.simulatePreviewResizeForTesting(width: width, height: height)
709
+ }
710
+
711
+ func commitPreviewResizeForTesting() {
712
+ imageResizeOverlayView.commitPreviewResizeForTesting()
713
+ }
714
+
715
+ /// Set initial content from HTML.
716
+ ///
717
+ /// - Parameter html: The HTML string to load.
718
+ func setContent(html: String) {
719
+ guard editorId != 0 else { return }
720
+ let updateJSON = EditorV2Shadow.setHtml(id: editorId, html: html)
721
+ if !textView.applyUpdateJSON(updateJSON, notifyDelegate: false) {
722
+ textView.applyUpdateJSON(EditorV2Shadow.getCurrentState(id: editorId), notifyDelegate: false)
723
+ }
724
+ }
725
+
726
+ /// Set initial content from ProseMirror JSON.
727
+ ///
728
+ /// - Parameter json: The JSON string to load.
729
+ func setContent(json: String) {
730
+ guard editorId != 0 else { return }
731
+ let updateJSON = EditorV2Shadow.setJson(id: editorId, json: json)
732
+ if !textView.applyUpdateJSON(updateJSON, notifyDelegate: false) {
733
+ textView.applyUpdateJSON(EditorV2Shadow.getCurrentState(id: editorId), notifyDelegate: false)
734
+ }
735
+ }
736
+
737
+ private func measuredEditorHeight() -> CGFloat {
738
+ let startedAt = DispatchTime.now().uptimeNanoseconds
739
+ defer {
740
+ recordHostedLayoutTrace(
741
+ durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
742
+ keyPath: .measuredEditorHeight
743
+ )
744
+ }
745
+ if cachedAutoGrowMeasuredHeight > 0 {
746
+ return cachedAutoGrowMeasuredHeight
747
+ }
748
+ let width = resolvedMeasurementWidth()
749
+ guard width > 0 else { return 0 }
750
+ let measuredHeight = textView.measuredAutoGrowHeightForTesting(width: width)
751
+ if measuredHeight > 0 {
752
+ cachedAutoGrowMeasuredHeight = measuredHeight
753
+ }
754
+ return measuredHeight
755
+ }
756
+
757
+ func remeasureAutoGrowHeight() -> CGFloat {
758
+ guard heightBehavior == .autoGrow else { return 0 }
759
+ cachedAutoGrowMeasuredHeight = 0
760
+ let measuredHeight = measuredEditorHeight()
761
+ invalidateIntrinsicContentSize()
762
+ return measuredHeight
763
+ }
764
+
765
+ private func resolvedMeasurementWidth() -> CGFloat {
766
+ if bounds.width > 0 {
767
+ return bounds.width
768
+ }
769
+ if superview?.bounds.width ?? 0 > 0 {
770
+ return superview?.bounds.width ?? 0
771
+ }
772
+ return UIScreen.main.bounds.width
773
+ }
774
+
775
+ private func layoutManagedSubviews() {
776
+ let managedFrame = bounds
777
+ if textView.frame != managedFrame {
778
+ textView.frame = managedFrame
779
+ }
780
+ if remoteSelectionOverlayView.frame != managedFrame {
781
+ remoteSelectionOverlayView.frame = managedFrame
782
+ }
783
+ if taskListMarkerTapOverlayView.frame != managedFrame {
784
+ taskListMarkerTapOverlayView.frame = managedFrame
785
+ }
786
+ if imageTapOverlayView.frame != textView.bounds {
787
+ imageTapOverlayView.frame = textView.bounds
788
+ }
789
+ if imageResizeOverlayView.frame != managedFrame {
790
+ imageResizeOverlayView.frame = managedFrame
791
+ }
792
+ }
793
+
794
+ func selectedImageGeometry() -> (docPos: UInt32, rect: CGRect)? {
795
+ guard let geometry = textView.selectedImageGeometry() else { return nil }
796
+ return (
797
+ docPos: geometry.docPos,
798
+ rect: textView.convert(geometry.rect, to: imageResizeOverlayView)
799
+ )
800
+ }
801
+
802
+ func setImageResizePreviewActive(_ active: Bool) {
803
+ textView.setImageResizePreviewActive(active)
804
+ }
805
+
806
+ func imagePreviewForResize(docPos: UInt32) -> UIImage? {
807
+ textView.imagePreviewForDocPos(docPos)
808
+ }
809
+
810
+ func imageResizePreviewBackgroundColor() -> UIColor {
811
+ textView.backgroundColor ?? .systemBackground
812
+ }
813
+
814
+ func maximumImageWidthForResizeGesture() -> CGFloat {
815
+ textView.maximumRenderableImageWidth()
816
+ }
817
+
818
+ func clampedImageSize(_ size: CGSize, maximumWidth: CGFloat? = nil) -> CGSize {
819
+ let aspectRatio = max(size.width / max(size.height, 1), 0.1)
820
+ let maxWidth = max(48, maximumWidth ?? textView.maximumRenderableImageWidth())
821
+ let clampedWidth = min(maxWidth, max(48, size.width))
822
+ let clampedHeight = max(48, clampedWidth / aspectRatio)
823
+ return CGSize(width: clampedWidth, height: clampedHeight)
824
+ }
825
+
826
+ func resizeImage(docPos: UInt32, size: CGSize) {
827
+ let clampedSize = clampedImageSize(size)
828
+ let width = max(48, Int(clampedSize.width.rounded()))
829
+ let height = max(48, Int(clampedSize.height.rounded()))
830
+ textView.resizeImageAtDocPos(docPos, width: UInt32(width), height: UInt32(height))
831
+ }
832
+
833
+ private func refreshOverlays() {
834
+ let startedAt = DispatchTime.now().uptimeNanoseconds
835
+ defer {
836
+ recordHostedLayoutTrace(
837
+ durationNanos: DispatchTime.now().uptimeNanoseconds - startedAt,
838
+ keyPath: .refreshOverlays
839
+ )
840
+ }
841
+ layoutAtomHostContainers()
842
+ remoteSelectionOverlayView.refresh()
843
+ imageResizeOverlayView.refresh()
844
+ }
845
+
846
+ private func refreshOverlaysIfNeeded() {
847
+ guard shouldRefreshOverlays() else { return }
848
+ refreshOverlays()
849
+ }
850
+
851
+ private func scheduleRefreshOverlaysIfNeeded() {
852
+ if !shouldRefreshOverlays() {
853
+ if captureHostedLayoutTraceForTesting {
854
+ hostedLayoutTraceCounts.overlayScheduleSkip += 1
855
+ }
856
+ return
857
+ }
858
+ scheduleRefreshOverlays()
859
+ }
860
+
861
+ private func scheduleRefreshOverlays() {
862
+ if captureHostedLayoutTraceForTesting {
863
+ hostedLayoutTraceCounts.overlayScheduleRequest += 1
864
+ }
865
+ guard !overlayRefreshScheduled else { return }
866
+ overlayRefreshScheduled = true
867
+ DispatchQueue.main.async { [weak self] in
868
+ guard let self else { return }
869
+ self.overlayRefreshScheduled = false
870
+ if self.captureHostedLayoutTraceForTesting {
871
+ self.hostedLayoutTraceCounts.overlayScheduleExecute += 1
872
+ }
873
+ self.refreshOverlays()
874
+ }
875
+ }
876
+
877
+ private func shouldRefreshOverlays() -> Bool {
878
+ if !atomHostContainers.isEmpty {
879
+ return true
880
+ }
881
+ if !remoteSelections.isEmpty || remoteSelectionOverlayView.hasVisibleDecorations {
882
+ return true
883
+ }
884
+ if imageResizeOverlayView.isOverlayVisible {
885
+ return true
886
+ }
887
+ if textView.selectedImageGeometry() != nil {
888
+ return true
889
+ }
890
+ return false
891
+ }
892
+
893
+ private enum HostedLayoutTraceKey {
894
+ case intrinsicContentSize
895
+ case measuredEditorHeight
896
+ case layoutSubviews
897
+ case refreshOverlays
898
+ case onHeightMayChange
899
+ }
900
+
901
+ private func recordHostedLayoutTrace(durationNanos: UInt64, keyPath: HostedLayoutTraceKey) {
902
+ guard captureHostedLayoutTraceForTesting else { return }
903
+ switch keyPath {
904
+ case .intrinsicContentSize:
905
+ hostedLayoutTraceNanos.intrinsicContentSize += durationNanos
906
+ hostedLayoutTraceCounts.intrinsicContentSize += 1
907
+ case .measuredEditorHeight:
908
+ hostedLayoutTraceNanos.measuredEditorHeight += durationNanos
909
+ hostedLayoutTraceCounts.measuredEditorHeight += 1
910
+ case .layoutSubviews:
911
+ hostedLayoutTraceNanos.layoutSubviews += durationNanos
912
+ hostedLayoutTraceCounts.layoutSubviews += 1
913
+ case .refreshOverlays:
914
+ hostedLayoutTraceNanos.refreshOverlays += durationNanos
915
+ hostedLayoutTraceCounts.refreshOverlays += 1
916
+ case .onHeightMayChange:
917
+ hostedLayoutTraceNanos.onHeightMayChange += durationNanos
918
+ hostedLayoutTraceCounts.onHeightMayChange += 1
919
+ }
920
+ }
921
+
922
+ // MARK: - Cleanup
923
+
924
+ deinit {
925
+ textView.imageLoadOwner?.cancelAll()
926
+ if editorId != 0 {
927
+ textView.unbindEditor()
928
+ }
929
+ }
930
+ }