@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,124 @@
1
+ /** Internal scroll layout ownership. Sizes and offsets stay in AppKit points. */
2
+
3
+ import {
4
+ NSNotificationCenter,
5
+ NSUserInterfaceLayoutOrientation,
6
+ NSViewFrameDidChangeNotification,
7
+ } from '@octane-xplat/platform/macos'
8
+
9
+ export function scrollAxes(props: Record<string, any>) {
10
+ const orientation = props.orientation ?? (props.horizontal === true ? 'horizontal' : 'vertical')
11
+ return { inline: orientation !== 'vertical', block: orientation !== 'horizontal' }
12
+ }
13
+
14
+ export function configureScrollAxes(node: any) {
15
+ const { inline, block } = scrollAxes(node.props)
16
+ const indicators = node.props.scrollBarIndicatorVisible !== false
17
+ const axisKey = `${inline}:${block}:${indicators}`
18
+ if (node.scrollAxisKey === axisKey) {
19
+ return
20
+ }
21
+
22
+ node.scrollAxisKey = axisKey
23
+ for (const constraint of node.scrollAxisConstraints ?? []) {
24
+ constraint.active = false
25
+ }
26
+
27
+ const scroll = node.view
28
+ const document = scroll.documentView
29
+ const clip = scroll.contentView
30
+ scroll.hasHorizontalScroller = inline && indicators
31
+ scroll.hasVerticalScroller = block && indicators
32
+ document.orientation =
33
+ inline && !block
34
+ ? NSUserInterfaceLayoutOrientation.Horizontal
35
+ : NSUserInterfaceLayoutOrientation.Vertical
36
+
37
+ // A disabled axis fits the viewport. Enabled axes retain the document's
38
+ // natural size and at least fill the viewport, including empty content.
39
+ node.scrollAxisConstraints = [
40
+ inline
41
+ ? document.widthAnchor.constraintGreaterThanOrEqualToAnchor(clip.widthAnchor)
42
+ : document.widthAnchor.constraintEqualToAnchor(clip.widthAnchor),
43
+ block
44
+ ? document.heightAnchor.constraintGreaterThanOrEqualToAnchor(clip.heightAnchor)
45
+ : document.heightAnchor.constraintEqualToAnchor(clip.heightAnchor),
46
+ ]
47
+
48
+ for (const constraint of node.scrollAxisConstraints) {
49
+ constraint.active = true
50
+ }
51
+
52
+ node.scrollGeometrySchedule?.()
53
+ }
54
+
55
+ /** Coalesce a layout solve's frame notifications. Observe the clip and the
56
+ * document as well as the arranged viewport: content growth and scroller
57
+ * chrome can resize these without changing the outer frame. */
58
+ export function observeScrollGeometry(node: any, target: any, notify: () => void): () => void {
59
+ const scroll = node.view
60
+ const clip = scroll.contentView
61
+ const document = scroll.documentView
62
+ let disposed = false
63
+ let scheduled = false
64
+ const schedule = () => {
65
+ if (scheduled || disposed) {
66
+ return
67
+ }
68
+
69
+ scheduled = true
70
+ queueMicrotask(() => {
71
+ if (disposed) {
72
+ return
73
+ }
74
+
75
+ scroll.layoutSubtreeIfNeeded?.()
76
+ scheduled = false
77
+ const { inline, block } = scrollAxes(node.props)
78
+ const bounds = clip.bounds
79
+ const size = document.frame.size
80
+ const maxX = Math.max(0, Number(size.width) - Number(bounds.size.width))
81
+ const maxY = Math.max(0, Number(size.height) - Number(bounds.size.height))
82
+ const flip =
83
+ typeof document.isFlipped === 'function' ? document.isFlipped() : document.isFlipped
84
+
85
+ const flipped = flip === true || Number(flip) === 1
86
+ const clamp = (value: number, max: number) => Math.min(max, Math.max(0, value))
87
+ const x = inline ? clamp(Number(bounds.origin.x), maxX) : 0
88
+ const initial = node.props.orientation != null && !node.scrollInitialPositioned
89
+ const y = !block || initial ? (flipped ? 0 : maxY) : clamp(Number(bounds.origin.y), maxY)
90
+ if (Number(bounds.size.width) > 0 && Number(bounds.size.height) > 0) {
91
+ node.scrollInitialPositioned = true
92
+ }
93
+
94
+ if (x !== Number(bounds.origin.x) || y !== Number(bounds.origin.y)) {
95
+ clip.scrollToPoint({ x, y })
96
+ scroll.reflectScrolledClipView(clip)
97
+ }
98
+
99
+ notify()
100
+ })
101
+ }
102
+
103
+ const center = NSNotificationCenter.defaultCenter
104
+ const observers = [...new Set([target, clip, document])].map((view) => {
105
+ view.postsFrameChangedNotifications = true
106
+ return center.addObserverForNameObjectQueueUsingBlock(
107
+ NSViewFrameDidChangeNotification,
108
+ view,
109
+ null,
110
+ schedule,
111
+ )
112
+ })
113
+
114
+ node.scrollGeometrySchedule = schedule
115
+ schedule()
116
+ return () => {
117
+ disposed = true
118
+ for (const observer of observers) {
119
+ center.removeObserver(observer)
120
+ }
121
+
122
+ node.scrollGeometrySchedule = null
123
+ }
124
+ }
@@ -0,0 +1,512 @@
1
+ import type { ElementNode, AppKitView } from './index'
2
+ import {
3
+ NSAttributedString,
4
+ NSBundle,
5
+ NSColor,
6
+ NSFontAttributeName,
7
+ NSForegroundColorAttributeName,
8
+ NSImage,
9
+ NSObject,
10
+ NSTabView,
11
+ NSTabViewDelegate,
12
+ NSTabViewItem,
13
+ NSURL,
14
+ NSURLSession,
15
+ NSView,
16
+ } from '@octane-xplat/platform/macos'
17
+
18
+ import { registerGestureClass } from './gestures'
19
+ import { defineUniversalComponent } from 'octane/universal/native'
20
+ import { loadImage } from './image'
21
+ import { resolveFont } from './fonts'
22
+
23
+ /** Compiler adapter for JSX spreads: read getters once and remove the outer
24
+ * root's children channel while preserving ordinary key/ref/prop ordering. */
25
+ export function __tabItemProps(props: any) {
26
+ if (props == null) {
27
+ return props
28
+ }
29
+
30
+ const attributes = { ...props }
31
+ if (Object.prototype.hasOwnProperty.call(attributes, 'children')) {
32
+ attributes.__tabContent = attributes.children
33
+ delete attributes.children
34
+ }
35
+
36
+ return attributes
37
+ }
38
+
39
+ const contentComponent = defineUniversalComponent('macos', (props: any) => props.content)
40
+
41
+ // A brand cannot be forged by supplying an object with a matching type string.
42
+ const handles = new WeakSet<object>()
43
+ const owners = new WeakMap<object, ElementNode>()
44
+ const delegates = new WeakMap<object, ElementNode>()
45
+
46
+ class TabDelegate extends NSObject {
47
+ static ObjCProtocols = [NSTabViewDelegate]
48
+ static {
49
+ registerGestureClass(this)
50
+ }
51
+ tabViewDidSelectTabViewItem(view: any, _item: any) {
52
+ const node = delegates.get(this)
53
+ if (!node || node.tabSyncing) {
54
+ return
55
+ }
56
+
57
+ const index = Number(view.indexOfTabViewItem(view.selectedTabViewItem))
58
+ const deliver = () => {
59
+ node.tabRequested = index
60
+ notifySelection(node, index)
61
+ }
62
+
63
+ if (node.container.root) {
64
+ node.container.root.eventScope('discrete', deliver)
65
+ } else {
66
+ deliver()
67
+ }
68
+ }
69
+ }
70
+
71
+ /** NSTabView's ordinary tabs do not display the item's image. Include it in
72
+ * label measurement/drawing rather than accepting an invisible iconSource. */
73
+ class IconTabItem extends NSTabViewItem {
74
+ static {
75
+ registerGestureClass(this)
76
+ }
77
+ sizeOfLabel(minimum: boolean) {
78
+ const size = super.sizeOfLabel(minimum)
79
+ return this.image ? { width: size.width + 20, height: Math.max(16, size.height) } : size
80
+ }
81
+
82
+ drawLabelInRect(truncate: boolean, rect: any) {
83
+ if (!this.image) {
84
+ super.drawLabelInRect(truncate, rect)
85
+ return
86
+ }
87
+
88
+ this.image.drawInRectFromRectOperationFraction(
89
+ { origin: rect.origin, size: { width: 16, height: 16 } },
90
+ { origin: { x: 0, y: 0 }, size: { width: 0, height: 0 } },
91
+ 2,
92
+ 1,
93
+ )
94
+
95
+ super.drawLabelInRect(truncate, {
96
+ origin: { x: rect.origin.x + 20, y: rect.origin.y },
97
+ size: { width: Math.max(0, rect.size.width - 20), height: rect.size.height },
98
+ })
99
+ }
100
+ }
101
+
102
+ export function validateTabProps(type: string, props: Record<string, any>) {
103
+ if (type === 'tabview') {
104
+ if (
105
+ props.items != null &&
106
+ (!Array.isArray(props.items) ||
107
+ props.items.some((item: any) => !item || !handles.has(item) || item.tabDisposed))
108
+ ) {
109
+ throw new TypeError(
110
+ 'AppKit tabview items must be an array of live tabviewitem logical handles obtained through refs',
111
+ )
112
+ }
113
+
114
+ if (props.items && new Set(props.items).size !== props.items.length) {
115
+ throw new TypeError('AppKit tabview items must not contain duplicate handles')
116
+ }
117
+
118
+ if (props.selectedIndex != null && !Number.isInteger(props.selectedIndex)) {
119
+ throw new TypeError('AppKit tabview selectedIndex must be an integer')
120
+ }
121
+ } else {
122
+ if (props.title != null && typeof props.title !== 'string') {
123
+ throw new TypeError('AppKit tabviewitem title must be a string')
124
+ }
125
+
126
+ if ('view' in props || 'contentRoot' in props) {
127
+ throw new TypeError(
128
+ 'AppKit tabviewitem content root cannot be reassigned; update JSX children instead',
129
+ )
130
+ }
131
+
132
+ if (props.iconSource != null && typeof props.iconSource === 'object') {
133
+ iconImage(props.iconSource)
134
+ }
135
+
136
+ if (
137
+ props.iconSource != null &&
138
+ typeof props.iconSource !== 'string' &&
139
+ typeof props.iconSource !== 'object'
140
+ ) {
141
+ throw new TypeError('AppKit tabviewitem iconSource must be a string or host image source')
142
+ }
143
+ }
144
+ }
145
+
146
+ export function makeTabView(node: ElementNode): AppKitView {
147
+ const view = NSTabView.alloc().init()
148
+ view.translatesAutoresizingMaskIntoConstraints = false
149
+ const delegate = TabDelegate.new()
150
+ delegates.set(delegate, node)
151
+ view.delegate = delegate
152
+ node.tabDelegate = delegate // AppKit holds its delegate weakly.
153
+ node.tabItems = []
154
+ node.tabIndex = -1
155
+ node.tabRequested = -1
156
+ return view as AppKitView
157
+ }
158
+
159
+ export function makeTabItem(node: ElementNode) {
160
+ handles.add(node)
161
+ node.tabContentRoot = null
162
+ const item = IconTabItem.alloc().initWithIdentifier(String(node.id))
163
+ const content = NSView.new()
164
+ content.autoresizingMask = 18 // NSViewWidthSizable | NSViewHeightSizable
165
+ node.tabNativeItem = item
166
+ // Logical refs always have view:null; the host owns this content root.
167
+ node.childHost = content as AppKitView
168
+ item.view = content
169
+ Object.defineProperty(node, 'contentRoot', {
170
+ get: () => node.tabContentRoot,
171
+ set: () => {
172
+ throw new TypeError(
173
+ 'AppKit tabviewitem content root cannot be reassigned; update JSX children instead',
174
+ )
175
+ },
176
+ })
177
+ }
178
+
179
+ function iconImage(source: any, style: Record<string, any> = {}): any {
180
+ if (source == null || source === '') {
181
+ return null
182
+ }
183
+
184
+ if (typeof source !== 'string') {
185
+ // Shared ImageSource-like handles expose their platform image.
186
+ const image = source.macos ?? source.nativeImage ?? source
187
+ if (typeof image.drawInRectFromRectOperationFraction !== 'function') {
188
+ throw new TypeError('AppKit tabviewitem iconSource host object must contain an NSImage')
189
+ }
190
+
191
+ return image
192
+ }
193
+
194
+ if (source.startsWith('sys://')) {
195
+ return NSImage.imageWithSystemSymbolNameAccessibilityDescription(source.slice(6), null)
196
+ }
197
+
198
+ if (source.startsWith('font://')) {
199
+ const text = NSAttributedString.alloc().initWithStringAttributes(source.slice(7), {
200
+ [NSFontAttributeName]: resolveFont(
201
+ Number(style.fontSize ?? 17),
202
+ style.fontWeight ?? 400,
203
+ style.iconFontFamily ?? style.fontFamily,
204
+ ),
205
+ [NSForegroundColorAttributeName]:
206
+ style.color == null ? NSColor.labelColor : resolveIconColor(style.color),
207
+ })
208
+
209
+ const size = text.size()
210
+ if (size.width <= 0 || size.height <= 0) {
211
+ return null
212
+ }
213
+
214
+ const image = NSImage.alloc().initWithSize(size)
215
+ image.lockFocus()
216
+ try {
217
+ text.drawAtPoint({ x: 0, y: 0 })
218
+ } finally {
219
+ image.unlockFocus()
220
+ }
221
+
222
+ return image
223
+ }
224
+
225
+ if (source.startsWith('res://')) {
226
+ return NSImage.imageNamed(source.slice(6))
227
+ }
228
+
229
+ if (source.startsWith('~/')) {
230
+ return loadImage(NSBundle.mainBundle.resourcePath + '/' + source.slice(2))
231
+ }
232
+
233
+ return loadImage(source)
234
+ }
235
+
236
+ // Ownership transfers can detach and attach the same handle in one batch.
237
+ // Publish only its final target, avoiding a transient content unmount.
238
+ function notifyContentTarget(node: ElementNode) {
239
+ if (typeof node.props.__onContentTarget !== 'function') {
240
+ return
241
+ }
242
+
243
+ const generation = (node.tabPortalGeneration = (node.tabPortalGeneration ?? 0) + 1)
244
+ queueMicrotask(() => {
245
+ if (node.tabPortalGeneration !== generation) {
246
+ return
247
+ }
248
+
249
+ node.props.__onContentTarget?.(!node.tabDisposed && owners.has(node) ? node.childHost : null)
250
+ })
251
+ }
252
+
253
+ function mountContent(node: ElementNode) {
254
+ if (node.props.__portalContent) {
255
+ notifyContentTarget(node)
256
+ return
257
+ }
258
+
259
+ if (!node.tabContentRoot) {
260
+ node.tabContentRoot = createContentRoot(node.childHost!, node.container.fontFamily)
261
+ }
262
+
263
+ node.tabContentRoot.render(contentComponent, { content: node.props.__tabContent })
264
+ }
265
+
266
+ // Injected by index.ts to avoid a runtime import cycle through the host driver.
267
+ let createContentRoot: (host: AppKitView, fontFamily?: string) => any
268
+ let resolveIconColor: (color: any) => any
269
+ export function configureTabContentRoots(
270
+ create: typeof createContentRoot,
271
+ color: (value: any) => any,
272
+ ) {
273
+ createContentRoot = create
274
+ resolveIconColor = color
275
+ }
276
+
277
+ export function syncTabItem(node: ElementNode) {
278
+ if (node.tabDisposed) {
279
+ return
280
+ }
281
+
282
+ const item = node.tabNativeItem
283
+ item.label = node.props.title ?? ''
284
+ const style = node.props.style ?? {}
285
+ const fontInputs =
286
+ typeof node.props.iconSource === 'string' && node.props.iconSource.startsWith('font://')
287
+ ? [style.fontSize, style.fontWeight, style.fontFamily, style.iconFontFamily, style.color]
288
+ : []
289
+
290
+ if (
291
+ !Object.is(node.tabIconInput, node.props.iconSource) ||
292
+ fontInputs.some((value, index) => !Object.is(value, node.tabIconFontInputs?.[index]))
293
+ ) {
294
+ node.tabIconTask?.cancel()
295
+ node.tabIconTask = null
296
+ const generation = (node.tabIconGeneration = (node.tabIconGeneration ?? 0) + 1)
297
+ const source = node.props.iconSource
298
+ if (typeof source === 'string' && /^https?:\/\//i.test(source)) {
299
+ item.image = null
300
+ node.tabIconTask = NSURLSession.sharedSession.dataTaskWithURLCompletionHandler(
301
+ NSURL.URLWithString(source),
302
+ (data: any, _response: any, error: any) => {
303
+ if (node.tabDisposed || node.tabIconGeneration !== generation) {
304
+ return
305
+ }
306
+
307
+ item.image = data && !error ? NSImage.alloc().initWithData(data) : null
308
+ const owner = owners.get(node)
309
+ if (owner) {
310
+ owner.view!.needsDisplay = true
311
+ }
312
+ },
313
+ )
314
+
315
+ node.tabIconTask.resume()
316
+ } else {
317
+ item.image = iconImage(source, style)
318
+ }
319
+
320
+ node.tabIconFontInputs = fontInputs
321
+ node.tabIconInput = source
322
+ }
323
+
324
+ const owner = owners.get(node)
325
+ if (owner) {
326
+ owner.view!.needsDisplay = true
327
+ mountContent(node)
328
+ }
329
+ }
330
+
331
+ function notifySelection(node: ElementNode, value: number) {
332
+ const oldIndex = node.tabIndex
333
+ node.tabIndex = value
334
+ if (oldIndex === value || node.tabDisposed) {
335
+ return
336
+ }
337
+
338
+ const event = { object: node.view, value, oldIndex, newIndex: value }
339
+ node.props.onSelectedIndexChanged?.({ ...event, eventName: 'selectedIndexChanged' })
340
+ node.props.onSelectedIndexChange?.({ ...event, eventName: 'selectedIndexChange' })
341
+ }
342
+
343
+ /** Detach from the current physical owner, even when the handle arrived via
344
+ * an items array rather than the renderer's parent/children relation. */
345
+ export function detachTabItem(item: ElementNode, preserveContent = false) {
346
+ const owner = owners.get(item)
347
+ if (!owner) {
348
+ return
349
+ }
350
+
351
+ owner.tabSyncing = true
352
+ try {
353
+ owner.view!.removeTabViewItem(item.tabNativeItem)
354
+ } finally {
355
+ owner.tabSyncing = false
356
+ }
357
+
358
+ owner.tabItems = owner.tabItems.filter((candidate: ElementNode) => candidate !== item)
359
+ owners.delete(item)
360
+ if (!preserveContent) {
361
+ item.tabContentRoot?.unmount()
362
+ item.tabContentRoot = null
363
+ notifyContentTarget(item)
364
+ }
365
+
366
+ const index = owner.tabItems.length
367
+ ? Math.min(Math.max(0, owner.tabRequested), owner.tabItems.length - 1)
368
+ : -1
369
+
370
+ if (index >= 0) {
371
+ owner.tabSyncing = true
372
+ try {
373
+ owner.view!.selectTabViewItemAtIndex(index)
374
+ } finally {
375
+ owner.tabSyncing = false
376
+ }
377
+ }
378
+
379
+ notifySelection(owner, index)
380
+ }
381
+
382
+ export function flushTabViews(container: any) {
383
+ const dirty = container.tabDirty
384
+ container.tabDirty = undefined
385
+ if (!dirty) {
386
+ return
387
+ }
388
+
389
+ const releases = (container.tabReleases = new Set<ElementNode>())
390
+ try {
391
+ for (const node of dirty) {
392
+ syncTabView(node)
393
+ }
394
+ } finally {
395
+ container.tabReleases = undefined
396
+ for (const item of releases) {
397
+ if (!owners.has(item)) {
398
+ item.tabContentRoot?.unmount()
399
+ item.tabContentRoot = null
400
+ notifyContentTarget(item)
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ export function syncTabView(node: ElementNode) {
407
+ if (node.tabDisposed) {
408
+ return
409
+ }
410
+
411
+ if (node.container.layoutDirty) {
412
+ ;(node.container.tabDirty ??= new Set()).add(node)
413
+ return
414
+ }
415
+
416
+ validateTabProps('tabview', node.props)
417
+ const items: ElementNode[] =
418
+ node.props.items != null ? [...node.props.items] : node.children.slice()
419
+
420
+ const view = node.view!
421
+ node.tabSyncing = true
422
+ try {
423
+ for (const item of node.tabItems as ElementNode[]) {
424
+ if (!items.includes(item)) {
425
+ view.removeTabViewItem(item.tabNativeItem)
426
+ owners.delete(item)
427
+ if (node.container.tabReleases) {
428
+ node.container.tabReleases.add(item)
429
+ } else {
430
+ item.tabContentRoot?.unmount()
431
+ item.tabContentRoot = null
432
+ notifyContentTarget(item)
433
+ }
434
+ }
435
+ }
436
+
437
+ for (const [index, item] of items.entries()) {
438
+ if (owners.get(item) && owners.get(item) !== node) {
439
+ const previous = owners.get(item)!
440
+ detachTabItem(item, true)
441
+ previous.children = previous.children.filter((child) => child !== item)
442
+ if (previous.props.items) {
443
+ previous.props.items = previous.props.items.filter((child: ElementNode) => child !== item)
444
+ }
445
+
446
+ if (item.parent === previous) {
447
+ item.parent = null
448
+ }
449
+ }
450
+
451
+ // Reordering keeps the native item and its content-root identity.
452
+ if (
453
+ index >= Number(view.numberOfTabViewItems) ||
454
+ view.tabViewItemAtIndex(index) !== item.tabNativeItem
455
+ ) {
456
+ if (owners.get(item) === node) {
457
+ view.removeTabViewItem(item.tabNativeItem)
458
+ }
459
+
460
+ view.insertTabViewItemAtIndex(item.tabNativeItem, index)
461
+ }
462
+
463
+ owners.set(item, node)
464
+ syncTabItem(item)
465
+ }
466
+
467
+ node.tabItems = items
468
+ if (!Object.is(node.tabIndexInput, node.props.selectedIndex)) {
469
+ node.tabRequested = node.props.selectedIndex ?? -1
470
+ node.tabIndexInput = node.props.selectedIndex
471
+ }
472
+
473
+ const index = items.length ? Math.min(Math.max(0, node.tabRequested), items.length - 1) : -1
474
+ if (index >= 0) {
475
+ view.selectTabViewItemAtIndex(index)
476
+ }
477
+
478
+ notifySelection(node, index)
479
+ } finally {
480
+ node.tabSyncing = false
481
+ }
482
+ }
483
+
484
+ export function releaseTabView(node: ElementNode) {
485
+ node.tabDisposed = true
486
+ for (const item of (node.tabItems ?? []).slice()) {
487
+ detachTabItem(item)
488
+ }
489
+
490
+ delegates.delete(node.tabDelegate)
491
+ node.view!.delegate = null
492
+ node.tabDelegate = null
493
+ }
494
+
495
+ export function releaseTabItem(node: ElementNode) {
496
+ detachTabItem(node)
497
+ node.tabContentRoot?.unmount()
498
+ node.tabContentRoot = null
499
+ node.tabDisposed = true
500
+ node.tabIconGeneration = (node.tabIconGeneration ?? 0) + 1
501
+ node.tabIconTask?.cancel()
502
+ node.tabIconTask = null
503
+ node.tabIconInput = undefined
504
+ node.tabIconFontInputs = undefined
505
+ node.props.__tabContent = undefined
506
+ node.props.iconSource = undefined
507
+ node.props.title = undefined
508
+ node.tabNativeItem.view = null
509
+ node.tabNativeItem.image = null
510
+ node.childHost?.removeFromSuperview()
511
+ node.childHost = undefined
512
+ }