@octane-xplat/macos-renderer 0.14.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.
- package/README.md +95 -20
- package/package.json +15 -2
- package/src/appearance.ts +2 -0
- package/src/fonts.ts +2 -0
- package/src/formatted-text.ts +147 -0
- package/src/gestures.ts +196 -0
- package/src/host-globals.d.ts +55 -0
- package/src/html-text.ts +88 -0
- package/src/image.ts +91 -16
- package/src/index.d.ts +9 -0
- package/src/index.ts +5030 -678
- package/src/layer.ts +11 -9
- package/src/list-template.ts +266 -0
- package/src/list-view.ts +445 -0
- package/src/portal-components.macos.tsx +86 -0
- package/src/presentation.ts +48 -15
- package/src/scroll-view.ts +124 -0
- package/src/tab-view.ts +512 -0
- package/src/template-props.ts +402 -0
- package/src/vendor/listview/LICENSE +19 -0
- package/src/vendor/listview/README.md +1 -0
- package/src/vendor/listview/binding-options.ts +189 -0
- package/src/vendor/listview/expressions.ts +285 -0
- package/src/webview.ts +36 -7
- package/types/ARKitCore.d.ts +199 -0
- package/types/AVFAudio.d.ts +2304 -0
- package/types/AVFoundation.d.ts +6974 -0
- package/types/AVKit.d.ts +215 -0
- package/types/AVRouting.d.ts +7 -0
- package/types/Accelerate.d.ts +4776 -0
- package/types/Accessibility.d.ts +69 -0
- package/types/Accounts.d.ts +134 -0
- package/types/AdSupport.d.ts +12 -0
- package/types/AddressBook.d.ts +1012 -0
- package/types/AppKit.d.ts +34247 -0
- package/types/AppTrackingTransparency.d.ts +23 -0
- package/types/AppleScriptKit.d.ts +6 -0
- package/types/ApplicationServices.d.ts +6346 -0
- package/types/AudioToolbox.d.ts +7129 -0
- package/types/AudioVideoBridging.d.ts +613 -0
- package/types/AuthenticationServices.d.ts +982 -0
- package/types/AutomaticAssessmentConfiguration.d.ts +46 -0
- package/types/Automator.d.ts +248 -0
- package/types/BackgroundAssets.d.ts +9 -0
- package/types/BrowserEngineKit.d.ts +166 -0
- package/types/BusinessChat.d.ts +29 -0
- package/types/CFNetwork.d.ts +931 -0
- package/types/CalendarStore.d.ts +354 -0
- package/types/CallKit.d.ts +30 -0
- package/types/Carbon.d.ts +16207 -0
- package/types/ClassKit.d.ts +281 -0
- package/types/CloudKit.d.ts +1565 -0
- package/types/Collaboration.d.ts +123 -0
- package/types/ColorSync.d.ts +562 -0
- package/types/CompositorServices.d.ts +60 -0
- package/types/ComputeGraph.d.ts +16 -0
- package/types/Contacts.d.ts +1558 -0
- package/types/ContactsUI.d.ts +39 -0
- package/types/CoreAudio.d.ts +1376 -0
- package/types/CoreAudioKit.d.ts +122 -0
- package/types/CoreAudioTypes.d.ts +1003 -0
- package/types/CoreBluetooth.d.ts +657 -0
- package/types/CoreData.d.ts +2479 -0
- package/types/CoreFoundation.d.ts +5546 -0
- package/types/CoreGraphics.d.ts +3676 -0
- package/types/CoreHaptics.d.ts +361 -0
- package/types/CoreImage.d.ts +4781 -0
- package/types/CoreLocation.d.ts +564 -0
- package/types/CoreMIDI.d.ts +1538 -0
- package/types/CoreML.d.ts +905 -0
- package/types/CoreMedia.d.ts +2615 -0
- package/types/CoreMediaIO.d.ts +1046 -0
- package/types/CoreMotion.d.ts +316 -0
- package/types/CoreServices.d.ts +24810 -0
- package/types/CoreSpotlight.d.ts +789 -0
- package/types/CoreTelephony.d.ts +31 -0
- package/types/CoreText.d.ts +3973 -0
- package/types/CoreTransferable.d.ts +3 -0
- package/types/CoreVideo.d.ts +1162 -0
- package/types/CoreWLAN.d.ts +556 -0
- package/types/CryptoTokenKit.d.ts +548 -0
- package/types/DVDPlayback.d.ts +931 -0
- package/types/DataDetection.d.ts +66 -0
- package/types/DeviceCheck.d.ts +46 -0
- package/types/DeviceDiscoveryExtension.d.ts +142 -0
- package/types/DirectoryService.d.ts +966 -0
- package/types/DiscRecording.d.ts +2345 -0
- package/types/DiscRecordingUI.d.ts +288 -0
- package/types/DiskArbitration.d.ts +366 -0
- package/types/DriverKit.d.ts +639 -0
- package/types/EventKit.d.ts +640 -0
- package/types/ExceptionHandling.d.ts +56 -0
- package/types/ExecutionPolicy.d.ts +30 -0
- package/types/ExternalAccessory.d.ts +98 -0
- package/types/FSKit.d.ts +237 -0
- package/types/FileProvider.d.ts +495 -0
- package/types/FileProviderUI.d.ts +31 -0
- package/types/FinderSync.d.ts +165 -0
- package/types/FocusEngine.d.ts +133 -0
- package/types/ForceFeedback.d.ts +399 -0
- package/types/Foundation.d.ts +18216 -0
- package/types/GLKit.d.ts +937 -0
- package/types/GLUT.d.ts +1018 -0
- package/types/GSS.d.ts +855 -0
- package/types/GameController.d.ts +1394 -0
- package/types/GameKit.d.ts +1958 -0
- package/types/GameSave.d.ts +54 -0
- package/types/GameplayKit.d.ts +1645 -0
- package/types/HealthKit.d.ts +278 -0
- package/types/Hypervisor.d.ts +2079 -0
- package/types/ICADevices.d.ts +1400 -0
- package/types/IOBluetooth.d.ts +6094 -0
- package/types/IOBluetoothUI.d.ts +250 -0
- package/types/IOKit.d.ts +25804 -0
- package/types/IOSurface.d.ts +435 -0
- package/types/IOUSBHost.d.ts +809 -0
- package/types/ImageCaptureCore.d.ts +1104 -0
- package/types/ImageIO.d.ts +1697 -0
- package/types/InputMethodKit.d.ts +264 -0
- package/types/InstallerPlugins.d.ts +102 -0
- package/types/InstantMessage.d.ts +229 -0
- package/types/Intents.d.ts +1833 -0
- package/types/IntentsUI.d.ts +5 -0
- package/types/JavaRuntimeSupport.d.ts +520 -0
- package/types/JavaScriptCore.d.ts +934 -0
- package/types/Kerberos.d.ts +3754 -0
- package/types/Kernel.d.ts +33698 -0
- package/types/KernelManagement.d.ts +5 -0
- package/types/LatentSemanticMapping.d.ts +152 -0
- package/types/LinkPresentation.d.ts +57 -0
- package/types/LocalAuthentication.d.ts +94 -0
- package/types/MLCompute.d.ts +2343 -0
- package/types/MapKit.d.ts +2125 -0
- package/types/Matter.d.ts +151 -0
- package/types/MediaAccessibility.d.ts +137 -0
- package/types/MediaLibrary.d.ts +394 -0
- package/types/MediaPlayer.d.ts +475 -0
- package/types/MediaToolbox.d.ts +70 -0
- package/types/Metal.d.ts +4542 -0
- package/types/MetalFX.d.ts +107 -0
- package/types/MetalKit.d.ts +524 -0
- package/types/MetalPerformanceShaders.d.ts +9306 -0
- package/types/MetalPerformanceShadersGraph.d.ts +1394 -0
- package/types/MetricKit.d.ts +231 -0
- package/types/ModelIO.d.ts +2908 -0
- package/types/MultipeerConnectivity.d.ts +348 -0
- package/types/NaturalLanguage.d.ts +556 -0
- package/types/NearbyInteraction.d.ts +51 -0
- package/types/NetFS.d.ts +99 -0
- package/types/Network.d.ts +1622 -0
- package/types/NetworkExtension.d.ts +1574 -0
- package/types/NotificationCenter.d.ts +103 -0
- package/types/OSAKit.d.ts +260 -0
- package/types/OSLog.d.ts +208 -0
- package/types/OpenAL.d.ts +314 -0
- package/types/OpenCL.d.ts +2489 -0
- package/types/OpenDirectory.d.ts +1919 -0
- package/types/OpenGL.d.ts +11322 -0
- package/types/PCSC.d.ts +534 -0
- package/types/PDFKit.d.ts +1575 -0
- package/types/PHASE.d.ts +166 -0
- package/types/ParavirtualizedGraphics.d.ts +153 -0
- package/types/PassKit.d.ts +1287 -0
- package/types/PencilKit.d.ts +201 -0
- package/types/Photos.d.ts +1125 -0
- package/types/PhotosUI.d.ts +397 -0
- package/types/PreferencePanes.d.ts +68 -0
- package/types/ProximityReaderStub.d.ts +5 -0
- package/types/PushKit.d.ts +45 -0
- package/types/Quartz.d.ts +1642 -0
- package/types/QuartzCore.d.ts +1398 -0
- package/types/QuickLook.d.ts +212 -0
- package/types/QuickLookThumbnailing.d.ts +131 -0
- package/types/QuickLookUI.d.ts +117 -0
- package/types/REVISION.json +5 -0
- package/types/ReplayKit.d.ts +294 -0
- package/types/Runtime.d.ts +51605 -0
- package/types/SafariServices.d.ts +430 -0
- package/types/SafetyKit.d.ts +7 -0
- package/types/SceneKit.d.ts +4996 -0
- package/types/ScreenCaptureKit.d.ts +16 -0
- package/types/ScreenSaver.d.ts +41 -0
- package/types/ScreenTime.d.ts +41 -0
- package/types/ScriptingBridge.d.ts +96 -0
- package/types/Security.d.ts +14336 -0
- package/types/SecurityFoundation.d.ts +53 -0
- package/types/SecurityInterface.d.ts +297 -0
- package/types/SensorKit.d.ts +7 -0
- package/types/ServiceManagement.d.ts +71 -0
- package/types/SharedWithYou.d.ts +9 -0
- package/types/SharedWithYouCore.d.ts +5 -0
- package/types/ShazamKit.d.ts +12 -0
- package/types/Social.d.ts +306 -0
- package/types/SoundAnalysis.d.ts +177 -0
- package/types/Speech.d.ts +246 -0
- package/types/SpriteKit.d.ts +2766 -0
- package/types/StoreKit.d.ts +509 -0
- package/types/SyncServices.d.ts +551 -0
- package/types/SystemConfiguration.d.ts +1415 -0
- package/types/SystemExtensions.d.ts +89 -0
- package/types/TWAIN.d.ts +380 -0
- package/types/Tcl.d.ts +4013 -0
- package/types/ThreadNetwork.d.ts +77 -0
- package/types/Tk.d.ts +3750 -0
- package/types/UIUtilities.d.ts +31 -0
- package/types/UniformTypeIdentifiers.d.ts +358 -0
- package/types/UserNotifications.d.ts +538 -0
- package/types/UserNotificationsUI.d.ts +38 -0
- package/types/VideoDecodeAcceleration.d.ts +50 -0
- package/types/VideoSubscriberAccount.d.ts +139 -0
- package/types/VideoToolbox.d.ts +907 -0
- package/types/Virtualization.d.ts +302 -0
- package/types/Vision.d.ts +1723 -0
- package/types/WebKit.d.ts +5195 -0
- package/types/WidgetKit.d.ts +9 -0
- package/types/_LocationEssentials.d.ts +159 -0
- package/types/_ScreenCaptureKit_SwiftUI.d.ts +5 -0
- package/types/iTunesLibrary.d.ts +528 -0
- package/types/index.d.ts +9 -0
- package/types/vecLib.d.ts +33586 -0
- package/types/vmnet.d.ts +178 -0
- 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
|
+
}
|
package/src/tab-view.ts
ADDED
|
@@ -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
|
+
}
|