@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.
- 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
package/src/list-view.ts
ADDED
|
@@ -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
|
+
}
|
package/src/presentation.ts
CHANGED
|
@@ -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<
|
|
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):
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
}
|