@octane-xplat/macos-renderer 0.11.0 → 0.12.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.
@@ -27,13 +27,19 @@ export namespace JSX {
27
27
  stack: HostProps & {
28
28
  children?: unknown
29
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'
30
36
  }
31
37
  label: HostProps & {
32
38
  text?: string | number
33
39
  fontSize?: number
34
- whiteSpace?: string
40
+ whiteSpace?: 'normal' | 'nowrap'
35
41
  maxLines?: number
36
- textOverflow?: string
42
+ textOverflow?: 'clip' | 'ellipsis'
37
43
  }
38
44
  button: HostProps & {
39
45
  title?: string
@@ -1,6 +1,58 @@
1
- const notificationOwners = new WeakMap()
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.count) {
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.frame
77
+ view.postsFrameChangedNotifications = record!.frame
26
78
  }
27
79
 
28
80
  if (view.postsBoundsChangedNotifications === true) {
29
- view.postsBoundsChangedNotifications = record.bounds
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.removeMonitor(monitor)
186
+ Event!.removeMonitor(monitor)
135
187
  }
136
188
 
137
189
  for (const observer of observers) {
138
- Center.defaultCenter.removeObserver(observer)
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.defaultCenter.addObserverForNameObjectQueueUsingBlock(
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.defaultCenter.addObserverForNameObjectQueueUsingBlock(
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.defaultCenter.addObserverForNameObjectQueueUsingBlock(
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
- export function installPresentationBridge(bridge, createRoot, fontFamilyForView) {
4
- const entries = []
5
- const layers = new WeakMap()
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) => (typeof view?.[key] === 'function' ? view[key]() : view?.[key])
35
- const focusables = (view) => {
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' ? options.position : {}
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
  }