@octane-xplat/macos-renderer 0.10.0 → 0.11.1
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 +153 -9
- package/package.json +4 -3
- package/src/appearance.ts +56 -0
- package/src/appkit.d.ts +303 -30
- package/src/image.ts +92 -0
- package/src/index.ts +1115 -391
- package/src/intrinsics.d.ts +17 -2
- package/src/{layer.mjs → layer.ts} +74 -22
- package/src/presentation.d.mts +29 -0
- package/src/{presentation.mjs → presentation.ts} +102 -17
package/src/intrinsics.d.ts
CHANGED
|
@@ -7,24 +7,39 @@ export namespace JSX {
|
|
|
7
7
|
|
|
8
8
|
export interface HostProps {
|
|
9
9
|
id?: string
|
|
10
|
+
key?: string | number
|
|
11
|
+
ref?: unknown
|
|
10
12
|
className?: string
|
|
11
13
|
style?: Record<string, unknown>
|
|
12
14
|
children?: unknown
|
|
15
|
+
flexGrow?: number
|
|
16
|
+
textAlignment?: string
|
|
17
|
+
onTap?: (event?: any) => void
|
|
13
18
|
accessibilityLabel?: string
|
|
14
19
|
accessibilityRole?: string
|
|
20
|
+
accessibilityHint?: string
|
|
21
|
+
accessibilityValue?: string
|
|
22
|
+
accessibilityState?: Record<string, unknown>
|
|
23
|
+
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive'
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
export interface IntrinsicElements {
|
|
18
27
|
stack: HostProps & {
|
|
19
28
|
children?: unknown
|
|
20
29
|
spacing?: number
|
|
30
|
+
flexDirection?: 'row' | 'column'
|
|
31
|
+
gap?: number
|
|
32
|
+
rowGap?: number
|
|
33
|
+
columnGap?: number
|
|
34
|
+
alignItems?: 'start' | 'flex-start' | 'end' | 'flex-end' | 'center' | 'stretch' | 'baseline'
|
|
35
|
+
justifyContent?: 'start' | 'flex-start' | 'end' | 'flex-end' | 'center' | 'space-between'
|
|
21
36
|
}
|
|
22
37
|
label: HostProps & {
|
|
23
38
|
text?: string | number
|
|
24
39
|
fontSize?: number
|
|
25
|
-
whiteSpace?:
|
|
40
|
+
whiteSpace?: 'normal' | 'nowrap'
|
|
26
41
|
maxLines?: number
|
|
27
|
-
textOverflow?:
|
|
42
|
+
textOverflow?: 'clip' | 'ellipsis'
|
|
28
43
|
}
|
|
29
44
|
button: HostProps & {
|
|
30
45
|
title?: string
|
|
@@ -1,6 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
/** Top-left viewport-relative rectangle, in window content points. */
|
|
2
|
+
export interface WindowLayerBounds {
|
|
3
|
+
left: number
|
|
4
|
+
top: number
|
|
5
|
+
width: number
|
|
6
|
+
height: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Top-left point resolved from a bounds rect, layer size, and viewport. */
|
|
10
|
+
export interface WindowLayerPoint {
|
|
11
|
+
left: number
|
|
12
|
+
top: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WindowLayerOptions {
|
|
16
|
+
anchor: NSView
|
|
17
|
+
component?: any
|
|
18
|
+
props?: Record<string, any>
|
|
19
|
+
position(
|
|
20
|
+
anchor: WindowLayerBounds,
|
|
21
|
+
size: NSSize,
|
|
22
|
+
viewport: WindowLayerBounds,
|
|
23
|
+
): WindowLayerPoint
|
|
24
|
+
lightDismiss?: boolean
|
|
25
|
+
onClose?: () => void
|
|
26
|
+
[key: string]: any
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WindowLayer {
|
|
30
|
+
readonly contentView: NSView
|
|
31
|
+
closed: boolean
|
|
32
|
+
update(props: Record<string, any>, nextPositioning?: Partial<WindowLayerOptions>): void
|
|
33
|
+
close(): void
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WindowLayerDeps {
|
|
37
|
+
createRoot(contentView: NSView, anchor: NSView): {
|
|
38
|
+
render(component: any, props: Record<string, any>): unknown
|
|
39
|
+
unmount(): void
|
|
40
|
+
}
|
|
41
|
+
fittingSize(view: NSView): NSSize | null | undefined
|
|
42
|
+
View?: NSClass<NSView>
|
|
43
|
+
Event?: NSEventClass
|
|
44
|
+
Center?: NSNotificationCenterClass
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface NotificationRecord {
|
|
48
|
+
count: number
|
|
49
|
+
frame: boolean
|
|
50
|
+
bounds: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const notificationOwners = new WeakMap<NSView, NotificationRecord>()
|
|
2
54
|
|
|
3
|
-
function ownNotifications(view) {
|
|
55
|
+
function ownNotifications(view: NSView) {
|
|
4
56
|
let record = notificationOwners.get(view)
|
|
5
57
|
if (!record) {
|
|
6
58
|
record = {
|
|
@@ -16,17 +68,17 @@ function ownNotifications(view) {
|
|
|
16
68
|
|
|
17
69
|
record.count++
|
|
18
70
|
return () => {
|
|
19
|
-
if (--record
|
|
71
|
+
if (--record!.count) {
|
|
20
72
|
return
|
|
21
73
|
}
|
|
22
74
|
|
|
23
75
|
notificationOwners.delete(view)
|
|
24
76
|
if (view.postsFrameChangedNotifications === true) {
|
|
25
|
-
view.postsFrameChangedNotifications = record
|
|
77
|
+
view.postsFrameChangedNotifications = record!.frame
|
|
26
78
|
}
|
|
27
79
|
|
|
28
80
|
if (view.postsBoundsChangedNotifications === true) {
|
|
29
|
-
view.postsBoundsChangedNotifications = record
|
|
81
|
+
view.postsBoundsChangedNotifications = record!.bounds
|
|
30
82
|
}
|
|
31
83
|
}
|
|
32
84
|
}
|
|
@@ -34,15 +86,15 @@ function ownNotifications(view) {
|
|
|
34
86
|
/** A self-drawn in-window layer. Position callback uses top-left window points.
|
|
35
87
|
* AppKit popovers remain available for platform-authentic consumers. */
|
|
36
88
|
export function showWindowLayer(
|
|
37
|
-
options,
|
|
89
|
+
options: WindowLayerOptions,
|
|
38
90
|
{
|
|
39
91
|
createRoot,
|
|
40
92
|
fittingSize,
|
|
41
|
-
View = globalThis.NSView,
|
|
42
|
-
Event = globalThis.NSEvent,
|
|
43
|
-
Center = globalThis.NSNotificationCenter,
|
|
44
|
-
},
|
|
45
|
-
) {
|
|
93
|
+
View = (globalThis as any).NSView,
|
|
94
|
+
Event = (globalThis as any).NSEvent,
|
|
95
|
+
Center = (globalThis as any).NSNotificationCenter,
|
|
96
|
+
}: WindowLayerDeps,
|
|
97
|
+
): WindowLayer | null {
|
|
46
98
|
const anchor = options.anchor
|
|
47
99
|
const container = anchor?.window?.contentView
|
|
48
100
|
if (
|
|
@@ -62,10 +114,10 @@ export function showWindowLayer(
|
|
|
62
114
|
|
|
63
115
|
const root = createRoot(contentView, anchor)
|
|
64
116
|
let surface = options.props ?? {}
|
|
65
|
-
let positioning = options
|
|
66
|
-
let monitor = null
|
|
67
|
-
const observers = []
|
|
68
|
-
const releases = []
|
|
117
|
+
let positioning: WindowLayerOptions = options
|
|
118
|
+
let monitor: any = null
|
|
119
|
+
const observers: any[] = []
|
|
120
|
+
const releases: Array<() => void> = []
|
|
69
121
|
const updatePosition = () => {
|
|
70
122
|
if (layer.closed) {
|
|
71
123
|
return
|
|
@@ -101,7 +153,7 @@ export function showWindowLayer(
|
|
|
101
153
|
}
|
|
102
154
|
}
|
|
103
155
|
|
|
104
|
-
const layer = {
|
|
156
|
+
const layer: WindowLayer = {
|
|
105
157
|
contentView,
|
|
106
158
|
closed: false,
|
|
107
159
|
update(props, nextPositioning) {
|
|
@@ -131,11 +183,11 @@ export function showWindowLayer(
|
|
|
131
183
|
|
|
132
184
|
layer.closed = true
|
|
133
185
|
if (monitor) {
|
|
134
|
-
Event
|
|
186
|
+
Event!.removeMonitor(monitor)
|
|
135
187
|
}
|
|
136
188
|
|
|
137
189
|
for (const observer of observers) {
|
|
138
|
-
Center
|
|
190
|
+
Center!.defaultCenter.removeObserver(observer)
|
|
139
191
|
}
|
|
140
192
|
|
|
141
193
|
for (const release of releases) {
|
|
@@ -154,14 +206,14 @@ export function showWindowLayer(
|
|
|
154
206
|
container.addSubview(contentView)
|
|
155
207
|
layer.update(surface)
|
|
156
208
|
// Reposition for window resize, anchor movement and enclosing scroll views.
|
|
157
|
-
for (let view = anchor; view; view = view.superview) {
|
|
209
|
+
for (let view: NSView | null = anchor; view; view = view.superview) {
|
|
158
210
|
releases.push(ownNotifications(view))
|
|
159
211
|
for (const name of [
|
|
160
212
|
'NSViewFrameDidChangeNotification',
|
|
161
213
|
'NSViewBoundsDidChangeNotification',
|
|
162
214
|
]) {
|
|
163
215
|
observers.push(
|
|
164
|
-
Center
|
|
216
|
+
Center!.defaultCenter.addObserverForNameObjectQueueUsingBlock(
|
|
165
217
|
name,
|
|
166
218
|
view,
|
|
167
219
|
null,
|
|
@@ -172,7 +224,7 @@ export function showWindowLayer(
|
|
|
172
224
|
}
|
|
173
225
|
|
|
174
226
|
observers.push(
|
|
175
|
-
Center
|
|
227
|
+
Center!.defaultCenter.addObserverForNameObjectQueueUsingBlock(
|
|
176
228
|
'NSWindowWillCloseNotification',
|
|
177
229
|
anchor.window,
|
|
178
230
|
null,
|
|
@@ -181,7 +233,7 @@ export function showWindowLayer(
|
|
|
181
233
|
)
|
|
182
234
|
|
|
183
235
|
observers.push(
|
|
184
|
-
Center
|
|
236
|
+
Center!.defaultCenter.addObserverForNameObjectQueueUsingBlock(
|
|
185
237
|
'NSWindowDidResizeNotification',
|
|
186
238
|
anchor.window,
|
|
187
239
|
null,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { UniversalRoot } from 'octane/universal/native'
|
|
2
|
+
|
|
3
|
+
/** Handle returned by `presentSurface`. `panel` hosts the rendered subtree
|
|
4
|
+
* inside `layer`, the full-window hit-test surface that owns dismissal. */
|
|
5
|
+
export interface PresentationSurface {
|
|
6
|
+
update(options: Record<string, any>): void
|
|
7
|
+
close(reason?: string): void
|
|
8
|
+
readonly closed: boolean
|
|
9
|
+
readonly closedPromise: Promise<string | undefined>
|
|
10
|
+
panel: any
|
|
11
|
+
layer: any
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Members `installPresentationBridge` assigns onto `__xplatAppKit` for
|
|
15
|
+
* renderer-hosted in-window surfaces (sheets, toasts, lightboxes). */
|
|
16
|
+
export interface PresentationBridge {
|
|
17
|
+
setSurfaceAppearance?: (view: any, dark: boolean) => void
|
|
18
|
+
presentSurface?: (initial: Record<string, any>) => PresentationSurface
|
|
19
|
+
[key: string]: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Wires the shared-surface implementation into the AppKit bridge. The
|
|
23
|
+
* renderer supplies its root factory and the inherited-font lookup; native
|
|
24
|
+
* objects flow through `any` like the layer bridge. */
|
|
25
|
+
export function installPresentationBridge(
|
|
26
|
+
bridge: PresentationBridge,
|
|
27
|
+
createRoot: (hostView: any, options?: { fontFamily?: string }) => UniversalRoot,
|
|
28
|
+
fontFamilyForView: (view: any) => string | undefined,
|
|
29
|
+
): void
|
|
@@ -1,8 +1,89 @@
|
|
|
1
|
+
import type { UniversalRoot } from 'octane/universal/native'
|
|
2
|
+
|
|
1
3
|
// Shared surfaces live in the declaring window, independently of its layout.
|
|
2
4
|
// Kept separate from NSPopover and platform-authentic window sheets.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
/** Edge offsets accepted by surface positioning, in points or percent/vh/dip
|
|
7
|
+
* strings resolved against the host dimension. */
|
|
8
|
+
export interface SurfaceEdgeOffsets {
|
|
9
|
+
start?: number | string
|
|
10
|
+
end?: number | string
|
|
11
|
+
top?: number | string
|
|
12
|
+
bottom?: number | string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SurfaceOptions {
|
|
16
|
+
kind?: 'dialog' | 'sheet' | 'toast' | 'lightbox' | string
|
|
17
|
+
modal?: boolean
|
|
18
|
+
fullscreen?: boolean
|
|
19
|
+
component?: any
|
|
20
|
+
props?: Record<string, any>
|
|
21
|
+
owner?: any
|
|
22
|
+
width?: number | string
|
|
23
|
+
height?: number | 'tall' | 'hug' | string
|
|
24
|
+
maxHeight?: number | string
|
|
25
|
+
snapPoints?: readonly (number | string)[]
|
|
26
|
+
position?: string | SurfaceEdgeOffsets
|
|
27
|
+
inset?: { top?: number; bottom?: number; start?: number; end?: number }
|
|
28
|
+
dark?: boolean | null
|
|
29
|
+
label?: string
|
|
30
|
+
purpose?: 'info' | 'form' | 'required' | string
|
|
31
|
+
transitionKey?: unknown
|
|
32
|
+
zoom?: boolean
|
|
33
|
+
canDismiss?: (path: string) => boolean | void
|
|
34
|
+
onDismiss?: () => void
|
|
35
|
+
onKey?: (code: number) => boolean | void
|
|
36
|
+
finalFocusRef?: { current: any }
|
|
37
|
+
[key: string]: any
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SurfaceHandle {
|
|
41
|
+
update(options: Partial<SurfaceOptions>): void
|
|
42
|
+
close(reason?: string): void
|
|
43
|
+
readonly closed: boolean
|
|
44
|
+
readonly closedPromise: Promise<unknown>
|
|
45
|
+
readonly panel: NSView
|
|
46
|
+
readonly layer: NSView
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PresentationBridge {
|
|
50
|
+
setSurfaceAppearance?: (view: NSView, dark: boolean) => void
|
|
51
|
+
presentSurface?: (options: SurfaceOptions) => SurfaceHandle
|
|
52
|
+
[key: string]: any
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type SurfaceRootFactory = (
|
|
56
|
+
host: NSView,
|
|
57
|
+
options?: { fontFamily?: string },
|
|
58
|
+
) => UniversalRoot
|
|
59
|
+
|
|
60
|
+
interface SurfaceEntry {
|
|
61
|
+
options: SurfaceOptions
|
|
62
|
+
parent: SurfaceEntry | undefined
|
|
63
|
+
panel: NSView
|
|
64
|
+
layer: NSView
|
|
65
|
+
window: NSWindow
|
|
66
|
+
closed: boolean
|
|
67
|
+
observers: any[]
|
|
68
|
+
monitor: any
|
|
69
|
+
closedPromise: Promise<unknown>
|
|
70
|
+
stops?: number[]
|
|
71
|
+
stop?: number
|
|
72
|
+
didRender?: boolean
|
|
73
|
+
drag?: { y: number; height: number } | null
|
|
74
|
+
layout(): void
|
|
75
|
+
update(options: Partial<SurfaceOptions>): void
|
|
76
|
+
close(reason?: string): void
|
|
77
|
+
requestDismiss(path: string): void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function installPresentationBridge(
|
|
81
|
+
bridge: PresentationBridge,
|
|
82
|
+
createRoot: SurfaceRootFactory,
|
|
83
|
+
fontFamilyForView: (view: NSView | undefined) => string | undefined,
|
|
84
|
+
) {
|
|
85
|
+
const entries: SurfaceEntry[] = []
|
|
86
|
+
const layers = new WeakMap<NSView, SurfaceEntry>()
|
|
6
87
|
class PresentationLayer extends NSView {
|
|
7
88
|
static {
|
|
8
89
|
NativeClass(this)
|
|
@@ -10,7 +91,7 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
10
91
|
acceptsFirstResponder() {
|
|
11
92
|
return true
|
|
12
93
|
}
|
|
13
|
-
hitTest(point) {
|
|
94
|
+
hitTest(point: NSPoint): NSView | null {
|
|
14
95
|
const hit = super.hitTest(point)
|
|
15
96
|
const entry = layers.get(this)
|
|
16
97
|
return (hit === this || hit?.isEqual?.(this)) && !entry?.options.modal ? null : hit
|
|
@@ -21,8 +102,8 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
21
102
|
}
|
|
22
103
|
}
|
|
23
104
|
|
|
24
|
-
const children = (view) => {
|
|
25
|
-
const result = []
|
|
105
|
+
const children = (view: any): NSView[] => {
|
|
106
|
+
const result: NSView[] = []
|
|
26
107
|
const list = view?.subviews
|
|
27
108
|
for (let i = 0; i < Number(list?.count ?? 0); i++) {
|
|
28
109
|
result.push(list.objectAtIndex(i))
|
|
@@ -31,8 +112,10 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
31
112
|
return result
|
|
32
113
|
}
|
|
33
114
|
|
|
34
|
-
const truth = (view, key) =>
|
|
35
|
-
|
|
115
|
+
const truth = (view: any, key: string) =>
|
|
116
|
+
typeof view?.[key] === 'function' ? view[key]() : view?.[key]
|
|
117
|
+
|
|
118
|
+
const focusables = (view: any): NSView[] => {
|
|
36
119
|
if (!view || truth(view, 'hidden')) {
|
|
37
120
|
return []
|
|
38
121
|
}
|
|
@@ -41,7 +124,7 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
41
124
|
return [...own, ...children(view).flatMap(focusables)]
|
|
42
125
|
}
|
|
43
126
|
|
|
44
|
-
const inside = (view, panel) => {
|
|
127
|
+
const inside = (view: any, panel: NSView): boolean => {
|
|
45
128
|
for (let current = view; current; current = current.superview) {
|
|
46
129
|
if (current === panel || current.isEqual?.(panel)) {
|
|
47
130
|
return true
|
|
@@ -51,7 +134,7 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
51
134
|
return false
|
|
52
135
|
}
|
|
53
136
|
|
|
54
|
-
const length = (value, budget, fallback) => {
|
|
137
|
+
const length = (value: unknown, budget: number, fallback: number): number => {
|
|
55
138
|
if (typeof value === 'number') {
|
|
56
139
|
return Math.max(0, value)
|
|
57
140
|
}
|
|
@@ -91,10 +174,10 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
91
174
|
panel.wantsLayer = true
|
|
92
175
|
layer.addSubview(panel)
|
|
93
176
|
const root = createRoot(panel, { fontFamily: fontFamilyForView(owner ?? host) })
|
|
94
|
-
let resolveClosed
|
|
177
|
+
let resolveClosed: (reason?: unknown) => void = () => {}
|
|
95
178
|
const responder = nativeWindow.firstResponder
|
|
96
179
|
const previousFocus = truth(responder, 'isFieldEditor') ? responder.delegate : responder
|
|
97
|
-
const entry = {
|
|
180
|
+
const entry: SurfaceEntry = {
|
|
98
181
|
options: initial,
|
|
99
182
|
parent: [...entries]
|
|
100
183
|
.reverse()
|
|
@@ -165,7 +248,7 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
165
248
|
}
|
|
166
249
|
|
|
167
250
|
if (options.kind === 'toast') {
|
|
168
|
-
const edge = options.position ?? 'bottomEnd',
|
|
251
|
+
const edge = (options.position as string | undefined) ?? 'bottomEnd',
|
|
169
252
|
inset = options.inset ?? {}
|
|
170
253
|
|
|
171
254
|
x = edge.endsWith('Start') ? 16 + (inset.start ?? 0) : w - width - 16 - (inset.end ?? 0)
|
|
@@ -173,7 +256,9 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
173
256
|
}
|
|
174
257
|
|
|
175
258
|
const pos =
|
|
176
|
-
!full && options.position && typeof options.position === 'object'
|
|
259
|
+
!full && options.position && typeof options.position === 'object'
|
|
260
|
+
? options.position
|
|
261
|
+
: {}
|
|
177
262
|
|
|
178
263
|
if (pos.start != null) {
|
|
179
264
|
x = length(pos.start, w, 0)
|
|
@@ -259,7 +344,7 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
259
344
|
}
|
|
260
345
|
|
|
261
346
|
if (entry.options.kind === 'lightbox') {
|
|
262
|
-
const visit = (view) => {
|
|
347
|
+
const visit = (view: any) => {
|
|
263
348
|
if ('allowsMagnification' in view) {
|
|
264
349
|
view.allowsMagnification = entry.options.zoom !== false
|
|
265
350
|
view.minMagnification = 1
|
|
@@ -280,8 +365,8 @@ export function installPresentationBridge(bridge, createRoot, fontFamilyForView)
|
|
|
280
365
|
}
|
|
281
366
|
|
|
282
367
|
entry.closed = true
|
|
283
|
-
const owns = (candidate) => {
|
|
284
|
-
for (let current = candidate; current; current = current.parent) {
|
|
368
|
+
const owns = (candidate: SurfaceEntry) => {
|
|
369
|
+
for (let current = candidate; current; current = current.parent!) {
|
|
285
370
|
if (current === entry) {
|
|
286
371
|
return true
|
|
287
372
|
}
|