@octane-xplat/macos-renderer 0.13.0 → 0.15.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 (222) hide show
  1. package/README.md +95 -20
  2. package/package.json +15 -2
  3. package/src/appearance.ts +2 -0
  4. package/src/fonts.ts +2 -0
  5. package/src/formatted-text.ts +147 -0
  6. package/src/gestures.ts +196 -0
  7. package/src/host-globals.d.ts +55 -0
  8. package/src/html-text.ts +88 -0
  9. package/src/image.ts +91 -16
  10. package/src/index.d.ts +9 -0
  11. package/src/index.ts +5030 -678
  12. package/src/layer.ts +11 -9
  13. package/src/list-template.ts +266 -0
  14. package/src/list-view.ts +445 -0
  15. package/src/portal-components.macos.tsx +86 -0
  16. package/src/presentation.ts +48 -15
  17. package/src/scroll-view.ts +124 -0
  18. package/src/tab-view.ts +512 -0
  19. package/src/template-props.ts +402 -0
  20. package/src/vendor/listview/LICENSE +19 -0
  21. package/src/vendor/listview/README.md +1 -0
  22. package/src/vendor/listview/binding-options.ts +189 -0
  23. package/src/vendor/listview/expressions.ts +285 -0
  24. package/src/webview.ts +36 -7
  25. package/types/ARKitCore.d.ts +199 -0
  26. package/types/AVFAudio.d.ts +2304 -0
  27. package/types/AVFoundation.d.ts +6974 -0
  28. package/types/AVKit.d.ts +215 -0
  29. package/types/AVRouting.d.ts +7 -0
  30. package/types/Accelerate.d.ts +4776 -0
  31. package/types/Accessibility.d.ts +69 -0
  32. package/types/Accounts.d.ts +134 -0
  33. package/types/AdSupport.d.ts +12 -0
  34. package/types/AddressBook.d.ts +1012 -0
  35. package/types/AppKit.d.ts +34247 -0
  36. package/types/AppTrackingTransparency.d.ts +23 -0
  37. package/types/AppleScriptKit.d.ts +6 -0
  38. package/types/ApplicationServices.d.ts +6346 -0
  39. package/types/AudioToolbox.d.ts +7129 -0
  40. package/types/AudioVideoBridging.d.ts +613 -0
  41. package/types/AuthenticationServices.d.ts +982 -0
  42. package/types/AutomaticAssessmentConfiguration.d.ts +46 -0
  43. package/types/Automator.d.ts +248 -0
  44. package/types/BackgroundAssets.d.ts +9 -0
  45. package/types/BrowserEngineKit.d.ts +166 -0
  46. package/types/BusinessChat.d.ts +29 -0
  47. package/types/CFNetwork.d.ts +931 -0
  48. package/types/CalendarStore.d.ts +354 -0
  49. package/types/CallKit.d.ts +30 -0
  50. package/types/Carbon.d.ts +16207 -0
  51. package/types/ClassKit.d.ts +281 -0
  52. package/types/CloudKit.d.ts +1565 -0
  53. package/types/Collaboration.d.ts +123 -0
  54. package/types/ColorSync.d.ts +562 -0
  55. package/types/CompositorServices.d.ts +60 -0
  56. package/types/ComputeGraph.d.ts +16 -0
  57. package/types/Contacts.d.ts +1558 -0
  58. package/types/ContactsUI.d.ts +39 -0
  59. package/types/CoreAudio.d.ts +1376 -0
  60. package/types/CoreAudioKit.d.ts +122 -0
  61. package/types/CoreAudioTypes.d.ts +1003 -0
  62. package/types/CoreBluetooth.d.ts +657 -0
  63. package/types/CoreData.d.ts +2479 -0
  64. package/types/CoreFoundation.d.ts +5546 -0
  65. package/types/CoreGraphics.d.ts +3676 -0
  66. package/types/CoreHaptics.d.ts +361 -0
  67. package/types/CoreImage.d.ts +4781 -0
  68. package/types/CoreLocation.d.ts +564 -0
  69. package/types/CoreMIDI.d.ts +1538 -0
  70. package/types/CoreML.d.ts +905 -0
  71. package/types/CoreMedia.d.ts +2615 -0
  72. package/types/CoreMediaIO.d.ts +1046 -0
  73. package/types/CoreMotion.d.ts +316 -0
  74. package/types/CoreServices.d.ts +24810 -0
  75. package/types/CoreSpotlight.d.ts +789 -0
  76. package/types/CoreTelephony.d.ts +31 -0
  77. package/types/CoreText.d.ts +3973 -0
  78. package/types/CoreTransferable.d.ts +3 -0
  79. package/types/CoreVideo.d.ts +1162 -0
  80. package/types/CoreWLAN.d.ts +556 -0
  81. package/types/CryptoTokenKit.d.ts +548 -0
  82. package/types/DVDPlayback.d.ts +931 -0
  83. package/types/DataDetection.d.ts +66 -0
  84. package/types/DeviceCheck.d.ts +46 -0
  85. package/types/DeviceDiscoveryExtension.d.ts +142 -0
  86. package/types/DirectoryService.d.ts +966 -0
  87. package/types/DiscRecording.d.ts +2345 -0
  88. package/types/DiscRecordingUI.d.ts +288 -0
  89. package/types/DiskArbitration.d.ts +366 -0
  90. package/types/DriverKit.d.ts +639 -0
  91. package/types/EventKit.d.ts +640 -0
  92. package/types/ExceptionHandling.d.ts +56 -0
  93. package/types/ExecutionPolicy.d.ts +30 -0
  94. package/types/ExternalAccessory.d.ts +98 -0
  95. package/types/FSKit.d.ts +237 -0
  96. package/types/FileProvider.d.ts +495 -0
  97. package/types/FileProviderUI.d.ts +31 -0
  98. package/types/FinderSync.d.ts +165 -0
  99. package/types/FocusEngine.d.ts +133 -0
  100. package/types/ForceFeedback.d.ts +399 -0
  101. package/types/Foundation.d.ts +18216 -0
  102. package/types/GLKit.d.ts +937 -0
  103. package/types/GLUT.d.ts +1018 -0
  104. package/types/GSS.d.ts +855 -0
  105. package/types/GameController.d.ts +1394 -0
  106. package/types/GameKit.d.ts +1958 -0
  107. package/types/GameSave.d.ts +54 -0
  108. package/types/GameplayKit.d.ts +1645 -0
  109. package/types/HealthKit.d.ts +278 -0
  110. package/types/Hypervisor.d.ts +2079 -0
  111. package/types/ICADevices.d.ts +1400 -0
  112. package/types/IOBluetooth.d.ts +6094 -0
  113. package/types/IOBluetoothUI.d.ts +250 -0
  114. package/types/IOKit.d.ts +25804 -0
  115. package/types/IOSurface.d.ts +435 -0
  116. package/types/IOUSBHost.d.ts +809 -0
  117. package/types/ImageCaptureCore.d.ts +1104 -0
  118. package/types/ImageIO.d.ts +1697 -0
  119. package/types/InputMethodKit.d.ts +264 -0
  120. package/types/InstallerPlugins.d.ts +102 -0
  121. package/types/InstantMessage.d.ts +229 -0
  122. package/types/Intents.d.ts +1833 -0
  123. package/types/IntentsUI.d.ts +5 -0
  124. package/types/JavaRuntimeSupport.d.ts +520 -0
  125. package/types/JavaScriptCore.d.ts +934 -0
  126. package/types/Kerberos.d.ts +3754 -0
  127. package/types/Kernel.d.ts +33698 -0
  128. package/types/KernelManagement.d.ts +5 -0
  129. package/types/LatentSemanticMapping.d.ts +152 -0
  130. package/types/LinkPresentation.d.ts +57 -0
  131. package/types/LocalAuthentication.d.ts +94 -0
  132. package/types/MLCompute.d.ts +2343 -0
  133. package/types/MapKit.d.ts +2125 -0
  134. package/types/Matter.d.ts +151 -0
  135. package/types/MediaAccessibility.d.ts +137 -0
  136. package/types/MediaLibrary.d.ts +394 -0
  137. package/types/MediaPlayer.d.ts +475 -0
  138. package/types/MediaToolbox.d.ts +70 -0
  139. package/types/Metal.d.ts +4542 -0
  140. package/types/MetalFX.d.ts +107 -0
  141. package/types/MetalKit.d.ts +524 -0
  142. package/types/MetalPerformanceShaders.d.ts +9306 -0
  143. package/types/MetalPerformanceShadersGraph.d.ts +1394 -0
  144. package/types/MetricKit.d.ts +231 -0
  145. package/types/ModelIO.d.ts +2908 -0
  146. package/types/MultipeerConnectivity.d.ts +348 -0
  147. package/types/NaturalLanguage.d.ts +556 -0
  148. package/types/NearbyInteraction.d.ts +51 -0
  149. package/types/NetFS.d.ts +99 -0
  150. package/types/Network.d.ts +1622 -0
  151. package/types/NetworkExtension.d.ts +1574 -0
  152. package/types/NotificationCenter.d.ts +103 -0
  153. package/types/OSAKit.d.ts +260 -0
  154. package/types/OSLog.d.ts +208 -0
  155. package/types/OpenAL.d.ts +314 -0
  156. package/types/OpenCL.d.ts +2489 -0
  157. package/types/OpenDirectory.d.ts +1919 -0
  158. package/types/OpenGL.d.ts +11322 -0
  159. package/types/PCSC.d.ts +534 -0
  160. package/types/PDFKit.d.ts +1575 -0
  161. package/types/PHASE.d.ts +166 -0
  162. package/types/ParavirtualizedGraphics.d.ts +153 -0
  163. package/types/PassKit.d.ts +1287 -0
  164. package/types/PencilKit.d.ts +201 -0
  165. package/types/Photos.d.ts +1125 -0
  166. package/types/PhotosUI.d.ts +397 -0
  167. package/types/PreferencePanes.d.ts +68 -0
  168. package/types/ProximityReaderStub.d.ts +5 -0
  169. package/types/PushKit.d.ts +45 -0
  170. package/types/Quartz.d.ts +1642 -0
  171. package/types/QuartzCore.d.ts +1398 -0
  172. package/types/QuickLook.d.ts +212 -0
  173. package/types/QuickLookThumbnailing.d.ts +131 -0
  174. package/types/QuickLookUI.d.ts +117 -0
  175. package/types/REVISION.json +5 -0
  176. package/types/ReplayKit.d.ts +294 -0
  177. package/types/Runtime.d.ts +51605 -0
  178. package/types/SafariServices.d.ts +430 -0
  179. package/types/SafetyKit.d.ts +7 -0
  180. package/types/SceneKit.d.ts +4996 -0
  181. package/types/ScreenCaptureKit.d.ts +16 -0
  182. package/types/ScreenSaver.d.ts +41 -0
  183. package/types/ScreenTime.d.ts +41 -0
  184. package/types/ScriptingBridge.d.ts +96 -0
  185. package/types/Security.d.ts +14336 -0
  186. package/types/SecurityFoundation.d.ts +53 -0
  187. package/types/SecurityInterface.d.ts +297 -0
  188. package/types/SensorKit.d.ts +7 -0
  189. package/types/ServiceManagement.d.ts +71 -0
  190. package/types/SharedWithYou.d.ts +9 -0
  191. package/types/SharedWithYouCore.d.ts +5 -0
  192. package/types/ShazamKit.d.ts +12 -0
  193. package/types/Social.d.ts +306 -0
  194. package/types/SoundAnalysis.d.ts +177 -0
  195. package/types/Speech.d.ts +246 -0
  196. package/types/SpriteKit.d.ts +2766 -0
  197. package/types/StoreKit.d.ts +509 -0
  198. package/types/SyncServices.d.ts +551 -0
  199. package/types/SystemConfiguration.d.ts +1415 -0
  200. package/types/SystemExtensions.d.ts +89 -0
  201. package/types/TWAIN.d.ts +380 -0
  202. package/types/Tcl.d.ts +4013 -0
  203. package/types/ThreadNetwork.d.ts +77 -0
  204. package/types/Tk.d.ts +3750 -0
  205. package/types/UIUtilities.d.ts +31 -0
  206. package/types/UniformTypeIdentifiers.d.ts +358 -0
  207. package/types/UserNotifications.d.ts +538 -0
  208. package/types/UserNotificationsUI.d.ts +38 -0
  209. package/types/VideoDecodeAcceleration.d.ts +50 -0
  210. package/types/VideoSubscriberAccount.d.ts +139 -0
  211. package/types/VideoToolbox.d.ts +907 -0
  212. package/types/Virtualization.d.ts +302 -0
  213. package/types/Vision.d.ts +1723 -0
  214. package/types/WebKit.d.ts +5195 -0
  215. package/types/WidgetKit.d.ts +9 -0
  216. package/types/_LocationEssentials.d.ts +159 -0
  217. package/types/_ScreenCaptureKit_SwiftUI.d.ts +5 -0
  218. package/types/iTunesLibrary.d.ts +528 -0
  219. package/types/index.d.ts +9 -0
  220. package/types/vecLib.d.ts +33586 -0
  221. package/types/vmnet.d.ts +178 -0
  222. package/src/appkit.d.ts +0 -472
@@ -0,0 +1,445 @@
1
+ import {
2
+ defineUniversalComponent,
3
+ universalPlan,
4
+ universalValue,
5
+ type UniversalRenderable,
6
+ type UniversalRoot,
7
+ } from 'octane/universal/native'
8
+
9
+ import {
10
+ NSIndexSet,
11
+ NSObject,
12
+ NSScrollView,
13
+ NSTableColumn,
14
+ NSTableColumnResizingOptions,
15
+ NSTableView,
16
+ NSTableViewColumnAutoresizingStyle,
17
+ NSTableViewDataSource,
18
+ NSTableViewDelegate,
19
+ NSTableViewGridLineStyle,
20
+ NSView,
21
+ interop,
22
+ } from '@octane-xplat/platform/macos'
23
+
24
+ import type { NSColor } from '@octane-xplat/platform/macos'
25
+
26
+ import { listFactory, parseListTemplate } from './list-template'
27
+
28
+ type View = NSView & Record<string, any>
29
+ type Props = Record<string, any>
30
+ let nextPortalCellId = 0
31
+
32
+ interface Cell {
33
+ rowView?: NSView
34
+ portalId?: number
35
+ portalGeneration?: number
36
+ portalItem?: any
37
+ portalActive?: boolean
38
+ portalNotify?: (event: any) => void
39
+ host: View
40
+ root: UniversalRoot | null
41
+ factoryView: View | null
42
+ index: number
43
+ mode: 'render' | 'factory' | 'string' | 'default' | null
44
+ }
45
+
46
+ interface ListState {
47
+ view: View
48
+ table: View
49
+ delegate: NSObject
50
+ props: Props
51
+ cells: Set<Cell>
52
+ count: number
53
+ closed: boolean
54
+ stopItems?: () => void
55
+ createRoot: (host: NSView) => UniversalRoot
56
+ releaseOwnedRoot: (host: NSView) => void
57
+ color: (value: any) => NSColor
58
+ defaultSeparator: NSColor
59
+ }
60
+
61
+ const states = new WeakMap<object, ListState>()
62
+ const cellsByHost = new WeakMap<object, Cell>()
63
+ const factoryOwners = new WeakMap<object, Cell>()
64
+ const CellContent = defineUniversalComponent<{ render: () => UniversalRenderable }>(
65
+ 'macos',
66
+ (props) => props.render(),
67
+ )
68
+
69
+ const defaultPlan = universalPlan('macos', { kind: 'host', type: 'label', bindings: [['text', 0]] })
70
+
71
+ function itemAt(state: ListState, index: number) {
72
+ const source = state.props.items
73
+ return typeof source?.getItem === 'function' ? source.getItem(index) : source?.[index]
74
+ }
75
+
76
+ function releasePortal(cell: Cell) {
77
+ if (!cell.portalActive) {
78
+ return
79
+ }
80
+
81
+ cell.portalActive = false
82
+ cell.portalNotify?.({ type: 'release', id: cell.portalId, generation: cell.portalGeneration })
83
+ cell.portalNotify = undefined
84
+ }
85
+
86
+ function clearContent(state: ListState, cell: Cell) {
87
+ releasePortal(cell)
88
+ cell.root?.unmount()
89
+ cell.root = null
90
+ if (cell.factoryView) {
91
+ const host = cell.factoryView
92
+ factoryOwners.delete(host)
93
+ state.releaseOwnedRoot(host)
94
+ host.bindingContext = undefined
95
+ host.removeFromSuperview()
96
+ cell.factoryView = null
97
+ }
98
+
99
+ cell.mode = null
100
+ }
101
+
102
+ function bind(state: ListState, cell: Cell, index: number) {
103
+ const item = itemAt(state, index),
104
+ props = state.props
105
+
106
+ const factory = listFactory(props)
107
+ const mode = props.renderItem
108
+ ? 'render'
109
+ : factory
110
+ ? 'factory'
111
+ : props.itemTemplate
112
+ ? 'string'
113
+ : 'default'
114
+
115
+ if (cell.mode !== mode || mode === 'factory') {
116
+ clearContent(state, cell)
117
+ }
118
+
119
+ cell.mode = mode
120
+ cell.index = index
121
+ cell.host.bindingContext = item
122
+ if (mode === 'factory') {
123
+ const host = factory!() as View
124
+ if (!(host instanceof NSView)) {
125
+ throw new TypeError(
126
+ '[macos-host] listview template factory must return an AppKit NSView host',
127
+ )
128
+ }
129
+
130
+ if (factoryOwners.has(host)) {
131
+ throw new TypeError(
132
+ '[macos-host] listview template factory returned a host already owned by another cell',
133
+ )
134
+ }
135
+
136
+ factoryOwners.set(host, cell)
137
+ cell.factoryView = host
138
+ host.bindingContext = item
139
+ host.removeFromSuperview()
140
+ host.translatesAutoresizingMaskIntoConstraints = false
141
+ cell.host.addSubview(host)
142
+ for (const axis of ['leading', 'trailing', 'top', 'bottom']) {
143
+ host[axis + 'Anchor'].constraintEqualToAnchor(cell.host[axis + 'Anchor']).active = true
144
+ }
145
+ } else if (mode === 'render' && props.__onCellTarget) {
146
+ if (cell.root) {
147
+ cell.root.unmount()
148
+ cell.root = null
149
+ }
150
+
151
+ const changed =
152
+ !cell.portalActive || !Object.is(cell.portalItem, item) || cell.host.__portalIndex !== index
153
+
154
+ cell.portalId ??= nextPortalCellId++
155
+ if (changed) {
156
+ cell.portalGeneration = (cell.portalGeneration ?? 0) + 1
157
+ }
158
+
159
+ cell.portalActive = true
160
+ cell.portalItem = item
161
+ cell.host.__portalIndex = index
162
+ cell.portalNotify = props.__onCellTarget
163
+ cell.portalNotify?.({
164
+ type: 'bind',
165
+ id: cell.portalId,
166
+ generation: cell.portalGeneration,
167
+ target: cell.host,
168
+ item,
169
+ index,
170
+ })
171
+ } else {
172
+ releasePortal(cell)
173
+ cell.root ??= state.createRoot(cell.host)
174
+ cell.root.render(CellContent, {
175
+ render: () =>
176
+ mode === 'render'
177
+ ? props.renderItem(item, index)
178
+ : mode === 'string'
179
+ ? parseListTemplate(props.itemTemplate).render(item)
180
+ : universalValue(defaultPlan, [item == null ? '' : String(item)]),
181
+ })
182
+ }
183
+
184
+ const event = Object.freeze({
185
+ eventName: 'itemLoading',
186
+ object: state.view,
187
+ index,
188
+ view: cell.host,
189
+ })
190
+
191
+ props.onItemLoading?.(event)
192
+ }
193
+
194
+ function cellFor(state: ListState, index: number) {
195
+ let cell = [...state.cells].find((entry) => entry.index === index)
196
+ if (!cell) {
197
+ const host = state.table.makeViewWithIdentifierOwner(
198
+ 'xplat-list-cell',
199
+ state.delegate,
200
+ ) as View | null
201
+
202
+ cell = host ? cellsByHost.get(host) : undefined
203
+ if (!cell) {
204
+ const host = NSView.alloc().initWithFrame({
205
+ origin: { x: 0, y: 0 },
206
+ size: { width: 200, height: 24 },
207
+ }) as View
208
+
209
+ host.identifier = 'xplat-list-cell'
210
+ cell = { host, root: null, factoryView: null, index: -1, mode: null }
211
+ state.cells.add(cell)
212
+ cellsByHost.set(host, cell)
213
+ }
214
+ }
215
+
216
+ bind(state, cell, index)
217
+ return cell
218
+ }
219
+
220
+ const ListDelegate = NSObject.extend(
221
+ {
222
+ // AppKit validates this selector even when the delegate supplies row views.
223
+ ['tableView:objectValueForTableColumn:row:'](
224
+ _table: NSView,
225
+ _column: unknown,
226
+ _row: number,
227
+ ): any {
228
+ return null
229
+ },
230
+ ['numberOfRowsInTableView:'](table: NSView): number {
231
+ return states.get(table)?.count ?? 0
232
+ },
233
+ ['tableView:viewForTableColumn:row:'](table: NSView, _column: unknown, row: number): NSView {
234
+ const state = states.get(table)!
235
+ if (state.closed || row < 0 || row >= state.count) {
236
+ return NSView.alloc().init()
237
+ }
238
+
239
+ return cellFor(state, row).host
240
+ },
241
+ ['tableView:didAddRowView:forRow:'](table: NSView, rowView: NSView, row: number) {
242
+ const host = (rowView as any).viewAtColumn?.(0)
243
+ const cell = host && cellsByHost.get(host)
244
+ if (cell) {
245
+ cell.rowView = rowView
246
+ }
247
+
248
+ const state = states.get(table)
249
+ if (state && !state.closed && row >= 0 && row === state.count - 1) {
250
+ state.props.onLoadMoreItems?.({ eventName: 'loadMoreItems', object: state.view })
251
+ }
252
+ },
253
+ ['tableView:didRemoveRowView:forRow:'](table: NSView, rowView: any, row: number) {
254
+ const state = states.get(table),
255
+ host = rowView.viewAtColumn(0)
256
+
257
+ const cell = host && cellsByHost.get(host)
258
+ if (state && cell && cell.index === row && (!cell.rowView || cell.rowView === rowView)) {
259
+ cell.rowView = undefined
260
+ // renderItem roots retain their tree for the next recycled binding.
261
+ // Callable templates own an independent root; release it immediately.
262
+ if (cell.mode === 'factory') {
263
+ clearContent(state, cell)
264
+ }
265
+
266
+ releasePortal(cell)
267
+ cell.index = -1
268
+ cell.host.bindingContext = undefined
269
+ }
270
+ },
271
+ ['itemClicked:'](table: any) {
272
+ const state = states.get(table)
273
+ const index =
274
+ Number(table.clickedRow) >= 0 ? Number(table.clickedRow) : Number(table.selectedRow)
275
+
276
+ if (!state || state.closed || index < 0 || index >= state.count) {
277
+ return
278
+ }
279
+
280
+ const cell = [...state.cells].find((cell) => cell.index === index)
281
+ if (cell) {
282
+ state.props.onItemTap?.(
283
+ Object.freeze({ eventName: 'itemTap', object: state.view, index, view: cell.host }),
284
+ )
285
+ }
286
+ },
287
+ },
288
+ {
289
+ name: 'XplatRawListViewDelegate',
290
+ protocols: [NSTableViewDataSource, NSTableViewDelegate],
291
+ exposedMethods: {
292
+ 'itemClicked:': { returns: interop.types.void, params: [NSObject] },
293
+ 'numberOfRowsInTableView:': { returns: interop.types.int64, params: [NSObject] },
294
+ 'tableView:objectValueForTableColumn:row:': {
295
+ returns: interop.types.id,
296
+ params: [NSObject, NSObject, interop.types.int64],
297
+ },
298
+ 'tableView:viewForTableColumn:row:': {
299
+ returns: interop.types.id,
300
+ params: [NSObject, NSObject, interop.types.int64],
301
+ },
302
+ 'tableView:didAddRowView:forRow:': {
303
+ returns: interop.types.void,
304
+ params: [NSObject, NSObject, interop.types.int64],
305
+ },
306
+ 'tableView:didRemoveRowView:forRow:': {
307
+ returns: interop.types.void,
308
+ params: [NSObject, NSObject, interop.types.int64],
309
+ },
310
+ },
311
+ },
312
+ )
313
+
314
+ /** Create the raw scroll/table host; weak native delegate edges have a retained JS owner. */
315
+ export function makeListView(
316
+ deps: Pick<ListState, 'createRoot' | 'releaseOwnedRoot' | 'color'>,
317
+ ): NSView {
318
+ const view = NSScrollView.alloc().initWithFrame({
319
+ origin: { x: 0, y: 0 },
320
+ size: { width: 200, height: 200 },
321
+ }) as View
322
+
323
+ view.translatesAutoresizingMaskIntoConstraints = false
324
+ view.hasVerticalScroller = true
325
+ view.hasHorizontalScroller = false
326
+ view.borderType = 0
327
+ const table = NSTableView.alloc().initWithFrame({
328
+ origin: { x: 0, y: 0 },
329
+ size: { width: 200, height: 200 },
330
+ }) as View
331
+
332
+ table.headerView = null
333
+ table.columnAutoresizingStyle = NSTableViewColumnAutoresizingStyle.LastColumnOnlyAutoresizingStyle
334
+ table.gridStyleMask = NSTableViewGridLineStyle.SolidHorizontalGridLineMask
335
+ table.intercellSpacing = { width: 0, height: 1 }
336
+ table.usesAutomaticRowHeights = true
337
+ table.rowHeight = 24
338
+ const column = NSTableColumn.alloc().initWithIdentifier('default')
339
+ column.width = 200
340
+ column.resizingMask = NSTableColumnResizingOptions.AutoresizingMask
341
+ table.addTableColumn(column)
342
+ const delegate = ListDelegate.alloc().init()
343
+ const state: ListState = {
344
+ ...deps,
345
+ view,
346
+ table,
347
+ delegate,
348
+ props: {},
349
+ cells: new Set(),
350
+ count: 0,
351
+ closed: false,
352
+ defaultSeparator: table.gridColor,
353
+ }
354
+
355
+ states.set(table, state)
356
+ states.set(view, state)
357
+ table.dataSource = delegate
358
+ table.delegate = delegate
359
+ table.target = delegate
360
+ table.action = 'itemClicked:'
361
+ view.documentView = table
362
+ return view
363
+ }
364
+
365
+ /** Complete next props, including explicit clears from the renderer. */
366
+ export function updateListView(view: NSView, props: Props) {
367
+ const state = states.get(view)!
368
+ const previous = state.props
369
+ state.props = props
370
+ state.table.gridColor =
371
+ props.separatorColor == null ? state.defaultSeparator : state.color(props.separatorColor)
372
+
373
+ if (previous.items !== props.items) {
374
+ state.stopItems?.()
375
+ state.stopItems = undefined
376
+ const source = props.items
377
+ if (typeof source?.on === 'function' && typeof source.off === 'function') {
378
+ const changed = () => {
379
+ if (!state.closed) {
380
+ refreshList(state)
381
+ }
382
+ }
383
+
384
+ source.on('change', changed)
385
+ state.stopItems = () => source.off('change', changed)
386
+ }
387
+ }
388
+
389
+ refreshList(state)
390
+ }
391
+
392
+ function refreshList(state: ListState) {
393
+ const count = Number(state.props.items?.length ?? 0),
394
+ previousCount = state.count
395
+
396
+ state.count = count
397
+ for (const cell of state.cells) {
398
+ if (cell.index >= count) {
399
+ clearContent(state, cell)
400
+ cell.index = -1
401
+ cell.host.bindingContext = undefined
402
+ } else if (cell.index >= 0) {
403
+ bind(state, cell, cell.index)
404
+ }
405
+ }
406
+
407
+ // Same-count rebinds preserve the native cell and its root. Structural item
408
+ // changes ask AppKit to recycle cells and recompute row geometry.
409
+ if (count !== previousCount) {
410
+ state.table.reloadData()
411
+ } else {
412
+ state.table.noteHeightOfRowsWithIndexesChanged(
413
+ NSIndexSet.indexSetWithIndexesInRange({ location: 0, length: count }),
414
+ )
415
+ }
416
+ }
417
+
418
+ export function disposeListView(view: NSView) {
419
+ const state = states.get(view)
420
+ if (!state || state.closed) {
421
+ return
422
+ }
423
+
424
+ state.closed = true
425
+ state.stopItems?.()
426
+ state.stopItems = undefined
427
+ state.table.delegate = null
428
+ state.table.dataSource = null
429
+ state.table.target = null
430
+ state.table.action = null
431
+ states.delete(state.table)
432
+ states.delete(view)
433
+ for (const cell of state.cells) {
434
+ clearContent(state, cell)
435
+ cell.host.bindingContext = undefined
436
+ cell.host.removeFromSuperview()
437
+ cellsByHost.delete(cell.host)
438
+ }
439
+
440
+ state.cells.clear()
441
+ state.props = {}
442
+ state.count = 0
443
+ state.table.reloadData()
444
+ state.view.documentView = null
445
+ }
@@ -0,0 +1,86 @@
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import { createPortal, type UniversalRenderable } from 'octane/universal/native'
3
+ import { useCallback, useState } from 'octane'
4
+
5
+ /** @internal Compiler-owned tab content; refs still expose the raw logical handle. */
6
+ export function __PortalTabItem(props: any) {
7
+ const [target, setTarget] = useState<any>(null)
8
+ const onTarget = useCallback((host: any) => setTarget(host), [])
9
+ const { children, ...attributes } = props
10
+ return (
11
+ <>
12
+ <tabviewitem {...attributes} __portalContent={true} __onContentTarget={onTarget} />
13
+ {target ? createPortal(children as UniversalRenderable, target) : null}
14
+ </>
15
+ )
16
+ }
17
+
18
+ interface CellBinding {
19
+ id: number
20
+ generation: number
21
+ target: unknown
22
+ item: any
23
+ index: number
24
+ }
25
+
26
+ function CellContent(props: {
27
+ key?: number
28
+ render: (item: any, index: number) => any
29
+ item: any
30
+ index: number
31
+ }) {
32
+ return <>{props.render(props.item, props.index)}</>
33
+ }
34
+
35
+ function CellPortal(props: {
36
+ key?: number
37
+ cell: CellBinding
38
+ render: (item: any, index: number) => any
39
+ }) {
40
+ const cell = props.cell
41
+ return createPortal(
42
+ (
43
+ <CellContent
44
+ key={cell.generation}
45
+ render={props.render}
46
+ item={cell.item}
47
+ index={cell.index}
48
+ />
49
+ ) as UniversalRenderable,
50
+ cell.target,
51
+ )
52
+ }
53
+
54
+ /** @internal Recycled native hosts share the declaring list's Octane tree. */
55
+ export function __PortalList(props: any) {
56
+ const [cells, setCells] = useState<CellBinding[]>([])
57
+ const onTarget = useCallback((event: CellBinding & { type: 'bind' | 'release' }) => {
58
+ setCells((current) => {
59
+ const existing = current.find((cell) => cell.id === event.id)
60
+ if (event.type === 'release') {
61
+ return existing?.generation === event.generation
62
+ ? current.filter((cell) => cell.id !== event.id)
63
+ : current
64
+ }
65
+
66
+ if (
67
+ existing?.generation === event.generation &&
68
+ existing.index === event.index &&
69
+ Object.is(existing.item, event.item)
70
+ ) {
71
+ return current
72
+ }
73
+
74
+ return [...current.filter((cell) => cell.id !== event.id), event]
75
+ })
76
+ }, [])
77
+
78
+ return (
79
+ <>
80
+ <listview {...props} __onCellTarget={props.renderItem ? onTarget : undefined} />
81
+ {cells.map((cell) => (
82
+ <CellPortal key={cell.id} cell={cell} render={props.renderItem} />
83
+ ))}
84
+ </>
85
+ )
86
+ }
@@ -1,4 +1,18 @@
1
1
  import type { UniversalRoot } from 'octane/universal/native'
2
+ import {
3
+ CABasicAnimation,
4
+ NSAppearance,
5
+ NSApplication,
6
+ NSColor,
7
+ NSEvent,
8
+ NSNotificationCenter,
9
+ NSNumber,
10
+ NSView,
11
+ NSWindow,
12
+ NativeClass,
13
+ } from '@octane-xplat/platform/macos'
14
+
15
+ import type { NSPoint } from '@octane-xplat/platform/macos'
2
16
 
3
17
  // Shared surfaces live in the declaring window, independently of its layout.
4
18
  // Kept separate from NSPopover and platform-authentic window sheets.
@@ -17,6 +31,12 @@ export interface SurfaceOptions {
17
31
  modal?: boolean
18
32
  fullscreen?: boolean
19
33
  component?: any
34
+ /** Internal source-tree outlet. Omit for a standalone presentation root. */
35
+ createContentRoot?: (host: NSView) => {
36
+ render(component: any, props?: any): void
37
+ unmount(): void
38
+ whenCommitted?(callback: () => void): void
39
+ }
20
40
  props?: Record<string, any>
21
41
  owner?: any
22
42
  width?: number | string
@@ -52,10 +72,7 @@ export interface PresentationBridge {
52
72
  [key: string]: any
53
73
  }
54
74
 
55
- export type SurfaceRootFactory = (
56
- host: NSView,
57
- options?: { fontFamily?: string },
58
- ) => UniversalRoot
75
+ export type SurfaceRootFactory = (host: NSView, options?: { fontFamily?: string }) => UniversalRoot
59
76
 
60
77
  interface SurfaceEntry {
61
78
  options: SurfaceOptions
@@ -83,18 +100,21 @@ export function installPresentationBridge(
83
100
  fontFamilyForView: (view: NSView | undefined) => string | undefined,
84
101
  ) {
85
102
  const entries: SurfaceEntry[] = []
86
- const layers = new WeakMap<NSView, SurfaceEntry>()
103
+ const layers = new WeakMap<PresentationLayer, SurfaceEntry>()
87
104
  class PresentationLayer extends NSView {
88
105
  static {
89
106
  NativeClass(this)
90
107
  }
108
+ // @ts-ignore ObjC accessor methods satisfy the generated @property member.
91
109
  acceptsFirstResponder() {
92
110
  return true
93
111
  }
94
- hitTest(point: NSPoint): NSView | null {
112
+ hitTest(point: NSPoint): any {
95
113
  const hit = super.hitTest(point)
96
114
  const entry = layers.get(this)
97
- return (hit === this || hit?.isEqual?.(this)) && !entry?.options.modal ? null : hit
115
+ return (hit === (this as unknown as NSView) || hit?.isEqual?.(this)) && !entry?.options.modal
116
+ ? null
117
+ : hit
98
118
  }
99
119
  layout() {
100
120
  super.layout()
@@ -173,7 +193,10 @@ export function installPresentationBridge(
173
193
  const panel = NSView.alloc().initWithFrame(host.bounds)
174
194
  panel.wantsLayer = true
175
195
  layer.addSubview(panel)
176
- const root = createRoot(panel, { fontFamily: fontFamilyForView(owner ?? host) })
196
+ const root =
197
+ initial.createContentRoot?.(panel) ??
198
+ createRoot(panel, { fontFamily: fontFamilyForView(owner ?? host) })
199
+
177
200
  let resolveClosed: (reason?: unknown) => void = () => {}
178
201
  const responder = nativeWindow.firstResponder
179
202
  const previousFocus = truth(responder, 'isFieldEditor') ? responder.delegate : responder
@@ -187,7 +210,7 @@ export function installPresentationBridge(
187
210
  (owner ? inside(owner, entry.panel) : entry.options.modal),
188
211
  ),
189
212
  panel,
190
- layer,
213
+ layer: layer as unknown as NSView,
191
214
  window: nativeWindow,
192
215
  closed: false,
193
216
  observers: [],
@@ -205,7 +228,9 @@ export function installPresentationBridge(
205
228
  const w = Number(bounds.width),
206
229
  h = Number(bounds.height)
207
230
 
208
- const content = panel.subviews?.objectAtIndex?.(0)
231
+ const content =
232
+ Number(panel.subviews?.count ?? 0) > 0 ? panel.subviews.objectAtIndex(0) : undefined
233
+
209
234
  const fit = content?.documentView?.fittingSize ?? content?.fittingSize ?? panel.fittingSize
210
235
  const full = options.kind === 'lightbox' || options.fullscreen
211
236
  const width = full
@@ -256,9 +281,7 @@ export function installPresentationBridge(
256
281
  }
257
282
 
258
283
  const pos =
259
- !full && options.position && typeof options.position === 'object'
260
- ? options.position
261
- : {}
284
+ !full && options.position && typeof options.position === 'object' ? options.position : {}
262
285
 
263
286
  if (pos.start != null) {
264
287
  x = length(pos.start, w, 0)
@@ -317,6 +340,16 @@ export function installPresentationBridge(
317
340
 
318
341
  entry.options = { ...entry.options, ...options }
319
342
  root.render(entry.options.component, entry.options.props ?? {})
343
+ if ('whenCommitted' in root) {
344
+ root.whenCommitted?.(() => {
345
+ if (!entry.closed) {
346
+ entry.layout()
347
+ if (entry.options.modal)
348
+ {nativeWindow.makeFirstResponder(focusables(panel)[0] ?? layer)}
349
+ }
350
+ })
351
+ }
352
+
320
353
  entry.layout()
321
354
  entry.didRender = true
322
355
  if (hadFocus && entry.options.modal) {
@@ -441,7 +474,7 @@ export function installPresentationBridge(
441
474
  layers.set(layer, entry)
442
475
  entries.push(entry)
443
476
  try {
444
- host.addSubview(layer)
477
+ host.addSubview(layer as unknown as NSView)
445
478
  entry.update(initial)
446
479
  entry.observers.push(
447
480
  NSNotificationCenter.defaultCenter.addObserverForNameObjectQueueUsingBlock(
@@ -580,7 +613,7 @@ export function installPresentationBridge(
580
613
  },
581
614
  closedPromise: entry.closedPromise,
582
615
  panel,
583
- layer,
616
+ layer: layer as unknown as NSView,
584
617
  }
585
618
  }
586
619
  }